kopular 1.0.0 → 1.1.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/GUIDE.md ADDED
@@ -0,0 +1,238 @@
1
+ # Kopular quick guide
2
+
3
+ Everything you need for typical app work, in one page. For anything not covered here —
4
+ generics, nullable types, advanced routing, the full diagnostic list — see this package's
5
+ own `LLM.md` and `node_modules/kopscript/LLM.md`.
6
+
7
+ ## Setup
8
+
9
+ Every file starts with `using "kopular";` for the DOM and every Kopular type, plus
10
+ `using "./other_file";` per project file it needs (`using` isn't transitive — list every
11
+ file you reference, not just direct dependencies' dependencies).
12
+
13
+ ```ks
14
+ using "kopular";
15
+ using "./counter";
16
+
17
+ Counter app = new Counter();
18
+ app.Mount(document.body);
19
+ ```
20
+
21
+ Run `npm run build && npm run serve`, or `npm start` (a scaffolded project already has
22
+ both scripts). `ks check src/app.ks` type-checks without building, if you just want errors.
23
+
24
+ ## The language you need
25
+
26
+ - `Type name = value;` for every local — no `let`/`var`, no inference on declarations.
27
+ - No implicit `this` — every member reference is `this.Field`/`this.Method()`, always,
28
+ including inside a lambda.
29
+ - `if`/`else`/`while`/`for`/`foreach (Type x in xs)` — all statements, no ternary. Use
30
+ `match` for a conditional value: `match x { 1 => "one", _ => "other" }` (the `_` arm is
31
+ required unless every case is covered, e.g. an enum).
32
+ - Lambdas need explicit parameter types: `(number x) => x * 2`.
33
+ - Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
34
+ narrowing is scoped to that `if` block, not reachability-based (an early
35
+ `if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
36
+ `if (x != null) { ... }` instead).
37
+ - Strings: `s.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
38
+ `.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
39
+ Both `"double"` and `'single'` quotes work — use whichever the other isn't, e.g. inside
40
+ a template attribute (`*if="Name != ''"`).
41
+ - Arrays: `xs.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
42
+ `.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
43
+ `.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
44
+ - `x == null` / `x != null` also match `undefined` — a missed `Array.Find`, an absent
45
+ optional field, etc. all read as `null`.
46
+ - No object-literal syntax anywhere (`{ key: value }` doesn't exist as a value). A JS API
47
+ that needs one (rare — `fetch`'s options, `addEventListener`'s options object) needs a
48
+ small hand-written `.js` shim; Kopular's own `Http`/`FormField` cover the common cases so
49
+ you'll rarely hit this.
50
+
51
+ ## Components
52
+
53
+ ```ks
54
+ class Counter : Component {
55
+ public state<number> Count;
56
+ constructor() : base() { this.Count = state(0); }
57
+ public void Increment() { this.Count.Value = this.Count.Value + 1; }
58
+ template from "./counter.html";
59
+ }
60
+ ```
61
+ ```html
62
+ <button (click)="Increment()">Count: {{ Count.Value }}</button>
63
+ ```
64
+
65
+ `state<T>` referenced directly in a template auto-subscribes — no manual `Subscribe`
66
+ needed. State reached *indirectly* (through a method, or `this.SomeService.Count`) does
67
+ need one: `this.SomeService.Count.Subscribe((v) => this.Update());` in the constructor.
68
+
69
+ **Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
70
+ `id`/`className`/`value`/`disabled`/`checked`; anything else is a plain attribute),
71
+ `(click)`/`(input)`/`(blur)`/`(change)` events, `[(value)]="Field"` two-way binding
72
+ (`Field` can be a bare name, `this.Field`, or a path like `Qty.Value` — not a method call),
73
+ `*if="expr"`, `*for="Type v of expr"` (element type required, no inference), `*mount="expr"`
74
+ (embeds a live child component, composes with `*for`).
75
+
76
+ **Hand-written `Render()`** (needed when logic is too dynamic for a template, or a template
77
+ would obscure more than it clarifies):
78
+
79
+ ```ks
80
+ public override VElement Render() {
81
+ VElement button = VElement.Create("button");
82
+ button.TextContent = "Count: " + this.Count.Value;
83
+ button.OnClick = (Event e) => { this.Increment(); };
84
+ return button;
85
+ }
86
+ ```
87
+ `VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
88
+ `.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the
89
+ escape hatch for anything without a named field). Both styles compile to the same thing and
90
+ mix freely across a project.
91
+
92
+ **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
93
+ uncaught crash if `Render()` throws.
94
+
95
+ ## Nested components & content projection
96
+
97
+ ```ks
98
+ this.Items.ForEach((Item item) => {
99
+ VElement slot = VElement.Mount(item); // item : Component, e.g. its own `public string Id;`
100
+ slot.Id = item.Id; // stable key for reordering
101
+ list.AppendChild(slot);
102
+ });
103
+ ```
104
+ A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
105
+
106
+ Content projection (React's `children`): pass a `() => VElement` into a constructor, call
107
+ it from `Render()` — no separate mechanism needed.
108
+
109
+ ## Services — no DI container
110
+
111
+ A service is a plain class; "injecting" it is a constructor argument. Wire everything once
112
+ in a composition root:
113
+
114
+ ```ks
115
+ class AppContainer {
116
+ public Router Nav;
117
+ constructor() {
118
+ CounterService counter = new CounterService();
119
+ this.Nav = new Router(new NotFoundPage());
120
+ this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
121
+ }
122
+ }
123
+ ```
124
+
125
+ ## Router
126
+
127
+ ```ks
128
+ Router nav = new Router(new NotFoundPage()); // fallback page, required
129
+ nav.AddRoute("/", new HomePage(nav));
130
+ nav.AddRoute("/dogs/:id", new DogDetailPage(nav)); // nav.Param inside that page
131
+ nav.SetGuard("/login", (string path) => {
132
+ if (path == "/admin") { return authService.LoggedIn; }
133
+ return true;
134
+ });
135
+ nav.Navigate("/dogs/1");
136
+ nav.Mount(document.body);
137
+ ```
138
+ - Routes hold already-built `Component`s, not factories — built once, state survives
139
+ navigating away and back.
140
+ - `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
141
+ re-renders on every navigation already).
142
+ - `SetGuard(redirectPath, guard)`: one guard for the whole router; `guard` returns `false`
143
+ to redirect. Defaults to always-allow.
144
+ - `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
145
+ - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
146
+ matching file) — a plain HTTP limitation, not a Kopular one.
147
+
148
+ ## Forms
149
+
150
+ ```ks
151
+ FormField<string> email = new FormField<string>("", (string v) => {
152
+ string? required = Validators.Required(v);
153
+ if (required != null) { return required; }
154
+ return Validators.Email(v);
155
+ });
156
+ email.Value.Value = "not-an-email";
157
+ print(email.Error.Value); // "Must be a valid email"
158
+ ```
159
+ `Validators`: `Required/MinLength/MaxLength/Email/Min/Max`, each returns an error message
160
+ or `null`. No array-of-validators param — chain checks in one lambda, as above.
161
+
162
+ ## HTTP
163
+
164
+ ```ks
165
+ Response r = await Http.Get("/api/dogs");
166
+ if (r.ok) { string body = await r.text(); }
167
+ await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
168
+ ```
169
+ No typed JSON deserialization built in — describe the shape as `extern class` and parse
170
+ with `extern MyShape Parse(string json) as "JSON.parse";`.
171
+
172
+ ## Async: `Delay`, `Computed`, `Resource<T>`
173
+
174
+ ```ks
175
+ await Delay(500); // real setTimeout-backed delay
176
+
177
+ Computed2<number, number, number> total = new Computed2<number, number, number>(
178
+ qty, price, (number q, number p) => q * p
179
+ );
180
+ total.Value.Subscribe((number v) => this.Update()); // total.Value is itself state<number>
181
+
182
+ Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
183
+ match r.Status.Value {
184
+ AsyncStatus.Loading => BuildSpinner(),
185
+ AsyncStatus.Success => BuildContent(r.Data.Value),
186
+ AsyncStatus.Failure => BuildError(r.Error.Value)
187
+ };
188
+ ```
189
+
190
+ ## Scoped styles
191
+
192
+ ```ks
193
+ class Widget : Component {
194
+ template from "./widget.html";
195
+ styles from "./widget.css";
196
+ }
197
+ ```
198
+ Every selector in `widget.css` is rewritten to only match this class's own elements —
199
+ never a sibling's or child's. Needs a constructor to exist on the class.
200
+
201
+ ## Lazy routes (real code-splitting)
202
+
203
+ ```js
204
+ // admin_page_loader.js — hand-written, not compiled from .ks
205
+ export async function LoadAdminPage() {
206
+ const { AdminPage } = await import("./admin_page.js");
207
+ return new AdminPage();
208
+ }
209
+ ```
210
+ ```ks
211
+ extern task<Component> LoadAdminPage() from "./admin_page_loader";
212
+ nav.AddLazyRoute("/admin", LoadAdminPage);
213
+ ```
214
+ The lazy page (`admin_page.ks`) must be built as its own entry too — it's deliberately not
215
+ `using`'d from your app's entry (that's what keeps it out of the eager bundle), so add a
216
+ second build line: `ks build src/app.ks && ks build src/admin_page.ks`. Testing it via
217
+ `kopular/testing`'s `runKopularApp` needs no extra setup — it compiles any file in your
218
+ `srcDir` the entry doesn't reach.
219
+
220
+ ## Testing
221
+
222
+ ```js
223
+ import { runKopularApp } from "kopular/testing";
224
+ const { window, cleanup } = await runKopularApp(srcDir, "app.ks", { includeKopularPackage: true });
225
+ // assert against window.document, then:
226
+ cleanup();
227
+ ```
228
+ Real jsdom, real compile, real DOM assertions — not a mock.
229
+
230
+ ## Common mistakes
231
+
232
+ - Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
233
+ error, not automatic.
234
+ - `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
235
+ - A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
236
+ a method call.
237
+ - `*mount` accepts only `id`/`[id]` on its element — any other attr/binding is a compile
238
+ error, since the mounted child's own `Render()` owns all of its content.
package/LLM.md CHANGED
@@ -1,10 +1,12 @@
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 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
- published `LLM.md` on the `kopscript` npm package) that reference is a prerequisite,
7
- not repeated here.
4
+ see `README.md` for narrative/rationale, or this package's own **`GUIDE.md`** for a short,
5
+ task-oriented page covering everything a typical app needs (start there for most tasks;
6
+ come back here for anything it doesn't cover). Kopular is 10 files total; this covers all
7
+ of them. For the host language, see KopScript's own `LLM.md` in the `KopScript` repo (or
8
+ its published `LLM.md` on the `kopscript` npm package, `kopscript@1.1.0`+) — that
9
+ reference is a prerequisite, not repeated here.
8
10
 
9
11
  Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
10
12
  `kopular/velement` (VElement — what `Render()` returns; see "Component" below),
@@ -13,30 +15,50 @@ Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Compo
13
15
  (Computed1, Computed2), `kopular/resource` (Resource, AsyncStatus), `kopular/vdom`
14
16
  (`ScopedStyles` — the runtime half of `styles from`; everything else in this file, the
15
17
  diff/patch engine behind `Update()`, is internal, nothing else here needs importing
16
- directly), `kopular/testing` (runKopularApp, runKopularFixture — see below). Also ships a
17
- bin, `kp` — `npx kp new
18
- <dir>` scaffolds a new project (the `extern` bindings below, plus the vendor/serve
19
- scripts needed to run in a browser) rather than requiring it be reconstructed by hand;
20
- prefer it over hand-writing the section below for a new project. Templates (`template
21
- from "./x.html";`, see below) are a KopScript language feature, not a Kopular export —
22
- there is no `kopular/template` entry point to import.
18
+ directly), `kopular/timers` (`Delay`), `kopular/testing` (runKopularApp,
19
+ runKopularFixture — see below). Also ships a bin, `kp` — `npx kp new <dir>` scaffolds a
20
+ new project (an `index.html` with every module already wired, a `Counter` component, and
21
+ the vendor/serve scripts needed to run in a browser). Templates (`template from
22
+ "./x.html";`, see below) are a KopScript language feature, not a Kopular export there is
23
+ no `kopular/template` entry point to import.
23
24
 
24
25
  ## Consuming Kopular from your own KopScript project
25
26
 
26
- `using` only resolves same-project relative paths reaching into an npm package (Kopular
27
- included) always goes through `extern`, re-describing exactly the members you use.
28
-
29
- **Don't reach for `Element`/`Document`/`Event` via `extern ... from "kopular/dom";`, even
30
- though that actually works** (`kopular/dom` genuinely re-exports real `globalThis`
31
- bindings, same as any other Kopular export) — **redeclare your own ambient block
32
- instead.** The reason isn't that importing wouldn't work; it's that Kopular's own internal
33
- `dom.ks` only declares what Kopular's own framework code itself touches, a smaller
34
- surface than a real app typically needs (`Element.value`/`.placeholder`, `.href`/`.src`/
35
- `.alt`, `.querySelector`, ...) — importing it would silently cap you at that subset with
36
- no signal you're missing something until you hit a real compile error for each one, one
37
- at a time. Every consuming project re-declares its own Element/Document/Event, same as
38
- this one does. **Copy the block below rather than hand-rolling a smaller one from scratch
39
- and adding members as compile errors demand them** — a real, complete first-attempt
27
+ `using "kopular";` (`kopscript@1.1.0`+) resolves this package's own declarations
28
+ everything in the "Entry points" list above — through `node_modules`, the same way `npm
29
+ install` already makes them available to plain JS. One line, every Kopular type in scope,
30
+ each consuming file's own compiled output importing only the bindings it actually
31
+ references (never the whole surface). This is the normal way to consume Kopular now; `kp
32
+ new` generates it by default.
33
+
34
+ ```ks
35
+ using "kopular";
36
+ using "./counter";
37
+
38
+ Counter app = new Counter();
39
+ app.Mount(document.body);
40
+ ```
41
+
42
+ `document`/`Element`/`Event`/etc. come from the same `using "kopular";` — no separate ambient
43
+ block needed. The rest of this file describes every one of those bindings' real shapes, for
44
+ when you need something `GUIDE.md` doesn't cover, or need to know exactly what a type looks
45
+ like.
46
+
47
+ ### The hand-copied `extern` block (older projects, or trimming what you import)
48
+
49
+ Before `kopscript@1.1.0`, every consuming project hand-declared its own copy of Kopular's
50
+ `extern` bindings (`using` couldn't reach into a package). That still works — a real,
51
+ hand-written `extern` block is exactly as valid as one resolved via `using "kopular";",
52
+ just more to maintain — and is occasionally still the right call if you want a visibly
53
+ trimmed subset. **Don't reach for `Element`/`Document`/`Event` via `extern ... from
54
+ "kopular/dom";`, even though that actually works** (`kopular/dom` genuinely re-exports real
55
+ `globalThis` bindings, same as any other Kopular export) — Kopular's own internal `dom.ks`
56
+ only declares what Kopular's own framework code itself touches, a smaller surface than a
57
+ real app typically needs. `using "kopular";`'s own declarations (`src/kopular.ks` in this
58
+ package) don't have that problem — they're written for consumers, not just for Kopular's
59
+ own internals — so prefer it over a hand-copied ambient block for new code. If you do
60
+ hand-copy one anyway, copy the block below rather than hand-rolling a smaller one from
61
+ scratch and adding members as compile errors demand them — a real, complete first-attempt
40
62
  implementation of a Kopular app hit the exact same missing property (`Element.value`)
41
63
  twice from two independently-trimmed subsets, because the compile error only ever names
42
64
  the one member actually touched, never warns that a *sibling* feature (a template's
@@ -95,6 +117,10 @@ extern class VElement {
95
117
  string Id { get; set; }
96
118
  string Value { get; set; }
97
119
  string RawHtml { get; set; }
120
+ // Real bool properties, not attribute strings — a boolean attribute is on
121
+ // whenever it's present at all, so only a real property can turn it back off.
122
+ bool Disabled { get; set; }
123
+ bool Checked { get; set; }
98
124
  (Event) => void OnClick { get; set; }
99
125
  (Event) => void OnInput { get; set; }
100
126
  (Event) => void OnBlur { get; set; }
@@ -109,6 +135,14 @@ extern class VElement {
109
135
  static VElement Mount(Component component);
110
136
  } from "kopular/velement";
111
137
 
138
+ // Only needed if you use `styles from "./x.css";` in a class body (see
139
+ // "Scoped styles" below) — the compiler splices a call to this into the
140
+ // constructor automatically, but (like every other Kopular export) it
141
+ // still needs its own `extern` declaration; it is NOT auto-imported.
142
+ extern class ScopedStyles {
143
+ static void Inject(string id, string css);
144
+ } from "kopular/vdom";
145
+
112
146
  extern class Component {
113
147
  constructor();
114
148
  virtual VElement Render(); // `virtual` here is what lets your subclass `override` it
@@ -133,6 +167,11 @@ extern class Router {
133
167
  // Every registered path (AddRoute + AddLazyRoute), in registration
134
168
  // order — see "Router" below.
135
169
  string[] AllPaths();
170
+ // Overridable outlet content shown while an AddLazyRoute page's loader
171
+ // is in flight — default: `<div class="router-loading">Loading...</div>`.
172
+ // Only declare this if you actually override it (a class extending
173
+ // Router) — see "Lazy routes" below.
174
+ protected virtual VElement BuildLoadingPlaceholder();
136
175
  void Navigate(string path);
137
176
  void Mount(Element parent);
138
177
  // One guard for the whole Router, not per-route — see "Router" below.
@@ -317,6 +356,11 @@ w.Bump(); // re-render + diff + patch
317
356
  return ul;
318
357
  }
319
358
  ```
359
+ `item.Id` above means exactly what it looks like: for a keyed list of mounted children,
360
+ the mounted `Component` itself needs its own public `Id` (or similarly-named) field/
361
+ property to copy onto `slot.Id` — `VElement.Id` lives on the wrapper slot, not on the
362
+ component, so there's nothing to key by without one. Set it however suits the type
363
+ (constructor param, or a plain field assigned right after construction).
320
364
  The SAME `Mountable` instance still in a slot across a re-render is patched in place
321
365
  (`Update()` inside that child re-renders just its own subtree, siblings untouched); a
322
366
  DIFFERENT instance (or the slot disappearing) tears the old one down first — calling its
@@ -434,7 +478,11 @@ class Widget : Component {
434
478
  - Independent of `template from` — works with a hand-written `Render()` too. The compiler
435
479
  rewrites the referenced `.css` so every selector requires a per-class
436
480
  `data-kop-scope="<id>"` attribute, then splices one `ScopedStyles.Inject(id, css);` call
437
- into the constructor. **Unlike `template from`, this needed real framework code**:
481
+ into the constructor. **This means `ScopedStyles` must be `extern`-declared in your own
482
+ project the same as `Component`/`VElement`/etc. (see the copy-paste block above) — it is
483
+ NOT auto-imported just because you wrote `styles from`.** Forgetting it is a real `KS4048
484
+ Undefined identifier 'ScopedStyles'` at the constructor the compiler spliced the call
485
+ into. **Unlike `template from`, this needed real framework code**:
438
486
  `ScopedStyles` (`vdom.ks`, new) — idempotent, static-array-registry dedup shape same as
439
487
  `Batching`, injects one real `<style>` per component *type* (not per instance) into
440
488
  `document.head` (`dom.ks`'s `head { get; }`, new) the first time any instance is
@@ -532,10 +580,29 @@ nav.Navigate("/about"); // pushState + immediate re-ren
532
580
  extern task<Component> LoadDogsPage() from "./dogs_page_loader";
533
581
  nav.AddLazyRoute("/dogs", LoadDogsPage);
534
582
  ```
535
- The outlet shows a plain loading placeholder (override `BuildLoadingPlaceholder()`) while
536
- the fetch is in flight; the loaded page is cached after the first fetch, same as an eager
537
- page navigating away and back reuses it, no re-fetch. Mixes freely with `AddRoute` in
538
- the same `Router`.
583
+ The outlet shows a plain loading placeholder (`<div class="router-loading">Loading...</div>`
584
+ by default; override `protected virtual VElement BuildLoadingPlaceholder()` on a class
585
+ extending `Router` to customize) while the fetch is in flight; the loaded page is cached
586
+ after the first fetch, same as an eager page — navigating away and back reuses it, no
587
+ re-fetch. Mixes freely with `AddRoute` in the same `Router`.
588
+ - **Building it for real**: the whole point of a lazy route is that `dogs_page.ks` is
589
+ deliberately *not* `using`'d from your app's own entry file — that's what keeps its
590
+ code out of the eager bundle. That also means your normal build command (`ks build
591
+ src/app.ks`) never compiles it, since `ks build`/`compileGraph` only walk the `using`
592
+ graph reachable from the entry you give them. Build the lazy page as its own separate
593
+ entry too, e.g. `ks build src/app.ks && ks build src/dogs_page.ks` in your build
594
+ script (or one `ks build` invocation per lazy route, if you have several) — each
595
+ already-compiled shared dependency just gets written again with identical output, so
596
+ this is safe to add without restructuring anything else.
597
+ - **Testing it**: `kopular/testing`'s `runKopularApp` (kopular 0.24.0+) compiles any
598
+ other real `.ks` file present in your app's own `srcDir` that the entry doesn't
599
+ reach — a lazy-route target is exactly that — so a test exercising `AddLazyRoute`
600
+ needs no special setup: write `dogs_page.ks`/`dogs_page_loader.js` into the same
601
+ directory as your entry file, same as any other page, and the loader's
602
+ `import("./dogs_page.js")` finds a real, freshly-compiled file. (`runKopularFixture`,
603
+ used only by Kopular's own internal test suite, does not do this — it copies
604
+ Kopular's entire framework source tree alongside a fixture, where the same behavior
605
+ would mean recompiling most of the framework on every test.)
539
606
  - **`AllPaths(): string[]`** — every registered path (`AddRoute` + `AddLazyRoute`), in
540
607
  registration order. For enumerating real routes (a build-time prerender step, most
541
608
  likely) without a second, hand-maintained list.
package/README.md CHANGED
@@ -166,8 +166,13 @@ class Widget : Component {
166
166
  ```
167
167
 
168
168
  Unlike `template from`, this **did** need real framework code — `ScopedStyles.Inject`
169
- (`vdom.ks`) is the runtime half: idempotent, injects one real `<style>` per component
170
- *type* into `document.head` the first time any instance of that type is constructed
169
+ (`vdom.ks`) is the runtime half, and (like every other Kopular export) it needs its own
170
+ `extern` declaration in your project `extern class ScopedStyles { static void
171
+ Inject(string id, string css); } from "kopular/vdom";` — it is not auto-imported just
172
+ because you wrote `styles from`; omitting it is a real `KS4048 Undefined identifier
173
+ 'ScopedStyles'` at the constructor the compiler spliced the call into. It's idempotent,
174
+ injecting one real `<style>` per component *type* into `document.head` the first time any
175
+ instance of that type is constructed
171
176
  (dedup is per-type, not per-instance — every instance's constructor calls `Inject` with
172
177
  the same compile-time `id`/rewritten-`css`, so only the first actually creates a tag).
173
178
  Never removed once injected — a scoped stylesheet is global infrastructure for as long as
@@ -368,11 +373,23 @@ export async function LoadDogsPage() {
368
373
  extern task<Component> LoadDogsPage() from "./dogs_page_loader";
369
374
  ```
370
375
 
371
- The outlet shows a plain loading placeholder (override `BuildLoadingPlaceholder()` to
372
- customize it) while the fetch is in flight, then the real page once it resolves — and,
373
- like an eager page, it's only ever fetched once: navigating away and back reuses the same
374
- already-loaded instance, keeping whatever state it built up. A lazy and an eager route mix
375
- freely in the same `Router`; nothing about `AddRoute`'s own existing signature changes.
376
+ The outlet shows a plain loading placeholder (`<div class="router-loading">Loading...</div>`
377
+ by default; override `protected virtual VElement BuildLoadingPlaceholder()` on a class
378
+ extending `Router` to customize it) while the fetch is in flight, then the real page once
379
+ it resolves — and, like an eager page, it's only ever fetched once: navigating away and
380
+ back reuses the same already-loaded instance, keeping whatever state it built up. A lazy
381
+ and an eager route mix freely in the same `Router`; nothing about `AddRoute`'s own existing
382
+ signature changes.
383
+
384
+ **Building and testing a lazy route**: `dogs_page.ks` is deliberately *not* `using`'d from
385
+ your app's own entry — that's what keeps it out of the eager bundle — which also means your
386
+ normal `ks build src/app.ks` never compiles it, since `ks build` only walks the `using`
387
+ graph reachable from the entry you give it. Build it as its own separate entry too:
388
+ `ks build src/app.ks && ks build src/dogs_page.ks` (already-compiled shared dependencies
389
+ just get written again with identical output, so this is safe to add). For tests,
390
+ `kopular/testing`'s `runKopularApp` (0.24.0+) compiles any other real `.ks` file present in
391
+ your `srcDir` that the entry doesn't reach — a lazy-route target is exactly that — so
392
+ `AddLazyRoute` needs no special test setup at all.
376
393
 
377
394
  **`AllPaths()`** returns every registered path (both `AddRoute` and `AddLazyRoute`), in
378
395
  registration order — for a caller that needs to enumerate real routes (a build-time static
@@ -819,11 +836,11 @@ error, not a runtime blank screen.
819
836
 
820
837
  ## Starting a new project: `kp new`
821
838
 
822
- Everything in the next section the `extern` bindings, plus a `vendor/kopular/` copy of
823
- this package's browser files and an import map pointing at it (a browser can't resolve a
824
- bare specifier like `"kopular/component"` the way Node's own module resolution does) is
825
- boilerplate every Kopular project needs verbatim. Generate it instead of reconstructing it
826
- by hand (or from memory, if you're an AI agent):
839
+ `npx kp new my-app` scaffolds a real, working project: a `Counter` component
840
+ (`src/counter.ks`), an `index.html` with every Kopular module already mapped, and the
841
+ vendor/serve scripts needed to run in a browser (a browser can't resolve a bare specifier
842
+ like `"kopular/component"` the way Node's own module resolution does, so `vendor-kopular.mjs`
843
+ copies Kopular's runtime into `vendor/` and `index.html`'s import map points there).
827
844
 
828
845
  ```bash
829
846
  npx kp new my-app
@@ -832,37 +849,19 @@ npm install
832
849
  npm start # builds, vendors kopular's browser files, and serves at :8080
833
850
  ```
834
851
 
835
- This scaffolds a real, working `Component` (`src/counter.ks` the same Counter shown
836
- above), the ambient DOM/Kopular `extern` bindings it needs (`src/kopular_bindings.ks`),
837
- and a `README.md` that points an AI agent at this package's own `LLM.md` before it starts
852
+ Its `README.md` points an AI agent at this package's own `GUIDE.md` before it starts
838
853
  generating code. `kp` ships from this package (not from `kopscript`'s own `ks` CLI) since
839
854
  scaffolding a *Kopular* app is a framework concern, not a language one — `ks` stays a
840
855
  pure-language tool with no framework knowledge baked in.
841
856
 
842
857
  ## Using Kopular from another KopScript project
843
858
 
844
- KopScript's own `using "./path";` only resolves relative paths within a project — it has
845
- no package-import mechanism yet. Cross-package consumption goes through `extern`
846
- instead, the same way KopScript already describes any other JS/npm dependency
847
- (`kp new` above generates exactly this, if you'd rather not hand-write it):
859
+ `using "kopular";` (`kopscript@1.1.0`+) resolves this package's own KopScript declarations
860
+ through `node_modules` one line, every Kopular type in scope, with each file's own
861
+ compiled output importing only what it actually references:
848
862
 
849
863
  ```ks
850
- extern class VElement {
851
- static VElement Create(string tag);
852
- string TextContent { get; set; }
853
- } from "kopular/velement";
854
-
855
- extern class Component {
856
- constructor();
857
- virtual VElement Render();
858
- void Mount(Element parent);
859
- } from "kopular/component";
860
-
861
- extern class Router {
862
- constructor(Component notFoundPage);
863
- void AddRoute(string path, Component page);
864
- void Navigate(string path);
865
- } from "kopular/router";
864
+ using "kopular";
866
865
 
867
866
  class MyWidget : Component {
868
867
  public override VElement Render() {
@@ -873,19 +872,14 @@ class MyWidget : Component {
873
872
  }
874
873
  ```
875
874
 
876
- Marking `Render()` `virtual` in the `extern` declaration is what lets a real subclass
877
- `override` it see [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
878
- for a full working example (components, a service, and routing, all consuming Kopular
879
- this way).
875
+ See [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) for
876
+ a full working example (components, a service, and routing).
880
877
 
881
- The snippet above is illustrative, not exhaustive real code needs a fuller
882
- `VElement`/`Component`/`Router`, and `Element` itself (used above as `Mount`'s
883
- parameter type but never shown declared) is a plain ambient browser global your own
884
- project declares, not something Kopular exports — `LLM.md`'s "Consuming Kopular from
885
- your own KopScript project" section has the complete, copy-ready block for all of
886
- these, `Element`/`Document`/`Event` included. `npx kp new` (above) generates this
887
- boilerplate for a fresh project either way — reach for `LLM.md`'s block when adding to
888
- an existing one instead.
878
+ Before `kopscript@1.1.0`, every project hand-declared its own copy of Kopular's `extern`
879
+ bindings instead `using` couldn't reach into a package yet. That still works (`kp new`
880
+ above can generate it), and is still the right call if you want a visibly trimmed subset —
881
+ see `LLM.md`'s "Consuming Kopular from your own KopScript project" for the complete,
882
+ copy-ready block, `Element`/`Document`/`Event` included.
889
883
 
890
884
  `extern class` can carry its own `<T>` (kopscript >= 0.5.0), so a generic export like
891
885
  `FormField<T>` describes the same way a real generic class does — see `LLM.md`'s