@zerotal/arch 1.7.4 → 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.
- package/CHANGELOG.md +34 -2
- package/README.md +4 -2
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +105 -1
- package/docs/client/index.md +13 -13
- package/docs/commands.md +4 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +28 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +2 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +5 -5
- package/docs/flow/index.md +20 -20
- package/docs/flow/layouts.md +35 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +6 -6
- package/docs/inertia/props.md +21 -21
- package/docs/inertia/rendering.md +9 -9
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +23 -10
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +3 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/index.ts +25 -1
- package/src/tools/searchDocs.ts +7 -0
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:
|
|
8
|
-
|
|
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:
|
|
4
|
-
|
|
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/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.
|
|
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";
|
package/docs/admin/actions.md
CHANGED
|
@@ -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() {
|
package/docs/admin/auth.md
CHANGED
|
@@ -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
|
```
|
package/docs/admin/dashboard.md
CHANGED
|
@@ -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
|
|
package/docs/admin/extending.md
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
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) => {
|
package/docs/admin/forms.md
CHANGED
|
@@ -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.")
|
package/docs/admin/index.md
CHANGED
|
@@ -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",
|
package/docs/admin/operations.md
CHANGED
|
@@ -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());
|
package/docs/admin/resources.md
CHANGED
|
@@ -18,7 +18,7 @@ Scaffold one with the generator, which writes `app/admin/<Name>Resource.ts`:
|
|
|
18
18
|
bun zt make:admin-resource Post
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
```ts
|
|
21
|
+
```ts fragment
|
|
22
22
|
class PostResource extends Resource {
|
|
23
23
|
static model = Post;
|
|
24
24
|
|
|
@@ -71,7 +71,7 @@ class PostResource extends Resource {
|
|
|
71
71
|
A resource does nothing until the panel knows about it. Register it where the panel
|
|
72
72
|
is configured:
|
|
73
73
|
|
|
74
|
-
```ts
|
|
74
|
+
```ts fragment
|
|
75
75
|
Panel.register(PostResource);
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -93,7 +93,7 @@ any of its links changing.
|
|
|
93
93
|
Override `can(ability, record?)` to gate the built-in actions. Abilities used by the
|
|
94
94
|
presets: `create`, `update`, `delete`, `restore`, `forceDelete`.
|
|
95
95
|
|
|
96
|
-
```ts
|
|
96
|
+
```ts fragment
|
|
97
97
|
static can(ability: string, record?: AdminRecord) {
|
|
98
98
|
return Gate.allows(ability, record ?? this.model);
|
|
99
99
|
}
|