@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,568 @@
1
+ ---
2
+ title: Flow
3
+ description: Build reactive, server-driven UIs as plain TypeScript classes — no API layer, no client store, no hand-written reactivity.
4
+ ---
5
+
6
+ # Flow
7
+
8
+ Flow is Zerotal's server-driven UI layer. Each component is a TypeScript class that lives on the server. Decorators expose state and actions to the browser over a WebSocket; the client applies morphing patches without a full page reload. No API layer, no client-side store, no hand-written reactivity — the server is the source of truth.
9
+
10
+ **You write plain JSX and Flow takes care of the rest.** Bind a handler with `onClick={this.save}`, two-way an input with `value={this.name}`, show a validation message with `error={this.errors.email}`. There is no `flow()` wrapper, no `this.bind(...)`, and no manual `flow:*` attributes in your components — Flow's compiler reads the shape of each prop and emits the right directive for you.
11
+
12
+ ## Getting Started
13
+
14
+ ### How it works
15
+
16
+ 1. A `GET` request renders the page on the server, serialises its state into a signed snapshot embedded in the HTML, and streams the result.
17
+ 2. The browser restores the page from the snapshot. User interactions (clicks, input changes, navigation) are sent as WebSocket frames to the server.
18
+ 3. The server hydrates the page from the snapshot, runs the action method, re-renders, and diffs the output. Only the changed DOM patches are sent back and morphed in.
19
+
20
+ This means your component code runs exclusively on the server. There's no bundler step for your application logic, no client hydration mismatch, and the browser never receives your business logic.
21
+
22
+ ### Installation
23
+
24
+ ```bash
25
+ # in your project root
26
+ bun add @zerotal/flow
27
+ ```
28
+
29
+ ### Register the provider
30
+
31
+ Add `FlowProvider` to the providers array in `bootstrap/providers.ts`:
32
+
33
+ ```typescript
34
+ // bootstrap/providers.ts
35
+ import { FlowProvider } from "@zerotal/flow";
36
+
37
+ const providers = [
38
+ // …your other providers
39
+ FlowProvider,
40
+ ];
41
+
42
+ export default providers;
43
+ ```
44
+
45
+ Registering the provider switches on the following (in lifecycle order):
46
+
47
+ - `onRegister` — registers the `Router.flow()` macro and the file-route resolver so Flow pages can be declared and auto-discovered.
48
+ - `onBooting` — builds the client runtime bundle (Alpine + the Flow bridge) and mounts the routes under `/__flow` (the WebSocket endpoint, `runtime.js`, the upload endpoint, and the session-relay endpoint).
49
+ - `onBooted` — wires `serve --dev` rebuild hooks for `resources/css/app.css` and `resources/js/app.js` (a no-op when those entry points are absent).
50
+ - `onStarting` — AOT-compiles and validates every registered page's `render()` method.
51
+
52
+ ### Configuration
53
+
54
+ Flow runs with no configuration at all. Add `config/flow.ts` only when you want to
55
+ change one of its two knobs — the file is auto-discovered, and every key you leave
56
+ out keeps its default:
57
+
58
+ ```typescript
59
+ // config/flow.ts
60
+ import { FlowConfig } from "@zerotal/flow";
61
+
62
+ export default FlowConfig({
63
+ cspSafe: true,
64
+ });
65
+ ```
66
+
67
+ | Key | Default | Controls |
68
+ | ---------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
69
+ | `cspSafe` | `false` | Serve the eval-free client runtime — see [CSP-safe mode](/docs/flow/components#csp-safe-mode). |
70
+ | `persistentMiddleware` | `["SessionMiddleware", "PersistUserMiddleware", "BearerTokenMiddleware"]` | Which **global** middleware re-runs on every WebSocket update. |
71
+
72
+ `cspSafe` also reads the `ZT_FLOW_CSP_SAFE` (or `APP_CSP_SAFE`) env flag when no
73
+ config file sets it, so an existing env-driven deployment keeps working untouched.
74
+
75
+ ### What re-runs on a WebSocket action
76
+
77
+ A Flow action is not an HTTP request, so the pipeline is rebuilt for each one:
78
+
79
+ - **Route middleware re-runs in full, automatically.** Anything attached to the
80
+ page's route — `Router.flow("/admin", Page, [AuthMiddleware])`, a surrounding
81
+ `Router.group`, or a `_middleware.ts` in the file-route tree — is re-applied on
82
+ every action. There is nothing to declare, and this is where most app middleware
83
+ lives.
84
+ - **Global middleware re-runs only if listed.** Most of the global pipeline exists
85
+ to shape an HTTP request/response that a WebSocket frame does not have: CORS and
86
+ security headers decorate a response, CSRF guards a form post the snapshot HMAC
87
+ already authenticates, and a request logger would fire on every keystroke. The
88
+ three defaults are the ones that establish _identity_ — session, user, bearer
89
+ token — which the action genuinely needs.
90
+
91
+ Because Flow pages are file-routed by default, the natural place for shared
92
+ middleware is a `_middleware.ts` in the page tree. It stacks from the root down,
93
+ covers every page beneath it, and — being route middleware — re-runs on every
94
+ action with nothing else to configure:
95
+
96
+ ```typescript
97
+ // app/flow/pages/_middleware.ts
98
+ import { TenantMiddleware } from "../../middleware/TenantMiddleware.ts";
99
+
100
+ export const middleware = [TenantMiddleware];
101
+ ```
102
+
103
+ Scope it to part of the tree with a `(group)` directory — `(protected)/_middleware.ts`
104
+ guards the pages inside it without changing their URLs. See
105
+ [Directory middleware](/docs/routing#directory-middleware).
106
+
107
+ Reach for `persistentMiddleware` only when the middleware is genuinely **global** —
108
+ registered app-wide with `Application.use()` because non-Flow routes need it too:
109
+
110
+ ```typescript
111
+ // app/providers/AppServiceProvider.ts — inside onRegister():
112
+ FlowProvider.persistMiddleware(TenantMiddleware);
113
+ ```
114
+
115
+ > **Warning** — Setting `persistentMiddleware` in `config/flow.ts` **replaces** the
116
+ > whole list, so include the three defaults alongside your own or WebSocket actions
117
+ > lose the session and the signed-in user. `persistMiddleware()` appends, which is
118
+ > why it is the safer way to add one.
119
+
120
+ Point the JSX transform at Flow in `tsconfig.json`. Set it once and every `.tsx`
121
+ component in the project is covered — the Flow scaffold already writes this:
122
+
123
+ ```json
124
+ // tsconfig.json
125
+ {
126
+ "compilerOptions": {
127
+ "jsx": "react-jsx",
128
+ "jsxImportSource": "@zerotal/flow"
129
+ }
130
+ }
131
+ ```
132
+
133
+ > **Note** — A project that mixes runtimes (Flow components alongside React or
134
+ > Vue Inertia pages) can only set one of these globally. Files on the other
135
+ > runtime need a `/** @jsxImportSource … */` comment of their own — which is why
136
+ > `make:flow` writes one into every class it generates.
137
+
138
+ ### Your first component
139
+
140
+ ```tsx
141
+ import { Component, expose } from "@zerotal/flow";
142
+
143
+ export class CounterPage extends Component {
144
+ @expose count: number = 0;
145
+
146
+ @expose increment(): void {
147
+ this.count++;
148
+ }
149
+
150
+ override async render() {
151
+ return (
152
+ <div>
153
+ <p>Count: {this.count}</p>
154
+ <button onClick={this.increment}>+</button>
155
+ <button onClick={() => this.count--}>−</button>
156
+ </div>
157
+ );
158
+ }
159
+ }
160
+ ```
161
+
162
+ `onClick={this.increment}` is a **server action** — it round-trips over the WebSocket, runs `increment()` on the server, and patches only the changed DOM nodes back. `onClick={() => this.count--}` is a **client expression** — it updates the DOM instantly with no round-trip.
163
+
164
+ The distinction is the syntax: a named method reference is always a server action; an arrow function is always a client expression. You express the distinction the same way you would in React.
165
+
166
+ Register the route:
167
+
168
+ ```typescript
169
+ // routes/web.ts
170
+ import { Router } from "zerotal";
171
+ import { CounterPage } from "./components/CounterPage.tsx";
172
+
173
+ Router.flow("/counter", CounterPage);
174
+ ```
175
+
176
+ ### Reserved member names
177
+
178
+ `Component` brings its own members, and a property of yours that collides with one is a
179
+ type error. It is caught at compile time and the message is specific, but the name that
180
+ trips people is `title` — an obvious field for a row representing a media item, a guide or
181
+ a review, and taken by the page-title accessor.
182
+
183
+ The names in use:
184
+
185
+ | Group | Names |
186
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------- |
187
+ | Lifecycle | `onBoot` `onMount` `onHydrate` `onDehydrate` `onRendering` `onRendered` `onUpdate` `onUpdating` `onUpdated` `onError` |
188
+ | Rendering | `render` `layout` `placeholder` `slot` `hasSlot` `child` `title` |
189
+ | Actions & state | `bind` `validate` `resetValidation` `errors` `addError` `refresh` `$refresh` `$set` `cancelled` `signal` |
190
+ | Navigation | `redirect` `redirectRoute` `redirectIntended` `currentUrl` `navigateCurrent` |
191
+ | Events & realtime | `dispatch` `dispatchSelf` `dispatchTo` `stream` `client` |
192
+ | Misc | `flash` `download` `clearDurable` |
193
+
194
+ Anything beginning with `_` is also framework-internal, as is the static `durable`.
195
+
196
+ If the natural name is taken, the usual fix is a more specific one — `headline`,
197
+ `mediaTitle` — which often reads better than `title` did.
198
+
199
+ ### Scaffolding with make:flow
200
+
201
+ You don't have to write each component from a blank file. `make:flow` generates a ready-to-run class with the JSX pragma, the right imports, and an `@expose`/`render` skeleton already in place:
202
+
203
+ ```bash
204
+ bun zt make:flow Dashboard # a page under app/flow/pages
205
+ bun zt make:flow Users/Index --crud # a resourceful page: list + create/edit/delete + validation
206
+ bun zt make:flow StarRating --child # a child component (props from its parent)
207
+ bun zt make:flow Billing --layout AppLayout # a page wrapped in a layout
208
+ ```
209
+
210
+ The name may be nested (`Users/Index` → `app/flow/pages/Users/Index.tsx`), and the target directory is auto-detected (`app/flow/pages` for pages, `app/flow/components` for children) or set with `--dir`. Because Flow uses file-based routing, a generated page is served automatically — the command also prints the explicit `Router.flow(...)` line for apps that register routes by hand.
211
+
212
+ The command lives on `FlowProvider`, so it's available in any app that registers the provider. (If `make:flow` isn't found, add `FlowProvider` to your `bootstrap/providers.ts` — an app that only pulls it in transitively through the admin panel won't have it in CLI runs.)
213
+
214
+ ### Server actions vs. client expressions
215
+
216
+ | Syntax | Behaviour | Round-trip |
217
+ | ----------------------------------- | -------------------------------------------------- | ----------------------- |
218
+ | `onClick={this.save}` | Calls `save()` on the server, re-renders | Yes |
219
+ | `onClick={() => this.count++}` | Updates `count` in the browser instantly | Yes, after (state sync) |
220
+ | `onSubmit={this.register}` | Calls `register()` on the server, prevents default | Yes |
221
+ | `onClick={() => this.open = false}` | Sets `open` in the browser instantly | Yes, after (state sync) |
222
+
223
+ Client expressions read and write the same `@expose` properties your server sees. Assigning to an `@expose` property from a client expression updates the UI immediately, then syncs to the server, which re-renders with the new value — so anything `render()` derives from that property (a selected row's detail, a conditional branch) reflects the write without needing a separate action. The sync is skipped when the same expression also calls a server action (that action's round-trip already carries the write), and when the write leaves the value back where it started. `@locked` properties are read-only on the client.
224
+
225
+ State that the server should never see — a hover flag, which accordion section is open — belongs in the [global client store](#the-global-client-store) (`this.store()`), which is client-only and never round-trips.
226
+
227
+ ### Passing arguments to a server action
228
+
229
+ 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:
230
+
231
+ ```tsx
232
+ {
233
+ this.enquiries.map((row) => (
234
+ <tr>
235
+ <td>{row.reference}</td>
236
+ <td>
237
+ <button onClick={() => this.archive(row.id)}>Archive</button>
238
+ </td>
239
+ </tr>
240
+ ));
241
+ }
242
+ ```
243
+
244
+ 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:
245
+
246
+ ```ts
247
+ @expose async archive(id: number) {
248
+ await Enquiry.findOrFail(id).archive();
249
+ }
250
+ ```
251
+
252
+ You can also write `data-args` yourself, which is useful when the handler is built dynamically:
253
+
254
+ ```tsx
255
+ <button onClick={this.archive} data-args={JSON.stringify([row.id])}>
256
+ Archive
257
+ </button>
258
+ ```
259
+
260
+ ::: warning Arguments are evaluated once, at render time
261
+ An argument that reads `this` is **not** frozen — `onClick={() => this.setPage(this.page + 1)}` stays a live client expression and re-evaluates in the browser against current reactive state. Only arguments that close over server-side values (a loop variable, a computed local) are serialised.
262
+
263
+ Anything else a client expression references must exist in the browser. A handler that reaches for an enclosing server-side variable outside a call — `onClick={() => (window.location = row.url)}` — is reported at boot, naming the identifier, because it would otherwise be emitted verbatim and throw a `ReferenceError` in the browser, where nothing surfaces it. The page falls back to the runtime renderer rather than failing the build, since the check works from a known list of globals and a false positive should not stop your server starting. Under `cspSafe`, where there is no runtime fallback, it is fatal.
264
+
265
+ By contrast, a handler pointing at a method you forgot to `@expose` **is** a hard error — that one is certain, not heuristic, and the alternative is a button that silently does nothing.
266
+ :::
267
+
268
+ ### Two-way inputs
269
+
270
+ 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`:
271
+
272
+ ```tsx
273
+ <input value={this.name} /> {/* @expose → two-way */}
274
+ <input type="checkbox" checked={this.agree} /> {/* @expose → two-way */}
275
+ <input value={this.ownerName} /> {/* @locked → read-only */}
276
+ ```
277
+
278
+ 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()`:
279
+
280
+ ```tsx
281
+ {
282
+ ["CUSTOM", "ROUTE", "TEAMS"].map((t) => (
283
+ <label>
284
+ <input type="radio" name="type" {...this.bind("type", t)} /> {t}
285
+ </label>
286
+ ));
287
+ }
288
+ ```
289
+
290
+ Each option renders with the shared `flow:model="type"`, its own `value`, and `checked` on whichever one matches the current state. A bare `value={…}`/`checked={…}` on a radio is emitted as a plain attribute and never inferred as a binding — one option in a group cannot own the group's state.
291
+
292
+ 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:
293
+
294
+ ```tsx
295
+ <input value={this.draft} /> {/* local; flushed with the next action */}
296
+ <input value={this.search} live /> {/* syncs to the server as you type */}
297
+ <input value={this.title} blur /> {/* syncs to the server on blur */}
298
+ ```
299
+
300
+ `live` inputs also update other client-side bindings reactively as you type. A `live` **text** input debounces its server sync by ~150ms, so fast typing doesn't fire a round-trip on every keystroke (real-time validation and reactive server state update when you pause, not per character). Discrete controls — checkbox, radio, `<select>`, range — sync immediately, since a pick isn't a stream of keystrokes. The DOM and any client-reactive bindings still update on every keystroke either way; only the server sync waits. (The value is never lost: your local state is current, and any action flushes the full snapshot.)
301
+
302
+ 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`:
303
+
304
+ ```tsx
305
+ <input type="number" value={this.age} number /> {/* the bound value is a real number, not "42" */}
306
+ <input value={this.name} trim /> {/* whitespace stripped before it syncs */}
307
+ ```
308
+
309
+ 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:
310
+
311
+ ```tsx
312
+ <textarea value={this.body} draft="post-body" /> {/* survives a refresh until you submit */}
313
+ ```
314
+
315
+ And two focus helpers close the loop after a re-render, where focus is easily lost:
316
+
317
+ ```tsx
318
+ <input value={this.email} autoFocus /> {/* focus on mount (won't steal focus you've placed) */}
319
+ <input value={this.email} focusOnError /> {/* after a failed submit, focus jumps to the first invalid field */}
320
+ ```
321
+
322
+ `focusOnError` is the WCAG "send focus to the first error" behaviour, and it won't yank focus out of a field you're actively editing.
323
+
324
+ ### Validation messages
325
+
326
+ 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:
327
+
328
+ ```tsx
329
+ <input value={this.email} />
330
+ <span error={this.errors.email} class="text-red-500" />
331
+ ```
332
+
333
+ No `errors.has(...)` checks, no manual show/hide.
334
+
335
+ ### Reactive classes and attributes
336
+
337
+ 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:
338
+
339
+ ```tsx
340
+ <span className={this.count > 10 ? "text-emerald-400" : "text-white"}>
341
+ {this.count}
342
+ </span>
343
+
344
+ <button className={this.active ? "btn-primary" : "btn-secondary"}>
345
+ {this.active ? "Active" : "Inactive"}
346
+ </button>
347
+ ```
348
+
349
+ ### Control flow
350
+
351
+ Use normal TypeScript control flow in `render()` — `.map()`, ternaries, and `&&` are all supported:
352
+
353
+ ```tsx
354
+ override async render() {
355
+ return (
356
+ <div>
357
+ {this.todos.length === 0 ? (
358
+ <p>Nothing yet.</p>
359
+ ) : (
360
+ <ul>
361
+ {this.todos.map((t) => (
362
+ <li key={String(t.id)}>{t.title}</li>
363
+ ))}
364
+ </ul>
365
+ )}
366
+
367
+ {this.isAdmin && (
368
+ <button onClick={this.purge}>Purge all</button>
369
+ )}
370
+ </div>
371
+ );
372
+ }
373
+ ```
374
+
375
+ Always provide a `key` when mapping over items — the morph algorithm uses it to match DOM nodes and avoid re-creating elements unnecessarily.
376
+
377
+ ### Loading, confirm, polling, and show/hide
378
+
379
+ Common interaction states are first-class props:
380
+
381
+ ```tsx
382
+ {/* Disable the button while the action is in flight */}
383
+ <button onClick={this.save} loadingAttr="disabled">Save</button>
384
+
385
+ {/* Show a spinner while any action runs */}
386
+ <div showOnLoading class="spinner" />
387
+
388
+ {/* Show content when no action is in flight */}
389
+ <div hideOnLoading>Ready</div>
390
+
391
+ {/* Ask for confirmation before calling the action */}
392
+ <button onClick={this.delete} confirm="Delete this permanently?">Delete</button>
393
+
394
+ {/* SPA-style navigation — layout stays mounted */}
395
+ <a href="/dashboard" navigate>Dashboard</a>
396
+
397
+ {/* Prefetch the page on hover (after ~60ms) */}
398
+ <a href="/posts" navigate hover>Posts</a>
399
+
400
+ {/* Poll a server action on an interval */}
401
+ <div poll={{ every: "5s", action: this.tick }} />
402
+
403
+ {/* Reactive show/hide off a boolean @expose prop */}
404
+ <button onClick={() => (this.showModal = true)}>Open</button>
405
+ <div show={this.showModal} class="modal">…</div>
406
+ ```
407
+
408
+ `show={this.showModal}` toggles visibility reactively off a boolean `@expose`/`@locked` property — flip it from a client expression (instant) or a server action, and the element shows/hides with no manual `style` juggling.
409
+
410
+ 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):
411
+
412
+ ```tsx
413
+ <div show={this.modal} transition class="modal">…</div> {/* default: fade */}
414
+ <div show={this.menu} transition="scale">…</div> {/* preset */}
415
+ <aside show={this.drawer} transition="slide-right">…</aside> {/* directional */}
416
+ ```
417
+
418
+ Presets: `fade` (default), `scale`, `slide-up`, `slide-down`, `slide-left`, `slide-right`. The animation runs entirely on the client (no round-trip), the first paint snaps to the final state without animating, and `prefers-reduced-motion` is honoured automatically.
419
+
420
+ ### Accessible validation, wired for free
421
+
422
+ You don't wire ARIA by hand. When an input is bound with `value={this.email}` and its message rendered with `<span error={this.errors.email} />`, the runtime links them for you — the input gets `aria-describedby` pointing at the message region and `aria-invalid` while the field is invalid, so a screen-reader user hears exactly what a sighted user sees. The ids are stable across morphs, so the relationship never breaks on a re-render. Nothing to add to your markup beyond the `value=` / `error=` props you're already writing.
423
+
424
+ ### Raw Alpine escape hatch
425
+
426
+ The underlying client state is exposed to Alpine as the `$flow` magic, so any raw Alpine attribute works when you need it:
427
+
428
+ ```tsx
429
+ <span x-text="$flow.count > 10 ? 'High' : 'Low'" />
430
+ ```
431
+
432
+ The `flow:*` directives (`flow:click`, `flow:model`, `flow:show`, …) that the compiler emits are also accepted in plain HTML for cases where you're generating markup outside the JSX compiler.
433
+
434
+ ### Client magics live on $flow, not the class
435
+
436
+ 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.
437
+
438
+ ```tsx
439
+ <button onClick={() => $flow.set("open", true)}>Open</button> {/* write + sync an @expose prop */}
440
+ <button onClick={() => $flow.toggle("open")}>Toggle</button>
441
+ <button onClick={() => $flow.parent.save()}>Save</button> {/* call a parent action */}
442
+ <button onClick={() => $flow.cancel()}>Cancel</button> {/* stop a running @task */}
443
+ ```
444
+
445
+ The full set: `$flow.set` / `$flow.get` / `$flow.toggle` / `$flow.call` / `$flow.commit` / `$flow.refresh`, `$flow.dispatch` / `$flow.dispatchTo` / `$flow.dispatchSelf` / `$flow.on`, `$flow.watch`, `$flow.parent`, `$flow.store`, `$flow.whisper` / `$flow.onWhisper`, `$flow.cancel`, and `$flow.appendOptimistic` / `$flow.removeOptimistic`. `$flow` is typed globally, so they all autocomplete with no import.
446
+
447
+ Because they sit on `$flow` and not on `this`, a name like `set` or `on` is free to be _your_ `@expose` method — if you define `set()`, then `this.set()` calls yours, while `$flow.set()` is still the framework helper. (`this.refresh()`, `this.dispatch()`, and the other real Component methods keep working directly on `this` in both server and client code.)
448
+
449
+ ### The global client store
450
+
451
+ Most state in Flow belongs to one component — `@expose` and `@locked` put it in that component's snapshot, and the server stays authoritative. But some UI state is genuinely app-wide and the server has no stake in it: whether dark mode is on, whether the sidebar is open, whether a command palette is showing. Routing that through the server would mean a WebSocket round-trip every time a user flips a switch, for a value the server never needs to see.
452
+
453
+ `$flow.store` is for exactly that — a single client-only object, shared across every component on the page, that you read and write in JSX client expressions:
454
+
455
+ ```tsx
456
+ import { Component } from "@zerotal/flow";
457
+
458
+ export class Header extends Component {
459
+ override async render() {
460
+ return (
461
+ <header class={$flow.store.ui.dark ? "bg-black text-white" : "bg-white text-black"}>
462
+ <button onClick={() => ($flow.store.ui.dark = !$flow.store.ui.dark)}>
463
+ {$flow.store.ui.dark ? "Light" : "Dark"} mode
464
+ </button>
465
+ <aside show={$flow.store.ui.sidebar}>…</aside>
466
+ </header>
467
+ );
468
+ }
469
+ }
470
+ ```
471
+
472
+ Every one of those `$flow.store.*` reads updates **instantly, with no server round-trip** — the compiler turns them into native Alpine bindings backed by client-side reactivity. A write from a client handler (the `onClick` above) mutates the shared object, and every component reading that value re-renders at once. Because it lives entirely in the browser, it also keeps working while the WebSocket is down.
473
+
474
+ Declare the store's initial shape once, at app start, in `resources/js/app.ts`. Import `defineStore` from the browser-safe `@zerotal/flow/store` subpath (not the package barrel, which pulls server code into the browser bundle):
475
+
476
+ ```ts
477
+ import { defineStore } from "@zerotal/flow/store";
478
+
479
+ defineStore({ ui: { dark: false, sidebar: true } });
480
+ ```
481
+
482
+ And type it by augmenting the `FlowStore` interface (from the `/store` subpath, where it's declared), which makes every `$flow.store.*` access checked and autocompleted:
483
+
484
+ ```ts
485
+ declare module "@zerotal/flow/store" {
486
+ interface FlowStore {
487
+ ui: { dark: boolean; sidebar: boolean };
488
+ }
489
+ }
490
+ ```
491
+
492
+ **Which to use.** Reach for `$flow.store` when the state is live UI shared _across_ components and the server doesn't need it. Reach for `@session` when a preference must survive a page refresh — it's persisted server-side, and the common pattern is both: the store drives the instant UI, and a single deferred action syncs the settled value to `@session`. Reach for `@expose` when one component owns the state and the server may reconcile it. The rule of thumb: if getting the value wrong could only ever be a cosmetic glitch — never a wrong answer or a security decision — it's a good fit for the store.
493
+
494
+ The store is client-only by design: use it inside JSX client expressions (handlers, attribute bindings, text children), where the compiler routes it to the client global. It can't switch between JSX subtrees (`{$flow.store.x ? <A/> : <B/>}`); drive the DOM from it with a class, `show`, or text binding instead.
495
+
496
+ A page that _reads_ `$flow.store` in a binding must be AOT-compilable — the compiler turns those reads into client bindings, but the runtime fallback (used when a page can't be statically compiled) would try to evaluate `$flow` on the server, where it doesn't exist. In practice this means a page with a reactive `$flow.store` read should avoid the patterns that force the runtime path: an imported child component in `render()` (`<Header/>`), a `class={someLocalConst}`, or a numeric-literal attribute (`rows={3}` → use `rows="3"`). Writing `$flow.store` in a handler (`onClick={() => …}`) is always fine.
497
+
498
+ ### Full page example
499
+
500
+ ```tsx
501
+ import { Component, expose, locked, validate, url } from "@zerotal/flow";
502
+ import type { Post } from "#app/models/Post.ts";
503
+
504
+ export class PostsPage extends Component {
505
+ @url page: number = 1;
506
+ @url search: string = "";
507
+ @locked posts: Post[] = [];
508
+ @locked total: number = 0;
509
+
510
+ override async onMount() {
511
+ await this.loadPosts();
512
+ }
513
+
514
+ @expose async loadPosts() {
515
+ const q = Post.query()
516
+ .when(this.search, (q) => q.whereLike("title", `%${this.search}%`))
517
+ .orderBy("created_at", "desc");
518
+
519
+ const page = await q.paginate(15);
520
+ this.posts = page.data;
521
+ this.total = page.total;
522
+ }
523
+
524
+ override async render() {
525
+ return (
526
+ <div>
527
+ <input value={this.search} live placeholder="Search posts…" />
528
+ <ul>
529
+ {this.posts.map((post) => (
530
+ <li key={String(post.id)}>{post.title}</li>
531
+ ))}
532
+ </ul>
533
+ <p>
534
+ {this.total} total — page {this.page}
535
+ </p>
536
+ </div>
537
+ );
538
+ }
539
+ }
540
+ ```
541
+
542
+ ## The rest of the guide
543
+
544
+ Flow is a large surface. Each section below is its own page.
545
+
546
+ | Page | What it covers |
547
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
548
+ | [Routing](/docs/flow/routing) | Map URLs to Flow pages — file-based routes, route parameters, and navigation. |
549
+ | [Decorators](/docs/flow/decorators) | The decorators that expose state and actions to the client: @expose, @locked, @computed, and friends. |
550
+ | [Lifecycle Hooks](/docs/flow/lifecycle) | Where to run code as a component mounts, updates, and tears down. |
551
+ | [Events & Broadcasting](/docs/flow/events) | Component events, browser events, and multiplayer state shared over WebSockets. |
552
+ | [Forms, Validation & Uploads](/docs/flow/forms) | Two-way bound inputs, real-time validation, and server-handled file uploads. |
553
+ | [Pagination](/docs/flow/pagination) | The Pagination mixin, URL-synced pages, and named paginators. |
554
+ | [Built-in Components](/docs/flow/components) | The component library that ships with Flow — forms, overlays, tables, and feedback. |
555
+ | [Layouts & Composition](/docs/flow/layouts) | Wrap pages in layouts, compose behaviour with mixins, nest components, and pass markup between them. |
556
+ | [Transport & Performance](/docs/flow/performance) | How updates reach the browser, what to do on hostile networks, and the polish that hides latency. |
557
+ | [Testing](/docs/flow/testing) | Drive a component in-process, assert on its state, and test the rendered markup. |
558
+ | [Reference](/docs/flow/references) | Every decorator, prop, directive, and client global in one table. |
559
+
560
+ ## Next steps
561
+
562
+ - [Routing](/docs/routing) — how `Router.flow()`, groups, and middleware fit into the wider router.
563
+ - [Validator](/docs/validator) — the full rule chain behind `@validate` and `this.validate()`.
564
+ - [Middleware](/docs/middleware) — write the guards you attach to Flow routes.
565
+ - [Session](/docs/session) — the store behind `@session` and `SessionMiddleware`.
566
+ - [Broadcasting](/docs/broadcasting) — drive `@on("echo:…")` real-time updates from the server.
567
+ - [Storage](/docs/storage) — configure the disks that file uploads write to.
568
+ - [Testing](/docs/testing/index) — patterns for the `FlowTest` harness and the rest of the suite.