@zerotal/arch 1.7.3 → 1.7.5

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 (127) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +4 -4
  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 +165 -1
  32. package/docs/client/index.md +277 -70
  33. package/docs/commands.md +4 -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 +46 -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 +2 -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 +199 -0
  55. package/docs/flow/index.md +20 -20
  56. package/docs/flow/layouts.md +35 -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 +12 -7
  70. package/docs/inertia/props.md +22 -22
  71. package/docs/inertia/rendering.md +86 -7
  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 +36 -8
  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 +3 -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/index.ts +25 -1
  120. package/src/tools/searchDocs.ts +7 -0
  121. package/docs/client/auth.md +0 -113
  122. package/docs/client/errors.md +0 -139
  123. package/docs/client/files.md +0 -118
  124. package/docs/client/references.md +0 -58
  125. package/docs/client/requests.md +0 -131
  126. package/docs/client/resilience.md +0 -141
  127. package/docs/client/testing.md +0 -146
package/CHANGELOG.md CHANGED
@@ -4,11 +4,43 @@ All notable changes to this package are documented here. The format is
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- **Maturity: beta.** The API is close to final and breaking changes are rare, called out
8
- here with migration steps but a minor release may still contain one.
7
+ **Maturity: `stable`** matching this package's `maturity` field. The public API
8
+ follows SemVer strictly: anything importable without an `@internal` marker keeps its
9
+ shape for the rest of the 1.x line, and `api-surface.md` is diffed by CI on every
10
+ change. That promise covers the MCP tool contract too — the tool names, their inputs
11
+ and the shape of what they return — which is the part an agent client is configured
12
+ against. `mcp-surface.md` is diffed alongside it.
9
13
 
10
14
  ## [Unreleased]
11
15
 
16
+ ## [1.7.5] — 2026-08-22
17
+
18
+ ### Changed
19
+
20
+ - **`@zerotal/arch` is `stable`.** Reviewed ahead of its 1.9.0 date. The public API
21
+ follows SemVer strictly from here, and that promise covers the MCP tool contract —
22
+ tool names, their inputs, and the shape of what they return — because that is what
23
+ an agent client is configured against and nothing type-level can see it. The
24
+ protocol revision the server speaks is not covered; it follows the protocol.
25
+
26
+ - **INTERNAL — the writers behind `arch:install` are no longer public API.** `detectAgents`,
27
+ `applyMcpConfig`, `serverEntry`, `SERVER_ENTRY_PATH`, `applyBlock`, `fence`,
28
+ `BLOCK_START`, `BLOCK_END`, `agentsPreamble`, `buildGuidelines`, `claudeShim` and
29
+ their types. They are still exported and still work; they are no longer promised.
30
+ Their only caller is `ArchInstallCommand`, and freezing them would have committed
31
+ the shape of `.mcp.json` writing and marker fencing to the rest of the 1.x line on
32
+ behalf of a caller who never arrived. Marked before the label attached rather than
33
+ withdrawn after.
34
+
35
+ - **INTERNAL — `api-surface.md` now honours `@internal` across every package.** The
36
+ contract has always been stated as "anything importable without an `@internal`
37
+ marker keeps its shape", and the generator did not read the tag: symbols already
38
+ marked internal were recorded as though promised. They are omitted now — 374
39
+ entries across 13 packages, every one verified marked, either at its declaration
40
+ or by a module docblock covering a whole subpath as `@zerotal/core/dev` does.
41
+ Nothing changes at runtime or in the types. What changed is that the file listing
42
+ the promises lists the promises.
43
+
12
44
  ## [1.7.1] — 2026-08-16
13
45
 
14
46
  ### Fixed
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # @zerotal/arch
2
2
 
3
- **Maturity: beta.** The API is close to final and breaking changes are rare, called out
4
- in release notes with migration steps but a minor release may still contain one. See
3
+ **Maturity: stable.** The public API follows SemVer strictly: anything importable
4
+ without an `@internal` marker keeps its shape for the rest of the 1.x line. That
5
+ covers the MCP tool contract too — tool names, their inputs, and the shape of what
6
+ they return — since that is what an agent client is configured against. See
5
7
  the [support policy](https://zerotal.dev/docs/support-policy#maturity-levels).
6
8
 
7
9
  The Zerotal agent surface: an MCP server that hands a coding agent the framework's
package/api-surface.md CHANGED
@@ -185,11 +185,11 @@ interface ToolContext = {
185
185
  root: string
186
186
  }
187
187
 
188
- type BlockOutcome = { status: 'created'; text: string;} | { status: 'updated'; text: string;} | { status: 'unchanged'; text: string;} | { status: 'conflict'; reason: string;}
188
+ type BlockOutcome = { status: 'created'; text: string } | { status: 'updated'; text: string } | { status: 'unchanged'; text: string } | { status: 'conflict'; reason: string }
189
189
 
190
- type ConfigOutcome = { status: 'created' | 'updated' | 'unchanged'; text: string;} | { status: 'conflict'; reason: string;}
190
+ type ConfigOutcome = { status: 'created' | 'updated' | 'unchanged'; text: string } | { status: 'conflict'; reason: string }
191
191
 
192
- type ProbeResult = { ok: true; data: unknown;} | { ok: false; message: string;}
192
+ type ProbeResult = { ok: true; data: unknown } | { ok: false; message: string }
193
193
 
194
194
  type ProbeTopic = 'doctor' | 'routes' | 'schema' | 'app-info'
195
195
 
@@ -272,7 +272,7 @@ interface ToolOutcome = {
272
272
  text: string
273
273
  }
274
274
 
275
- type DecodedFrame = { ok: true; request: JsonRpcRequest;} | { ok: false; failure: JsonRpcFailure;}
275
+ type DecodedFrame = { ok: true; request: JsonRpcRequest } | { ok: false; failure: JsonRpcFailure }
276
276
 
277
277
  type Era = 'modern' | 'legacy'
278
278
 
package/docs/about.md CHANGED
@@ -131,7 +131,7 @@ my-app/
131
131
  `zt.ts` is the entry point for **everything** — `bun zt serve`, `migrate`, `test`,
132
132
  and every `make:*` generator run through it. It boots `bootstrap/app.ts`:
133
133
 
134
- ```ts
134
+ ```ts fragment
135
135
  // bootstrap/app.ts
136
136
  import { Application } from "zerotal";
137
137
  import { Handler } from "../app/exceptions/Handler.ts";
@@ -166,7 +166,7 @@ Read [Request Lifecycle](/docs/lifecycle) for the exact sequence.
166
166
  Routes are registered by calling static `Router` methods at module load — map a path
167
167
  to a controller + action, or to an inline closure:
168
168
 
169
- ```ts
169
+ ```ts fragment
170
170
  // routes/index.ts
171
171
  import { Router, view, type HttpContext } from "zerotal";
172
172
  import HomeController from "../app/controllers/HomeController.ts";
@@ -200,7 +200,7 @@ the app builder. Full details in [Routing](/docs/routing).
200
200
  Controllers are plain classes; the action receives the request `HttpContext` and
201
201
  dependencies resolve from the container:
202
202
 
203
- ```ts
203
+ ```ts fragment
204
204
  // app/controllers/PostController.ts
205
205
  import type { HttpContext } from "zerotal";
206
206
  import Post from "../models/Post.ts";
@@ -233,7 +233,7 @@ e.g. `ctx: HttpContext<{ post: Post }>`. More in [Controllers](/docs/controllers
233
233
  Active Record–style models backed by `Bun.sql`. Columns and relationships are
234
234
  decorators; the table name is derived by convention (so `@table` is optional):
235
235
 
236
- ```ts
236
+ ```ts fragment
237
237
  // app/models/Post.ts
238
238
  import { Model, table, column, hasMany, belongsTo } from "@zerotal/orm";
239
239
 
@@ -255,7 +255,7 @@ export default class Post extends Model {
255
255
 
256
256
  The fluent query builder and relationship loading:
257
257
 
258
- ```ts
258
+ ```ts fragment
259
259
  // in a controller or service
260
260
  const posts = await Post.query()
261
261
  .withScopes((s) => s.published())
@@ -321,7 +321,7 @@ export class StorePostRequest extends FormRequest {
321
321
  }
322
322
  ```
323
323
 
324
- ```ts
324
+ ```ts fragment
325
325
  // in a controller — reads the current HttpContext, returns typed data,
326
326
  // throws a 422 (JSON) or a redirect-back on failure.
327
327
  const data = await StorePostRequest.validate();
@@ -333,7 +333,7 @@ More rules and patterns in [Validation](/docs/validator).
333
333
 
334
334
  Session auth, bearer tokens, and policy-based authorization ship together:
335
335
 
336
- ```ts
336
+ ```ts fragment
337
337
  // in a controller / service
338
338
  import { Auth, Gate, createToken } from "@zerotal/auth";
339
339
 
@@ -415,7 +415,7 @@ decorator and directive set.
415
415
  Build a React or Vue SPA with no separate API layer. Controllers return page
416
416
  responses; the Inertia client renders the matching component:
417
417
 
418
- ```ts
418
+ ```ts fragment
419
419
  // app/controllers/DashboardController.ts
420
420
  import { inertia } from "@zerotal/inertia";
421
421
  import { Post } from "../models/Post.ts";
@@ -428,7 +428,7 @@ export class DashboardController {
428
428
  }
429
429
  ```
430
430
 
431
- ```tsx
431
+ ```tsx fragment
432
432
  // resources/js/pages/Dashboard.tsx (React)
433
433
  import { Link } from "@inertiajs/react";
434
434
 
@@ -457,7 +457,7 @@ reloads, deferred props, SSR, and precognition. See [Inertia](/docs/inertia).
457
457
  Push slow work off the request. Jobs serialize to a plain payload so they survive the
458
458
  queue:
459
459
 
460
- ```ts
460
+ ```ts fragment
461
461
  // app/jobs/SendWelcomeEmail.ts
462
462
  import { Job, JobRegistry } from "@zerotal/queue";
463
463
  import { User } from "../models/User.ts";
@@ -487,7 +487,7 @@ export class SendWelcomeEmail extends Job {
487
487
  JobRegistry.register(SendWelcomeEmail);
488
488
  ```
489
489
 
490
- ```ts
490
+ ```ts fragment
491
491
  // dispatch from anywhere
492
492
  import { Queue, Bus } from "@zerotal/queue";
493
493
 
@@ -504,7 +504,7 @@ Run a worker with `bun zt queue:work`. There's also a
504
504
 
505
505
  ## Cache
506
506
 
507
- ```ts
507
+ ```ts fragment
508
508
  // in a controller or service
509
509
  import { Cache } from "@zerotal/cache";
510
510
 
@@ -512,7 +512,7 @@ const posts = await Cache.remember("posts.recent", 60, () =>
512
512
  Post.query().orderBy("created_at", "desc").limit(10).get(),
513
513
  );
514
514
 
515
- await Cache.put("key", value, 300);
515
+ await Cache.set("key", value, 300);
516
516
  await Cache.forget("key");
517
517
 
518
518
  // Tag-based invalidation
@@ -527,7 +527,7 @@ In-memory and Redis drivers, plus idempotency helpers. See [Cache](/docs/cache).
527
527
  One `Notification` class fans out across channels — mail, database, broadcast, Slack,
528
528
  SMS:
529
529
 
530
- ```ts
530
+ ```ts fragment
531
531
  // app/notifications/InvoicePaid.ts
532
532
  import { Notification, MailMessage } from "@zerotal/notifications";
533
533
 
@@ -548,7 +548,7 @@ export class InvoicePaid extends Notification {
548
548
  }
549
549
  ```
550
550
 
551
- ```ts
551
+ ```ts fragment
552
552
  // The User model mixes in Notifiable, which provides .notify()
553
553
  await user.notify(new InvoicePaid(invoice));
554
554
  ```
@@ -584,7 +584,7 @@ Generate or rotate the key with `bun zt key:generate`. Full system in
584
584
 
585
585
  First-class HTTP, database, and fake helpers:
586
586
 
587
- ```ts
587
+ ```ts fragment
588
588
  // tests/posts.test.ts
589
589
  import { createTestApp, Factory, assertDatabaseHas } from "@zerotal/testing";
590
590
  import { NotificationFake } from "@zerotal/notifications";
@@ -9,7 +9,7 @@ Actions power the row, header, and bulk buttons. The defaults are
9
9
  `viewAction()/editAction()/deleteAction()` (rows), `createAction()` (header), and
10
10
  `bulkDeleteAction()` (bulk) — override the corresponding methods to customize.
11
11
 
12
- ```ts
12
+ ```ts fragment
13
13
  import { action, editAction, deleteAction, textInput } from "@zerotal/admin";
14
14
 
15
15
  static recordActions() {
@@ -28,7 +28,7 @@ static recordActions() {
28
28
  A row with more than three visible actions collapses the surplus into an overflow
29
29
  menu. Actions can also open a **modal form**:
30
30
 
31
- ```ts
31
+ ```ts fragment
32
32
  action("assign")
33
33
  .label("Assign reviewer")
34
34
  .form([ select("reviewer_id").options(reviewers).required() ])
@@ -43,7 +43,7 @@ action("assign")
43
43
  A row with seven buttons is unreadable. `actionGroup` collapses several into one
44
44
  labelled dropdown, so the row keeps the two people reach for and files the rest:
45
45
 
46
- ```ts
46
+ ```ts fragment
47
47
  import { actionGroup, replicateAction, deleteAction } from "@zerotal/admin";
48
48
 
49
49
  static recordActions() {
@@ -67,7 +67,7 @@ nothing. Groups work in the header and the bulk toolbar too.
67
67
  `replicateAction()` copies a record and opens the copy for editing. The primary key
68
68
  and timestamps are always dropped; name anything else that must stay unique:
69
69
 
70
- ```ts
70
+ ```ts fragment
71
71
  replicateAction()
72
72
  .excludeAttributes(["sku", "slug"])
73
73
  .beforeReplicaSaved((data) => ({ ...data, name: `${data.name} (copy)`, status: "draft" })),
@@ -75,7 +75,7 @@ replicateAction()
75
75
 
76
76
  ## Import and export
77
77
 
78
- ```ts
78
+ ```ts fragment
79
79
  import { exportAction, importAction, bulkExportAction } from "@zerotal/admin";
80
80
 
81
81
  static headerActions() {
@@ -94,7 +94,7 @@ exports the selection instead.
94
94
 
95
95
  Pass `"xlsx"` for a spreadsheet instead:
96
96
 
97
- ```ts
97
+ ```ts fragment
98
98
  return [createAction(), exportAction(), exportAction("xlsx")];
99
99
  ```
100
100
 
@@ -107,7 +107,7 @@ header row is frozen with a filter over the used range.
107
107
  Keep a column out of the file with `.exportable(false)`. Pay, internal notes and
108
108
  anything else that should not travel in a spreadsheet belongs behind that flag:
109
109
 
110
- ```ts
110
+ ```ts fragment
111
111
  text("salary").exportable(false),
112
112
  ```
113
113
 
@@ -129,7 +129,7 @@ An import runs inline by default, capped at 2,000 rows — a synchronous import
129
129
  holds a WebSocket round-trip open, and a bigger file looks like a hang. Hand it
130
130
  to a queue instead and the cap lifts:
131
131
 
132
- ```ts
132
+ ```ts fragment
133
133
  static headerActions() {
134
134
  return [createAction(), exportAction(), importAction({ queue: true })];
135
135
  }
@@ -153,7 +153,7 @@ imported only when an import is actually queued.
153
153
 
154
154
  Relation managers appear as tables on the View page.
155
155
 
156
- ```ts
156
+ ```ts fragment
157
157
  import { hasMany, belongsToMany } from "@zerotal/admin";
158
158
 
159
159
  static relations() {
@@ -9,7 +9,7 @@ Opt in with `Panel.auth({...})` (or `Panel.configure({ auth: {...} })`). The pag
9
9
  live behind the `@zerotal/admin/auth` subpath, so the `@zerotal/auth` dependency
10
10
  stays optional unless you enable them.
11
11
 
12
- ```ts
12
+ ```ts fragment
13
13
  import { Panel } from "@zerotal/admin";
14
14
 
15
15
  Panel.auth({
@@ -52,7 +52,7 @@ left to your auth middleware — the rest of the flow ships here.
52
52
 
53
53
  The panel is public until you set guard middleware. A typical setup:
54
54
 
55
- ```ts
55
+ ```ts fragment
56
56
  Panel.configure({ middleware: [AuthMiddleware, RequireRoleMiddleware.with("admin")] });
57
57
  Panel.auth({ enabled: true }); // login lives outside that guard automatically
58
58
  ```
@@ -7,7 +7,7 @@ description: Widgets, global search, the command palette, notifications, and the
7
7
 
8
8
  Register widgets with `Panel.widgets(...)`; they render on the dashboard.
9
9
 
10
- ```ts
10
+ ```ts fragment
11
11
  import { Panel, statsWidget, stat, chartWidget, tableWidget } from "@zerotal/admin";
12
12
 
13
13
  Panel.widgets(
@@ -41,7 +41,7 @@ The dashboard answers "how is the business doing". A resource's own widgets
41
41
  answer "what is going on in _this_ list" — a pending count above the orders
42
42
  table, stock value above products:
43
43
 
44
- ```ts
44
+ ```ts fragment
45
45
  export class OrderResource extends Resource {
46
46
  static override widgets() {
47
47
  return [
@@ -62,7 +62,7 @@ someone watching a queue actually wants.
62
62
  A dashboard on a second screen is stale the moment it renders. `.poll()` gives a
63
63
  widget an interval:
64
64
 
65
- ```ts
65
+ ```ts fragment
66
66
  statsWidget(async () => [
67
67
  stat("Awaiting payment", await Order.query().where("status", "pending").count()),
68
68
  ]).poll("30s"),
@@ -85,7 +85,7 @@ page.
85
85
  The admin owns the bell + notifications page UI; your app supplies the data through a
86
86
  provider (the same split as relations — admin UI, app data):
87
87
 
88
- ```ts
88
+ ```ts fragment
89
89
  import { Panel } from "@zerotal/admin";
90
90
 
91
91
  Panel.notifications({
@@ -124,7 +124,7 @@ notifications" depends on your auth and your schema. When both are the ordinary
124
124
  ones — `@zerotal/auth` for the user, `@zerotal/notifications`' `DatabaseChannel`
125
125
  for storage — there is a ready-made one:
126
126
 
127
- ```ts
127
+ ```ts fragment
128
128
  import { databaseNotifications } from "@zerotal/admin";
129
129
 
130
130
  Panel.notifications(databaseNotifications());
@@ -23,7 +23,7 @@ everything else alone.
23
23
  **Table cells.** The column still owns its label, sorting, search and export;
24
24
  only the cell is yours:
25
25
 
26
- ```ts
26
+ ```ts fragment
27
27
  text("health")
28
28
  .label("Health")
29
29
  .sortable()
@@ -32,7 +32,7 @@ text("health")
32
32
 
33
33
  **Infolist entries.** The section's grid still places it:
34
34
 
35
- ```ts
35
+ ```ts fragment
36
36
  textEntry("route").render((value) => <RouteMap path={String(value)} />),
37
37
  ```
38
38
 
@@ -40,7 +40,7 @@ textEntry("route").render((value) => <RouteMap path={String(value)} />),
40
40
  saves like any other — you supply only the control. Bind your markup to
41
41
  `form.<key>` for the value to round-trip:
42
42
 
43
- ```ts
43
+ ```ts fragment
44
44
  customField("coordinates")
45
45
  .label("Location")
46
46
  .required()
@@ -55,7 +55,7 @@ what lets a control react to a sibling field.
55
55
  A contributed page can only add a page. A hook adds markup at a named position
56
56
  in the chrome — a trial banner, a compliance notice, an environment badge:
57
57
 
58
- ```ts
58
+ ```ts fragment
59
59
  Panel.renderHook("page.header.end", () => <TrialBanner />);
60
60
 
61
61
  // Conditional placement: register once, decide per render.
@@ -92,7 +92,7 @@ A resource does not have to be backed by a model. Return rows from `data()` and
92
92
  the panel filters, sorts and paginates them in memory, so search, tabs,
93
93
  summaries and the query builder all keep working:
94
94
 
95
- ```ts
95
+ ```ts fragment
96
96
  export class RegionResource extends Resource {
97
97
  static override async data() {
98
98
  return await fetch("https://api.example.com/regions").then((r) => r.json());
@@ -115,7 +115,7 @@ rows and not millions. Past that, back it with a model or a view.
115
115
 
116
116
  Four statics change how a list renders:
117
117
 
118
- ```ts
118
+ ```ts fragment
119
119
  export class ProductResource extends Resource {
120
120
  static override tableLayout = "grid"; // "table" (default) | "grid" | "kanban" | "calendar"
121
121
  static override striped = true;
@@ -131,7 +131,7 @@ title, and the next few render as label/value pairs. No second description.
131
131
 
132
132
  **Kanban** turns a status column into lanes, for records that read as a pipeline:
133
133
 
134
- ```ts
134
+ ```ts fragment
135
135
  static override tableLayout = "kanban";
136
136
  static override kanbanColumn = "status";
137
137
  static override kanbanLanes = { pending: "Pending", paid: "Paid", shipped: "Shipped" };
@@ -144,7 +144,7 @@ does, not a second source of truth. A value present in the data but missing from
144
144
 
145
145
  **Calendar** lays the page out as a month grid keyed on a date column:
146
146
 
147
- ```ts
147
+ ```ts fragment
148
148
  static override tableLayout = "calendar";
149
149
  static override calendarColumn = "startsOn";
150
150
  ```
@@ -159,7 +159,7 @@ on wide or long tables.
159
159
 
160
160
  A resource whose records nest under each other renders as a tree:
161
161
 
162
- ```ts
162
+ ```ts fragment
163
163
  export class CategoryResource extends Resource {
164
164
  static override treeParentColumn = "parentId";
165
165
  }
@@ -176,7 +176,7 @@ than disappearing, so a filtered tree never hides a record.
176
176
 
177
177
  A resource whose text exists in several languages edits one at a time:
178
178
 
179
- ```ts
179
+ ```ts fragment
180
180
  export class PostResource extends Resource {
181
181
  static override translatable = ["title", "excerpt"];
182
182
  static override locales = ["en", "fr"];
@@ -193,7 +193,7 @@ turning this on for an existing column does not blank it.
193
193
 
194
194
  A column can carry its own filter box in the table header:
195
195
 
196
- ```ts
196
+ ```ts fragment
197
197
  text("sku").filterable();
198
198
  selectColumn("status", STATUS).filterable();
199
199
  ```
@@ -209,7 +209,7 @@ the active-filter chips, and land in a saved view.
209
209
  Filters sit above the table by default. Once there are more than a few, collapse
210
210
  them:
211
211
 
212
- ```ts
212
+ ```ts fragment
213
213
  static override filterLayout = "panel"; // "inline" (default) | "panel" | "drawer"
214
214
  ```
215
215
 
@@ -55,11 +55,20 @@ That last row is the important one. A panel with no authorization wired stays
55
55
  closed in production, the same posture the panel guard already takes, which is
56
56
  what makes it safe for a package to add pages without the app asking.
57
57
 
58
- ```ts
58
+ ```ts fragment
59
59
  // config/admin.ts
60
- export default {
61
- authorize: (ability) => currentUser()?.permissions.includes(ability) ?? false,
62
- };
60
+ import { AdminConfig } from "@zerotal/admin";
61
+ import { Auth } from "@zerotal/auth";
62
+
63
+ // Through the factory, not a bare object: it is what types `ability`, and every
64
+ // other config in the framework is written this way.
65
+ export default AdminConfig({
66
+ // `userOrNull()`, not `user()` — this runs for guests too, and `user()` throws
67
+ // rather than returning one. `can()` comes from the permissions mixin on your
68
+ // own User model, which is why this block is a fragment: the type depends on
69
+ // your app, not on the framework.
70
+ authorize: (ability) => Auth.userOrNull()?.can(ability) ?? false,
71
+ });
63
72
  ```
64
73
 
65
74
  Resources are the exception: they authorize through their own
@@ -75,7 +84,7 @@ and without depending on `@zerotal/admin`.
75
84
 
76
85
  A contributing provider resolves the binding in its `onBooting` and pushes:
77
86
 
78
- ```ts
87
+ ```ts fragment
79
88
  // packages/queue/src/admin.ts
80
89
  interface AdminHost {
81
90
  enabled(id: string): boolean;
@@ -117,7 +126,7 @@ JSX, no `@zerotal/flow` dependency and no build configuration. The panel owns
117
126
  the markup, which also means every console looks like the rest of the admin
118
127
  without trying to.
119
128
 
120
- ```ts
129
+ ```ts fragment
121
130
  panel.console({
122
131
  slug: "jobs",
123
132
  title: "Jobs",
@@ -180,7 +189,7 @@ is read early enough.
180
189
  Application code can name the panel directly, so it doesn't need the container
181
190
  dance. Group a set of related contributions behind an `AdminPlugin`:
182
191
 
183
- ```ts
192
+ ```ts fragment
184
193
  await Panel.plugin({
185
194
  id: "billing",
186
195
  install: (panel) => {
@@ -8,7 +8,7 @@ description: Build create and edit forms, and lay out the read-only view screen.
8
8
  `form()` returns fields (and layout components). An empty `form()` disables
9
9
  Create/Edit for the resource.
10
10
 
11
- ```ts
11
+ ```ts fragment
12
12
  import {
13
13
  textInput, textarea, select, toggle, datePicker, fileUpload, richEditor,
14
14
  formSection, formTabs, formTab, wizard, wizardStep,
@@ -80,7 +80,7 @@ patch)` runs server-side and merges a patch into the form (e.g. derive a slug).
80
80
  `repeater` edits an array of objects sharing one sub-schema; `builder` edits an array
81
81
  of typed blocks. Rows can be added, removed, and reordered.
82
82
 
83
- ```ts
83
+ ```ts fragment
84
84
  import { repeater, builder, builderBlock, textInput, textarea, fileUpload } from "@zerotal/admin";
85
85
 
86
86
  repeater("contacts").minItems(1).addActionLabel("Add contact").schema([
@@ -101,7 +101,7 @@ Repeaters serialize to `[{ ...fields }]`; builders to `[{ type, data }]`.
101
101
  `infolist()` describes the read-only detail page. Omit it to fall back to a section
102
102
  derived from `columns()`.
103
103
 
104
- ```ts
104
+ ```ts fragment
105
105
  import { section, textEntry, iconEntry } from "@zerotal/admin";
106
106
 
107
107
  static infolist() {
@@ -136,7 +136,7 @@ usefully read as text:
136
136
  `repeatableEntry` is the read side of `repeater`: whatever the form wrote into a
137
137
  JSON column, this reads back in the same shape.
138
138
 
139
- ```ts
139
+ ```ts fragment
140
140
  section("Line items").schema([
141
141
  repeatableEntry("lines")
142
142
  .placeholder("This order has no line items.")
@@ -47,7 +47,7 @@ List / View / Create / Edit page per resource under the configured `path`.
47
47
 
48
48
  ## Quick start
49
49
 
50
- ```ts
50
+ ```ts fragment
51
51
  // app/admin.ts
52
52
  import { Panel, Resource, text, textInput } from "@zerotal/admin";
53
53
  import { User } from "./models/User.ts";
@@ -88,7 +88,7 @@ Edit / View / Delete — and the form above on the Create and Edit pages.
88
88
  Configure the panel with `Panel.configure(...)` in `app/admin.ts`, or by exporting
89
89
  an `admin` config object the provider merges on boot.
90
90
 
91
- ```ts
91
+ ```ts fragment
92
92
  Panel.configure({
93
93
  path: "/admin",
94
94
  brand: "Acme",
@@ -30,7 +30,7 @@ as it does today.
30
30
 
31
31
  Turn it on per resource:
32
32
 
33
- ```ts
33
+ ```ts fragment
34
34
  export class ProductResource extends Resource {
35
35
  static override history = true;
36
36
  }
@@ -50,7 +50,7 @@ ago should not also undo the description somebody fixed yesterday.
50
50
  The support request nobody can reproduce is usually solved by seeing what the person
51
51
  actually sees.
52
52
 
53
- ```ts
53
+ ```ts fragment
54
54
  export class UserResource extends Resource {
55
55
  static override impersonatable = true;
56
56
 
@@ -79,7 +79,7 @@ Every bit of list state already lives in the URL: search, filters, tab, sort, co
79
79
  visibility, grouping, page size. So a saved view is a saved query string, and
80
80
  restoring one is a link.
81
81
 
82
- ```ts
82
+ ```ts fragment
83
83
  Panel.savedViews({
84
84
  async list(resource) {
85
85
  /* … */
@@ -103,7 +103,7 @@ A file upload field puts a file somewhere and stores a path. That works until th
103
103
  logo is needed on twenty products, or somebody wants to know what is still pointing at
104
104
  a file before deleting it.
105
105
 
106
- ```ts
106
+ ```ts fragment
107
107
  import { databaseMedia, mediaPicker } from "@zerotal/admin";
108
108
 
109
109
  Panel.media(databaseMedia());
@@ -117,7 +117,7 @@ somewhere else.
117
117
  Configured, the panel gains a Media page — a grid with upload, search, folder
118
118
  filtering, alt-text editing and deletion — and forms can use the picker:
119
119
 
120
- ```ts
120
+ ```ts fragment
121
121
  mediaPicker("imageUrl").label("Image");
122
122
  ```
123
123
 
@@ -134,7 +134,7 @@ Authorization already works without a UI: a resource's `can()` answers every que
134
134
  the panel asks. What is missing is the other direction — seeing who can do what, and
135
135
  changing it, without editing code.
136
136
 
137
- ```ts
137
+ ```ts fragment
138
138
  import { authRoles } from "@zerotal/admin";
139
139
 
140
140
  Panel.roles(authRoles({ superusers: ["admin"] }));
@@ -162,7 +162,7 @@ week.
162
162
  What belongs at the top of a dashboard differs by role, and neither the finance lead
163
163
  nor support wants to scroll past the other's widget every morning.
164
164
 
165
- ```ts
165
+ ```ts fragment
166
166
  Panel.dashboardLayout({
167
167
  async load() {
168
168
  return Auth.user()?.dashboard ?? null;
@@ -193,7 +193,7 @@ stale key mentions it.
193
193
  Two people editing the same record is normal; one of them losing their work silently
194
194
  is not.
195
195
 
196
- ```ts
196
+ ```ts fragment
197
197
  export class ProductResource extends Resource {
198
198
  static override optimisticLock = "version";
199
199
  }
@@ -207,7 +207,7 @@ overwriting loses somebody's work, and merging blind is worse.
207
207
 
208
208
  The expensive mistake is editing production believing it is staging.
209
209
 
210
- ```ts
210
+ ```ts fragment
211
211
  import { environmentIndicator } from "@zerotal/admin";
212
212
 
213
213
  Panel.renderHook("body.start", environmentIndicator());