@weftui/core 0.28.0 → 0.29.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.
@@ -2,28 +2,30 @@
2
2
  title: "@weftui/dom"
3
3
  order: 2
4
4
  section: reference
5
- description: Full API surface for @weftui/dom the WeftApp client runtime (make, mount, hydrate, errors, dispose) and the server renderer (renderToString and streaming variants).
5
+ description: Full API surface for @weftui/dom, covering the WeftApp client runtime (make, mount, hydrate, errors, dispose), the server renderer (renderToString and streaming variants), and the Props.merge/Props.cx prop-bag composition utilities.
6
6
  ---
7
7
 
8
8
  # @weftui/dom API Reference
9
9
 
10
- The DOM renderer for Weft. It has two entry points `@weftui/dom/client` for the
11
- browser and `@weftui/dom/server` for Node plus a package root that re-exports the
12
- renderer error types. See the [Server-Side Rendering guide](https://weftui.dev/docs/how-to/render-on-the-server)
10
+ The DOM renderer for Weft. It has two entry points: `@weftui/dom/client` for the
11
+ browser and `@weftui/dom/server` for Node. A package root re-exports the renderer
12
+ error types. See the [Server-Side Rendering guide](https://weftui.dev/docs/how-to/render-on-the-server)
13
13
  for a narrative walkthrough.
14
14
 
15
15
  ## `@weftui/dom/client`
16
16
 
17
17
  `WeftApp` is the client entry point's app namespace (`export * as WeftApp from
18
18
  "./weft-app"`). One `WeftApp` value is one lazily-built `ManagedRuntime` (the app
19
- layer) + one root `Scope` + one unhandled-error hub. Each `WeftApp.mount` /
20
- `WeftApp.hydrate` call creates a child **root scope** under the app scope;
21
- layer-built services are shared **by reference** across every root mounted from the
22
- same app (layer memoization), which is what makes cross-island reactive state work
23
- (see [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands)). The
24
- barrel also re-exports `MountError`, `HydrateError`, `RootHandle`, `UnhandledError`,
25
- and the `WeftApp` interface's type as `WeftAppType` (renamed on export to avoid
26
- colliding with the `WeftApp` namespace import).
19
+ layer) + one root `Scope` + one unhandled-error hub.
20
+
21
+ Each `WeftApp.mount` / `WeftApp.hydrate` call creates a child **root scope** under
22
+ the app scope. Layer-built services are shared **by reference** across every root
23
+ mounted from the same app (layer memoization). This is what makes cross-island
24
+ reactive state work (see [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands)).
25
+
26
+ The barrel also re-exports `MountError`, `HydrateError`, `RootHandle`,
27
+ `UnhandledError`, and the `WeftApp` interface's type as `WeftAppType` (renamed on
28
+ export to avoid colliding with the `WeftApp` namespace import).
27
29
 
28
30
  ### `WeftApp.make`
29
31
 
@@ -37,12 +39,14 @@ const make: {
37
39
  };
38
40
  ```
39
41
 
40
- Creates a `WeftApp` from an app layer. Synchronous and side-effect-free with
41
- respect to the layer: the layer builds **lazily** on the first `mount` / `hydrate`
42
- (or the first direct `app.runtime` run) `ManagedRuntime.make` semantics. A layer
43
- whose construction has an observable side effect shows that effect only after the
44
- first mount, never at `make` time. `options.memoMap` shares layer memoization
45
- across multiple `WeftApp` instances.
42
+ Creates a `WeftApp` from an app layer. `make` is synchronous and side-effect-free
43
+ with respect to the layer: the layer builds **lazily** on the first `mount` /
44
+ `hydrate` (or the first direct `app.runtime` run), per `ManagedRuntime.make`
45
+ semantics.
46
+
47
+ A layer whose construction has an observable side effect shows that effect only
48
+ after the first mount, never at `make` time. `options.memoMap` shares layer
49
+ memoization across multiple `WeftApp` instances.
46
50
 
47
51
  There is deliberately no `makeScoped`. To bind an app's lifetime to a scope,
48
52
  compose it yourself:
@@ -64,16 +68,19 @@ const mount: <R, E>(
64
68
  ) => Effect.Effect<RootHandle, E | MountError>;
65
69
  ```
66
70
 
67
- Mounts `node` into `root` as a new root of `app`. Self-contained the returned
68
- effect's requirement channel is `never`, so it runs with a bare `Effect.runPromise`;
69
- services come exclusively from the app layer, and an `Effect.provide` wrapped around
70
- this call does not reach components. Clears `root`'s existing children, renders,
71
- appends the result. Completes after initial render; streams keep running in the
72
- background, owned by the root's scope (a child of the app scope). The app layer
73
- builds lazily here on first mount; its error channel `E` surfaces at that point. On
74
- render failure the root scope is closed before the error propagates; the app
75
- runtime and other roots are untouched. Mounting on a disposed app fails — it does
76
- not hang.
71
+ Mounts `node` into `root` as a new root of `app`. The returned effect is
72
+ self-contained: its requirement channel is `never`, so it runs with a bare
73
+ `Effect.runPromise`. Services come exclusively from the app layer; an
74
+ `Effect.provide` wrapped around this call does not reach components.
75
+
76
+ Clears `root`'s existing children, renders, appends the result. Completes after
77
+ initial render; streams keep running in the background, owned by the root's scope
78
+ (a child of the app scope).
79
+
80
+ The app layer builds lazily here on first mount; its error channel `E` surfaces at
81
+ that point. On render failure the root scope is closed before the error propagates;
82
+ the app runtime and other roots are untouched. Mounting on a disposed app fails
83
+ rather than hanging.
77
84
 
78
85
  ### `WeftApp.hydrate`
79
86
 
@@ -92,11 +99,13 @@ Continues, on the client, the DOM produced on the server by
92
99
  Unlike `mount`, does **not** clear `root`: it walks the node tree in lockstep with
93
100
  the existing server DOM, adopting nodes in place. Error channel is `E |
94
101
  HydrateError` (adds `HydrationMismatchError` on top of everything `mount` can fail
95
- with). Preserves the compile-time `AssertNoServerOnly` → `ServerOnlyLeak` guard: a
102
+ with).
103
+
104
+ Preserves the compile-time `AssertNoServerOnly` → `ServerOnlyLeak` guard. A
96
105
  server-only requirement left in `node`'s context degrades the return type to the
97
- `ServerOnlyLeak` sentinel (compile error at the call site) instead of a runtime
98
- failure. Hydration mechanics the readiness barrier, stream-id seeding are
99
- otherwise unchanged from `mount`.
106
+ `ServerOnlyLeak` sentinel (compile error at the call site), not a runtime failure.
107
+ Hydration mechanics (the readiness barrier, stream-id seeding) are otherwise
108
+ unchanged from `mount`.
100
109
 
101
110
  ### `WeftApp.errors`
102
111
 
@@ -107,9 +116,11 @@ const errors: <R, E>(app: WeftApp<R, E>) => Stream.Stream<UnhandledError>;
107
116
  The app's unhandled-error stream. While at least one subscriber exists, the default
108
117
  `Effect.logError` fallback is suppressed and every `UnhandledError` is delivered to
109
118
  all subscribers. With zero subscribers, each unhandled error runs the default log
110
- (annotated with `weft.region`) instead. No replay — a subscriber sees only errors
111
- published after it subscribed; multiple concurrent subscribers each receive every
112
- subsequent error. When the last subscriber unsubscribes, the default log resumes.
119
+ (annotated with `weft.region`) instead.
120
+
121
+ There is no replay: a subscriber sees only errors published after it subscribed.
122
+ Multiple concurrent subscribers each receive every subsequent error. When the last
123
+ subscriber unsubscribes, the default log resumes.
113
124
 
114
125
  ### `WeftApp.dispose`
115
126
 
@@ -117,9 +128,13 @@ subsequent error. When the last subscriber unsubscribes, the default log resumes
117
128
  const dispose: <R, E>(app: WeftApp<R, E>) => Effect.Effect<void>;
118
129
  ```
119
130
 
120
- Disposes the app: closes every root scope (in mount order), then releases the
121
- runtime's layers (`runtime.disposeEffect`), then shuts the error hub down.
122
- Idempotent teardown effects run once. Subsequent `mount` / `hydrate` calls fail.
131
+ Disposes the app, in order:
132
+
133
+ - closes every root scope (in mount order),
134
+ - releases the runtime's layers (`runtime.disposeEffect`),
135
+ - shuts the error hub down.
136
+
137
+ Idempotent: teardown effects run once. Subsequent `mount` / `hydrate` calls fail.
123
138
 
124
139
  ### `WeftApp<R, E>` (`WeftAppType`)
125
140
 
@@ -131,9 +146,9 @@ interface WeftApp<in R = never, out E = never> {
131
146
  ```
132
147
 
133
148
  Re-exported from the barrel as `WeftAppType`. `runtime` is the app's
134
- `ManagedRuntime`, for running app-level effects against the shared layer outside any
135
- root e.g. `app.runtime.runFork(trackPageviews)` (see
136
- `website/src/entry-client.ts`) or `app.runtime.runPromise(Router.push("/about"))`.
149
+ `ManagedRuntime`, for running app-level effects against the shared layer outside
150
+ any root. Examples: `app.runtime.runFork(trackPageviews)` (see
151
+ `website/src/entry-client.ts`) and `app.runtime.runPromise(Router.push("/about"))`.
137
152
 
138
153
  ### `RootHandle`
139
154
 
@@ -146,9 +161,10 @@ interface RootHandle {
146
161
 
147
162
  Returned by `mount` / `hydrate`. `element` is the DOM element the root was mounted
148
163
  into. `unmount()` closes **this root's scope only**: it interrupts its stream
149
- subscriptions and any scoped work forked from its event handlers. It does **not**
150
- dispose the app runtime, touch other roots, or remove the rendered DOM nodes from
151
- `element`. Idempotent teardown side effects fire once.
164
+ subscriptions and any scoped work forked from its event handlers.
165
+
166
+ It does **not** dispose the app runtime, touch other roots, or remove the rendered
167
+ DOM nodes from `element`. Idempotent: teardown side effects fire once.
152
168
 
153
169
  ### `UnhandledError`
154
170
 
@@ -168,8 +184,8 @@ in the render tree the error escaped. Sources (one entry per failing occurrence)
168
184
  (region e.g. `"attribute:class"`, `"child:stream-3"`),
169
185
  - an error escaping the **outermost** `Boundary` recovery (region
170
186
  `"boundary:outermost"`),
171
- - an event-handler effect **failing or dying** (region `"event:onClick"`) reported
172
- in development and production alike; there is no `NODE_ENV`-gated swallow.
187
+ - an event-handler effect **failing or dying** (region `"event:onClick"`), reported
188
+ in development and production alike (there is no `NODE_ENV`-gated swallow).
173
189
 
174
190
  Interrupt-only causes are never published. Errors handled by a nested `Boundary`
175
191
  never reach the hub.
@@ -236,28 +252,228 @@ hydration markers. These back streaming SSR and suspense.
236
252
  renderToHydratableShell(node: Renderable): Effect<HydratableShell, Error, R>
237
253
  ```
238
254
 
239
- Produces a `HydratableShell` the document scaffold around the app for servers
255
+ Produces a `HydratableShell` (the document scaffold around the app) for servers
240
256
  that assemble the response shell separately from the streamed body.
241
257
 
242
258
  ### Suspense failure handling
243
259
 
244
- `SuspenseFailureHandlerTag` is the service tag for a `SuspenseFailureHandler`, which
245
- maps a failed suspense boundary to a `SuspenseFailureSubstitute` (fallback markup)
246
- during streaming SSR.
260
+ `SuspenseFailureHandlerTag` is the service tag for a `SuspenseFailureHandler`. The
261
+ handler maps a failed suspense boundary to a `SuspenseFailureSubstitute` (fallback
262
+ markup) during streaming SSR.
247
263
 
248
264
  ## Package root (`@weftui/dom`)
249
265
 
250
266
  Re-exports the renderer error types:
251
267
 
252
- - `HydrationMismatchError` the client tree did not match the server markup.
253
- - `UnsupportedNodeTypeError` a node type the renderer cannot handle was encountered.
254
- - `RenderError` a general rendering failure.
255
- - `StreamSubscriptionError` a reactive stream backing the tree failed to subscribe.
268
+ - `HydrationMismatchError`: the client tree did not match the server markup.
269
+ - `UnsupportedNodeTypeError`: a node type the renderer cannot handle was encountered.
270
+ - `RenderError`: a general rendering failure.
271
+ - `StreamSubscriptionError`: a reactive stream backing the tree failed to subscribe.
272
+
273
+ Also re-exports the `Props` namespace, below.
274
+
275
+ ## `Props`
276
+
277
+ `export * as Props from "@weftui/dom"`. Two functions for reconciling DOM prop
278
+ bags: `merge` combines multiple bags into one, `cx` builds a class string.
279
+ Both are pure and synchronous; neither subscribes anything. A reactive result
280
+ is a `Stream` description, subscribed later by the renderer in the element's
281
+ scope.
282
+
283
+ ### `Props.merge`
284
+
285
+ ```ts
286
+ function merge<const Bags extends ReadonlyArray<DomProps>>(...bags: Bags): Merged<Bags>;
287
+ ```
288
+
289
+ `DomProps` is `object`. `merge` is variadic and left-to-right: `merge()` is
290
+ `{}`, `merge(a)` is observationally `a`, and `merge(a, b, c)` folds pairwise
291
+ (`merge(merge(a, b), c)`). `{}` is the identity on either side.
292
+
293
+ The fold is associative per key, with one exception: `style` is not
294
+ associative when a non-object form (a string, or a whole-object stream) takes
295
+ part. See the `style` rule below.
296
+
297
+ Keys present on only one side pass through unchanged, by reference. For a key
298
+ present on both sides, the merged value depends on the key:
299
+
300
+ | Key | Rule | Result |
301
+ | --------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------ |
302
+ | `on*` (event handler, per the renderer's `on` + lowercase-third-char check) | chained: both handler bodies run, left then right | new handler function |
303
+ | `class` | concatenated | `string` if both sides are static, else a derived `Stream<string>` |
304
+ | `style` | object sides merge per property; any other form is last-wins | plain object, or the right side as-is |
305
+ | `ref` | fanned out | readonly array of `SubscriptionRef`s |
306
+ | anything else | last-wins | the right side's value, as-is |
307
+
308
+ **Handlers.** Both handler bodies run synchronously when the merged handler
309
+ is invoked, left then right, before either side's returned `Effect` is
310
+ awaited. This is what makes `event.preventDefault()` written in either body
311
+ observable to the other: two separate DOM listeners would both run during
312
+ dispatch, so neither can wait on the other's `Effect` to decide whether the
313
+ default action should still happen.
314
+
315
+ Only the returned Effects are sequenced: left first, then right. Both always
316
+ run regardless of whether the other fails. A plain void-returning handler is
317
+ lifted to `Effect.void` (or a died `Effect` if it throws). The merged
318
+ handler's error channel is the union `E_left | E_right`; if both sides fail,
319
+ both causes are aggregated into one, not deduplicated (two equal-looking
320
+ failures are still two failures).
321
+
322
+ `null` or `undefined` on either side means "not provided": the other side
323
+ passes through unchanged, whatever shape it has, including a reactive
324
+ `Stream`/`Effect`-of-handler value (not chained, since only two plain
325
+ functions are chained; see [Accepted limitations](#accepted-limitations)
326
+ below). `false` on the **right** means "explicitly disabled" and wins, since
327
+ the renderer reads `false` as "no handler." That is how a caller switches a
328
+ behavior's handler off. A `false` on the left simply loses to the right side,
329
+ like any other last-wins value.
330
+ This is the only place `merge` treats a nullish value specially. The generic
331
+ rule below explains why every other key does not.
332
+
333
+ **`class`.** Two static strings concatenate with a single space, no dedupe:
334
+ `merge({ class: "a" }, { class: "b" }).class === "a b"`. If either side is
335
+ reactive (`Stream`, `Effect`, or `Subscribable`), the result is a derived
336
+ `Stream<string>` combining the latest value from each side, space-joined. A
337
+ static side contributes immediately; the first emission waits only on the
338
+ reactive side(s) (await-first). A reactive side that ends without ever
339
+ emitting fails the derived stream with `NoPropValue`, which joins the merged
340
+ `E` channel. When both sides contribute nothing (absent, `undefined`, or
341
+ empty), the result is `""`, matching `cx` and clsx: it is not normalized to
342
+ `undefined`.
343
+
344
+ The `class` rule for two present sides is exactly `cx(left, right)`; `cx` is
345
+ that same engine exposed directly (see below).
346
+
347
+ **`style`.** Two per-property objects (`style: { color: "red" }`) merge by
348
+ key union, right side winning per key; each surviving value, static or
349
+ `Source`, passes through by reference. Any other shape on either side (a
350
+ `string`, or a whole-object stream) is last-wins: the right side replaces the
351
+ left entirely. This is the one case where merge is not associative, since
352
+ last-wins discards a side instead of combining it, so grouping the fold
353
+ differently changes the result. Upgrading whole-object-stream style merging
354
+ to a real per-key merge is additive future work, not a breaking change.
355
+
356
+ **`ref`.** Both sides concatenate into one readonly array, flattening any
357
+ side that is already an array, so associativity holds:
358
+ `merge({ ref: [a, b] }, { ref: c }).ref` is `[a, b, c]`. Nullish sides are
359
+ dropped, so an optional ref forwarded as `undefined` never enters the array.
360
+ Each ref keeps the normal per-ref contract: set once to `Some(element)` when
361
+ the element mounts. The renderer's `ref` prop accepts a `SubscriptionRef` or
362
+ a `readonly SubscriptionRef[]` directly, so `h.div({ ref: [a, b] })` fans out
363
+ without `merge` too.
364
+
365
+ **Everything else (generic keys).** Plain last-wins, matching object spread:
366
+ the right side's value wins as-is, including an explicit `undefined`. There
367
+ is no nullish guard on this arm (unlike the handler rule): a guard was tried
368
+ and reverted, because it made the runtime return the left value while the
369
+ type still said the right value was present, silently dropping the left
370
+ side's `E`/`R` channels from the merged type.
371
+
372
+ #### Type-layer contract
373
+
374
+ `Merged<Bags>`'s **value** types stay coarse (Source-shaped, not narrowed to
375
+ exactly what the runtime returns); its `E`/`R` **channels** stay precise,
376
+ because `PropsE`/`PropsR` (the machinery that feeds a merged bag's channels
377
+ into `h.*`'s resulting `Node<E, R>`) match `P[K]` against an exact `Stream`
378
+ or function shape. A looser value type would fail that match and silently
379
+ drop the channel.
380
+
381
+ Consequences worth knowing:
382
+
383
+ - A shared key's merged value type is **required**, even when the key is
384
+ optional on both input bags and absent from one side at runtime. Typing it
385
+ optional would fail the `PropsE`/`PropsR` match and drop the channel for
386
+ the common case: a behavior primitive's bag with optional props.
387
+ - A handler cell types as callable whenever _either_ side can carry a
388
+ handler, even if that side is `null` at runtime. Narrowing this would
389
+ require unioning the nullish outcome back in, which fails the same match.
390
+ - The `ref`-array cell types as `SubscriptionRef<Option<any>>`, not narrowed
391
+ to a specific element type. `SubscriptionRef` is invariant in its value
392
+ type, so a precise union would reject the headline case: fanning a
393
+ behavior's `SubscriptionRef<Option<HTMLElement>>` out alongside a caller's
394
+ `SubscriptionRef<Option<HTMLInputElement>>`. A mistyped ref inside a
395
+ fan-out array is therefore not caught at compile time; the set-once
396
+ contract keeps reads sound regardless.
397
+ - A bag typed with core's `HTMLAttributes`/`DOMAttributes` gets `unknown`
398
+ handler channels, because those types declare handlers as returning
399
+ `void | Effect<void, unknown, unknown>`. A behavior primitive that
400
+ declares precise handler signatures keeps precise channels through the
401
+ merge.
402
+
403
+ #### Accepted limitations
404
+
405
+ - Reactive handler _values_ (a `Stream`/`Effect` of a handler function, the
406
+ form core's `EventHandler` union allows) are not chained: any non-function
407
+ handler side falls back to last-wins, consistent with the whole-object
408
+ style rule. Their `E`/`R` channels are still collected in the merged type.
409
+ - An inline handler written directly inside a `merge` call gets no
410
+ contextual type for its event parameter, because `DomProps` is `object`
411
+ and `merge` cannot know which element it will end up on. Write
412
+ `onclick: (ev: MouseEvent) => …` with an explicit annotation, or give the
413
+ bag its own type.
414
+
415
+ ### `Props.cx`
416
+
417
+ ```ts
418
+ function cx<const Inputs extends ReadonlyArray<CxInput>>(...inputs: Inputs): CxResult<Inputs>;
419
+
420
+ type CxInput =
421
+ | string
422
+ | false
423
+ | null
424
+ | undefined
425
+ | Stream.Stream<string, any, any>
426
+ | Effect.Effect<string, any, any>
427
+ | Subscribable.Subscribable<string, any, any>
428
+ | CxRecord
429
+ | ReadonlyArray<CxInput>;
430
+
431
+ interface CxRecord {
432
+ readonly [className: string]:
433
+ | boolean
434
+ | Stream.Stream<boolean, any, any>
435
+ | Effect.Effect<boolean, any, any>
436
+ | Subscribable.Subscribable<boolean, any, any>;
437
+ }
438
+ ```
439
+
440
+ A reactive class-name builder, clsx-compatible plus reactive conditions. Each
441
+ input is one of:
442
+
443
+ - a **string**: kept as a literal class name segment;
444
+ - a **falsy value** (`false`, `null`, `undefined`, `""`): skipped;
445
+ - a **nested array** of `CxInput`: flattened recursively;
446
+ - a **record** (`{ className: condition }`): each key is included when its
447
+ condition is truthy;
448
+ - a **reactive value** in place of a string (a `Source<string>`), or as a
449
+ record condition (a `Source<boolean>`).
450
+
451
+ `cx()` is `""`. All-static inputs join into a plain `string`, space-separated,
452
+ no dedupe, no empty segments. Any reactive input (a reactive value, or a
453
+ reactive condition in a record) derives a `Stream<string>` that recomputes
454
+ the full class string on any emission, combining the latest value from every
455
+ reactive input.
456
+
457
+ A reactive value that resolves to `""` contributes nothing to that emission.
458
+ A reactive input that ends without ever emitting fails the stream with
459
+ `NoPropValue`, which joins the result's `E` channel along with every
460
+ reactive input's own `E`/`R`.
461
+
462
+ Only a plain record (an object literal, not a class instance, `Date`, or
463
+ boxed value like `SubscriptionRef`) is read as a condition map; anything else
464
+ is ignored rather than risking a foreign field name leaking in as a class
465
+ name.
466
+
467
+ `merge`'s `class` rule for two present sides is observationally
468
+ `cx(left, right)`: one engine behind both names.
256
469
 
257
470
  ## See also
258
471
 
259
- - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) a narrative walkthrough of the server/client split
260
- - [Provide Services](https://weftui.dev/docs/how-to/provide-services) recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
261
- - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) hydrate-in-place and why there is no virtual DOM
262
- - [Services and Context](https://weftui.dev/docs/explanation/services-and-context) how services flow from the app layer to every root
472
+ - [Compose Behavior and Markup](https://weftui.dev/docs/how-to/compose-behavior-and-markup): using `Props.merge`/`Props.cx` to combine a behavior's props with the caller's
473
+ - [Style Reactively](https://weftui.dev/docs/how-to/style-reactively): the `style` prop's reactive forms
474
+ - [Use Element Refs](https://weftui.dev/docs/how-to/use-element-refs): the `ref` prop and its fan-out form
475
+ - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server): a narrative walkthrough of the server/client split
476
+ - [Provide Services](https://weftui.dev/docs/how-to/provide-services): recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
477
+ - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model): hydrate-in-place and why there is no virtual DOM
478
+ - [Services and Context](https://weftui.dev/docs/explanation/services-and-context): how services flow from the app layer to every root
263
479
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core) · [`@weftui/router` reference](https://weftui.dev/docs/reference/router)