@weftui/core 0.27.1 → 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,7 +2,7 @@
2
2
  title: "@weftui/router"
3
3
  order: 3
4
4
  section: reference
5
- description: Full API surface for @weftui/router universal nested routing, type-safe href, layouts, and SSR entry points.
5
+ description: "Full API surface for @weftui/router: universal nested routing, type-safe href, layouts, and SSR entry points."
6
6
  ---
7
7
 
8
8
  # @weftui/router API Reference
@@ -23,9 +23,9 @@ Three entry points mirror `@weftui/dom`:
23
23
 
24
24
  The service value carries:
25
25
 
26
- - **`currentMatch`** the current match as a hot `Subscribable<RouteMatch>`; drives the outlet.
27
- - **`navigate(to, options?)`** navigates to a path (with optional query). `options.replace` swaps `pushState` for `replaceState`. On the client this updates History state and re-renders the affected outlet; on the server it is a no-op.
28
- - **`httpApiClient`** the derived [`RouterHttpApiClient`](#types) as an `Option`: `Option.some` on the client (`RouterLive`) for network work (route prefetch, future loaders), `Option.none` on the server (it is itself the origin). SPA URL→leaf resolution does **not** use this it stays local via the shared matcher.
26
+ - **`currentMatch`**: the current match as a hot `Subscribable<RouteMatch>`; drives the outlet.
27
+ - **`navigate(to, options?)`**: navigates to a path (with optional query). `options.replace` swaps `pushState` for `replaceState`. On the client this updates History state and re-renders the affected outlet; on the server it is a no-op.
28
+ - **`httpApiClient`**: the derived [`RouterHttpApiClient`](#types) as an `Option`. It is `Option.some` on the client (`RouterLive`) for network work (route prefetch, future loaders), `Option.none` on the server (it is itself the origin). SPA URL→leaf resolution does **not** use this; it stays local via the shared matcher.
29
29
 
30
30
  ### `Router.route`
31
31
 
@@ -36,22 +36,24 @@ Router.route<Path, Query, S>(
36
36
  ): RouteNode<Path, Query, E, R>;
37
37
  ```
38
38
 
39
- Declares a leaf page. `segment` is relative to the parent and may contain `:name` placeholders. `component` is a [`ComponentSlot`](#types) its `E`/`R` channels are recovered and propagate up the tree. The returned `RouteNode` is also the reference passed to [`href`](#href).
39
+ Declares a leaf page. `segment` is relative to the parent and may contain `:name` placeholders. `component` is a [`ComponentSlot`](#types); its `E`/`R` channels are recovered and propagate up the tree.
40
+
41
+ The returned `RouteNode` is also the reference passed to [`href`](#href).
40
42
 
41
43
  A leaf `component` reads the live match in **either** of two forms:
42
44
 
43
- - **Handler-arg props** declare `(props: `[`RouteHandlerProps<Path, Query>`](#types)`)` and the router passes the decoded `{ path, query }` straight in (`path`/`query` inferred from the route's `path`/`query` fields). A plain zero-arg thunk works too, ignoring the props.
44
- - **Dependency injection** a `Component.make` / `Component.gen` component reading the live match via [`Router.params`](#routerparams--routerquery) / `Router.query`. Required for layouts/deep nodes, which can't take handler args.
45
+ - **Handler-arg props**: declare `(props: `[`RouteHandlerProps<Path, Query>`](#types)`)` and the router passes the decoded `{ path, query }` straight in. `path`/`query` are inferred from the route's `path`/`query` fields. A plain zero-arg thunk works too, ignoring the props.
46
+ - **Dependency injection**: a `Component.make` / `Component.gen` component reading the live match via [`Router.params`](#routerparams--routerquery) / `Router.query`. Required for layouts/deep nodes, which can't take handler args.
45
47
 
46
48
  ```typescript
47
- // Handler-arg props decoded { path, query } passed in directly.
49
+ // Handler-arg props: decoded { path, query } passed in directly.
48
50
  const userRoute = Router.route("users/:id", {
49
51
  path: { id: Schema.NumberFromString },
50
52
  query: { tab: Schema.optional(Schema.String) },
51
53
  component: ({ path, query }) => h.div(`User ${path.id} (${query.tab ?? "info"})`),
52
54
  });
53
55
 
54
- // Dependency injection read the match anywhere via Router.params / Router.query.
56
+ // Dependency injection: read the match anywhere via Router.params / Router.query.
55
57
  const userRouteDI = Router.route("users/:id", {
56
58
  path: { id: Schema.NumberFromString },
57
59
  component: Component.gen(function* () {
@@ -67,7 +69,9 @@ const userRouteDI = Router.route("users/:id", {
67
69
  Router.layout<C, S>(config: { component: S }, children: C): LayoutNode<E, R>;
68
70
  ```
69
71
 
70
- Declares a layout purely UI nesting, owning **no path or segment**. `component` splices the injected outlet via `yield* Router.Outlet`. `Router.Outlet` is excluded from the layout's aggregate requirement channel (the router discharges it per render); the subtree's real channels are unioned in.
72
+ Declares a layout: purely UI nesting, owning **no path or segment**. `component` splices the injected outlet via `yield* Router.Outlet`.
73
+
74
+ `Router.Outlet` is excluded from the layout's aggregate requirement channel (the router discharges it per render); the subtree's real channels are unioned in.
71
75
 
72
76
  ### `Router.router`
73
77
 
@@ -85,12 +89,14 @@ Router.lazy<S extends ComponentSlot>(
85
89
  ): () => Node<Node.Error<SlotNode<S>>, Node.Context<SlotNode<S>>>;
86
90
  ```
87
91
 
88
- Wraps a dynamic-import loader as a component slot, so a route's **component** is code-split into its own chunk while the **descriptor** (segment + param schemas) stays eager. `load` returns a `Promise` resolving the component — typically `() => import("./page").then((m) => m.Page)`. Drops directly into `Router.route({ component })` and `Router.layout({ component })`.
92
+ Wraps a dynamic-import loader as a component slot. The route's **component** is code-split into its own chunk while the **descriptor** (segment + param schemas) stays eager.
93
+
94
+ `load` returns a `Promise` resolving the component, typically `() => import("./page").then((m) => m.Page)`. Drops directly into `Router.route({ component })` and `Router.layout({ component })`.
89
95
 
90
- - **Channels preserved.** The returned slot's `E`/`R` equal the resolved component's, so a lazy route has the identical type to declaring it eagerly (an unmet requirement is still a compile error at `Router.router`).
91
- - **Only the matched branch loads**, on the server during render and on the client on navigation. Client navigation is **deferred-commit**: the chunk **and the leaf component's own effect** resolve _before_ the URL commits (see [`Router.navigating`](#routernavigating) and [Blocking vs streaming data](#blocking-vs-streaming-data)), so the previous page stays mounted through both the fetch and any data the leaf awaits, and the swap is blank-free. The load `Promise` is memoized per slot, so revisits are synchronous.
92
- - **A rejected `load` is a defect** (`Effect.promise` dies) a deploy-skew/offline condition surfaced through normal defect handling, kept off the `E` channel; the rejection is memoized (no silent retry).
93
- - The resolved value should be a `Component` (`Component.make`/`Component.gen`); a bare `() => Node` thunk loses its channels through the loader `Promise` wrap it in `Component.make`.
96
+ - **Channels preserved.** The returned slot's `E`/`R` equal the resolved component's, so a lazy route has the identical type to declaring it eagerly. An unmet requirement is still a compile error at `Router.router`.
97
+ - **Only the matched branch loads**, on the server during render and on the client on navigation. Client navigation is **deferred-commit**: the chunk **and the leaf component's own effect** resolve _before_ the URL commits (see [`Router.navigating`](#routernavigating) and [Blocking vs streaming data](#blocking-vs-streaming-data)). The previous page stays mounted through both the fetch and any data the leaf awaits, so the swap is blank-free. The load `Promise` is memoized per slot, so revisits are synchronous.
98
+ - **A rejected `load` is a defect** (`Effect.promise` dies): a deploy-skew/offline condition surfaced through normal defect handling, kept off the `E` channel. The rejection is memoized (no silent retry).
99
+ - The resolved value should be a `Component` (`Component.make`/`Component.gen`); a bare `() => Node` thunk loses its channels through the loader `Promise`, so wrap it in `Component.make`.
94
100
 
95
101
  See the [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily) how-to and `packages/router/src/lazy-component.specs.md`.
96
102
 
@@ -105,7 +111,9 @@ Router.params<F extends Fields>(fields: F): Effect<FieldsType<F>, RouterParamsEr
105
111
  Router.query<F extends Fields>(fields: F): Effect<FieldsType<F>, RouterParamsError, Router>;
106
112
  ```
107
113
 
108
- Snapshot accessors that read the **live match** (`currentMatch.get`) and pick the requested `fields` keys from the decoded path/query. The matcher already decoded the values against the leaf's full schema, so they are returned directly (no re-validation). Readable from **any** component, not just the leaf — this is the dependency-injection path layouts and deep nodes use (leaves can instead take [handler-arg props](#routerroute)). They fail with a [`RouterParamsError`](#routerparamserror) (`source: "path" | "query"`, plus the requested `keys`) when no route matches.
114
+ Snapshot accessors that read the **live match** (`currentMatch.get`) and pick the requested `fields` keys from the decoded path/query. The matcher already decoded the values against the leaf's full schema, so they are returned directly (no re-validation).
115
+
116
+ Readable from **any** component, not just the leaf: this is the dependency-injection path layouts and deep nodes use (leaves can instead take [handler-arg props](#routerroute)). They fail with a [`RouterParamsError`](#routerparamserror) (`source: "path" | "query"`, plus the requested `keys`) when no route matches.
109
117
 
110
118
  ### `Router.paramsStream` / `Router.queryStream`
111
119
 
@@ -114,7 +122,9 @@ Router.paramsStream<F extends Fields>(fields: F): Effect<Subscribable<FieldsType
114
122
  Router.queryStream<F extends Fields>(fields: F): Effect<Subscribable<FieldsType<F>>, never, Router>;
115
123
  ```
116
124
 
117
- The **reactive** counterparts. Each resolves a `Subscribable<FieldsType<F>>` derived from `currentMatch.changes`, so a component can render `[(yield* Router.queryStream(fields)).changes]` and update **in place** even when the outlet keeps the same leaf mounted exactly the query-only case (`setQuery` / `patchQuery`) a snapshot `Router.query` would miss. Resilient across navigations: a `NotFound` match yields the empty subset rather than failing, so the stream stays live.
125
+ The **reactive** counterparts. Each resolves a `Subscribable<FieldsType<F>>` derived from `currentMatch.changes`. A component can render `[(yield* Router.queryStream(fields)).changes]` and update **in place** even when the outlet keeps the same leaf mounted. That is exactly the query-only case (`setQuery` / `patchQuery`) a snapshot `Router.query` would miss.
126
+
127
+ Resilient across navigations: a `NotFound` match yields the empty subset rather than failing, so the stream stays live.
118
128
 
119
129
  ### `Router.navigating`
120
130
 
@@ -130,24 +140,28 @@ readonly navigating: Subscribable.Subscribable<NavState>;
130
140
  Router.navigatingStream: Effect<Subscribable<NavState>, never, Router>;
131
141
  ```
132
142
 
133
- The reactive navigation-state signal, for rendering pending UI (a top progress bar, dimmed outlet) during a **deferred-commit** navigation. It transitions `Idle → Navigating{ to }` while the router resolves the target branch's [`Router.lazy`](#routerlazy) chunk **and** the matched leaf's own component effect, and back to `Idle` on commit one signal covering both the code and data windows. `NavState` is exported from `@weftui/router` and `@weftui/router/client`.
143
+ The reactive navigation-state signal, for rendering pending UI (a top progress bar, dimmed outlet) during a **deferred-commit** navigation. It transitions `Idle → Navigating{ to }` while the router resolves the target branch's [`Router.lazy`](#routerlazy) chunk **and** the matched leaf's own component effect, then back to `Idle` on commit. One signal covers both the code and data windows.
134
144
 
135
- - **Eager navigations never flip it** — a branch with no lazy node and a synchronously-resolving leaf commits synchronously and `navigating` stays `Idle`, so reading it costs nothing in an eager app.
136
- - **Synchronous resolutions never emit `Navigating`** — a revisit with both the chunk and the leaf's effect memoized (or an eager leaf with no async work) still commits in the same tick; the signal only flips when a resolution is genuinely async.
137
- - **Latest-wins** across rapid navigations (a superseded navigation never resets it and its pre-run is interrupted); **popstate** (back/forward) into a lazy or data-fetching route also reports; a **rejected chunk load or a failing leaf pre-run** (typed error or defect) resets it to `Idle` before the failure surfaces through the normal render error path.
145
+ `NavState` is exported from `@weftui/router` and `@weftui/router/client`.
146
+
147
+ - **Eager navigations never flip it**: a branch with no lazy node and a synchronously-resolving leaf commits synchronously and `navigating` stays `Idle`. Reading it costs nothing in an eager app.
148
+ - **Synchronous resolutions never emit `Navigating`**: a revisit with both the chunk and the leaf's effect memoized (or an eager leaf with no async work) still commits in the same tick; the signal only flips when a resolution is genuinely async.
149
+ - **Latest-wins** across rapid navigations: a superseded navigation never resets it and its pre-run is interrupted.
150
+ - **popstate** (back/forward) into a lazy or data-fetching route also reports.
151
+ - A **rejected chunk load or a failing leaf pre-run** (typed error or defect) resets it to `Idle` before the failure surfaces through the normal render error path.
138
152
  - **Server-side it is a constant `Idle`** (server render is buffered), so a component reading it type-checks and renders on both sides.
139
153
 
140
154
  See the [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) how-to, the [Blocking vs streaming data](#blocking-vs-streaming-data) section below, and `packages/router/src/resolve-before-commit.specs.md`.
141
155
 
142
156
  ### Blocking vs streaming data
143
157
 
144
- A leaf `component` is already an `Effect` there is no separate loader so where you put an await decides whether it blocks the navigation commit or streams in afterward:
158
+ A leaf `component` is already an `Effect`; there is no separate loader. Where you put an await decides whether it blocks the navigation commit or streams in afterward:
145
159
 
146
- - **`yield*` in the component body** → commit-blocking. The pre-run executes the leaf's effect to completion before the URL commits; the previous page stays mounted for the whole window, and `Router.navigating` reports `Navigating{ to }` for its duration.
160
+ - **`yield*` in the component body** → commit-blocking. The pre-run executes the leaf's effect to completion before the URL commits. The previous page stays mounted for the whole window, and `Router.navigating` reports `Navigating{ to }` for its duration.
147
161
  - **An `Effect`/`Stream` placed as a child node** → streaming. The commit is not delayed; the leaf mounts immediately and the child region fills in place once its own effect resolves.
148
162
 
149
163
  ```typescript
150
- // Blocking the await is in the leaf's own body; navigation waits for it.
164
+ // Blocking: the await is in the leaf's own body; navigation waits for it.
151
165
  const DocPage = Component.gen(function* () {
152
166
  const { category, slug } = yield* Router.params({ category: Schema.String, slug: Schema.String });
153
167
  const docs = yield* Docs;
@@ -155,7 +169,7 @@ const DocPage = Component.gen(function* () {
155
169
  return yield* h.article([h.h1(doc.title), h.div({ innerHTML: doc.html })]);
156
170
  });
157
171
 
158
- // Streaming the await lives on a child; the leaf commits immediately.
172
+ // Streaming: the await lives on a child; the leaf commits immediately.
159
173
  const DocPage = Component.gen(function* () {
160
174
  const { category, slug } = yield* Router.params({ category: Schema.String, slug: Schema.String });
161
175
  const docs = yield* Docs;
@@ -171,9 +185,11 @@ const DocPage = Component.gen(function* () {
171
185
  });
172
186
  ```
173
187
 
174
- Choose blocking for primary content the page is meaningless without (an article body, a product's price) the old page stays visible with no blank or skeleton. Choose streaming for secondary or slow regions where partial content is still useful (a comments panel, a "related" rail) the commit isn't held hostage by one slow fetch.
188
+ Choose blocking for primary content the page is meaningless without (an article body, a product's price); the old page stays visible with no blank or skeleton. Choose streaming for secondary or slow regions where partial content is still useful (a comments panel, a "related" rail); the commit isn't held hostage by one slow fetch.
189
+
190
+ Only the matched **leaf** is pre-run this way. Layout components in the branch get their chunks preloaded, but their bodies still run at render, post-commit. Unchanged layouts don't re-render across navigations, so this rarely matters in practice.
175
191
 
176
- Only the matched **leaf** is pre-run this way; layout components in the branch get their chunks preloaded but their bodies still run at render, post-commit (unchanged layouts don't re-render across navigations, so this rarely matters in practice). Pre-run failures `notFound()`, a typed error, or a defect still commit the URL and replay through the normal render error path (the nearest `Boundary`, or the router's 404 boundary) without re-running the component.
192
+ Pre-run failures (`notFound()`, a typed error, or a defect) still commit the URL. They replay through the normal render error path (the nearest `Boundary`, or the router's 404 boundary) without re-running the component.
177
193
 
178
194
  See [Load Async Data](https://weftui.dev/docs/how-to/load-async-data#blocking-on-navigation-vs-streaming-in-place) and [RPC Data Boundaries](https://weftui.dev/docs/how-to/load-data-with-rpc) for the streaming patterns in full.
179
195
 
@@ -201,9 +217,9 @@ href(postsRoute, { path: { id: 1 }, query: { sort: "new" } }); // "/users/1/post
201
217
  RouterApp<E, R>(def: RouterDef<E, R>): Node<Exclude<E, RouterNotFound>, R | Router>;
202
218
  ```
203
219
 
204
- The universal router root node render this on both server and client. Wraps the nested outlet in the router's internal not-found boundary, so a `RouterNotFound` raised by a page renders the configured `notFound` page in place. Server dispatch runs through `HttpApiBuilder`: a page-raised `RouterNotFound` and a no-match surface their 404 through the platform request pipeline.
220
+ The universal router root node; render this on both server and client. Wraps the nested outlet in the router's internal not-found boundary, so a `RouterNotFound` raised by a page renders the configured `notFound` page in place. Server dispatch runs through `HttpApiBuilder`: a page-raised `RouterNotFound` and a no-match surface their 404 through the platform request pipeline.
205
221
 
206
- `RouterApp` requires `Router` in its environment provide it via `RouterLive` (client) or `RouterServer` (server), not `Effect.provide` at the node level (that would release the scoped layer immediately).
222
+ `RouterApp` requires `Router` in its environment. Provide it via `RouterLive` (client) or `RouterServer` (server), not `Effect.provide` at the node level. Services under `WeftApp` come exclusively from the app's layer, not ambient `Effect.provide`.
207
223
 
208
224
  ### `outletNode` (a.k.a. `RouterOutlet`)
209
225
 
@@ -211,9 +227,9 @@ The universal router root node — render this on both server and client. Wraps
211
227
  outletNode<E, R>(def: RouterDef<E, R>): Node<E | RouterNotFound, R | Router>;
212
228
  ```
213
229
 
214
- The bare nested-outlet node without the internal not-found boundary for callers placing their own not-found handling. Re-exported from `@weftui/router/client` as `RouterOutlet`.
230
+ The bare nested-outlet node without the internal not-found boundary, for callers placing their own not-found handling. Re-exported from `@weftui/router/client` as `RouterOutlet`.
215
231
 
216
- ## Client `@weftui/router/client`
232
+ ## Client: `@weftui/router/client`
217
233
 
218
234
  ### `RouterLive`
219
235
 
@@ -224,11 +240,15 @@ RouterLive(
224
240
  ): Layer.Layer<Router | AppRpcClientTag>;
225
241
  ```
226
242
 
227
- The client `Router` layer, backed by the History API. Seeds a `SubscriptionRef` from `window.location`, listens for `popstate`, installs the same-origin link-click interceptor, and derives the `HttpApiClient` exposed as `Router.httpApiClient` (over `FetchHttpClient`; `baseUrl` defaults to same-origin). Alongside `Router` it also provides the core [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam — a **network** flat rpc client over the app's merged `RpcGroup` (`RpcClient.make` → `POST /_eui/rpc`) — so `@weftui/dom` can resolve a [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) (hydrated refetch and client-first SPA mount) without depending on this package or `effect/unstable/rpc`. Pass the same merged `group` the server wires into [`RouterServer`](#routerserver). **Scoped** — it must outlive the mount, so provide it through a `ManagedRuntime`:
243
+ The client `Router` layer, backed by the History API. Seeds a `SubscriptionRef` from `window.location`, listens for `popstate`, installs the same-origin link-click interceptor, and derives the `HttpApiClient` exposed as `Router.httpApiClient` (over `FetchHttpClient`; `baseUrl` defaults to same-origin).
244
+
245
+ Alongside `Router` it also provides the core [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam: a **network** flat rpc client over the app's merged `RpcGroup` (`RpcClient.make` → `POST /_eui/rpc`). This lets `@weftui/dom` resolve a [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) (hydrated refetch and client-first SPA mount) without depending on this package or `effect/unstable/rpc`. Pass the same merged `group` the server wires into [`RouterServer`](#routerserver).
246
+
247
+ **Scoped**: it must outlive the mount. Give it to `WeftApp.make` and the app runtime owns its lifetime (built lazily on first mount, released at `WeftApp.dispose`):
228
248
 
229
249
  ```typescript
230
- const runtime = ManagedRuntime.make(RouterLive(App, { rpc: { group: StockRpcs } }));
231
- void runtime.runPromise(hydrate(RouterApp(App), root));
250
+ const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
251
+ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
232
252
  ```
233
253
 
234
254
  ### Programmatic navigation
@@ -245,10 +265,10 @@ patchQuery(partial: Record<string, unknown>, options?: NavigateOptions): Effect<
245
265
 
246
266
  Typed programmatic navigation, all run within the `RouterLive` layer (except `back`/`forward`, which only touch `window.history`):
247
267
 
248
- - **`navigate(ref, args)`** go to a leaf route `ref` with typed `{ path, query }`, building the URL via [`href`](#href) (so it round-trips with `match`) and pushing or, with `options.replace`, replacing — the History entry. Same requiredness rules as `href`: `path` required when the route has path params, `query` optional when every field is optional.
249
- - **`push` / `replace`** go to a raw `path + search` string, pushing or replacing the History entry.
250
- - **`back` / `forward`** step through History (`history.go(±1)`); the `popstate` handler resyncs.
251
- - **`setQuery` / `patchQuery`** change the current route's query in place, re-encoding through the matched leaf's `querySchema` (the path is kept, so the leaf stays mounted and reactive `queryStream` readers update). `setQuery` replaces the query; `patchQuery` merges. No-op when no route is matched.
268
+ - **`navigate(ref, args)`**: go to a leaf route `ref` with typed `{ path, query }`. Builds the URL via [`href`](#href) (so it round-trips with `match`) and pushes the History entry, or replaces it with `options.replace`. Same requiredness rules as `href`: `path` required when the route has path params, `query` optional when every field is optional.
269
+ - **`push` / `replace`**: go to a raw `path + search` string, pushing or replacing the History entry.
270
+ - **`back` / `forward`**: step through History (`history.go(±1)`); the `popstate` handler resyncs.
271
+ - **`setQuery` / `patchQuery`**: change the current route's query in place, re-encoding through the matched leaf's `querySchema`. The path is kept, so the leaf stays mounted and reactive `queryStream` readers update. `setQuery` replaces the query; `patchQuery` merges. No-op when no route is matched.
252
272
 
253
273
  ```typescript
254
274
  import { navigate, patchQuery, push } from "@weftui/router/client";
@@ -260,7 +280,9 @@ yield * patchQuery({ sort: "old" }); // keeps the current path + other query fie
260
280
 
261
281
  ### Scroll reset
262
282
 
263
- Every client-committed navigation whose path differs from the previously committed path `navigate`, `push`, `replace`, and the link interceptor resets `window.scrollTo(0, 0)` synchronously at commit. Query-only navigations (`setQuery` / `patchQuery`, or any push/replace to the same path) preserve scroll. `popstate` (`back` / `forward`) never resets; the browser's own `history.scrollRestoration: "auto"` restores the prior offset for those entries. Server rendering is unaffected. Not configurable — there is no `NavigateOptions` field to opt out.
283
+ Every client-committed navigation whose path differs from the previously committed path (`navigate`, `push`, `replace`, and the link interceptor) resets `window.scrollTo(0, 0)` synchronously at commit. Query-only navigations (`setQuery` / `patchQuery`, or any push/replace to the same path) preserve scroll.
284
+
285
+ `popstate` (`back` / `forward`) never resets; the browser's own `history.scrollRestoration: "auto"` restores the prior offset for those entries. Server rendering is unaffected. Not configurable: there is no `NavigateOptions` field to opt out.
264
286
 
265
287
  ### `installLinkInterceptor`
266
288
 
@@ -268,9 +290,9 @@ Every client-committed navigation whose path differs from the previously committ
268
290
  installLinkInterceptor(def: RouterDef, navigate: (to: string) => Effect<void>): Effect<void, never, Scope>;
269
291
  ```
270
292
 
271
- The delegated click interceptor `RouterLive` installs for you. Exposed for advanced/manual wiring. Intercepts plain same-origin clicks whose href resolves to a route (resolved against `def`); leaves modified clicks, `target=_blank`, `download`, external origins, same-document navigations, and non-matching hrefs to the browser.
293
+ The delegated click interceptor `RouterLive` installs for you. Exposed for advanced/manual wiring. Intercepts plain same-origin clicks whose href resolves to a route (resolved against `def`). It leaves modified clicks, `target=_blank`, `download`, external origins, same-document navigations, and non-matching hrefs to the browser.
272
294
 
273
- ## Server `@weftui/router/server`
295
+ ## Server: `@weftui/router/server`
274
296
 
275
297
  ### `RouterServer`
276
298
 
@@ -281,14 +303,14 @@ RouterServer.render(def, options: { document; rpc; url }): Effect<{ html; status
281
303
  RouterServer.toWebHandler(def, options: { document; rpc }): (request: Request) => Promise<Response>;
282
304
  ```
283
305
 
284
- Dispatch runs through the `def.httpApi` spine via `HttpApiBuilder`: platform owns request→leaf matching and path/query decode, then each leaf handler builds a fixed-match server `Router` and renders the universal outlet to hydratable HTML.
306
+ Dispatch runs through the `def.httpApi` spine via `HttpApiBuilder`: platform owns request→leaf matching and path/query decode. Each leaf handler then builds a fixed-match server `Router` and renders the universal outlet to hydratable HTML.
285
307
 
286
308
  - **`toWebHandler`** returns a Web `fetch`-style handler `(Request) => Promise<Response>` that dispatches through `HttpApiBuilder.toWebHandler` and replies `text/html`. Suitable for bridging into a dev server (Vite) or any Web-platform server.
287
- - **`render`** drives that handler for a single `url`, returning `{ html, status }` with `<!DOCTYPE html>` prepended. `status` is sourced from the platform pipeline `200`, or `404` for a no-match / a page-raised `RouterNotFound`.
309
+ - **`render`** drives that handler for a single `url`, returning `{ html, status }` with `<!DOCTYPE html>` prepended. `status` is sourced from the platform pipeline: `200`, or `404` for a no-match / a page-raised `RouterNotFound`.
288
310
  - **`document`** is a [`ComponentSlot`](#types) that splices the app via `yield* Router.Outlet`; the router provides both `Router.Outlet` (the app, per request) and `Router`.
289
- - **`rpc`** is the app's [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) data foundation: `{ group: RpcGroup<any>; handlers: Layer<any, never, never> }` the merged `RpcGroup` (shared with the client) plus its server-only handler Layer (`group.toLayer(...)` ⊕ its dependencies). `toWebHandler` serves the handlers at `POST /_eui/rpc` (so a client refetch / client-first mount re-runs them on the server), and an in-process client over the same handlers (backing the [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam) resolves SSR boundaries in-process never over the network.
311
+ - **`rpc`** is the app's [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) data foundation: `{ group: RpcGroup<any>; handlers: Layer<any, never, never> }`. That is the merged `RpcGroup` (shared with the client) plus its server-only handler Layer (`group.toLayer(...)` ⊕ its dependencies). `toWebHandler` serves the handlers at `POST /_eui/rpc`, so a client refetch / client-first mount re-runs them on the server. An in-process client over the same handlers (backing the [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam) resolves SSR boundaries in-process, never over the network.
290
312
 
291
- > The `HttpApi` is not generated on the server it is built once when the tree is sealed (`buildHttpApi` during `Router.router`) and lives on `def.httpApi` as the single source of truth both the server dispatch and the client matcher / derived `HttpApiClient` read from.
313
+ > The `HttpApi` is not generated on the server. It is built once when the tree is sealed (`buildHttpApi` during `Router.router`) and lives on `def.httpApi`. That is the single source of truth both the server dispatch and the client matcher / derived `HttpApiClient` read from.
292
314
 
293
315
  ## Errors
294
316
 
@@ -338,7 +360,7 @@ These power the runtime and are exported for tooling/tests; most apps never touc
338
360
  | `RouteHandlerProps<Path, Query>` | The `{ path, query }` decoded match a leaf `component` may declare as handler-arg props. |
339
361
  | `RouteMatch` | `{ _tag: "Matched"; leaf; path; query; url }` or `{ _tag: "NotFound"; url }`. |
340
362
  | `HrefArgs<Path, Query>` | The `href` argument object; `path`/`query` become optional when their decoded type has no required keys. |
341
- | `NavigateOptions` | `{ replace?: boolean }` for `navigate` / `setQuery` / `patchQuery` and `Router.navigate`. |
363
+ | `NavigateOptions` | `{ replace?: boolean }`, for `navigate` / `setQuery` / `patchQuery` and `Router.navigate`. |
342
364
  | `RouterHttpApiClient` | The platform `HttpApiClient` derived from a router's `HttpApi` spine (carried opaquely on `Router.httpApiClient`). |
343
365
  | `Fields` / `FieldsType<F>` | `Schema.Struct.Fields` and the `Type` side of its `Schema.Struct`. |
344
366
  | `Compiled` / `CompiledLeaf` / `CompiledLayout` | The compiled tree shapes. |
@@ -346,7 +368,7 @@ These power the runtime and are exported for tooling/tests; most apps never touc
346
368
 
347
369
  ## See also
348
370
 
349
- - [Add Routing](https://weftui.dev/docs/how-to/add-routing) the narrative guide to authoring a route tree
350
- - [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily) · [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) `Router.lazy` and `Router.navigating`
371
+ - [Add Routing](https://weftui.dev/docs/how-to/add-routing): the narrative guide to authoring a route tree
372
+ - [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily) · [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress): `Router.lazy` and `Router.navigating`
351
373
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core) · [`@weftui/dom` reference](https://weftui.dev/docs/reference/dom)
352
- - [`packages/router/router.specs.md`](https://github.com/stefvw93/weft/blob/main/packages/router/router.specs.md) the full specification
374
+ - [`packages/router/router.specs.md`](https://github.com/stefvw93/weft/blob/main/packages/router/router.specs.md): the full specification
@@ -2,14 +2,12 @@
2
2
  title: Your First App
3
3
  order: 1
4
4
  section: tutorial
5
- description: Install Weft, build a component with the h namespace, and mount it the smallest possible Weft app.
5
+ description: "Install Weft, build a component with the h namespace, and mount it: the smallest possible Weft app."
6
6
  ---
7
7
 
8
8
  # Your First App
9
9
 
10
- This is the first step of a four-part tutorial that builds up a Weft app from a static component to a server-rendered, error-handled one. By the end you will have touched every core idea; each step adds exactly one.
11
-
12
- We assume you know [Effect](https://effect.website/docs/getting-started/introduction) fundamentals — Weft is Effect for the UI, and we will not re-explain `Effect.gen`, services, or streams from scratch.
10
+ We assume you know [Effect](https://effect.website/docs/getting-started/introduction) fundamentals. Weft is Effect for the UI, so we will not re-explain `Effect.gen`, services, or streams from scratch.
13
11
 
14
12
  ## Install
15
13
 
@@ -23,28 +21,29 @@ Weft tracks Effect 4's beta line. This release is built and tested against `effe
23
21
 
24
22
  ## Build a component
25
23
 
26
- A **component is a plain function you call** there is no JSX and no `<Component/>` deferral. It returns a `Node`, which is just an `Effect` that produces a DOM node:
24
+ A **component is a plain function you call**. There is no JSX and no `<Component/>` deferral. It returns a `Node`, which is just an `Effect` that produces a DOM node:
27
25
 
28
26
  ```typescript
29
27
  import { h } from "@weftui/core";
30
- import { mount } from "@weftui/dom/client";
28
+ import { WeftApp } from "@weftui/dom/client";
31
29
  import { Effect } from "effect";
32
30
 
33
31
  function App() {
34
32
  return h.div({ class: "app" }, [h.h1("Hello, Weft"), h.p("A minimal app.")]);
35
33
  }
36
34
 
37
- void Effect.runPromise(mount(App(), document.getElementById("root")!));
35
+ const app = WeftApp.make();
36
+ void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
38
37
  ```
39
38
 
40
39
  The `h` namespace is the entry point: every property (`h.div`, `h.h1`, `h.button`, …) is a builder for that HTML tag. A builder takes optional props and children, and returns a `Node`.
41
40
 
42
41
  ## What just happened
43
42
 
44
- - `App()` returns a **`Node<never, never>`** the two type parameters are the error channel (`E`) and the requirement channel (`R`), both `never` here because this component neither fails nor needs a service. As your app grows, those channels accumulate what it can fail with and what it depends on. That is the whole point of Weft's types see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model).
45
- - `mount(node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns an `Effect<MountHandle>`; run it with your Effect runtime (`Effect.runPromise` is fine for a script).
46
- - The component function runs **once**. Nothing here re-runs on a timer or a state change because there is no state yet. That comes next.
43
+ - `App()` returns a **`Node<never, never>`**. The two type parameters are the error channel (`E`) and the requirement channel (`R`), both `never` here because this component neither fails nor needs a service. As your app grows, those channels accumulate what it can fail with and what it depends on. That is the whole point of Weft's types: see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model).
44
+ - `WeftApp.make()` creates a Weft app synchronously, with no layer to build yet. `WeftApp.mount(app, node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns `Effect<RootHandle, …>` with `R = never`, so a bare `Effect.runPromise` runs it with no `Effect.provide` needed. You will give `WeftApp.make` a `Layer` once components need services: see [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async).
45
+ - The component function runs **once**. Nothing here re-runs on a timer or a state change, because there is no state yet. That comes next.
47
46
 
48
47
  ## Next
49
48
 
50
- - [Reactivity →](https://weftui.dev/docs/tutorial/02-reactivity) make the UI change over time with `SubscriptionRef` and streams
49
+ - [Reactivity →](https://weftui.dev/docs/tutorial/02-reactivity): make the UI change over time with `SubscriptionRef` and streams
@@ -7,7 +7,7 @@ description: Add component-local state with SubscriptionRef and weave its stream
7
7
 
8
8
  # Reactivity
9
9
 
10
- [Previously](https://weftui.dev/docs/tutorial/01-your-first-app) we mounted a static component. Now we make it change over time the defining move in Weft: **weave a stream through the tree, and only that point updates.**
10
+ [Previously](https://weftui.dev/docs/tutorial/01-your-first-app) we mounted a static component. Now we make it change over time. This is the defining move in Weft: **weave a stream through the tree, and only that point updates.**
11
11
 
12
12
  ## Local state with `SubscriptionRef`
13
13
 
@@ -15,7 +15,7 @@ Use Effect's `SubscriptionRef` for component-local state. `SubscriptionRef.chang
15
15
 
16
16
  ```typescript
17
17
  import { h } from "@weftui/core";
18
- import { mount } from "@weftui/dom/client";
18
+ import { WeftApp } from "@weftui/dom/client";
19
19
  import { Effect, SubscriptionRef } from "effect";
20
20
 
21
21
  const Counter = () =>
@@ -29,18 +29,21 @@ const Counter = () =>
29
29
  ]);
30
30
  });
31
31
 
32
- void Effect.runPromise(mount(Counter(), document.getElementById("root")!));
32
+ const app = WeftApp.make();
33
+ void Effect.runPromise(WeftApp.mount(app, Counter(), document.getElementById("root")!));
33
34
  ```
34
35
 
35
- `Effect.gen` lets you `yield*` the `SubscriptionRef` to set up state **before** building the tree. Because a `Node` is an `Effect`, the component body is an ordinary generator no hooks, no dependency arrays.
36
+ `Effect.gen` lets you `yield*` the `SubscriptionRef` to set up state **before** building the tree. Because a `Node` is an `Effect`, the component body is an ordinary generator: no hooks, no dependency arrays.
36
37
 
37
38
  ## The key idea: the body runs once
38
39
 
39
- The `Counter` function runs **exactly once**. It creates the ref, builds the tree, and returns. After that, nothing re-invokes it the only thing that changes the DOM is the `SubscriptionRef.changes(count)` stream woven into the `h.span`. When you click `+`, `SubscriptionRef.update` pushes a new value, the stream emits, and the renderer patches _just that span's text_ in place. No diff, no re-render, no sibling touched.
40
+ The `Counter` function runs **exactly once**. It creates the ref, builds the tree, and returns. After that, nothing re-invokes it. The only thing that changes the DOM is the `SubscriptionRef.changes(count)` stream woven into the `h.span`.
41
+
42
+ When you click `+`, `SubscriptionRef.update` pushes a new value, the stream emits, and the renderer patches _just that span's text_ in place. No diff, no re-render, no sibling touched.
40
43
 
41
44
  This is what "streams are the weft" means in practice: reactivity is local to exactly where you thread a stream. Everything else is static. The full model is [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model); the vocabulary of stream-shaped values is [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives).
42
45
 
43
- > **Note.** `[SubscriptionRef.changes(count)]` the stream is passed as a child array. Static values (`"Hello"`, `5`) work in the same position and simply never change. The rule is uniform: a plain value is static, a stream-shaped value is reactive.
46
+ > **Note.** In `[SubscriptionRef.changes(count)]` the stream is passed as a child array. Static values (`"Hello"`, `5`) work in the same position and simply never change. The rule is uniform: a plain value is static, a stream-shaped value is reactive.
44
47
 
45
48
  ## Deriving values
46
49
 
@@ -50,8 +53,8 @@ Because `SubscriptionRef.changes(count)` returns a `Stream`, you shape reactive
50
53
  h.span([Stream.map(SubscriptionRef.changes(count), (n) => `Count: ${n}`)]);
51
54
  ```
52
55
 
53
- Anywhere you would compute a derived value, map the stream instead the derivation stays reactive.
56
+ Anywhere you would compute a derived value, map the stream instead. The derivation stays reactive.
54
57
 
55
58
  ## Next
56
59
 
57
- - [Services and Async →](https://weftui.dev/docs/tutorial/03-services-and-async) pull dependencies from the environment and render async loading states
60
+ - [Services and Async →](https://weftui.dev/docs/tutorial/03-services-and-async): pull dependencies from the environment and render async loading states
@@ -7,16 +7,16 @@ description: Give handlers access to services from the environment, and render a
7
7
 
8
8
  # Services and Async
9
9
 
10
- [So far](https://weftui.dev/docs/tutorial/02-reactivity) our state has been self-contained. Real apps talk to services and wait on async work. Both fall out of the same fact a `Node` is an `Effect` so both use plain Effect.
10
+ [So far](https://weftui.dev/docs/tutorial/02-reactivity) our state has been self-contained. Real apps talk to services and wait on async work. Both fall out of the same fact (a `Node` is an `Effect`), so both use plain Effect.
11
11
 
12
12
  ## Handlers that use services
13
13
 
14
- An event handler can **return an Effect**, and that Effect runs in the component's environment so it can read any service you provide at the mount boundary:
14
+ An event handler can **return an Effect**. That Effect runs in the app's environment, so it can read any service the app's layer provides:
15
15
 
16
16
  ```typescript
17
17
  import { h } from "@weftui/core";
18
- import { mount } from "@weftui/dom/client";
19
- import { Context, Effect, Layer, pipe } from "effect";
18
+ import { WeftApp } from "@weftui/dom/client";
19
+ import { Context, Effect, Layer } from "effect";
20
20
 
21
21
  class Logger extends Context.Service<Logger, { log: (message: string) => Effect.Effect<void> }>()(
22
22
  "Logger",
@@ -38,13 +38,14 @@ const LogButton = () =>
38
38
  "Log",
39
39
  );
40
40
 
41
- // Provide the layer at mount every handler in the tree can now read Logger.
42
- void Effect.runPromise(
43
- pipe(mount(LogButton(), document.getElementById("root")!), Effect.provide(LoggerLive)),
44
- );
41
+ // Give the app the layer: every handler in every root can now read Logger.
42
+ const app = WeftApp.make(LoggerLive);
43
+ void Effect.runPromise(WeftApp.mount(app, LogButton(), document.getElementById("root")!));
45
44
  ```
46
45
 
47
- `Logger` entered the tree's requirement channel the moment `LogButton` read it, and you discharged it **once**, at `mount`, with `Effect.provide`. Provide too little and it is a compile error at the mount call. This is Weft's entire dependency-injection story it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
46
+ `Logger` entered the tree's requirement channel the moment `LogButton` read it. You discharged it **once**, by passing `LoggerLive` to `WeftApp.make`. Provide too little and it is a compile error. The type of `app` (and so of `WeftApp.mount(app, LogButton(), …)`) names exactly which service is missing.
47
+
48
+ Services come exclusively from the app's layer: an `Effect.provide` wrapped around the `mount` call does **not** reach components or handlers. This is Weft's entire dependency-injection story; it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
48
49
 
49
50
  ## Async loading states
50
51
 
@@ -52,7 +53,7 @@ A component can return a **`Stream<Node>`** to show different content over time.
52
53
 
53
54
  ```typescript
54
55
  import { h } from "@weftui/core";
55
- import { mount } from "@weftui/dom/client";
56
+ import { WeftApp } from "@weftui/dom/client";
56
57
  import { Effect, Stream } from "effect";
57
58
 
58
59
  const AsyncGreeting = ({ name }: { name: string }) =>
@@ -66,11 +67,16 @@ const AsyncGreeting = ({ name }: { name: string }) =>
66
67
  ),
67
68
  );
68
69
 
69
- void Effect.runPromise(mount(AsyncGreeting({ name: "World" }), document.getElementById("root")!));
70
+ const app = WeftApp.make();
71
+ void Effect.runPromise(
72
+ WeftApp.mount(app, AsyncGreeting({ name: "World" }), document.getElementById("root")!),
73
+ );
70
74
  ```
71
75
 
72
- The stream emits the loading node first, then the resolved node the renderer swaps the DOM in place on the second emission. This is the raw mechanism; for coordinating _several_ async regions with a single fallback, reach for [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense), which you will meet in the next step.
76
+ The stream emits the loading node first, then the resolved node. The renderer swaps the DOM in place on the second emission.
77
+
78
+ This is the raw mechanism. To coordinate _several_ async regions with a single fallback, reach for [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense), which you will meet in the next step.
73
79
 
74
80
  ## Next
75
81
 
76
- - [Errors and Server Rendering →](https://weftui.dev/docs/tutorial/04-errors-and-server) catch failures with boundaries and render on the server
82
+ - [Errors and Server Rendering →](https://weftui.dev/docs/tutorial/04-errors-and-server): catch failures with boundaries and render on the server
@@ -2,12 +2,12 @@
2
2
  title: Errors and Server Rendering
3
3
  order: 4
4
4
  section: tutorial
5
- description: Catch rendering-path failures with Boundary, then render on the server and hydrate the last step of the tutorial.
5
+ description: Catch rendering-path failures with Boundary, then render on the server and hydrate. The last step of the tutorial.
6
6
  ---
7
7
 
8
8
  # Errors and Server Rendering
9
9
 
10
- The final step. [We can now](https://weftui.dev/docs/tutorial/03-services-and-async) use services and async; here we handle what happens when async work **fails**, and how the same tree renders on the server.
10
+ The final step. [We can now](https://weftui.dev/docs/tutorial/03-services-and-async) use services and async. Here we handle what happens when async work **fails**, and how the same tree renders on the server.
11
11
 
12
12
  ## Error boundaries
13
13
 
@@ -25,11 +25,13 @@ const SafeWidget = () =>
25
25
  ]);
26
26
  ```
27
27
 
28
- There are six failure-catch variants `catch`, `catchCause`, `catchTag`, `catchTags`, `catchFilter`, `catchIf` — mirroring Effect's own error operators. A failure that a boundary does not match re-raises to the **nearest enclosing** boundary; if none catches it, the mount fails. The conceptual model (and why the boundary's type reflects exactly which failures are handled) is [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense).
28
+ There are six failure-catch variants, mirroring Effect's own error operators: `catch`, `catchCause`, `catchTag`, `catchTags`, `catchFilter`, `catchIf`. A failure that a boundary does not match re-raises to the **nearest enclosing** boundary. If none catches it, the mount fails.
29
+
30
+ The conceptual model (and why the boundary's type reflects exactly which failures are handled) is [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense).
29
31
 
30
32
  ## Render on the server
31
33
 
32
- The same component tree renders to HTML on the server and **hydrates in place** on the client no re-render, no flash. The server produces markup (plus inline data), and `hydrate` adopts that existing DOM and resumes reactivity:
34
+ The same component tree renders to HTML on the server and **hydrates in place** on the client: no re-render, no flash. The server produces markup (plus inline data). `hydrate` adopts that existing DOM and resumes reactivity:
33
35
 
34
36
  ```typescript
35
37
  // server entry
@@ -42,14 +44,22 @@ export const render = () => Effect.runPromise(renderToStringHydratable(App()));
42
44
 
43
45
  ```typescript
44
46
  // client entry
45
- import { hydrate } from "@weftui/dom/client";
47
+ import { WeftApp } from "@weftui/dom/client";
46
48
  import { Effect } from "effect";
47
49
  import { App } from "./app";
48
50
 
49
- void Effect.runPromise(hydrate(App(), document.getElementById("root")!));
51
+ const app = WeftApp.make();
52
+ void Effect.runPromise(WeftApp.hydrate(app, App(), document.getElementById("root")!));
50
53
  ```
51
54
 
52
- The same side-effect-free `App` is imported by both entries. For server-resolved data that replays into the client without a second request, `Boundary.rpc` extends this model — resolve an rpc on the server, serialize its result into the HTML, replay it on hydrate, then keep the region live for refetch.
55
+ The same side-effect-free `App` is imported by both entries.
56
+
57
+ For server-resolved data that replays into the client without a second request, `Boundary.rpc` extends this model:
58
+
59
+ 1. Resolve an rpc on the server.
60
+ 2. Serialize its result into the HTML.
61
+ 3. Replay it on hydrate.
62
+ 4. Keep the region live for refetch.
53
63
 
54
64
  ## You're done
55
65
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@weftui/core",
3
- "version": "0.27.1",
4
- "description": "Element builders and combinators for Weft reactive UI, woven from Effect",
3
+ "version": "0.29.0",
4
+ "description": "Element builders and combinators for Weft: reactive UI, woven from Effect",
5
5
  "keywords": [
6
6
  "combinators",
7
7
  "dom",
@@ -41,13 +41,14 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^25.9.2",
44
+ "@types/node": "^26.1.1",
45
45
  "csstype": "^3.2.3",
46
46
  "effect": "4.0.0-beta.98",
47
- "tsx": "^4.22.4",
48
- "typescript": "^6.0.3",
49
- "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
50
- "vite-plus": "0.2.2"
47
+ "tstyche": "^7.2.2",
48
+ "tsx": "^4.23.1",
49
+ "typescript": "^7.0.2",
50
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.5",
51
+ "vite-plus": "0.2.5"
51
52
  },
52
53
  "peerDependencies": {
53
54
  "effect": ">=4.0.0-beta.98 <4.0.0"