kopular 0.13.0 → 0.15.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.
package/LLM.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # Kopular — LLM reference
2
2
 
3
3
  Complete reference for generating correct Kopular code. This is a spec, not a tutorial —
4
- see `README.md` for narrative/rationale. Kopular is 8 files total; this covers all of
5
- them. For the host language, see KopScript's own `LLM.md` in the `Kop` repo (or its
4
+ see `README.md` for narrative/rationale. Kopular is 10 files total; this covers all of
5
+ them. For the host language, see KopScript's own `LLM.md` in the `KopScript` repo (or its
6
6
  published `LLM.md` on the `kopscript` npm package) — that reference is a prerequisite,
7
7
  not repeated here.
8
8
 
9
9
  Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
10
+ `kopular/velement` (VElement — what `Render()` returns; see "Component" below),
10
11
  `kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives`
11
12
  (If), `kopular/http` (Http), `kopular/forms` (FormField, Validators), `kopular/testing`
12
- (runKopularApp, runKopularFixture — see below). Also ships a bin, `kp` `npx kp new
13
+ (runKopularApp, runKopularFixture — see below). `kopular/vdom` (the diff/patch engine
14
+ behind `Update()`) is internal — nothing outside `component.ks` needs to import it
15
+ directly. Also ships a bin, `kp` — `npx kp new
13
16
  <dir>` scaffolds a new project (the `extern` bindings below, plus the vendor/serve
14
17
  scripts needed to run in a browser) rather than requiring it be reconstructed by hand;
15
18
  prefer it over hand-writing the section below for a new project. Templates (`template
@@ -22,9 +25,25 @@ there is no `kopular/template` entry point to import.
22
25
  included) always goes through `extern`, re-describing exactly the members you use:
23
26
 
24
27
  ```ks
28
+ extern class VElement {
29
+ static VElement Create(string tag);
30
+ string TextContent { get; set; }
31
+ string ClassName { get; set; }
32
+ string Id { get; set; }
33
+ string Value { get; set; }
34
+ string RawHtml { get; set; }
35
+ (Event) => void OnClick { get; set; }
36
+ (Event) => void OnInput { get; set; }
37
+ (Event) => void OnBlur { get; set; }
38
+ (Event) => void OnChange { get; set; }
39
+ void AppendChild(VElement child);
40
+ void SetAttr(string name, string value);
41
+ } from "kopular/velement";
42
+
25
43
  extern class Component {
26
44
  constructor();
27
- virtual Element Render(); // `virtual` here is what lets your subclass `override` it
45
+ virtual VElement Render(); // `virtual` here is what lets your subclass `override` it
46
+ virtual void AfterRender(Element root); // see "Component" below
28
47
  void Mount(Element parent);
29
48
  void Update();
30
49
  } from "kopular/component";
@@ -36,7 +55,7 @@ extern class Router {
36
55
  void Mount(Element parent);
37
56
  } from "kopular/router";
38
57
 
39
- extern Element If(bool condition, () => Element whenTrue, () => Element whenFalse) from "kopular/directives";
58
+ extern VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse) from "kopular/directives";
40
59
 
41
60
  extern class Response {
42
61
  bool ok { get; }
@@ -86,7 +105,7 @@ Kopular's own copy in `dom.ks` isn't reachable across the package boundary; rede
86
105
  handful of members you actually use. See KopularDemo's `src/kopular_bindings.ks` for a
87
106
  complete, real example of both.
88
107
 
89
- ## `Component` (`component.ks`)
108
+ ## `Component` (`component.ks`, `velement.ks`, `vdom.ks`) — real vdom diffing
90
109
 
91
110
  ```ks
92
111
  class MyWidget : Component {
@@ -96,46 +115,68 @@ class MyWidget : Component {
96
115
  this.count = 0;
97
116
  }
98
117
 
99
- public override Element Render() {
100
- Element el = document.createElement("div");
101
- el.textContent = "Count: " + this.count;
118
+ public override VElement Render() {
119
+ VElement el = VElement.Create("div");
120
+ el.TextContent = "Count: " + this.count;
102
121
  return el;
103
122
  }
104
123
 
105
124
  public void Bump() {
106
125
  this.count = this.count + 1;
107
- this.Update(); // re-runs Render(), swaps the old root for the new one
126
+ this.Update(); // re-runs Render(), diffs it against the previous tree, patches real DOM
108
127
  }
109
128
  }
110
129
 
111
130
  MyWidget w = new MyWidget();
112
- w.Mount(document.body); // first Render() + append
113
- w.Bump(); // re-render
131
+ w.Mount(document.body); // first Render() + materialize + append
132
+ w.Bump(); // re-render + diff + patch
114
133
  ```
115
134
 
116
- - `Render()`: `virtual`, override it to build a fresh DOM subtree from current state.
117
- Called by both `Mount()` and `Update()`. **No diffing either way** — every call
118
- rebuilds the whole subtree from scratch. Provide it as a real markup file via
119
- KopScript's `template from "./x.html";` (see "Templates" below) instead of hand-writing
120
- it — both produce the exact same method; Kopular needed no code changes to support this.
121
- - `Mount(parent)`: calls `Render()` once, appends the result to `parent`, remembers both
122
- for `Update()` to use later.
135
+ - `Render()`: `virtual`, override it to describe the current state as a `VElement` tree —
136
+ a lightweight description of a DOM element (`kopular/velement`), not a real one. Called
137
+ by both `Mount()` and `Update()`. Provide it as a real markup file via KopScript's
138
+ `template from "./x.html";` (see "Templates" below) instead of hand-writing it — both
139
+ produce the exact same method; Kopular needed no framework code changes to support this.
140
+ - `VElement`: `Tag`, `TextContent`/`ClassName`/`Id`/`Value` (direct fields `Value` is the
141
+ one that must be a live DOM *property*, not an attribute: `SetAttr("value", x)` sets the
142
+ default value, not the current one), `RawHtml` (an opaque, undiffed leaf — set instead of
143
+ `TextContent`/children, for a raw-HTML-then-wire-handlers pattern), the four fixed named
144
+ event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange` (each a real no-op by default, never
145
+ null — no nullable function type to fall back on), `AppendChild(child)`, and
146
+ `SetAttr(name, value)` (the escape hatch for any other real HTML attribute — `href`,
147
+ `src`, `alt`, `placeholder`, ...; never `Value`, see above). `Id` doubles as a stable key
148
+ for list-child reconciliation — see `PatchChildren` below.
149
+ - `Mount(parent)`: calls `Render()` once, materializes the returned tree into real DOM
150
+ (`Materialize` in `vdom.ks`), appends it to `parent`, then calls `AfterRender(root)`.
123
151
  - `Update()` (protected — called from within the component, not externally): calls
124
- `Render()` again and `replaceChild`s the old root with the new one.
125
- - **Known limitation**: if a *parent* component's `Update()` runs while it has mounted
126
- children, those children are NOT automatically re-mounted into the new parent tree
127
- `Component` only handles a single component's own re-render cycle, not tree
128
- reconciliation. Compose independent components into stable slots (see `Router`'s own
129
- pattern of keeping page instances alive) to avoid this rather than nesting components
130
- that both re-render.
152
+ `Render()` again and `Patch`es the new tree against the previous one (`vdom.ks`), reusing
153
+ a real DOM node wherever a node's tag stays the same instead of rebuilding it sibling
154
+ nodes untouched by the change keep their exact identity (`===`), not just their content.
155
+ A list child without a stable `Id` still ends up correct after a reorder, but isn't
156
+ guaranteed to keep its own real node (see `PatchChildren`'s keyed-vs-positional matching
157
+ in `vdom.ks`). Then calls `AfterRender(root)`.
158
+ - `AfterRender(root)`: `virtual`, a no-op by default, called at the end of both `Mount()`
159
+ and `Update()` with the real, now-materialized/patched root `Element`. For a component
160
+ that needs to do further imperative work against its own real DOM — most commonly,
161
+ mounting a *nested* `Component` into a slot `Render()`'s tree just describes as data,
162
+ since a live mounted child isn't something `VElement` can express (see `Router`'s own
163
+ use of this to mount its matched page into the outlet, and "Dependency injection" below
164
+ for `RoutedApp` doing the same with `Router` itself).
165
+ - **Known limitation**: reconciliation is per-`Component`, not across nested ones — if a
166
+ *parent* component's own `Render()` output changes shape around a slot where a nested
167
+ Component was `Mount()`ed via `AfterRender`, that nested Component isn't automatically
168
+ re-`Mount()`ed or torn down as part of the parent's diff. Compose independent components
169
+ into a stable, unchanging slot (see `Router`'s own outlet, which never changes tag or
170
+ position) to avoid this rather than nesting components whose *surrounding* structure
171
+ also changes.
131
172
  - **`Update()` before `Mount()` is a safe no-op**, not an error. This matters for two
132
173
  sibling `Component`s (e.g. two `Router` pages) that share one injected service's
133
174
  `state<T>` and both `Subscribe()` it — every route's page is constructed eagerly (see
134
175
  `Router.AddRoute`), so at any given time most of them are constructed but never
135
176
  `Mount()`ed. Changing that shared state fires `Subscribe` on all of them, including the
136
- ones that aren't the currently-routed page — `Update()` just skips the render/replace
137
- for those, since `Mount()` will run a fresh `Render()` anyway whenever one of them
138
- actually becomes routed.
177
+ ones that aren't the currently-routed page — `Update()` just skips the render/patch for
178
+ those, since `Mount()` will run a fresh `Render()` anyway whenever one of them actually
179
+ becomes routed.
139
180
  - `RenderError(string message)`: `virtual`, called by `Mount()`/`Update()` (via a private
140
181
  `SafeRender()` wrapper) when `Render()` throws, instead of letting the exception
141
182
  propagate uncaught and crash whatever triggered the render (a click handler, a `Router`
@@ -143,9 +184,9 @@ w.Bump(); // re-render
143
184
  opt-in error recovery**; a `Component` that never overrides `RenderError` behaves
144
185
  exactly as before this existed. Override it to show a fallback UI instead:
145
186
  ```ks
146
- protected override Element RenderError(string message) {
147
- Element el = document.createElement("div");
148
- el.textContent = "Something went wrong: " + message;
187
+ protected override VElement RenderError(string message) {
188
+ VElement el = VElement.Create("div");
189
+ el.TextContent = "Something went wrong: " + message;
149
190
  return el;
150
191
  }
151
192
  ```
@@ -170,16 +211,22 @@ class Counter : Component {
170
211
 
171
212
  - `{{ expr }}` interpolation, `(event)="stmt"`, `[prop]="expr"`, `*if="expr"`,
172
213
  `*for="Type varName of expr"` — all real KopScript, checked at compile time, desugared
173
- to the exact same `document.createElement`/`.appendChild`/`.textContent`/
174
- `.addEventListener` calls a hand-written `Render()` would use.
214
+ to the exact same `VElement.Create`/`.AppendChild`/`.TextContent`/named-event-field
215
+ calls a hand-written `Render()` would use. `(event)` only accepts `click`/`input`/
216
+ `blur`/`change` — `VElement`'s own fixed set — anything else is a compile error
217
+ (`KS5016`). `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`;
218
+ anything else goes through `SetAttr` instead.
175
219
  - A `state<T>` field declared directly on the class and referenced directly in the
176
220
  template (`Count` above) gets `Subscribe((v) => this.Update())` wired automatically —
177
221
  no manual `Subscribe` in the constructor for that field. State reached indirectly
178
222
  (through a method, or `this.SomeService.Count`) still needs a manual `Subscribe`, same
179
223
  as a hand-written `Render()` always has.
224
+ - **Two-way binding**: `[(value)]="Field"` desugars to `[value]="Field"` +
225
+ `(input)="Field = e.target.value"` — `value` only, and `Field` must be a bare name or
226
+ `this.Field` (see KopScript's own LLM.md `KS5017`/`KS5018` for the two rejected cases).
180
227
  - One top-level element per template (hard error otherwise); no mixing text and element
181
- children under one element (no text-node type in `dom.ks`, only `.textContent`); no
182
- two-way binding, no pipes, at most one structural directive per element.
228
+ children under one element (`VElement` has no text-node sibling concept, only
229
+ `.TextContent`); no pipes, at most one structural directive per element.
183
230
  - This is entirely a KopScript compiler feature (parsed/desugared before type-checking
184
231
  runs) — Kopular's own framework code (`component.ks`, `dom.ks`) is unmodified and
185
232
  unaware templates exist; a template-generated `Render()` is indistinguishable from a
@@ -191,14 +238,14 @@ Real URLs via the History API (`pushState`/`popstate`), not hash routing.
191
238
 
192
239
  ```ks
193
240
  class NotFoundPage : Component {
194
- public override Element Render() { /* ... */ }
241
+ public override VElement Render() { /* ... */ }
195
242
  }
196
243
  class HomePage : Component {
197
244
  private Router Nav;
198
245
  constructor(Router nav) : base() { this.Nav = nav; }
199
- public override Element Render() {
200
- Element btn = document.createElement("button");
201
- btn.addEventListener("click", (Event e) => { this.Nav.Navigate("/about"); });
246
+ public override VElement Render() {
247
+ VElement btn = VElement.Create("button");
248
+ btn.OnClick = (Event e) => { this.Nav.Navigate("/about"); };
202
249
  return btn;
203
250
  }
204
251
  }
@@ -238,14 +285,14 @@ nav.Navigate("/about"); // pushState + immediate re-ren
238
285
  `"/search?sort=name"` -> `Query("sort") == "name"`. Values are **not percent-decoded** —
239
286
  no `decodeURIComponent` binding exists yet, a deliberate v1 cut; `%20`/`+` arrive
240
287
  exactly as written in the URL, not converted to a space.
241
- - **Why `Param` is a plain field, not `state<T>`**: `Render()` already rebuilds a fresh
242
- outlet and re-`Mount()`s the matched page on every `Navigate()`/`popstate`, which
288
+ - **Why `Param` is a plain field, not `state<T>`**: `Router`'s own `AfterRender` already
289
+ re-`Mount()`s the matched page into its outlet on every `Navigate()`/`popstate`, which
243
290
  re-runs that page's own `Render()` (reading the fresh `Param`) with no extra step.
244
291
  Making it `state<T>` and having a page `Subscribe()` to it — the pattern every *other*
245
292
  piece of state in this framework uses — actually crashes: `Match()` sets it *before*
246
- `Render()` finishes swapping the matched page into the outlet, so the very first route
247
- that matches a page nothing has `Mount()`ed yet fires that page's subscribed listener
248
- while its inherited `Update()` still has no `ParentElement` to `replaceChild` into.
293
+ `AfterRender` finishes mounting the matched page into the outlet, so the very first
294
+ route that matches a page nothing has `Mount()`ed yet fires that page's subscribed
295
+ listener while its inherited `Update()` still has no `ParentElement` to patch into.
249
296
  - **Navigation guards**: `SetGuard(redirectPath, (string) => bool guard)` — `guard` is
250
297
  called with the target path before every navigation (including a direct load/refresh);
251
298
  returning `false` redirects to `redirectPath` (via `pushState`, so the URL updates too —
@@ -282,11 +329,11 @@ a **hand-written** `Render()`, where `if` being a statement (not an expression)
282
329
  conditional value needs a helper to get one out of it:
283
330
 
284
331
  ```ks
285
- Element If(bool condition, () => Element whenTrue, () => Element whenFalse)
332
+ VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse)
286
333
  ```
287
334
 
288
335
  ```ks
289
- root.appendChild(If(this.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLogin()));
336
+ root.AppendChild(If(this.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLogin()));
290
337
  ```
291
338
 
292
339
  Both branches always required (no null "nothing" value); only the branch actually taken
@@ -295,20 +342,22 @@ at all in hand-written `Render()` either:
295
342
 
296
343
  ```ks
297
344
  // *ngFor — plain array method
298
- this.Items.ForEach((Item item) => { list.appendChild(this.BuildItemRow(item)); });
345
+ this.Items.ForEach((Item item) => { list.AppendChild(this.BuildItemRow(item)); });
299
346
 
300
347
  // *ngSwitch — plain KopScript `match` expression (exhaustiveness-checked, unlike *ngSwitch)
301
- root.appendChild(match this.Status {
348
+ root.AppendChild(match this.Status {
302
349
  "loading" => this.BuildSpinner(),
303
350
  "error" => this.BuildError(),
304
351
  _ => this.BuildContent()
305
352
  });
306
353
  ```
307
354
 
308
- No keyed/reuse-existing-DOM-nodes diffing (the performance angle of `*ngFor trackBy`) —
309
- that needs comparing old/new data by a caller key, generic over item type, and KopScript
310
- has no generics. Not planned as a workaround; would need real language-level generics
311
- first.
355
+ **Keyed/reuse-existing-DOM-nodes diffing (the `*ngFor trackBy` angle) exists** give each
356
+ item's `VElement` a stable `.Id` (e.g. the item's own id) and `Update()`'s diff engine
357
+ (`PatchChildren` in `vdom.ks`) matches children by `Id` across a re-render, reusing a
358
+ matched child's real DOM node rather than rebuilding it. Without a stable `Id`, a
359
+ reordered list still renders correctly, but a given item's real node isn't guaranteed to
360
+ follow its data.
312
361
 
313
362
  ## `Http` (`http.ks`) — thin wrapper over `fetch`
314
363
 
@@ -367,7 +416,7 @@ email.Valid(); // bool — Error.Value == null
367
416
  after the first as a nested function type, not an array element type, and errors expecting
368
417
  `=>`). Combine checks as an if-chain in one lambda (see the `email` example above), or via
369
418
  the fixed-arity `CombineValidators2<T>`/`CombineValidators3<T>` free functions (generic,
370
- inference-only — see `Kop`'s own "Generics" docs for why they're *free* functions, not
419
+ inference-only — see `KopScript`'s own "Generics" docs for why they're *free* functions, not
371
420
  `Validators` static methods):
372
421
  ```ks
373
422
  CombineValidators2((string v) => Validators.Required(v), (string v) => Validators.Email(v))
@@ -376,13 +425,12 @@ CombineValidators2((string v) => Validators.Required(v), (string v) => Validator
376
425
  Fixed-arity (2, 3 — add more the same way if a form ever needs to chain further), not a
377
426
  general `Validators.All(...)`, for the same array-of-function-values reason above.
378
427
 
379
- **No DOM binding** — wiring `.Value` to a real `<input>` is a plain `addEventListener`
380
- call in your own `Render()`, the same as any other event handler; there is no
381
- `[(ngModel)]`-equivalent.
382
-
383
- **No DOM binding** wiring `.Value` to a real `<input>` is a plain `addEventListener`
384
- call in your own `Render()`, the same as any other event handler; there is no
385
- `[(ngModel)]`-equivalent.
428
+ **No DOM binding in a hand-written `Render()`** — wiring `.Value` to a real `<input>` is a
429
+ plain `VElement.OnInput` assignment reading `e.target.value` (the same as any other event
430
+ handler; `VElement.Value` itself is one-way, host-to-DOM only), and reading it back out
431
+ via `.Touch()` on `OnBlur`. A **template** has real `[(value)]="Field"` sugar for the
432
+ value-binding half (see "Templates" above); `Touch()` on blur still needs its own explicit
433
+ `(blur)="Field.Touch()"` either way `[(value)]` only ever wires `value`/`input`.
386
434
 
387
435
  ## `kopular/testing` (`testing.js`, hand-written JS, not compiled from `.ks`)
388
436
 
@@ -435,10 +483,14 @@ class AppContainer {
435
483
  class RoutedApp : Component {
436
484
  private Router Nav;
437
485
  constructor(AppContainer services) : base() { this.Nav = services.Nav; }
438
- public override Element Render() {
439
- Element el = document.createElement("div");
440
- this.Nav.Mount(el);
441
- return el;
486
+ public override VElement Render() {
487
+ return VElement.Create("div");
488
+ }
489
+ // Mounting Router — a live, nested Component — isn't something a VElement
490
+ // tree can express as data; AfterRender gets the real, now-materialized
491
+ // root Element instead (see "Component" above).
492
+ protected override void AfterRender(Element root) {
493
+ this.Nav.Mount(root);
442
494
  }
443
495
  }
444
496
 
@@ -462,8 +514,8 @@ class CounterService {
462
514
  - **`Router.Param` is a plain `string`, not `state<T>` — don't `Subscribe()` to it.** A
463
515
  `state<T>`-based design for it was tried and genuinely crashes: `Navigate()` sets `Param`
464
516
  *before* the newly-matched page finishes mounting, so a page `Subscribe`-ing to it fires
465
- while its own `Update()` still has no `ParentElement` to `replaceChild` into. `Render()`
466
- already re-reads the fresh `Param` on every navigation with no extra step — just read
517
+ while its own `Update()` still has no `ParentElement` to patch into. `Render()` already
518
+ re-reads the fresh `Param` on every navigation with no extra step — just read
467
519
  `this.Nav.Param` directly inside `Render()`.
468
520
  - **Deploying a `Router`-based app needs SPA/history-fallback configured on the actual host**
469
521
  — this bit the real `KopularDemo` production site (worked when navigated to via a link,
@@ -480,19 +532,37 @@ class CounterService {
480
532
  independent mechanisms, not the same thing wired two ways** — a template never needs
481
533
  `directives.ks`'s `If()` imported or called; `*if`/`*for` compile to real `if`/`for`
482
534
  directly. Don't mix a template with a hand-written-`Render()` helper call.
483
- - **No two-way binding, anywhere.** Wiring a template's `[value]` or a hand-written
484
- `.Value` to a real `<input>` back and forth always needs an explicit
485
- `addEventListener("input", ...)` — there's no `[(ngModel)]`-equivalent to reach for in
486
- either authoring style.
535
+ - **`[(value)]` two-way binding is template-only, and `value`-only.** A hand-written
536
+ `Render()` still always needs an explicit `OnInput` assignment reading
537
+ `e.target.value` — there's no equivalent shorthand there, since it already has direct
538
+ field access. `[(id)]`/`[(className)]` don't exist either, even in a template — the
539
+ sugar only wires `value`/`input`, the one pairing with a real "user just changed this"
540
+ event.
541
+ - **A `VElement` event binding only accepts `click`/`input`/`blur`/`change`** — both in a
542
+ template's `(event)="..."` and a hand-written `OnClick`/`OnInput`/`OnBlur`/`OnChange`
543
+ assignment. There's no generic `addEventListener` on `VElement` (event handlers are part
544
+ of the tree's own data, not wired against a live DOM node until `Materialize`/`Patch`
545
+ runs) — an event Kopular doesn't have a named field for isn't reachable from `Render()`
546
+ at all yet.
547
+ - **Reconciliation doesn't cross a `Mount()`ed-via-`AfterRender` boundary.** Real vdom
548
+ diffing patches one `Component`'s own subtree; a *nested* Component mounted into a slot
549
+ via `AfterRender` (see `Router`'s outlet, or `RoutedApp` mounting `Router` itself) isn't
550
+ automatically re-`Mount()`ed if the *parent's* surrounding tree shape changes — see
551
+ "Component"'s own "Known limitation" above.
487
552
 
488
553
  ## Does not exist
489
554
 
490
555
  DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a runtime
491
556
  template engine or interpreted expression language — templates compile to the same
492
557
  imperative `Render()` code as the hand-written form, checked at compile time, not
493
- interpreted at runtime (see "Templates" above) · two-way binding (`[(ngModel)]`) ·
494
- vdom diffing / reconciliation beyond a single component's own re-render · pipes ·
495
- animations · typed/generic HTTP responses (`Http` returns raw text — see above) · SSR.
558
+ interpreted at runtime (see "Templates" above) · two-way binding in a hand-written
559
+ `Render()`, or on anything but `value` even in a template (`[(value)]="Field"` exists
560
+ see "Templates" above but it's `value`-only, and templates-only) ·
561
+ reconciliation across a nested-Component boundary (see "Common mistakes" above — real
562
+ vdom diffing exists *within* one Component's own subtree, via `Update()`/`vdom.ks`) ·
563
+ a generic/arbitrary `VElement` event binding — only `click`/`input`/`blur`/`change` ·
564
+ pipes · animations · typed/generic HTTP responses (`Http` returns raw text — see above) ·
565
+ SSR.
496
566
 
497
567
  (`FormField<T>`/`Validators` and `kp new` — see "FormField<T> / Validators" and "Starting
498
568
  a new project" above — are real, shipped features; they used to be listed here as gaps