@microsoft/webui-framework 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +209 -53
  2. package/dist/component-asset/asset.d.ts +4 -2
  3. package/dist/component-asset/asset.js +19 -3
  4. package/dist/component-asset/generated-manifest.d.ts +1 -0
  5. package/dist/component-asset/generated-manifest.js +32 -0
  6. package/dist/component-asset/loader.d.ts +2 -1
  7. package/dist/component-asset/loader.js +98 -22
  8. package/dist/component-asset/manifest.d.ts +2 -1
  9. package/dist/component-asset.d.ts +1 -1
  10. package/dist/component-asset.js +12 -2
  11. package/dist/element/link-styles.d.ts +20 -0
  12. package/dist/element/link-styles.js +1030 -0
  13. package/dist/element/markers.d.ts +4 -0
  14. package/dist/element/markers.js +60 -5
  15. package/dist/element/styles.d.ts +39 -1
  16. package/dist/element/styles.js +623 -13
  17. package/dist/element/types.d.ts +5 -3
  18. package/dist/element.js +9 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/interaction-hydration.d.ts +8 -0
  22. package/dist/interaction-hydration.js +167 -0
  23. package/dist/static-host.js +43 -1
  24. package/dist/streaming-activation.d.ts +2 -2
  25. package/dist/streaming-activation.js +4 -2
  26. package/dist/streaming-bootstrap.d.ts +2 -2
  27. package/dist/streaming-bootstrap.js +7 -1
  28. package/dist/streaming-cleanup.d.ts +1 -0
  29. package/dist/streaming-cleanup.js +66 -14
  30. package/dist/streaming-coordinator.d.ts +3 -2
  31. package/dist/streaming-coordinator.js +107 -47
  32. package/dist/streaming-deferred.d.ts +14 -4
  33. package/dist/streaming-deferred.js +200 -87
  34. package/dist/streaming-dom.d.ts +12 -3
  35. package/dist/streaming-dom.js +41 -27
  36. package/dist/streaming-mode.d.ts +5 -0
  37. package/dist/streaming-mode.js +4 -0
  38. package/dist/streaming-protocol.d.ts +19 -3
  39. package/dist/streaming-protocol.js +4 -3
  40. package/dist/streaming-spans.d.ts +9 -0
  41. package/dist/streaming-spans.js +200 -0
  42. package/dist/template-content.d.ts +17 -0
  43. package/dist/template-content.js +106 -0
  44. package/dist/template-element.d.ts +17 -2
  45. package/dist/template-element.js +493 -177
  46. package/dist/template-events.d.ts +7 -0
  47. package/dist/template-events.js +7 -1
  48. package/dist/template-types.d.ts +1 -2
  49. package/dist/template.d.ts +15 -8
  50. package/dist/template.js +79 -16
  51. package/package.json +9 -3
  52. package/dist/component-asset/resources.d.ts +0 -4
  53. package/dist/component-asset/resources.js +0 -83
package/README.md CHANGED
@@ -7,7 +7,7 @@ This package is the browser-side runtime used by `webui build --plugin=webui`. I
7
7
  - `WebUIElement` for SSR hydration and client-created elements
8
8
  - `@observable`, `@attr`, and `@volatile` decorators
9
9
  - direct DOM binding updates
10
- - light DOM or shadow DOM rendering (`--dom=light|shadow` flag)
10
+ - Shadow-default components with opt-in global Light and authored Shadow islands
11
11
  - SSR state seeding
12
12
 
13
13
  If you are building WebUI apps in this repo, this is the component model used by examples like `examples/app/todo-webui`, `examples/app/commerce`, and `examples/app/contact-book-manager`.
@@ -79,7 +79,9 @@ CounterCard.define('counter-card');
79
79
  <button @click="{increment()}">Increment</button>
80
80
  ```
81
81
 
82
- Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--dom=light` for light DOM rendering.
82
+ Unwrapped components default to Shadow. A `dom: "light"` build renders them as
83
+ Light while preserving any sole top-level
84
+ `<template shadowrootmode="open">` component as Shadow.
83
85
 
84
86
  ### Use it from your page
85
87
 
@@ -128,10 +130,9 @@ rendered block size of one instance; WebUI emits it as
128
130
  `contain-intrinsic-block-size: auto 18rem` before first layout. The SSR DOM
129
131
  remains present, searchable, and accessible while the browser skips offscreen
130
132
  style, layout, and paint work. The generated policy applies to instances in the
131
- document, Light DOM, and standard Shadow DOM components. A `--dom=light`
132
- component inside an authored shadow root needs the `style` CSS strategy or an
133
- equivalent rule in that root's stylesheet because document styles cannot cross
134
- the boundary.
133
+ document, Light DOM, and standard Shadow DOM components. A Light component
134
+ inside a Shadow root receives the rule through its precomputed style
135
+ closure, which delivers the stylesheet into that root under every CSS strategy.
135
136
 
136
137
  Import the optional coordinator entry once before component modules:
137
138
 
@@ -165,6 +166,52 @@ If the optional entry or `IntersectionObserver` is unavailable, hydration falls
165
166
  back to eager; `content-visibility` remains browser-managed. See
166
167
  [Lazy Hydration](https://microsoft.github.io/webui/guide/concepts/hydration#lazy-hydration).
167
168
 
169
+ Router applications should author `<template w-hydrate="interaction">` and use
170
+ the framework-agnostic `@microsoft/webui-router/preload.js` handle. FAST and
171
+ other hydration runtimes use that same handle with their own readiness signal.
172
+ Non-router apps use the lower-level framework entry:
173
+
174
+ ```ts
175
+ import {
176
+ installInteractionHydration,
177
+ isInteractionReplay,
178
+ } from
179
+ '@microsoft/webui-framework/interaction-hydration.js';
180
+
181
+ installInteractionHydration({
182
+ load: () => import('./components.js'),
183
+ });
184
+ ```
185
+
186
+ Pointer-down, focus, and keyboard intent starts `load()` without cancellation.
187
+ An unmodified primary click waits and replays on its original composed-path
188
+ target; `load()` must resolve only after listeners are ready. Hover, modified
189
+ clicks, and previously cancelled clicks do not replay. Use
190
+ `isInteractionReplay(event)` to deduplicate ancestor capture work.
191
+
192
+ This opt-in trades first-interaction latency for lower startup JS and heap.
193
+ Prefer an eager root with lazy descendants when request-to-hydrated time matters.
194
+ Synthetic replay cannot preserve transient user activation or target controls
195
+ inside closed shadow roots; hydrate those paths eagerly.
196
+
197
+ One offscreen singleton boundary can retain browser rendering deferral while
198
+ also deferring its module graph:
199
+
200
+ ```html
201
+ <template
202
+ w-render="lazy"
203
+ w-reserve-block-size="18rem"
204
+ w-hydrate="interaction"
205
+ >
206
+ <!-- Component content -->
207
+ </template>
208
+ ```
209
+
210
+ Do not use the combined form for repeated items. A visible app root gains no
211
+ rendering benefit from `content-visibility`; keep interaction on that root and
212
+ put `w-render="lazy"` on offscreen descendants.
213
+
214
+
168
215
  ### Build with the WebUI plugin
169
216
 
170
217
  ```bash
@@ -199,12 +246,35 @@ authored `<boundary>` directives through
199
246
  `@microsoft/webui-framework` entry has no dependency on the coordinator, so
200
247
  normal applications pay no streaming bundle or initialization cost.
201
248
 
202
- Each committed boundary receives its own ephemeral state object directly during
249
+ Boundaries may be authored in entries and reusable components, including
250
+ runtime conditions, outlets, and selected routes. A boundary-bearing subtree
251
+ reached from a `<for>` body fails the build with `boundary-in-repeat`. A whole
252
+ `<for>` may sit inside one boundary, and boundaries before or after a `<for>`
253
+ are valid. A component-local boundary uses a generated parent span, so an early
254
+ compiler-marked child can hydrate before the opaque parent tail in light or
255
+ shadow DOM. The server's boundary-only `resume` emits that checkpoint first;
256
+ `advance` emits the following parent tail, with no sibling boundary workaround.
257
+ Authored boundaries cannot nest.
258
+
259
+ Span resolution is entirely coordinator-owned: the generated `data-ws-span` and
260
+ `data-ws-enclosing` attributes, and the open-span registry that pairs them, live
261
+ only in the opt-in streaming entry. It resolves the one ancestor an entitled
262
+ early child may skip and passes that element to the activation hook, which
263
+ compares it by identity. The always-shipped entry therefore carries no span
264
+ attribute name and no span bookkeeping at all.
265
+
266
+ Each runtime occurrence receives an ephemeral state object directly during
203
267
  activation. The coordinator does not publish that state to
204
- `window.__webui.state`, and it removes generated checkpoint scaffolding after
205
- commit. Every commit also emits a `performance.mark()` — `webui:boundary:<id>`,
206
- `webui:boundary:<id>:update`, or `webui:streaming:terminal` — which needs no
207
- flag and no listener, so tooling that loads after hydration can still read it.
268
+ `window.__webui.state`, and it removes generated checkpoint and span
269
+ scaffolding after commit. Updates apply state to retained roots and never insert
270
+ markup or rerun hydration.
271
+
272
+ The browser reads version-2
273
+ `[2, sequence, kind, target, payload]` records for final checkpoints,
274
+ updatable checkpoints, updates, span completions, and terminal. Every commit
275
+ also emits a `performance.mark()` - `webui:boundary:<id>`,
276
+ `webui:boundary:<id>:update`, `webui:span:<id>`, or
277
+ `webui:streaming:terminal` - which needs no flag or listener.
208
278
  Set `window.__WEBUI_STREAMING_DEBUG__ = true` only when tooling needs the live
209
279
  `webui:boundary-hydrated` event as well.
210
280
 
@@ -240,9 +310,14 @@ development-only and is dead-code-eliminated from production bundles via the
240
310
 
241
311
  Override the protected `hydratedCallback()` hook for work that requires the
242
312
  component's bindings, events, and `w-ref` references to be ready. It runs
243
- synchronously exactly once after the first successful ordinary SSR hydration,
244
- client-created mount, lazy activation, deferred streamed activation, or dormant
245
- static-host wake. Its once-latch is set before author code runs, so a thrown
313
+ exactly once with the first successful ordinary SSR hydration, client-created
314
+ mount, lazy activation, deferred streamed activation, or dormant static-host
315
+ wake. If CSP blocks the temporary Link-mode prepaint guard, a client-created
316
+ mount keeps non-style content detached and delays `$ready` and this callback
317
+ until its native links load and the content is appended. Reactive writes made
318
+ while detached are reconciled immediately before append. A synchronous
319
+ disconnect/reconnect preserves the pending mount; a lasting disconnect cancels
320
+ it. The callback's once-latch is set before author code runs, so a thrown
246
321
  callback is not retried on reconnect.
247
322
 
248
323
  `connectedCallback()` remains a native per-connection lifecycle. On ordinary
@@ -253,22 +328,29 @@ post-hydration signal. Descendants must not structurally mutate a containing
253
328
  component's SSR subtree before it hydrates, because hydration relies on stable
254
329
  compiled paths.
255
330
 
256
- ### DOM strategy (`--dom`)
257
-
258
- The `--dom` flag controls how the server renders component content:
331
+ ### Light and Shadow DOM
259
332
 
260
- | Flag | Behavior |
261
- |------|----------|
262
- | `--dom=shadow` (default) | Wraps component HTML in `<template shadowrootmode="open">` |
263
- | `--dom=light` | Renders component content as direct children of the host element |
333
+ An unwrapped component receives a generated open Shadow root by default. In a
334
+ `dom: "light"` build it renders as direct children of its host. A component
335
+ whose sole top-level element is a bare `<template>` explicitly renders as Light
336
+ and is unwrapped, even under the Shadow fallback. A sole
337
+ `<template shadowrootmode="open">` remains Shadow in either mode. Templates with
338
+ attributes and policy wrappers do not select a mode. Closed roots and invalid
339
+ values or placement are build errors; `<slot>` is rejected only for effective
340
+ Light components.
264
341
 
265
342
  The runtime auto-detects which mode was used at hydration time:
266
343
  - If a `shadowRoot` already exists → shadow DOM SSR path
267
344
  - If `childNodes` exist but no shadow root → light DOM SSR path
268
345
  - If neither → client-created path (uses `meta.sd` to decide)
269
346
 
270
- Light DOM is useful for simpler styling (CSS inheritance works naturally) and
271
- better search-engine indexing. Shadow DOM provides style encapsulation.
347
+ Light components use authored/global ordinary CSS in the owning CSS tree.
348
+ Shadow components keep native Shadow scoping. `:host`, `:host-context`, and
349
+ `::slotted` fail in effective Light CSS.
350
+ The Link, Style, and Module delivery strategies all support both modes.
351
+
352
+ In a Light build, add open wrappers to slot, native-encapsulation, or CSS-heavy
353
+ frequently restyled components.
272
354
 
273
355
  ---
274
356
 
@@ -318,15 +400,38 @@ The asset graph keeps entry-owned templates external, leaves single-root
318
400
  dependencies inline, and emits dependencies shared by multiple roots once as
319
401
  flat dynamic chunks. Component assets cannot be combined with `<route>`. Load
320
402
  the normal entry bundle first so external prerequisites are registered.
321
-
322
- Shared chunk filenames are generated and must not be copied into the manifest.
323
- Each root asset carries its own dynamic imports; `--metafile` is available for
324
- analysis and build tooling. `preload(tag)` starts template, module, and optional
325
- data work. Concurrent roots share in-flight chunk imports by resolved URL and
326
- CSS module styles are deduped. `create(tag)` creates the element after
327
- template/module work is ready and does not block on optional data by default. Use
328
- `create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component must
329
- wait briefly for state before mounting.
403
+ Current assets require version 3 and an atomically validated
404
+ `componentStyles` catalog; any other version is rejected as unsupported.
405
+
406
+ The compiler records final Link stylesheet filenames in the protocol. For
407
+ Shadow builds, the handler emits that finite manifest as inert JSON in the
408
+ document head; body-only host protocols emit it at the start of their rendered
409
+ body fragment. Light builds emit the same hrefs as deduplicated document
410
+ stylesheets because their CSS must apply globally.
411
+ Automatic Shadow intent preloading therefore requires HTML rendered through the
412
+ WebUI handler or `Protocol`, which emits `#webui-component-assets`. A shell that
413
+ uses build artifacts without rendering the protocol still mounts safely through
414
+ the native stylesheet guard, but it does not receive the earlier
415
+ compiler-owned style preload.
416
+ Shared chunk and content-hashed stylesheet filenames are generated and must not
417
+ be copied into authored code. Each root asset carries its own dynamic imports;
418
+ `--metafile` remains available for analysis and build tooling.
419
+
420
+ In Shadow builds, `preload(tag)` reads the compiler-owned style metadata and
421
+ starts Link styles beside the authored root asset, component module, and
422
+ optional data request. Only the stable root asset URL remains in application
423
+ code; shared chunks and content-hashed CSS stay compiler-owned.
424
+
425
+ Bundler-generated loaders can use `asset: () => import('./settings-dialog.webui.js')`
426
+ instead of a URL. This keeps chunk naming and public-path rewriting inside the
427
+ bundler while preserving the same `preload(tag)` and `create(tag)` lifecycle.
428
+ Concurrent roots share in-flight chunk and stylesheet work. `create(tag)`
429
+ creates the element after template/module work is ready and does not block on
430
+ optional data by default. Use
431
+ `create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component
432
+ must wait briefly for state before mounting. A rejected root asset or authored
433
+ module is evicted from the registry so a later `preload(tag)` or `create(tag)`
434
+ retries it.
330
435
 
331
436
  ### `@observable`
332
437
 
@@ -445,10 +550,11 @@ resource-constrained devices.
445
550
  `$update(path)` only visits bindings that reference the changed property.
446
551
  Everything else is skipped via a per-path index built once at hydration time.
447
552
 
448
- 2. **Zero allocations during updates.**
553
+ 2. **Zero framework allocations during ordinary updates.**
449
554
  Targeted updates are a single `Map.get()` → direct array iteration.
450
555
  No intermediate arrays, no object creation, no spread operators on the
451
- update path.
556
+ update path. A changed raw HTML binding necessarily parses and creates its
557
+ replacement DOM nodes inside its pre-resolved ownership range.
452
558
 
453
559
  3. **Parse once, clone forever.**
454
560
  Compiled template HTML is parsed via `innerHTML` once per component tag
@@ -527,9 +633,8 @@ is driven by data (template metadata + state values), not code. Any language
527
633
  that can read the compiled metadata and produce HTML can serve as the SSR
528
634
  backend. No comment markers or data attributes are needed — the runtime
529
635
  resolves ordinary buffered SSR nodes via the lockstep hydration walk.
530
- Progressive streaming uses temporary checkpoint scaffolding only to delay
531
- activation until a complete region arrives; it removes that scaffolding after
532
- commit.
636
+ Progressive streaming uses temporary checkpoint and generated-span scaffolding
637
+ to activate complete runtime regions; it removes that scaffolding after commit.
533
638
 
534
639
  ### Build → Serve → Hydrate → Update
535
640
 
@@ -664,7 +769,6 @@ interface TemplateMeta {
664
769
  r?: [collection, itemVar, blockIdx, slot][]; // Repeat blocks
665
770
  eg?: [event, [[handler, argSpecs, targetIndex, usesEvent?]]][]; // Events
666
771
  b?: TemplateBlockMeta[]; // Nested block metadata
667
- sa?: string; // Adopted stylesheet specifier
668
772
  sd?: 1; // Shadow DOM flag for client-created
669
773
  re?: [event, handler, argSpecs][]; // Root-level events
670
774
  tr?: string[]; // Template state roots
@@ -770,7 +874,13 @@ State seeding uses `window.__webui.state` loaded from the server-emitted
770
874
  `@observable` and `@attr` keys from reachable authored components select
771
875
  initial state; HTML-only dormant components and authored template-only roots
772
876
  contribute no startup keys. Without projection metadata, the server preserves
773
- full state. During `$mount()`, `$applySSRState()` writes matching decorated keys
877
+ full state. The startup state is not a permanent application store. Eager
878
+ components consume it during hydration, lazy components copy their projected
879
+ roots before deferral, and the framework releases the global handoff when
880
+ `webui:hydration-complete` fires on a page without a route chain. Routed pages
881
+ retain it for router-owned lazy startup. Normalized template closure entries are
882
+ released as soon as their functions are embedded in template metadata. During
883
+ `$mount()`, `$applySSRState()` writes matching decorated keys
774
884
  directly to observable backing fields before any bindings are wired:
775
885
 
776
886
  ```mermaid
@@ -829,10 +939,50 @@ The framework supports three CSS delivery strategies:
829
939
 
830
940
  | Strategy | How it works |
831
941
  |----------|-------------|
832
- | **Link** | `<link>` tag baked into `meta.h` — loaded by the browser naturally |
833
- | **Inline** | `<style>` tag baked into `meta.h` — no external request |
942
+ | **Link** | `<link>` tag baked into `meta.h`; the first client-created shadow instance authorizes shared constructable sheets through native loading, then warm instances adopt them before paint |
943
+ | **Style** | `<style>` tag baked into `meta.h` — no external request |
834
944
  | **Module** | `<script type="importmap">{"imports":{"tag-name":"data:text/css,..."}}</script>` in the HTML payload registers the CSS as a module under `tag-name`. The framework imports it via `import(tag, { with: { type: 'css' } })` and applies the resulting `CSSStyleSheet` via `adoptedStyleSheets` for shadow DOM isolation |
835
945
 
946
+ Link promotion is progressive enhancement. Registration performs a bounded
947
+ `<link rel="preload" as="style">` using the stylesheet's CORS, integrity, and
948
+ referrer-policy attributes, so the native stylesheet link can reuse the same
949
+ style-destination request. Preload bytes are never applied directly, and a
950
+ preload cannot inspect response MIME type; the first client instance's native
951
+ link remains authoritative for CSP, MIME, integrity, CORS, redirects, and
952
+ service workers. The framework releases the instance's paint guard as soon as
953
+ every original link loads, before constructing the shared ordered set from
954
+ native CSSOM. It then adopts that set before existing sheets with one assignment
955
+ and shares it with later instances. Promoted links remain disabled in place so
956
+ reconnect hydration retains the compiled element indexes. Classes with an authored
957
+ `hydratedCallback()` still take the guarded native path on warm mounts, allowing
958
+ lifecycle-added `<style>` elements to preserve native cascade order. If
959
+ construction is unsupported, native CSSOM or
960
+ unredirected, non-service-worker timing is unavailable, or `@import`, unsafe URL
961
+ syntax, link attributes, bindings, compiled events, or authored DOM `<style>`
962
+ cascade semantics cannot be preserved, the original links remain. An anonymous
963
+ first-layer shadow guard prevents component CSS from overriding the loading
964
+ gate and cancels host transitions before hiding. When CSP blocks that guard,
965
+ non-style content, `$ready`, and `hydratedCallback()` remain deferred; current
966
+ reactive state is reconciled before append. If that reconciliation changes a
967
+ request-affecting bound link value, the framework waits for the replacement
968
+ native load before append. Disconnecting permanently cancels
969
+ the pending mount, while a synchronous reconnect preserves it. A link error
970
+ is reported, leaves the browser's native links in place, releases the temporary
971
+ guard, and completes deferred hydration. The component may be unstyled after a
972
+ definitive stylesheet failure, but it remains visible and usable. SSR hydration,
973
+ Style, Module, and authored/global Light DOM behavior are unchanged.
974
+
975
+ ### Intent-time Link preloading for component assets
976
+
977
+ In a Shadow build, call `assets.preload(tag)` from pointer, focus, or other
978
+ intent handling. The framework reads the compiler-owned head manifest, so
979
+ application code never derives or hardcodes content-hashed CSS names. Link
980
+ styles begin before the root asset executes, and later template registration
981
+ reuses the same style-destination request. Repeated intent is deduplicated. An
982
+ intent that never mounts the component may still produce the browser's standard
983
+ unused-preload warning. Light builds load component-asset CSS as document
984
+ stylesheets at the structural head boundary instead.
985
+
836
986
  CSS module stylesheets are cached so each component instance adopts the same
837
987
  parsed sheet without re-parsing CSS. The `meta.sa` field specifies the
838
988
  stylesheet specifier for a component.
@@ -843,7 +993,8 @@ stylesheet specifier for a component.
843
993
 
844
994
  Unlike frameworks that use comment markers or data attributes to locate each
845
995
  dynamic binding, this framework uses **compiled element indices** — each
846
- binding names its element by pre-order position within its compiled section. Progressive streaming's temporary boundary markers locate complete
996
+ binding names its element by pre-order position within its compiled section.
997
+ Progressive streaming's temporary boundary and span markers locate complete
847
998
  activation regions, not individual bindings.
848
999
 
849
1000
  ### Client-created resolution (`collectTemplateElements`)
@@ -862,24 +1013,28 @@ const target = elements[index];
862
1013
  ### SSR resolution (`buildSSRIndex`)
863
1014
 
864
1015
  SSR DOM differs from the compiled template: the renderer strips inter-element
865
- whitespace, and `<if>` / `<for>` bodies are rendered inline between markers.
1016
+ whitespace, `<if>` / `<for>` bodies are rendered inline between structural
1017
+ markers, and raw HTML values can contribute arbitrary element runs.
866
1018
  `buildSSRIndex` walks the SSR DOM and the compiled template DOM **in lockstep**,
867
1019
  skipping whole marker ranges, and numbers the result the same way:
868
1020
 
869
1021
  ```typescript
870
1022
  // Elements are paired positionally and numbered in pre-order.
871
- // Structural ranges are skipped whole — that content belongs to
872
- // the block's own metadata.
1023
+ // Structural and raw HTML ranges are skipped whole - that content
1024
+ // is not part of the enclosing section's static element numbering.
873
1025
  // Text is the exception: whitespace stripping means text nodes do
874
- // not line up, so text slots still resolve by ordinal.
1026
+ // not line up, so each text slot resolves from its compiled
1027
+ // right-hand static or marker boundary.
875
1028
  ```
876
1029
 
877
- This eliminates all *per-binding* annotation: no `data-w-*` attributes, no
878
- comment per text run, no DOM markers around individual bindings. What the SSR
879
- server does emit is the five structural comments documented above
880
- (`<!--wr-->`, `<!--wi-->`, `<!--/wr-->`, `<!--wc-->`, `<!--/wc-->`), which
881
- delimit `<if>` / `<for>` bodies and are what the walk skips over and anchors
882
- blocks on. They are removed once hydration completes.
1030
+ This eliminates annotations for ordinary bindings: no `data-w-*` attributes and
1031
+ no comments around escaped text. The SSR server emits five structural comments
1032
+ for `<if>` / `<for>` bodies plus paired `<!--wN-->` / `<!--/wN-->` markers around each raw HTML
1033
+ binding. Structural closing/item markers are removed after hydration. Raw HTML
1034
+ anchors remain so updates can replace zero or multiple direct sibling nodes
1035
+ without touching content outside the binding's range. These exact marker
1036
+ comments are framework-reserved and must not appear in the trusted raw value
1037
+ inside that range.
883
1038
 
884
1039
  ---
885
1040
 
@@ -897,7 +1052,8 @@ blocks on. They are removed once hydration completes.
897
1052
 
898
1053
  - **No virtual DOM** — no tree copy, no diff algorithm
899
1054
  - **No runtime template parsing** — the Rust compiler handles all syntax
900
- - **No `innerHTML` on updates** — only `textContent` and `setAttribute`
1055
+ - **No parent-wide `innerHTML` on updates** — raw HTML parses into its bounded
1056
+ contextual range; escaped text and attributes patch direct node references
901
1057
  - **No `querySelector` on updates** — all nodes are pre-resolved references
902
1058
  - **No recursion in hot paths** — conditions use iterative stack evaluation
903
1059
 
@@ -1,4 +1,5 @@
1
1
  import type { CompiledConditionFn, TemplateMeta } from '../template.js';
2
+ import { type ComponentStyles } from '../element/styles.js';
2
3
  export interface ComponentAssetImport {
3
4
  components: string[];
4
5
  href: string;
@@ -6,17 +7,18 @@ export interface ComponentAssetImport {
6
7
  }
7
8
  export interface ComponentAsset {
8
9
  type: 'webui-component-asset';
9
- version: 2;
10
+ version: 3;
10
11
  kind: 'root' | 'chunk';
11
12
  root?: string;
12
13
  components: string[];
13
14
  requiredComponents: string[];
14
15
  externalComponents: string[];
15
16
  imports: ComponentAssetImport[];
16
- templateStyles: string[];
17
+ componentStyles: ComponentStyles;
17
18
  templates: Record<string, TemplateMeta>;
18
19
  templateFunctions?: Record<string, CompiledConditionFn[]>;
19
20
  }
21
+ export declare function prepareAssetComponentStyles(value: unknown): ComponentStyles;
20
22
  export declare function readComponentAssetModule(module: unknown): unknown;
21
23
  export declare function validateAsset(value: unknown, expectedKind: ComponentAsset['kind']): asserts value is ComponentAsset;
22
24
  export declare function sameComponents(left: readonly string[], right: readonly string[]): boolean;
@@ -1,5 +1,18 @@
1
+ import { requireComponentStyles, } from '../element/styles.js';
1
2
  const ASSET_TYPE = 'webui-component-asset';
2
- const ASSET_VERSION = 2;
3
+ const COMPONENT_STYLES_ASSET_VERSION = 3;
4
+ const preparedAssetStyles = new WeakMap();
5
+ export function prepareAssetComponentStyles(value) {
6
+ if (typeof value === 'object' && value !== null) {
7
+ const cached = preparedAssetStyles.get(value);
8
+ if (cached)
9
+ return cached;
10
+ const prepared = requireComponentStyles(value);
11
+ preparedAssetStyles.set(value, prepared);
12
+ return prepared;
13
+ }
14
+ return requireComponentStyles(value);
15
+ }
3
16
  export function readComponentAssetModule(module) {
4
17
  if (!isObject(module) || !isObject(module.default)) {
5
18
  throw new Error('[WebUI] Component asset module must default-export an asset object.');
@@ -14,9 +27,12 @@ export function validateAsset(value, expectedKind) {
14
27
  if (asset.type !== ASSET_TYPE) {
15
28
  throw new Error(`[WebUI] Invalid component asset type: ${String(asset.type)}`);
16
29
  }
17
- if (asset.version !== ASSET_VERSION) {
30
+ if (asset.version !== COMPONENT_STYLES_ASSET_VERSION) {
18
31
  throw new Error(`[WebUI] Unsupported component asset version: ${String(asset.version)}`);
19
32
  }
33
+ if (asset.componentStyles === undefined) {
34
+ throw new Error('[WebUI] Version 3 component assets require componentStyles.');
35
+ }
20
36
  if (asset.kind !== expectedKind) {
21
37
  throw new Error(`[WebUI] Expected component asset kind "${expectedKind}", received "${String(asset.kind)}".`);
22
38
  }
@@ -26,7 +42,7 @@ export function validateAsset(value, expectedKind) {
26
42
  if (!Array.isArray(asset.imports)) {
27
43
  throw new Error('[WebUI] Component asset imports must be an array.');
28
44
  }
29
- validateStringArray(asset.templateStyles, 'templateStyles');
45
+ prepareAssetComponentStyles(asset.componentStyles);
30
46
  if (!isObject(asset.templates)) {
31
47
  throw new Error('[WebUI] Component asset templates must be an object.');
32
48
  }
@@ -0,0 +1 @@
1
+ export declare function takeGeneratedComponentAssetStyles(tag: string): readonly string[] | undefined;
@@ -0,0 +1,32 @@
1
+ const COMPONENT_ASSET_MANIFEST_ID = 'webui-component-assets';
2
+ let manifestLoaded = false;
3
+ export function takeGeneratedComponentAssetStyles(tag) {
4
+ if (typeof window !== 'object' || typeof document !== 'object')
5
+ return undefined;
6
+ const existing = window.__webui?.componentAssetStyles;
7
+ if (existing) {
8
+ const styles = existing[tag];
9
+ if (styles)
10
+ delete existing[tag];
11
+ return styles;
12
+ }
13
+ if (manifestLoaded)
14
+ return undefined;
15
+ const element = document.getElementById(COMPONENT_ASSET_MANIFEST_ID);
16
+ if (!element) {
17
+ manifestLoaded = true;
18
+ return undefined;
19
+ }
20
+ const text = element.textContent;
21
+ element.remove();
22
+ const manifest = text
23
+ ? JSON.parse(text)
24
+ : {};
25
+ const runtime = window.__webui ?? (window.__webui = {});
26
+ runtime.componentAssetStyles = manifest;
27
+ manifestLoaded = true;
28
+ const styles = manifest[tag];
29
+ if (styles)
30
+ delete manifest[tag];
31
+ return styles;
32
+ }
@@ -1 +1,2 @@
1
- export declare function loadComponentAsset(tag: string, url: string | URL): Promise<void>;
1
+ import type { ComponentAssetSource } from './manifest.js';
2
+ export declare function loadComponentAsset(tag: string, source: ComponentAssetSource): Promise<void>;