@zerotal/arch 1.7.0 → 1.7.3

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.
@@ -87,6 +87,7 @@ Visual loading indicators (`showOnLoading`, `hideOnLoading`, `loadingClass`) wai
87
87
  | ----------------- | --------------------------------------------------------- | ----------------------------------- |
88
88
  | `navigate` | SPA navigation to `href`, layout stays mounted | `flow:navigate` |
89
89
  | `navigate hover` | Prefetch page on hover (~60ms debounce) | `flow:navigate flow:navigate.hover` |
90
+ | `navigate down` | Prefetch page on pointer-down (no dwell; dense lists) | `flow:navigate flow:navigate.down` |
90
91
  | `current={false}` | Disable automatic `data-current` attribute | — |
91
92
  | `exact` | `data-current` only on an exact URL match (not sub-pages) | `flow:current.exact` |
92
93
 
@@ -177,12 +178,26 @@ override async render() {
177
178
 
178
179
  > A dynamic `sortItem={String(it.id)}` inside a `.map()` renders through the standard runtime (not the AOT fast path) — the drag behaviour is identical either way.
179
180
 
181
+ > **The payload does not say which container took the drop.** The client reads `flow:sort` off the
182
+ > container a child was dropped **into** and calls it `(key, index)` — so the destination is
183
+ > encoded in _which method runs_, and nowhere else. For a single sortable list that is invisible.
184
+ > For dragging **between** containers under one `sortGroup` it means one action per container:
185
+ >
186
+ > ```tsx
187
+ > <ul onSort={this.dropInTodo} sortGroup="tasks">…</ul>
188
+ > <ul onSort={this.dropInDone} sortGroup="tasks">…</ul>
189
+ > ```
190
+ >
191
+ > An arrow (`onSort={(k, i) => this.move("todo", k, i)}`) cannot stand in, because the attribute's
192
+ > value is used as a method _name_ rather than evaluated. `onSort` accepts the name as a string,
193
+ > so the handlers can come from a lookup table keyed by column, but they must be declared members.
194
+
180
195
  ### DOM utilities
181
196
 
182
197
  | You write | Behaviour | Compiles to |
183
198
  | ----------------- | ------------------------------------------------------------ | --------------- |
184
199
  | `teleport="body"` | Move the element to a CSS selector target (modals, tooltips) | `flow:teleport` |
185
- | `ref="name"` | Name this element as `$refs.name` for `this.client()` calls | `x-ref` |
200
+ | `ref="name"` | Name this element as `$refs.name` for ``this.$`…` `` scripts | `x-ref` |
186
201
 
187
202
  ### Alpine plugins
188
203
 
@@ -426,15 +426,27 @@ export class DashboardPage extends Component {
426
426
  }
427
427
  ```
428
428
 
429
- For per-action dynamic titles, use `this.title()` inside an action:
429
+ ### The page title
430
+
431
+ `static title` takes a string, or a function of the component:
432
+
433
+ ```typescript
434
+ static title = "Posts";
435
+ static title = (c: PostPage) => `${c.post?.title ?? "Loading"} — My App`;
436
+ ```
437
+
438
+ The function form is resolved on the server for every render and every patch, so a title
439
+ that depends on state follows it without an action doing anything:
430
440
 
431
441
  ```typescript
432
442
  @expose async loadPost(slug: string): Promise<void> {
433
443
  this.post = await Post.where("slug", slug).firstOrFail();
434
- this.title(`${this.post.title}My App`);
444
+ // the title updates with it nothing else to call
435
445
  }
436
446
  ```
437
447
 
448
+ Only the resolved string is sent to the browser; the function stays on the server.
449
+
438
450
  For per-render `<head>` content (meta tags, OG tags), use `<Head>` inside `render()`:
439
451
 
440
452
  ```tsx
@@ -177,6 +177,38 @@ expect(page.user?.email).toBe("alice@example.com");
177
177
  expect(page.totalRevenue).toBe(450.0);
178
178
  ```
179
179
 
180
+ ## No request scope
181
+
182
+ `FlowTest` drives the server-side pipeline but does **not** open a request context. There is no
183
+ `RequestContext.run` inside it, so anything reaching for the request throws rather than
184
+ returning empty:
185
+
186
+ - `Auth.user()` → `E_UNAUTHORIZED`
187
+ - `Auth.attempt()` / `Auth.login()` → `E_CONTEXT_OUTSIDE_REQUEST`
188
+ - request-scoped pagination, and any facade that reads `RequestContext`
189
+
190
+ That covers most actions on any page behind a sign-in, so open the scope yourself. `HttpContext.fake()`
191
+ rather than an object literal cast to the type — it carries a real `Request`, which matters as soon
192
+ as anything downstream reads a header (an audited model records the actor's IP, for one):
193
+
194
+ ```typescript
195
+ import { RequestContext, HttpContext } from "@zerotal/core";
196
+
197
+ function asUser<T>(user: User | null, fn: () => Promise<T>): Promise<T> {
198
+ const ctx = HttpContext.fake("http://localhost/");
199
+ if (user) ctx.user = user;
200
+ return RequestContext.run(ctx, fn);
201
+ }
202
+
203
+ await asUser(alice, async () => {
204
+ const t = await FlowTest.mount(IssuePage, { issue });
205
+ await t.call("postComment");
206
+ });
207
+ ```
208
+
209
+ `app.actingAs()` does not help here: it encodes a session cookie for `app.get()`, and `FlowTest`
210
+ never makes a request to send it on.
211
+
180
212
  ## Testing with a database
181
213
 
182
214
  `FlowTest` does not set up or tear down a database — use your test suite's standard database helpers. With Bun, wrap tests in a transaction that rolls back after each test for full isolation:
@@ -11,7 +11,7 @@ provider.
11
11
 
12
12
  ## Requirements
13
13
 
14
- - **Bun** ≥ 1.1 — [install](https://bun.sh/docs/installation)
14
+ - **Bun** ≥ 1.3.14 — [install](https://bun.sh/docs/installation)
15
15
  - A PostgreSQL, MySQL, or SQLite database (SQLite requires nothing extra)
16
16
 
17
17
  ## Create a new project
@@ -36,6 +36,31 @@ It writes a fresh `APP_KEY` into the generated `.env.example` for you (no manual
36
36
  generation needed). For Postgres/MySQL it reminds you to set `DATABASE_URL`
37
37
  before migrating.
38
38
 
39
+ ### Without a terminal — CI, scripts, agents
40
+
41
+ Every prompt has a flag, and the scaffolder never asks a question when there is
42
+ no TTY to answer it:
43
+
44
+ ```bash
45
+ bunx create-zerotal my-app --template=api --db=postgres --no-install
46
+ bunx create-zerotal my-app --yes # take the defaults for anything unset
47
+ bunx create-zerotal --help
48
+ ```
49
+
50
+ | Flag | |
51
+ | ------------------------- | -------------------------------------------------- |
52
+ | `-t`, `--template <name>` | `api`, `admin`, `flow`, `react`, `vue`, `minimal` |
53
+ | `--db <name>` | `sqlite`, `postgres`, `mysql` — API template only |
54
+ | `-y`, `--yes` | Take defaults for anything not given; never prompt |
55
+ | `--no-install` | Skip `bun install` |
56
+ | `-h`, `--help` | Usage |
57
+ | `-v`, `--version` | The scaffolder's own version |
58
+
59
+ An answer that is missing and cannot be asked for is an error naming the flag
60
+ that would supply it, and the exit code is non-zero — so a pipeline fails where
61
+ it used to wait. A failed `bun install` also exits non-zero when there is no
62
+ terminal: a half-built project that reports success is worse than one that stops.
63
+
39
64
  ### Which template should I use?
40
65
 
41
66
  - **API** — JSON REST API with core, [ORM](/docs/orm), [auth](/docs/authentication),