@zerotal/arch 1.7.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +2 -0
  4. package/docs/about.md +16 -16
  5. package/docs/admin/actions.md +9 -9
  6. package/docs/admin/auth.md +2 -2
  7. package/docs/admin/dashboard.md +5 -5
  8. package/docs/admin/extending-ui.md +12 -12
  9. package/docs/admin/extending.md +16 -7
  10. package/docs/admin/forms.md +4 -4
  11. package/docs/admin/index.md +2 -2
  12. package/docs/admin/operations.md +9 -9
  13. package/docs/admin/resources.md +3 -3
  14. package/docs/admin/structure.md +6 -6
  15. package/docs/admin/tables.md +8 -8
  16. package/docs/admin/testing.md +4 -4
  17. package/docs/ai.md +118 -11
  18. package/docs/application.md +12 -12
  19. package/docs/arch.md +25 -3
  20. package/docs/assets.md +11 -11
  21. package/docs/audit.md +13 -13
  22. package/docs/authentication.md +39 -39
  23. package/docs/authorization.md +13 -13
  24. package/docs/broadcasting/channels.md +2 -2
  25. package/docs/broadcasting/client.md +2 -2
  26. package/docs/broadcasting/events.md +9 -10
  27. package/docs/broadcasting/index.md +1 -1
  28. package/docs/broadcasting/testing.md +5 -5
  29. package/docs/cache.md +15 -13
  30. package/docs/carbon.md +35 -35
  31. package/docs/changelog.md +268 -1
  32. package/docs/client/index.md +13 -13
  33. package/docs/commands.md +36 -4
  34. package/docs/components.md +116 -115
  35. package/docs/config-system.md +4 -4
  36. package/docs/container.md +27 -27
  37. package/docs/context.md +27 -27
  38. package/docs/contributing.md +28 -0
  39. package/docs/controllers.md +10 -10
  40. package/docs/conventions.md +23 -15
  41. package/docs/cookies.md +3 -3
  42. package/docs/csrf.md +6 -6
  43. package/docs/database.md +22 -17
  44. package/docs/deployment.md +99 -2
  45. package/docs/devtools.md +16 -16
  46. package/docs/email-verification.md +8 -8
  47. package/docs/encryption.md +7 -7
  48. package/docs/errors.md +6 -6
  49. package/docs/events.md +12 -11
  50. package/docs/flow/components.md +32 -32
  51. package/docs/flow/decorators.md +18 -18
  52. package/docs/flow/events.md +16 -16
  53. package/docs/flow/forms.md +17 -17
  54. package/docs/flow/icons.md +5 -5
  55. package/docs/flow/index.md +21 -21
  56. package/docs/flow/layouts.md +99 -35
  57. package/docs/flow/lifecycle.md +16 -16
  58. package/docs/flow/models.md +9 -9
  59. package/docs/flow/pagination.md +8 -8
  60. package/docs/flow/performance.md +7 -7
  61. package/docs/flow/references.md +3 -3
  62. package/docs/flow/routing.md +26 -26
  63. package/docs/flow/testing.md +20 -20
  64. package/docs/getting-started.md +18 -13
  65. package/docs/health.md +17 -8
  66. package/docs/helpers.md +17 -17
  67. package/docs/i18n.md +10 -10
  68. package/docs/inertia/devtools.md +4 -4
  69. package/docs/inertia/index.md +6 -6
  70. package/docs/inertia/props.md +21 -21
  71. package/docs/inertia/rendering.md +9 -9
  72. package/docs/inertia/ssr.md +2 -2
  73. package/docs/lifecycle.md +2 -2
  74. package/docs/lock.md +11 -11
  75. package/docs/logger.md +12 -12
  76. package/docs/media.md +22 -22
  77. package/docs/middleware.md +17 -17
  78. package/docs/migrations.md +13 -13
  79. package/docs/monitor.md +6 -6
  80. package/docs/notifications.md +24 -24
  81. package/docs/orm/casts.md +14 -14
  82. package/docs/orm/factories.md +8 -8
  83. package/docs/orm/index.md +22 -20
  84. package/docs/orm/lifecycle.md +13 -13
  85. package/docs/orm/queries.md +28 -28
  86. package/docs/orm/relationships.md +16 -16
  87. package/docs/orm/serialization.md +11 -11
  88. package/docs/package-development.md +10 -10
  89. package/docs/pagination.md +14 -14
  90. package/docs/password-reset.md +6 -6
  91. package/docs/providers.md +11 -11
  92. package/docs/query-builder.md +18 -18
  93. package/docs/queue.md +14 -12
  94. package/docs/rate-limiting.md +6 -6
  95. package/docs/responses.md +14 -14
  96. package/docs/roles-and-2fa.md +9 -9
  97. package/docs/routing.md +44 -44
  98. package/docs/scaffolding.md +1 -1
  99. package/docs/scheduler.md +10 -10
  100. package/docs/seeding.md +6 -6
  101. package/docs/session.md +16 -14
  102. package/docs/social.md +14 -14
  103. package/docs/storage.md +25 -13
  104. package/docs/structure.md +2 -2
  105. package/docs/support-policy.md +23 -10
  106. package/docs/telemetry.md +12 -12
  107. package/docs/tenancy.md +34 -40
  108. package/docs/testing/browser.md +6 -6
  109. package/docs/testing/console.md +3 -3
  110. package/docs/testing/database.md +10 -10
  111. package/docs/testing/flow-browser.md +6 -6
  112. package/docs/testing/http.md +12 -12
  113. package/docs/testing/index.md +29 -3
  114. package/docs/testing/mocking.md +8 -8
  115. package/docs/upgrade.md +1 -1
  116. package/docs/validator.md +19 -19
  117. package/docs/view.md +14 -14
  118. package/package.json +4 -4
  119. package/src/config.ts +8 -0
  120. package/src/index.ts +25 -1
  121. package/src/install/ArchInstallCommand.ts +46 -3
  122. package/src/install/guidelines.ts +84 -0
  123. package/src/install/shape.ts +177 -0
  124. package/src/install/skills.ts +271 -0
  125. package/src/provider/ArchProvider.ts +148 -0
  126. package/src/tools/searchDocs.ts +7 -0
@@ -59,7 +59,7 @@ rules and signs the result with the app's `APP_KEY`, and the client echoes the s
59
59
  or other headers via `auth.headers`, or set `authEndpoint: false` to skip the fetch and rely on
60
60
  connection-level authorization instead:
61
61
 
62
- ```ts
62
+ ```ts fragment
63
63
  // in your client code
64
64
  const socket = new Socket({
65
65
  auth: { headers: { "X-CSRF-TOKEN": csrf } }, // sent on POST /broadcasting/auth
@@ -76,7 +76,7 @@ client.
76
76
  The server is also Pusher-protocol compatible, so the reference
77
77
  [pusher-js](https://github.com/pusher/pusher-js) client works unchanged:
78
78
 
79
- ```ts
79
+ ```ts fragment
80
80
  // in your client code
81
81
  import Pusher from "pusher-js";
82
82
 
@@ -7,7 +7,7 @@ description: Write a broadcast event, dispatch it, and broadcast model changes a
7
7
 
8
8
  You only have to implement `broadcastOn()`; the rest default sensibly.
9
9
 
10
- ```ts
10
+ ```ts fragment
11
11
  // app/events/OrderShipmentStatusUpdated.ts
12
12
  import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
13
13
  import type { Order } from "../models/Order.ts";
@@ -48,7 +48,7 @@ export class OrderShipmentStatusUpdated extends BroadcastingEvent {
48
48
 
49
49
  Three ways to broadcast, in order of ergonomics:
50
50
 
51
- ```ts
51
+ ```ts fragment
52
52
  // in a controller
53
53
  import { broadcast, Broadcast } from "@zerotal/broadcasting";
54
54
 
@@ -75,7 +75,7 @@ several statements; otherwise treat `broadcast(...)` as the whole call.
75
75
 
76
76
  A broadcastable event emitted on the [event bus](/docs/events) is broadcast automatically:
77
77
 
78
- ```ts
78
+ ```ts fragment
79
79
  // in a controller
80
80
  import { Events } from "zerotal";
81
81
 
@@ -84,7 +84,7 @@ Events.emit(new OrderShipmentStatusUpdated(order)); // runs listeners AND broadc
84
84
 
85
85
  Broadcast to multiple channels by returning an array from `broadcastOn()`:
86
86
 
87
- ```ts
87
+ ```ts fragment
88
88
  // in an event's broadcastOn()
89
89
  broadcastOn() {
90
90
  return [privateChannel(`orders.${this.order.id}`), privateChannel(`users.${this.order.userId}`)];
@@ -95,7 +95,7 @@ broadcastOn() {
95
95
 
96
96
  When a full event class is overkill, broadcast inline:
97
97
 
98
- ```ts
98
+ ```ts fragment
99
99
  // in a controller
100
100
  Broadcast.on(`orders.${order.id}`).as("OrderPlaced").with(order).toOthers().send();
101
101
  Broadcast.private(`orders.${order.id}`).as("OrderPlaced").with({ id: order.id }).send();
@@ -110,7 +110,7 @@ Broadcast.to("posts", "PostViewed", { id: post.id, viewedAt: Date.now() });
110
110
  Get the members currently subscribed to a presence channel (real driver only —
111
111
  returns `[]` under the fake/null driver):
112
112
 
113
- ```ts
113
+ ```ts fragment
114
114
  // in a controller
115
115
  const members = Broadcast.getMembers("presence-chat.room1");
116
116
  // → [{ id, info }, …]
@@ -122,7 +122,7 @@ Broadcast a model's lifecycle changes by mapping them to a `BroadcastingEvent` t
122
122
  `dispatchesEvents`. When the model fires the event on the [event bus](/docs/events), it is
123
123
  broadcast automatically (see [dispatching](#dispatching-events)) — no manual broadcast call.
124
124
 
125
- ```ts
125
+ ```ts fragment
126
126
  // app/events/PostCreated.ts
127
127
  import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
128
128
  import type { Post } from "../models/Post.ts";
@@ -140,7 +140,7 @@ export class PostCreated extends BroadcastingEvent {
140
140
  }
141
141
  ```
142
142
 
143
- ```ts
143
+ ```ts fragment
144
144
  // app/models/Post.ts
145
145
  @table("posts")
146
146
  export class Post extends Model {
@@ -164,7 +164,6 @@ import { broadcastsModelEvents, privateChannel } from "@zerotal/broadcasting";
164
164
 
165
165
  @table("orders")
166
166
  export class Order extends Model {
167
- @column() id!: number;
168
167
  @column() status!: string;
169
168
  }
170
169
 
@@ -179,7 +178,7 @@ broadcastsModelEvents(Order, {
179
178
  The wire event name defaults to `${ModelName}${Event}` (e.g. `OrderUpdated`) and the payload to
180
179
  `{ order }` (the model under its camel-cased name). On the client:
181
180
 
182
- ```ts
181
+ ```ts fragment
183
182
  // in your client code
184
183
  Socket.private(`orders.${id}`).listen("OrderUpdated", (e) => render(e.order));
185
184
  ```
@@ -125,7 +125,7 @@ import {
125
125
  Extend `BroadcastingEvent`, implement `broadcastOn()`, and dispatch — every subscribed client on
126
126
  that channel receives the payload:
127
127
 
128
- ```ts
128
+ ```ts fragment
129
129
  // app/events/OrderShipmentStatusUpdated.ts
130
130
  import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
131
131
  import type { Order } from "../models/Order.ts";
@@ -11,7 +11,7 @@ live manager for an in-memory recorder, so the code under test broadcasts exactl
11
11
  as it normally would and the test inspects what came out — no Redis, no Pusher
12
12
  credentials, and no waiting on a socket.
13
13
 
14
- ```ts
14
+ ```ts fragment
15
15
  // in a test
16
16
  import { Broadcast } from "@zerotal/broadcasting";
17
17
 
@@ -31,7 +31,7 @@ Broadcast.resetFake();
31
31
  `afterEach` — otherwise the first test to fake broadcasting silently mutes every
32
32
  test that follows it.
33
33
 
34
- ```ts
34
+ ```ts fragment
35
35
  afterEach(() => Broadcast.resetFake());
36
36
  ```
37
37
 
@@ -76,7 +76,7 @@ Proving a broadcast did _not_ happen is often the more valuable test, because a
76
76
  stray broadcast reaches real users. Both negative assertions are worth reaching
77
77
  for:
78
78
 
79
- ```ts
79
+ ```ts fragment
80
80
  it("does not broadcast when validation fails", async () => {
81
81
  const fake = Broadcast.fake();
82
82
 
@@ -102,7 +102,7 @@ no special handling — save the model and assert. The event name is whatever
102
102
  `broadcastAs()` returns and the payload whatever `broadcastWith()` builds, so
103
103
  asserting on both is what pins that mapping down:
104
104
 
105
- ```ts
105
+ ```ts fragment
106
106
  const fake = Broadcast.fake();
107
107
 
108
108
  await Post.create({ title: "Hello" });
@@ -117,7 +117,7 @@ its socket id along. The fake records the broadcast either way, so a test that o
117
117
  asserts the event fired will pass whether or not `toOthers()` was used. To pin that
118
118
  behaviour down, read the recorded entries directly:
119
119
 
120
- ```ts
120
+ ```ts fragment
121
121
  const [entry] = fake.recorded();
122
122
  expect(entry.event).toBe("PostUpdated");
123
123
  ```
package/docs/cache.md CHANGED
@@ -45,7 +45,9 @@ import { CacheConfig } from "@zerotal/cache";
45
45
  import { env } from "zerotal";
46
46
 
47
47
  export default CacheConfig({
48
- driver: env("CACHE_DRIVER", "sqlite"), // 'sqlite' | 'redis' | 'memory'
48
+ // One of 'sqlite' | 'redis' | 'memory', written literally: the field is that
49
+ // union and `env()` returns a plain string.
50
+ driver: "sqlite",
49
51
  prefix: env("CACHE_PREFIX", "zerotal:"), // prepended to every key
50
52
  ttl: env("CACHE_TTL", 3600), // default TTL in seconds (1 hour)
51
53
 
@@ -66,7 +68,7 @@ export default CacheConfig({
66
68
 
67
69
  ## Basic operations
68
70
 
69
- ```ts
71
+ ```ts fragment
70
72
  // in a controller or service
71
73
  import { Cache } from "@zerotal/cache";
72
74
 
@@ -93,13 +95,13 @@ await Cache.flush();
93
95
 
94
96
  `remember()` checks the cache and, on a miss, calls the factory, stores the result, and returns it. Under high concurrency, multiple callers for the same key coalesce — the factory runs exactly once:
95
97
 
96
- ```ts
98
+ ```ts fragment
97
99
  function remember<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T>;
98
100
  ```
99
101
 
100
102
  > **Tip** — Reach for `remember()` instead of a manual `get`/`set` pair. It avoids the cache stampede where many requests miss at once and all hit the database.
101
103
 
102
- ```ts
104
+ ```ts fragment
103
105
  // in a controller
104
106
  import { Cache } from "@zerotal/cache";
105
107
  import { Post } from "../app/models/Post.ts";
@@ -130,7 +132,7 @@ const stats = await Cache.remember(`user:${userId}:stats`, 60, async () => {
130
132
 
131
133
  Group related keys under named tags so you can invalidate them together. `Cache.tags()` returns a `TaggedCache` whose keys all share a tag prefix — useful when several cache entries depend on the same underlying data:
132
134
 
133
- ```ts
135
+ ```ts fragment
134
136
  // in a controller
135
137
  import { Cache } from "@zerotal/cache";
136
138
 
@@ -156,7 +158,7 @@ await Cache.tags([`user:${userId}`]).flush();
156
158
 
157
159
  ### Tag-based invalidation in model hooks
158
160
 
159
- ```ts
161
+ ```ts fragment
160
162
  // app/models/Post.ts — inside a lifecycle hook or observer
161
163
  async afterCreate(post: Post) {
162
164
  await Cache.tags(["posts"]).flush();
@@ -171,7 +173,7 @@ async afterUpdate(post: Post) {
171
173
 
172
174
  There is no multi-get primitive — batch with `Promise.all` to cut round-trips to the backend:
173
175
 
174
- ```ts
176
+ ```ts fragment
175
177
  // in a controller
176
178
  import { Cache } from "@zerotal/cache";
177
179
 
@@ -211,7 +213,7 @@ await Promise.all([
211
213
 
212
214
  The `Cache` facade always uses the configured driver. To use a different driver for one use case, construct a `CacheManager` directly:
213
215
 
214
- ```ts
216
+ ```ts fragment
215
217
  // in a service
216
218
  import { CacheManager, MemoryDriver } from "@zerotal/cache";
217
219
 
@@ -220,7 +222,7 @@ const local = new CacheManager(new MemoryDriver(), "req:", 30);
220
222
  await local.set("computed-total", total);
221
223
  ```
222
224
 
223
- ```ts
225
+ ```ts fragment
224
226
  new CacheManager(driver: CacheDriver, prefix?: string, defaultTtl?: number)
225
227
  ```
226
228
 
@@ -230,7 +232,7 @@ new CacheManager(driver: CacheDriver, prefix?: string, defaultTtl?: number)
230
232
 
231
233
  `with()` needs a `CacheManager` instance — resolve the framework's bound manager from the container with `app.container.makeSync("cache")`:
232
234
 
233
- ```ts
235
+ ```ts fragment
234
236
  // routes/api.ts
235
237
  import { IdempotencyMiddleware, CacheManager } from "@zerotal/cache";
236
238
 
@@ -283,7 +285,7 @@ A replayed response includes `Idempotency-Replay: true` so clients can distingui
283
285
 
284
286
  Pre-populate the cache at boot so the first real request is always fast. Call from a provider's `onStarted()`, which runs after the application has finished booting:
285
287
 
286
- ```ts
288
+ ```ts fragment
287
289
  // bootstrap/providers/AppServiceProvider.ts
288
290
  import { ServiceProvider } from "zerotal";
289
291
  import { Cache } from "@zerotal/cache";
@@ -305,7 +307,7 @@ Use the `memory` driver in tests for speed and isolation — it never persists b
305
307
 
306
308
  To swap the cache for a single suite, rebind the `cache` singleton on the container before resolving the facade, then flush between tests to avoid bleed:
307
309
 
308
- ```ts
310
+ ```ts fragment
309
311
  // in test setup
310
312
  import { Application } from "zerotal";
311
313
  import { Cache, CacheManager, MemoryDriver } from "@zerotal/cache";
@@ -354,7 +356,7 @@ Cache errors extend `CacheError`, which extends the framework's `ZerotalError`.
354
356
  | `CacheSerializationError` | `E_CACHE_SERIALIZATION` | A value cannot be serialised for storage. |
355
357
  | `CacheDeserializationError` | `E_CACHE_DESERIALIZATION` | A stored value cannot be read back — usually a shape change. |
356
358
 
357
- ```typescript
359
+ ```typescript fragment
358
360
  // in a service
359
361
  import { CacheDeserializationError } from "@zerotal/cache";
360
362
 
package/docs/carbon.md CHANGED
@@ -28,7 +28,7 @@ import { Carbon, CarbonInterval } from "zerotal/carbon";
28
28
  The constructor accepts a string, a millisecond timestamp, a native `Date`, a
29
29
  Temporal type, or another Carbon. A second argument sets the timezone.
30
30
 
31
- ```typescript
31
+ ```typescript fragment
32
32
  // anywhere in your app
33
33
  // Current date and time (system timezone)
34
34
  const now = new Carbon();
@@ -60,7 +60,7 @@ const d8 = new Carbon(Temporal.Now.instant());
60
60
  Named static factories cover the common boundary cases (all accept an optional
61
61
  timezone string):
62
62
 
63
- ```typescript
63
+ ```typescript fragment
64
64
  // anywhere in your app
65
65
  Carbon.today(); // today at 00:00:00
66
66
  Carbon.tomorrow(); // tomorrow at 00:00:00
@@ -79,7 +79,7 @@ Carbon.create(input); // same as new Carbon(input)
79
79
  Every modifier returns a **new** Carbon. Chain freely — the base instance never
80
80
  changes.
81
81
 
82
- ```typescript
82
+ ```typescript fragment
83
83
  // anywhere in your app
84
84
  const base = new Carbon("2026-01-15");
85
85
  const nextWeek = base.addDays(7);
@@ -95,7 +95,7 @@ deadline.format("YYYY-MM-DD"); // '2026-02-15'
95
95
  A Carbon carries its timezone; converting produces a new instance pointing at the
96
96
  same instant.
97
97
 
98
- ```typescript
98
+ ```typescript fragment
99
99
  // anywhere in your app
100
100
  // Read the timezone of an instance
101
101
  const tz = Carbon.now("Asia/Tokyo").timezone; // 'Asia/Tokyo'
@@ -117,7 +117,7 @@ Reading a Carbon never changes it. Calendar fields are plain properties, so they
117
117
  carry no parentheses; the three counts that depend on the surrounding calendar are
118
118
  methods, because they compute an answer rather than expose a stored field.
119
119
 
120
- ```typescript
120
+ ```typescript fragment
121
121
  // anywhere in your app
122
122
  const d = new Carbon("2026-06-15 09:30:45.123");
123
123
 
@@ -162,7 +162,7 @@ out and comparing them by hand.
162
162
 
163
163
  ### Predicates
164
164
 
165
- ```typescript
165
+ ```typescript fragment
166
166
  // anywhere in your app
167
167
  d.isToday();
168
168
  d.isTomorrow();
@@ -221,7 +221,7 @@ ranges, where inclusive bounds would place a boundary value in both buckets.
221
221
 
222
222
  All arithmetic returns a new Carbon.
223
223
 
224
- ```typescript
224
+ ```typescript fragment
225
225
  // anywhere in your app
226
226
  d.addNanoseconds(1) d.subtractNanoseconds(1)
227
227
  d.addMicroseconds(1) d.subtractMicroseconds(1)
@@ -245,7 +245,7 @@ either direction from a computed value without branching.
245
245
 
246
246
  To add a [CarbonInterval](#carboninterval), use `add()` / `subtract()`:
247
247
 
248
- ```typescript
248
+ ```typescript fragment
249
249
  // anywhere in your app
250
250
  const interval = CarbonInterval.days(3).andHours(6);
251
251
 
@@ -259,7 +259,7 @@ Months and years are calendar units rather than fixed spans, so adding one lands
259
259
  on the same day number in the target month — and clamps when that day does not
260
260
  exist there:
261
261
 
262
- ```typescript
262
+ ```typescript fragment
263
263
  new Carbon("2026-01-31").addMonths(1); // → 2026-02-28
264
264
  new Carbon("2024-02-29").addYears(1); // → 2025-02-28
265
265
  ```
@@ -267,7 +267,7 @@ new Carbon("2024-02-29").addYears(1); // → 2025-02-28
267
267
  Clamping discards information, which makes month arithmetic asymmetric. Adding a
268
268
  month and taking it back does not always return the original date:
269
269
 
270
- ```typescript
270
+ ```typescript fragment
271
271
  new Carbon("2026-01-31").addMonths(1).subtractMonths(1); // → 2026-01-28
272
272
  ```
273
273
 
@@ -285,7 +285,7 @@ and always reverse cleanly.
285
285
 
286
286
  Snap to the start or end of a time period — all return a new Carbon.
287
287
 
288
- ```typescript
288
+ ```typescript fragment
289
289
  // anywhere in your app
290
290
  d.startOfMinute(); // :00.000
291
291
  d.endOfMinute(); // :59.999999999
@@ -325,7 +325,7 @@ day without also catching midnight the next morning.
325
325
 
326
326
  The `with` prefix signals a new instance is returned.
327
327
 
328
- ```typescript
328
+ ```typescript fragment
329
329
  // anywhere in your app
330
330
  d.withYear(2030);
331
331
  d.withMonth(12); // 1-indexed
@@ -351,7 +351,7 @@ of setters.
351
351
  All numeric diff methods return a **number** — positive when `this` is after
352
352
  `other`.
353
353
 
354
- ```typescript
354
+ ```typescript fragment
355
355
  // anywhere in your app
356
356
  const a = new Carbon("2026-01-01");
357
357
  const b = new Carbon("2026-06-15");
@@ -373,7 +373,7 @@ b.diffInYears(a); // ~0.42
373
373
  For a calendar-aware breakdown use `diffAsCarbonInterval`, which delegates to
374
374
  Temporal's `until()`:
375
375
 
376
- ```typescript
376
+ ```typescript fragment
377
377
  // anywhere in your app
378
378
  const age = birthday.diffAsCarbonInterval(Carbon.now(), "year");
379
379
  // → CarbonInterval { years: 28, months: 3, days: 12, … }
@@ -394,7 +394,7 @@ The `largestUnit` defaults to `'day'` and accepts `'year'`, `'month'`, `'week'`,
394
394
  Returns a human-readable relative string using `Intl.RelativeTimeFormat`. With no
395
395
  argument it compares against now.
396
396
 
397
- ```typescript
397
+ ```typescript fragment
398
398
  // anywhere in your app
399
399
  const posted = new Carbon("2026-06-08");
400
400
  posted.diffForHumans(); // '1 week ago'
@@ -407,7 +407,7 @@ a.diffForHumans(b); // '5 months ago'
407
407
 
408
408
  Pass an options object to tune the output:
409
409
 
410
- ```typescript
410
+ ```typescript fragment
411
411
  // anywhere in your app
412
412
  a.diffForHumans(b, {
413
413
  parts: 2, // include up to 2 units → '5 months, 1 week ago'
@@ -419,7 +419,7 @@ a.diffForHumans(b, {
419
419
  });
420
420
  ```
421
421
 
422
- ```typescript
422
+ ```typescript fragment
423
423
  // anywhere in your app
424
424
  const future = Carbon.now().addDays(3);
425
425
  future.diffForHumans(); // '3 days from now'
@@ -434,7 +434,7 @@ future.diffForHumans({ absolute: true }); // '3 days'
434
434
 
435
435
  ### Token-based
436
436
 
437
- ```typescript
437
+ ```typescript fragment
438
438
  // anywhere in your app
439
439
  d.format(); // '2026-06-15 09:30:00' (default)
440
440
  d.format("YYYY-MM-DD"); // '2026-06-15'
@@ -472,7 +472,7 @@ d.format("YYYY-MM-DDTHH:mm:ssZ"); // '2026-06-15T09:30:00+02:00'
472
472
 
473
473
  `intlFormat` delegates to `Intl.DateTimeFormat`:
474
474
 
475
- ```typescript
475
+ ```typescript fragment
476
476
  // anywhere in your app
477
477
  d.intlFormat("en-US", { dateStyle: "full" });
478
478
  // → 'Monday, June 15, 2026'
@@ -489,7 +489,7 @@ d.intlFormat("ja-JP", { year: "numeric", month: "long", day: "numeric" });
489
489
 
490
490
  ### Convenience formatters
491
491
 
492
- ```typescript
492
+ ```typescript fragment
493
493
  // anywhere in your app
494
494
  d.toDateString(); // '2026-06-15'
495
495
  d.toDateTimeString(); // '2026-06-15 09:30:00'
@@ -509,7 +509,7 @@ d.toLongDate(); // '15 June 2026'
509
509
 
510
510
  ## Serialisation
511
511
 
512
- ```typescript
512
+ ```typescript fragment
513
513
  // anywhere in your app
514
514
  d.toDate(); // native Date
515
515
  d.toISOString(); // '2026-06-15T07:30:00+00:00' (UTC)
@@ -529,7 +529,7 @@ d.toPlainDate(); // Temporal.PlainDate (loses time + timezone)
529
529
 
530
530
  `valueOf()` enables direct comparison with `<`, `>`, `-`:
531
531
 
532
- ```typescript
532
+ ```typescript fragment
533
533
  // anywhere in your app
534
534
  const a = new Carbon("2026-01-01");
535
535
  const b = new Carbon("2026-06-15");
@@ -544,7 +544,7 @@ Math.min(+a, +b) === +a; // true
544
544
  `CarbonInterval` is an immutable duration value object backed by
545
545
  `Temporal.Duration`. Every method returns a new instance.
546
546
 
547
- ```typescript
547
+ ```typescript fragment
548
548
  // anywhere in your app
549
549
  import { CarbonInterval } from "zerotal/carbon";
550
550
 
@@ -575,7 +575,7 @@ const built = new CarbonInterval({ years: 1, months: 6, days: 3 });
575
575
 
576
576
  Chain `and*` methods to compose multi-unit intervals:
577
577
 
578
- ```typescript
578
+ ```typescript fragment
579
579
  // anywhere in your app
580
580
  CarbonInterval.days(3).andHours(6).andMinutes(30);
581
581
  // → 3 days 6 hours 30 minutes
@@ -592,7 +592,7 @@ Available: `andYears`, `andMonths`, `andWeeks`, `andDays`, `andHours`,
592
592
 
593
593
  ### Interval getters
594
594
 
595
- ```typescript
595
+ ```typescript fragment
596
596
  // anywhere in your app
597
597
  const i = CarbonInterval.fromISO("P1Y2M3DT4H5M6S");
598
598
 
@@ -613,7 +613,7 @@ i.isZero; // false
613
613
 
614
614
  ### Interval arithmetic
615
615
 
616
- ```typescript
616
+ ```typescript fragment
617
617
  // anywhere in your app
618
618
  const a = CarbonInterval.hours(2);
619
619
  const b = CarbonInterval.minutes(30);
@@ -630,7 +630,7 @@ CarbonInterval.abs(a.negate()); // 2 hours (all fields positive)
630
630
  `cascade()` rolls excess sub-units up into higher ones. It needs a reference date
631
631
  for calendar-aware units (months, years) and defaults to now in UTC.
632
632
 
633
- ```typescript
633
+ ```typescript fragment
634
634
  // anywhere in your app
635
635
  CarbonInterval.seconds(90).cascade();
636
636
  // → { minutes: 1, seconds: 30 }
@@ -653,7 +653,7 @@ CarbonInterval.days(32).cascade(Carbon.today().inTimezone("UTC").toZonedDateTime
653
653
 
654
654
  Calendar units (years, months) are approximated as average lengths.
655
655
 
656
- ```typescript
656
+ ```typescript fragment
657
657
  // anywhere in your app
658
658
  CarbonInterval.hours(2).andMinutes(30).totalMinutes(); // 150
659
659
  CarbonInterval.days(3).andHours(6).totalHours(); // 78
@@ -667,7 +667,7 @@ i.totalWeeks();
667
667
 
668
668
  ### Interval comparison
669
669
 
670
- ```typescript
670
+ ```typescript fragment
671
671
  // anywhere in your app
672
672
  const a = CarbonInterval.hours(2);
673
673
  const b = CarbonInterval.minutes(90);
@@ -681,7 +681,7 @@ CarbonInterval.compare(a, b); // 1 (a > b), -1 (a < b), 0 (equal)
681
681
 
682
682
  ### Human-readable output
683
683
 
684
- ```typescript
684
+ ```typescript fragment
685
685
  // anywhere in your app
686
686
  CarbonInterval.days(1).andHours(2).andMinutes(30).forHumans();
687
687
  // → '1 day 2 hours 30 minutes'
@@ -697,7 +697,7 @@ i.toString(); // alias for forHumans()
697
697
 
698
698
  ### Interval serialisation
699
699
 
700
- ```typescript
700
+ ```typescript fragment
701
701
  // anywhere in your app
702
702
  CarbonInterval.days(1).andHours(2).toISO();
703
703
  // → 'P1DT2H'
@@ -715,7 +715,7 @@ An interval is what [`add()` and `subtract()`](#add-and-subtract) accept, and wh
715
715
  [`diffAsCarbonInterval()`](#differences) returns — the two directions between the
716
716
  types:
717
717
 
718
- ```typescript
718
+ ```typescript fragment
719
719
  // anywhere in your app
720
720
  const interval = CarbonInterval.days(3).andHours(6);
721
721
 
@@ -732,7 +732,7 @@ immutable and pure, so it needs no application — but a test that reaches for
732
732
  **Pin the instant.** Pass a fixed input rather than using the current time, and
733
733
  the assertion holds forever:
734
734
 
735
- ```typescript
735
+ ```typescript fragment
736
736
  // tests/dates/BillingPeriod.test.ts
737
737
  import { test, expect } from "bun:test";
738
738
  import { Carbon } from "zerotal/carbon";
@@ -751,7 +751,7 @@ test("a mid-month signup bills to the end of the month", () => {
751
751
  calls `Carbon.now()` internally can only be tested by waiting or by mocking; one
752
752
  that accepts a `now` parameter is tested by passing a date:
753
753
 
754
- ```typescript
754
+ ```typescript fragment
755
755
  // app/services/billing.ts
756
756
  export function periodFor(signedUp: Carbon, now: Carbon = Carbon.now()): Period {
757
757
  // …
@@ -766,7 +766,7 @@ month end.
766
766
  transitions are where date code breaks, and none of them appear in a test written
767
767
  around today:
768
768
 
769
- ```typescript
769
+ ```typescript fragment
770
770
  // tests/dates/BillingPeriod.test.ts
771
771
  test("handles a leap day", () => {
772
772
  expect(Carbon.create("2028-02-29T12:00:00Z").addYears(1).toDateString()).toBe("2029-02-28");