@microsoft/webui-framework 0.0.17 → 0.0.19

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/README.md CHANGED
@@ -6,9 +6,9 @@ This package is the browser-side runtime used by `webui build --plugin=webui`. I
6
6
 
7
7
  - `WebUIElement` for SSR hydration and client-created elements
8
8
  - `@observable`, `@attr`, and `@volatile` decorators
9
- - compiled template path mapping for direct DOM binding resolution
9
+ - direct DOM binding updates
10
10
  - light DOM or shadow DOM rendering (`--dom=light|shadow` flag)
11
- - SSR state seeding from `window.__webui.state` (like Preact's props)
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`.
14
14
 
@@ -87,13 +87,44 @@ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--
87
87
  <counter-card label="Taps"></counter-card>
88
88
  ```
89
89
 
90
+ ### HTML-only dormant components
91
+
92
+ If a component has no event handlers, custom lifecycle code, or client-only
93
+ methods, it can ship only `component.html` and optional `component.css`.
94
+
95
+ The sibling `.ts` or `.js` file is the authored behavior boundary. With
96
+ manifest-enabled projection, only `@observable` and `@attr` fields opt into
97
+ initial state hydration; template-only roots stay in the trusted SSR DOM.
98
+ Without a module, template bindings render on the server and the component
99
+ contributes no projected keys. Without projection metadata, the server
100
+ preserves full state. The compiler still emits template metadata for scriptless
101
+ components. When the framework is loaded, it can activate that template when
102
+ browser state or client-side creation needs it.
103
+ If that first write omits a repeat collection, the host preserves the existing
104
+ SSR items until the collection is explicitly supplied.
105
+
106
+ Create a custom element only for an Interactive Island: event handlers, custom
107
+ lifecycle code, imperative methods, or state that TypeScript code reads or
108
+ mutates. `@observable` and `@attr` are optional; add them when JavaScript needs
109
+ to access the value or when the value is part of the component's public API.
110
+
90
111
  ### Build with the WebUI plugin
91
112
 
92
113
  ```bash
93
114
  cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
94
115
  ```
95
116
 
96
- The compiler/plugin generates the template metadata and condition closure arrays consumed by the runtime. In normal app code, you should not need to hand-author `window.__webui.templates` or `window.__webui.templateFns`.
117
+ The WebUI plugin prepares component templates for the browser. Bundle your
118
+ source browser entry directly. Import `@microsoft/webui-framework` from authored
119
+ component modules. An app that stays static after SSR needs no framework
120
+ browser import. Import the framework once when HTML-only components must accept
121
+ browser state or participate in soft navigation.
122
+
123
+ The plugin alone preserves full server state. To emit exact `@observable` and
124
+ `@attr` state surfaces, run the application's bundler first with
125
+ `@microsoft/webui/projection.js`, then pass its manifest to `webui build` with
126
+ `--projection-manifest`. The manifest tooling is build-only; this runtime
127
+ package does not depend on esbuild or TypeScript.
97
128
 
98
129
  ### Property binding lifecycle
99
130
 
@@ -103,7 +134,29 @@ Property bindings use the `:` prefix to pass values directly to child DOM proper
103
134
  <profile-card :config="{{settings}}"></profile-card>
104
135
  ```
105
136
 
106
- For client-created component trees, the runtime upgrades the cloned child elements while they are still detached, wires bindings, and applies the first binding pass before appending them to the connected DOM. A child can read an initial parent-provided property in `connectedCallback`. If the parent value is not set, the child may initialize its own fallback there, and later parent updates still flow through the live binding.
137
+ For client-created component trees, WebUI applies initial property bindings
138
+ before child `connectedCallback` methods run. A child can read an initial
139
+ parent-provided property in `connectedCallback`. If the parent value is not set,
140
+ the child may initialize its own fallback there, and later parent updates still
141
+ flow through the live binding.
142
+
143
+ During SSR hydration the framework trusts the server-rendered DOM and does not
144
+ re-render it. An `@observable` written before hydration finishes — in a field
145
+ initializer, the `constructor`, or before `super.connectedCallback()` — cannot
146
+ update that DOM, so the write is dropped and the runtime logs a
147
+ `[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
148
+ the SSR state, or assign them after `super.connectedCallback()`. The warning is
149
+ development-only and is dead-code-eliminated from production bundles via the
150
+ `__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
151
+ `false`). See the
152
+ [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
153
+
154
+ `super.connectedCallback()` is the synchronous hydration boundary for an
155
+ authored component. When it returns, bindings, events, and `w-ref` references
156
+ are wired. Use a parser-inserted, non-async ES module script or a classic
157
+ `defer` script. A blocking classic script must follow every SSR instance it may
158
+ upgrade. Descendants must not structurally mutate a containing component's SSR
159
+ subtree before it hydrates, because hydration relies on stable compiled paths.
107
160
 
108
161
  ### DOM strategy (`--dom`)
109
162
 
@@ -135,15 +188,50 @@ Base class for framework components.
135
188
  | `static define(tagName)` | Register the class as a custom element |
136
189
  | `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
137
190
  | `$update()` | Force a reactive update (normally called automatically) |
138
- | `setState(state)` | Populate `@observable` properties from router/server state |
139
191
  | `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
140
192
 
141
193
  In most components you do not call `$update()` directly. Property changes through `@observable` and `@attr` trigger updates for you.
142
194
 
195
+ ### Static component assets
196
+
197
+ `webui build --plugin=webui --emit-component-assets settings-dialog` emits
198
+ `settings-dialog.webui.js` next to `protocol.bin`. Load the ESM asset before
199
+ creating the component when you are not using `@microsoft/webui-router`:
200
+
201
+ ```ts
202
+ import { settingsAssets } from './lazy-assets.js';
203
+
204
+ settingsAssets.preload('settings-dialog');
205
+ panelSlot.replaceChildren(await settingsAssets.create('settings-dialog'));
206
+ ```
207
+
208
+ ```ts
209
+ // lazy-assets.ts
210
+ import { defineComponentAssets } from '@microsoft/webui-framework/component-asset.js';
211
+
212
+ export const settingsAssets = defineComponentAssets({
213
+ 'settings-dialog': {
214
+ asset: '/settings-dialog.webui.js',
215
+ module: () => import('./settings-dialog/settings-dialog.js'),
216
+ data: async () => await (await fetch('/settings-dialog-data.json')).json(),
217
+ },
218
+ });
219
+ ```
220
+
221
+ The asset module carries the component's template and style payload. Use
222
+ `preload(tag)` to start template, module, and optional data work early, then
223
+ `create(tag)` to create the element after template/module work is ready.
224
+ Concurrent asset requests share one in-flight load and CSS module styles are
225
+ deduped. `create(tag)` does not block on optional data by default. Use
226
+ `create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component must
227
+ wait briefly for state before mounting.
228
+
143
229
  ### `@observable`
144
230
 
145
- Marks a property as reactive. When the value changes, the framework
146
- re-evaluates the compiled bindings that reference it.
231
+ Marks a property as reactive. When the value changes, the framework
232
+ updates template bindings that reference it. Use it for state that TypeScript
233
+ code reads or mutates. Values used only by the template do not need an
234
+ `@observable` class field.
147
235
 
148
236
  ```ts
149
237
  class SearchPanel extends WebUIElement {
@@ -170,7 +258,8 @@ Notes:
170
258
 
171
259
  - default attribute names use kebab-case
172
260
  - attribute values arrive as strings
173
- - use `@observable` for richer client-only state
261
+ - during SSR hydration, an existing host attribute wins over projected state
262
+ - use `@observable` for state that client code reads or mutates
174
263
 
175
264
  ### `@volatile`
176
265
 
@@ -189,7 +278,7 @@ class CartSummary extends WebUIElement {
189
278
 
190
279
  ## Template Features
191
280
 
192
- The WebUI plugin compiles these template features into runtime metadata:
281
+ The WebUI plugin supports these template features:
193
282
 
194
283
  - text bindings: `{{title}}`
195
284
  - attribute bindings: `href="{{item.href}}"`
@@ -198,6 +287,10 @@ The WebUI plugin compiles these template features into runtime metadata:
198
287
  - conditionals: `<if condition="...">`
199
288
  - repeats: `<for each="item in items">`
200
289
 
290
+ Components that use `@event` must have authored `.ts` or `.js` code that
291
+ defines a `WebUIElement` for the tag. HTML-only components do not provide
292
+ application event handlers.
293
+
201
294
  Example from `examples/app/todo-webui`:
202
295
 
203
296
  ```html
@@ -222,11 +315,17 @@ Root-level events (e.g. `@toggle-item="{onToggleItem(e)}"`) can be declared on t
222
315
 
223
316
  ## Recommended Patterns
224
317
 
225
- - Treat decorated properties as the source of truth.
318
+ - Treat decorated properties as the source of truth for state used by
319
+ TypeScript code.
226
320
  - Update state with property assignments such as `this.open = !this.open`.
227
321
  - Use `$emit()` for child-to-parent communication.
228
322
  - Use `w-ref` for true DOM-only concerns like focus or reading input values.
229
- - Prefer `@observable someValue!: T;` when a value is expected to be seeded externally after construction.
323
+ - Omit `@observable` for values that are only read by the template and seeded
324
+ externally after construction.
325
+ - Omit the TypeScript class when compiled template behavior is sufficient,
326
+ including browser-applied state, route updates, and client-created instances.
327
+ Add a same-named module only for authored events, lifecycle, decorators, or
328
+ imperative APIs.
230
329
 
231
330
  Avoid imperative DOM mutation for application state that can be represented by reactive properties.
232
331
 
@@ -360,7 +459,7 @@ flowchart LR
360
459
  graph TD
361
460
  EL["element.ts (~850 lines)<br/><i>Orchestrator</i><br/>$mount, $wire, $hydrate,<br/>$resolveSSR, $applySSRState,<br/>$update, events, cleanup"]
362
461
 
363
- DIFF["element/diff.ts (~130 lines)<br/><i>List Reconciliation</i><br/>keyed/sequential diffing<br/>for @for repeat blocks"]
462
+ DIFF["element/diff.ts<br/><i>List Reconciliation</i><br/>positional + explicit-key diffing<br/>for &lt;for&gt; repeat blocks"]
364
463
 
365
464
  COND["element/conditions.ts<br/><i>Condition Evaluation</i><br/>evaluateCondition (iterative),<br/>conditionUsesPath"]
366
465
 
@@ -407,7 +506,7 @@ sequenceDiagram
407
506
  CE->>CE: attributeChangedCallback (pre-existing attrs)
408
507
  CE->>FW: connectedCallback() → $mount()
409
508
  FW->>FW: SSR DOM detected (shadow root or children exist)
410
- FW->>FW: $applySSRState() — seed observables from __webui.state
509
+ FW->>FW: $applySSRState() — seed decorated state
411
510
  FW->>FW: $hydrate() — template-parallel path resolution
412
511
  FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
413
512
  FW->>FW: $wireEvents() + $wireRefs()
@@ -454,15 +553,15 @@ interface TemplateMeta {
454
553
  tx?: [slot, parts][]; // Text run locators
455
554
  a?: CompiledAttrMeta[]; // Attribute bindings
456
555
  ag?: [path, start, count][]; // Attribute target groups
457
- c?: [conditionAST, blockIndex][]; // Conditional blocks
458
- cl?: SlotPath[]; // Conditional anchor slots
459
- r?: [collection, itemVar, blockIdx][];// Repeat blocks
460
- rl?: SlotPath[]; // Repeat anchor slots
461
- e?: [event, handler, argSpecs, targetPath][]; // Events
556
+ c?: [conditionAST, blockIndex, slot][]; // Conditional blocks
557
+ r?: [collection, itemVar, blockIdx, slot][]; // Repeat blocks
558
+ eg?: [event, [[handler, argSpecs, targetPath, usesEvent?]]][]; // Events
462
559
  b?: TemplateBlockMeta[]; // Nested block metadata
463
560
  sa?: string; // Adopted stylesheet specifier
464
561
  sd?: boolean; // Shadow DOM flag for client-created
465
562
  re?: [event, handler, argSpecs][]; // Root-level events
563
+ tr?: string[]; // Template state roots
564
+ ta?: string[]; // Host attributes aligned with tr
466
565
  }
467
566
  ```
468
567
 
@@ -482,7 +581,7 @@ Compiled metadata:
482
581
  [[[0], 0], [["title"]]], // slot in <h1>, dynamic "title"
483
582
  [[[1], 1], ["Count: ", ["count"]]] // slot in <button>, static + dynamic
484
583
  ],
485
- e: [["click", "increment", [], [1]]] // click -> increment, no event args
584
+ eg: [["click", [["increment", [], [1]]]]] // click -> increment, no event args
486
585
  }
487
586
  ```
488
587
 
@@ -523,15 +622,16 @@ sequenceDiagram
523
622
  ### Why Updates Are O(affected)
524
623
 
525
624
  After hydration, every dynamic value in the template is connected to a direct
526
- DOM node reference stored in a binding array. A per-path index maps each
527
- `@observable` property name to the subset of bindings that reference it.
625
+ DOM node reference stored in a binding array. A per-path index maps each
626
+ decorated property or compiled template root to the subset of bindings that
627
+ reference it.
528
628
 
529
629
  When `this.count = 5` fires, the `@observable` setter calls `$update('count')`,
530
630
  which looks up `'count'` in the index and only patches the bindings that
531
631
  actually depend on `count` — not every binding in the component.
532
632
 
533
- Computed/volatile getters (paths not in the `@observable` set) are stored
534
- under a wildcard key and always included in targeted updates.
633
+ Computed/volatile getters and other paths that are not known state roots are
634
+ stored under a wildcard key and always included in targeted updates.
535
635
 
536
636
  ```typescript
537
637
  // Targeted update (simplified):
@@ -553,64 +653,58 @@ path index ensures only affected pointers are visited.
553
653
 
554
654
  ## SSR State Seeding
555
655
 
556
- When the server renders `<span>42</span>` for `@observable count = 0`, the
557
- browser sees `42` in the DOM but the JavaScript property `this.count` is still
558
- `0` (the class default). Without seeding, the first `$update()` would
559
- overwrite the SSR content with the wrong value.
656
+ When the server renders `<span>42</span>` for a template binding, the browser
657
+ sees `42` in the DOM before the component's JavaScript state exists. Without
658
+ seeding, the first `$update()` would overwrite the SSR content with the wrong
659
+ value.
560
660
 
561
- State seeding uses `window.__webui.state` — a JSON object loaded from the
562
- server-emitted `#webui-data` block. Like Preact's props, this delivers the
563
- same data used for SSR rendering to the client. During `$mount()`,
564
- `$applySSRState()` writes matching keys directly to observable backing fields
565
- before any bindings are wired:
661
+ State seeding uses `window.__webui.state` loaded from the server-emitted
662
+ `#webui-data` block. When the protocol contains projection metadata, only
663
+ `@observable` and `@attr` keys from reachable authored components select
664
+ initial state; HTML-only dormant components and authored template-only roots
665
+ contribute no startup keys. Without projection metadata, the server preserves
666
+ full state. During `$mount()`, `$applySSRState()` writes matching decorated keys
667
+ directly to observable backing fields before any bindings are wired:
566
668
 
567
669
  ```mermaid
568
670
  flowchart LR
569
- SCRIPT["&lt;script type='application/json' id='webui-data'&gt;<br/>{ state: { count: 42, title: 'Hello' } }"] --> APPLY["$applySSRState()"]
570
- APPLY --> SEED["Write to backing fields:<br/>this._count = 42<br/>this._title = 'Hello'"]
671
+ SCRIPT["&lt;script type='application/json' id='webui-data'&gt;<br/>{ state: { count: 42 } }"] --> APPLY["$applySSRState()"]
672
+ APPLY --> SEED["Write decorated backing fields"]
571
673
  SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
572
674
  ```
573
675
 
574
- `$applySSRState()` only sets properties that exist in the component's
575
- `@observable` set — unknown keys are ignored. Writes go to the backing
576
- field (`_prop`) directly, avoiding reactive updates before bindings are wired.
676
+ Decorated writes go to the backing field (`_prop`) directly, avoiding reactive
677
+ updates before bindings are wired. For `@attr`, an existing SSR host attribute
678
+ takes precedence and the projected value is skipped. Template-only values
679
+ remain represented by the SSR DOM until browser state explicitly changes them.
680
+ Later `setState()` calls, including router partials, accept both decorated
681
+ properties and compiled template roots; undecorated roots are stored in hidden
682
+ framework state. The first write to a dormant HTML-only host replays only the
683
+ roots present in that write, preserving omitted SSR text, attributes,
684
+ conditions, and repeats.
577
685
 
578
686
  ---
579
687
 
580
688
  ## Repeat Reconciliation
581
689
 
582
- `@for(item of items)` blocks support two reconciliation strategies,
583
- implemented in `element/diff.ts` (~130 lines):
584
-
585
- ### Keyed Reconciliation
586
-
587
- When the repeat block's root element has attribute bindings (e.g.
588
- `<todo-item id="{{item.id}}">`), the framework uses the first attribute as a
589
- key. This preserves DOM nodes across reorders:
590
-
591
- ```mermaid
592
- flowchart TD
593
- subgraph Before ["Before: items = [A, B, C]"]
594
- A1["&lt;todo-item&gt; key=A"]
595
- B1["&lt;todo-item&gt; key=B"]
596
- C1["&lt;todo-item&gt; key=C"]
597
- end
598
-
599
- subgraph After ["After: items = [C, A]"]
600
- C2["&lt;todo-item&gt; key=C ← reused"]
601
- A2["&lt;todo-item&gt; key=A ← reused"]
602
- B2["key=B ← removed"]
603
- end
604
-
605
- A1 -.->|"moved"| A2
606
- C1 -.->|"moved"| C2
607
- B1 -.->|"destroyed"| B2
608
- ```
609
-
610
- ### Sequential Reconciliation
611
-
612
- When no keying attributes exist, items are matched by position. Excess items
613
- are removed; new items are appended.
690
+ `<for>` blocks reconcile by array position by default. The existing block at
691
+ index `i` receives the current item at index `i`; only a new or removed tail
692
+ creates or removes blocks.
693
+
694
+ Duplicate values and attributes are safe because dynamic attributes never act
695
+ as hidden keys. Reordering rebinds existing blocks in place, so local
696
+ browser-owned or component state remains associated with positions rather than
697
+ logical items.
698
+
699
+ For reorderable or stateful lists, author `key="{{item.id}}"` on the first
700
+ child inside `<for>` to move existing blocks with their logical items.
701
+ `key="{{item}}"` supports arrays of unique string or finite-number primitives.
702
+ `key` is compiler-only metadata and is removed from SSR and client HTML;
703
+ `data-key` remains an ordinary attribute with no identity semantics. Key paths
704
+ are compiler-validated and stored only for explicitly keyed repeats, so
705
+ unkeyed bindings carry no key state or map allocation. Duplicate or invalid
706
+ runtime keys warn once, clear identity, and use positional reconciliation until
707
+ valid identity is re-established.
614
708
 
615
709
  ### SSR State Reading
616
710
 
@@ -684,7 +778,7 @@ This template-parallel traversal eliminates the need for any marker comments,
684
778
  | Initial hydration | O(bindings) | Single pass over compiled path mappings |
685
779
  | Reactive update | O(affected) | Per-path index skips unrelated bindings |
686
780
  | Conditional toggle | O(block size) | Create/destroy a block instance |
687
- | Repeat reconciliation | O(items) | Keyed map lookup or sequential scan |
781
+ | Repeat reconciliation | O(items) | Positional scan; explicit keys use a reusable map only when order changes |
688
782
  | Event wiring | O(events) | One-time during hydration |
689
783
 
690
784
  ### What the framework does NOT do
@@ -0,0 +1,30 @@
1
+ import { type CompiledConditionFn, type TemplateMeta } from './template.js';
2
+ export interface ComponentAsset {
3
+ type?: 'webui-component-asset';
4
+ version?: number;
5
+ components?: string[];
6
+ templateStyles?: string[];
7
+ templates?: Record<string, TemplateMeta>;
8
+ templateFunctions?: Record<string, CompiledConditionFn[]>;
9
+ }
10
+ export type ComponentAssetState = Record<string, unknown>;
11
+ export interface ComponentAssetManifestEntry<Data extends ComponentAssetState = ComponentAssetState> {
12
+ asset: string | URL;
13
+ module?: () => Promise<unknown>;
14
+ data?: () => Promise<Data>;
15
+ }
16
+ export type ComponentAssetManifest = Record<string, ComponentAssetManifestEntry>;
17
+ export interface ComponentAssetPreload<Data extends ComponentAssetState = ComponentAssetState> {
18
+ asset: Promise<void>;
19
+ module?: Promise<unknown>;
20
+ data?: Promise<Data>;
21
+ }
22
+ export interface ComponentAssetCreateOptions {
23
+ awaitData?: boolean;
24
+ dataTimeoutMs?: number;
25
+ }
26
+ export interface ComponentAssetRegistry {
27
+ preload<Data extends ComponentAssetState = ComponentAssetState>(tag: string): ComponentAssetPreload<Data>;
28
+ create(tag: string, options?: ComponentAssetCreateOptions): Promise<HTMLElement>;
29
+ }
30
+ export declare function defineComponentAssets(manifest: ComponentAssetManifest): ComponentAssetRegistry;
@@ -0,0 +1,213 @@
1
+ import { getTemplate, registerTemplateData, } from './template.js';
2
+ const ASSET_TYPE = 'webui-component-asset';
3
+ const ASSET_VERSION = 1;
4
+ const injectedAssetStyles = new Set();
5
+ const assetLoadPromises = new Map();
6
+ let assetStylesSeeded = false;
7
+ function assetGlobal() {
8
+ return window.__webui;
9
+ }
10
+ export function defineComponentAssets(manifest) {
11
+ const preloads = new Map();
12
+ function preload(tag) {
13
+ const existing = preloads.get(tag);
14
+ if (existing)
15
+ return existing;
16
+ const entry = manifest[tag];
17
+ if (!entry) {
18
+ throw new Error(`[WebUI] No component asset manifest entry for <${tag}>.`);
19
+ }
20
+ const next = {
21
+ asset: loadComponentAsset(tag, entry.asset),
22
+ };
23
+ if (entry.module) {
24
+ next.module = entry.module();
25
+ }
26
+ if (entry.data) {
27
+ next.data = entry.data();
28
+ }
29
+ next.asset.catch(() => { });
30
+ next.module?.catch(() => { });
31
+ next.data?.catch(() => { });
32
+ preloads.set(tag, next);
33
+ return next;
34
+ }
35
+ async function create(tag, options = {}) {
36
+ const pending = preload(tag);
37
+ await waitForElementResources(pending);
38
+ const element = document.createElement(tag);
39
+ if (pending.data) {
40
+ if (options.awaitData) {
41
+ const state = options.dataTimeoutMs === undefined
42
+ ? await pending.data
43
+ : await dataWithTimeout(pending.data, options.dataTimeoutMs);
44
+ if (state) {
45
+ applyState(element, state);
46
+ }
47
+ else {
48
+ applyDataWhenReady(element, pending.data);
49
+ }
50
+ }
51
+ else {
52
+ applyDataWhenReady(element, pending.data);
53
+ }
54
+ }
55
+ return element;
56
+ }
57
+ return { preload, create };
58
+ }
59
+ async function waitForElementResources(pending) {
60
+ await pending.asset;
61
+ if (pending.module)
62
+ await pending.module;
63
+ }
64
+ function applyState(element, state) {
65
+ const setState = element.setState;
66
+ if (typeof setState === 'function') {
67
+ setState.call(element, state);
68
+ }
69
+ }
70
+ function applyDataWhenReady(element, data) {
71
+ const elementRef = new WeakRef(element);
72
+ void data.then(state => {
73
+ const liveElement = elementRef.deref();
74
+ if (liveElement)
75
+ applyState(liveElement, state);
76
+ }).catch(() => { });
77
+ }
78
+ function dataWithTimeout(data, timeoutMs) {
79
+ if (timeoutMs < 0)
80
+ return data;
81
+ return Promise.race([
82
+ data,
83
+ new Promise(resolve => {
84
+ setTimeout(() => resolve(undefined), timeoutMs);
85
+ }),
86
+ ]);
87
+ }
88
+ function loadComponentAsset(tag, url) {
89
+ if (getTemplate(tag))
90
+ return Promise.resolve();
91
+ const assetUrl = new URL(url, document.baseURI);
92
+ const href = assetUrl.href;
93
+ let promise = assetLoadPromises.get(href);
94
+ if (promise)
95
+ return promise;
96
+ promise = importAndRegisterComponentAsset(assetUrl)
97
+ .finally(() => {
98
+ assetLoadPromises.delete(href);
99
+ });
100
+ assetLoadPromises.set(href, promise);
101
+ return promise;
102
+ }
103
+ function registerComponentAsset(asset) {
104
+ validateAsset(asset);
105
+ if (asset.templates && templatesAlreadyRegistered(asset.templates))
106
+ return;
107
+ registerAssetStyles(asset.templateStyles, readNonce());
108
+ if (asset.templates) {
109
+ registerTemplateData(asset.templates, asset.templateFunctions);
110
+ }
111
+ }
112
+ async function importAndRegisterComponentAsset(assetUrl) {
113
+ const imported = await import(assetUrl.href);
114
+ registerComponentAsset(readComponentAssetModule(imported));
115
+ }
116
+ function readComponentAssetModule(module) {
117
+ if (!isObject(module) || !isObject(module.default)) {
118
+ throw new Error('[WebUI] Component asset module must default-export an asset object.');
119
+ }
120
+ return module.default;
121
+ }
122
+ function isObject(value) {
123
+ return typeof value === 'object' && value !== null;
124
+ }
125
+ function validateAsset(asset) {
126
+ if (asset.type !== ASSET_TYPE) {
127
+ throw new Error(`[WebUI] Invalid component asset type: ${String(asset.type)}`);
128
+ }
129
+ if (asset.version !== ASSET_VERSION) {
130
+ throw new Error(`[WebUI] Unsupported component asset version: ${String(asset.version)}`);
131
+ }
132
+ }
133
+ function templatesAlreadyRegistered(templates) {
134
+ const tags = Object.keys(templates);
135
+ if (tags.length === 0)
136
+ return false;
137
+ for (let i = 0; i < tags.length; i++) {
138
+ if (!getTemplate(tags[i]))
139
+ return false;
140
+ }
141
+ return true;
142
+ }
143
+ function readNonce() {
144
+ const nonce = assetGlobal()?.nonce;
145
+ if (nonce)
146
+ return nonce;
147
+ const meta = document.querySelector('meta[name="webui-nonce"]');
148
+ return meta?.content ?? '';
149
+ }
150
+ function seedAssetStyleSet() {
151
+ if (assetStylesSeeded)
152
+ return;
153
+ assetStylesSeeded = true;
154
+ const styles = assetGlobal()?.styles;
155
+ if (!styles)
156
+ return;
157
+ for (let i = 0; i < styles.length; i++) {
158
+ injectedAssetStyles.add(styles[i]);
159
+ }
160
+ }
161
+ function registerAssetStyles(templateStyles, nonce) {
162
+ if (!templateStyles || templateStyles.length === 0)
163
+ return;
164
+ seedAssetStyleSet();
165
+ for (let i = 0; i < templateStyles.length; i++) {
166
+ const imports = parseImportMap(templateStyles[i]);
167
+ const nextImports = {};
168
+ let hasNewImport = false;
169
+ const specifiers = Object.keys(imports);
170
+ for (let j = 0; j < specifiers.length; j++) {
171
+ const specifier = specifiers[j];
172
+ if (injectedAssetStyles.has(specifier))
173
+ continue;
174
+ injectedAssetStyles.add(specifier);
175
+ nextImports[specifier] = imports[specifier];
176
+ hasNewImport = true;
177
+ }
178
+ if (!hasNewImport)
179
+ continue;
180
+ const script = document.createElement('script');
181
+ script.type = 'importmap';
182
+ if (nonce)
183
+ script.nonce = nonce;
184
+ script.textContent = JSON.stringify({ imports: nextImports });
185
+ document.head.appendChild(script);
186
+ }
187
+ }
188
+ function parseImportMap(scriptMarkup) {
189
+ const trimmed = scriptMarkup.trim();
190
+ if (!trimmed.startsWith('<script')) {
191
+ throw new Error('[WebUI] Component asset templateStyles entry must be a <script type="importmap"> tag.');
192
+ }
193
+ const openTagEnd = trimmed.indexOf('>');
194
+ const closeTagStart = trimmed.lastIndexOf('</script>');
195
+ if (openTagEnd < 0 || closeTagStart <= openTagEnd) {
196
+ throw new Error('[WebUI] Component asset importmap tag is malformed.');
197
+ }
198
+ const parsed = JSON.parse(trimmed.substring(openTagEnd + 1, closeTagStart));
199
+ if (!parsed.imports || typeof parsed.imports !== 'object') {
200
+ throw new Error('[WebUI] Component asset importmap is missing an imports object.');
201
+ }
202
+ const imports = {};
203
+ const specifiers = Object.keys(parsed.imports);
204
+ for (let i = 0; i < specifiers.length; i++) {
205
+ const specifier = specifiers[i];
206
+ const uri = parsed.imports[specifier];
207
+ if (typeof uri !== 'string' || !uri.startsWith('data:text/css,')) {
208
+ throw new Error(`[WebUI] Component asset importmap entry "${specifier}" must be a data:text/css URI.`);
209
+ }
210
+ imports[specifier] = uri;
211
+ }
212
+ return imports;
213
+ }