@microsoft/webui-framework 0.0.17 → 0.0.18

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,33 @@ 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 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
+ When HTML-only components receive server or route state, import
96
+ `@microsoft/webui-framework` somewhere in the browser entry. The framework root
97
+ installs the static host runtime, which only claims compiler-owned HTML-only
98
+ components whose templates need hidden state or observed host attributes. Fully
99
+ static HTML-only components stay as plain SSR DOM.
100
+
101
+ Create a custom element only for an Interactive Island: event handlers, custom
102
+ lifecycle code, imperative methods, or state that TypeScript code reads or
103
+ mutates. `@observable` and `@attr` are optional; add them when JavaScript needs
104
+ to access the value or when the value is part of the component's public API.
105
+
90
106
  ### Build with the WebUI plugin
91
107
 
92
108
  ```bash
93
109
  cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
94
110
  ```
95
111
 
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`.
112
+ The WebUI plugin prepares component templates for the browser. Bundle your
113
+ source browser entry directly. Import `@microsoft/webui-framework` from authored
114
+ component modules, or once from the browser entry when the app has no authored
115
+ components but still uses HTML-only components that receive server or route
116
+ state.
97
117
 
98
118
  ### Property binding lifecycle
99
119
 
@@ -103,7 +123,22 @@ Property bindings use the `:` prefix to pass values directly to child DOM proper
103
123
  <profile-card :config="{{settings}}"></profile-card>
104
124
  ```
105
125
 
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.
126
+ For client-created component trees, WebUI applies initial property bindings
127
+ before child `connectedCallback` methods run. A child can read an initial
128
+ parent-provided property in `connectedCallback`. If the parent value is not set,
129
+ the child may initialize its own fallback there, and later parent updates still
130
+ flow through the live binding.
131
+
132
+ During SSR hydration the framework trusts the server-rendered DOM and does not
133
+ re-render it. An `@observable` written before hydration finishes — in a field
134
+ initializer, the `constructor`, or before `super.connectedCallback()` — cannot
135
+ update that DOM, so the write is dropped and the runtime logs a
136
+ `[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
137
+ the SSR state, or assign them after `super.connectedCallback()`. The warning is
138
+ development-only and is dead-code-eliminated from production bundles via the
139
+ `__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
140
+ `false`). See the
141
+ [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
107
142
 
108
143
  ### DOM strategy (`--dom`)
109
144
 
@@ -135,15 +170,50 @@ Base class for framework components.
135
170
  | `static define(tagName)` | Register the class as a custom element |
136
171
  | `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
137
172
  | `$update()` | Force a reactive update (normally called automatically) |
138
- | `setState(state)` | Populate `@observable` properties from router/server state |
139
173
  | `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
140
174
 
141
175
  In most components you do not call `$update()` directly. Property changes through `@observable` and `@attr` trigger updates for you.
142
176
 
177
+ ### Static component assets
178
+
179
+ `webui build --plugin=webui --emit-component-assets settings-dialog` emits
180
+ `settings-dialog.webui.js` next to `protocol.bin`. Load the ESM asset before
181
+ creating the component when you are not using `@microsoft/webui-router`:
182
+
183
+ ```ts
184
+ import { settingsAssets } from './lazy-assets.js';
185
+
186
+ settingsAssets.preload('settings-dialog');
187
+ panelSlot.replaceChildren(await settingsAssets.create('settings-dialog'));
188
+ ```
189
+
190
+ ```ts
191
+ // lazy-assets.ts
192
+ import { defineComponentAssets } from '@microsoft/webui-framework/component-asset.js';
193
+
194
+ export const settingsAssets = defineComponentAssets({
195
+ 'settings-dialog': {
196
+ asset: '/settings-dialog.webui.js',
197
+ module: () => import('./settings-dialog/settings-dialog.js'),
198
+ data: async () => await (await fetch('/settings-dialog-data.json')).json(),
199
+ },
200
+ });
201
+ ```
202
+
203
+ The asset module carries the component's template and style payload. Use
204
+ `preload(tag)` to start template, module, and optional data work early, then
205
+ `create(tag)` to create the element after template/module work is ready.
206
+ Concurrent asset requests share one in-flight load and CSS module styles are
207
+ deduped. `create(tag)` does not block on optional data by default. Use
208
+ `create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component must
209
+ wait briefly for state before mounting.
210
+
143
211
  ### `@observable`
144
212
 
145
- Marks a property as reactive. When the value changes, the framework
146
- re-evaluates the compiled bindings that reference it.
213
+ Marks a property as reactive. When the value changes, the framework
214
+ updates template bindings that reference it. Use it for state that TypeScript
215
+ code reads or mutates. Values used only by the template do not need an
216
+ `@observable` class field.
147
217
 
148
218
  ```ts
149
219
  class SearchPanel extends WebUIElement {
@@ -170,7 +240,7 @@ Notes:
170
240
 
171
241
  - default attribute names use kebab-case
172
242
  - attribute values arrive as strings
173
- - use `@observable` for richer client-only state
243
+ - use `@observable` for state that client code reads or mutates
174
244
 
175
245
  ### `@volatile`
176
246
 
@@ -189,7 +259,7 @@ class CartSummary extends WebUIElement {
189
259
 
190
260
  ## Template Features
191
261
 
192
- The WebUI plugin compiles these template features into runtime metadata:
262
+ The WebUI plugin supports these template features:
193
263
 
194
264
  - text bindings: `{{title}}`
195
265
  - attribute bindings: `href="{{item.href}}"`
@@ -198,6 +268,9 @@ The WebUI plugin compiles these template features into runtime metadata:
198
268
  - conditionals: `<if condition="...">`
199
269
  - repeats: `<for each="item in items">`
200
270
 
271
+ Components that use `@event` must have authored `.ts` or `.js` code that
272
+ defines a `WebUIElement` for the tag; HTML-only components are declarative only.
273
+
201
274
  Example from `examples/app/todo-webui`:
202
275
 
203
276
  ```html
@@ -222,11 +295,15 @@ Root-level events (e.g. `@toggle-item="{onToggleItem(e)}"`) can be declared on t
222
295
 
223
296
  ## Recommended Patterns
224
297
 
225
- - Treat decorated properties as the source of truth.
298
+ - Treat decorated properties as the source of truth for state used by
299
+ TypeScript code.
226
300
  - Update state with property assignments such as `this.open = !this.open`.
227
301
  - Use `$emit()` for child-to-parent communication.
228
302
  - 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.
303
+ - Omit `@observable` for values that are only read by the template and seeded
304
+ externally after construction.
305
+ - Omit the TypeScript class for HTML-only components that only need template
306
+ bindings and router/server state.
230
307
 
231
308
  Avoid imperative DOM mutation for application state that can be represented by reactive properties.
232
309
 
@@ -407,7 +484,7 @@ sequenceDiagram
407
484
  CE->>CE: attributeChangedCallback (pre-existing attrs)
408
485
  CE->>FW: connectedCallback() → $mount()
409
486
  FW->>FW: SSR DOM detected (shadow root or children exist)
410
- FW->>FW: $applySSRState() — seed observables from __webui.state
487
+ FW->>FW: $applySSRState() — seed decorated + template state
411
488
  FW->>FW: $hydrate() — template-parallel path resolution
412
489
  FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
413
490
  FW->>FW: $wireEvents() + $wireRefs()
@@ -454,15 +531,14 @@ interface TemplateMeta {
454
531
  tx?: [slot, parts][]; // Text run locators
455
532
  a?: CompiledAttrMeta[]; // Attribute bindings
456
533
  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
534
+ c?: [conditionAST, blockIndex, slot][]; // Conditional blocks
535
+ r?: [collection, itemVar, blockIdx, slot][]; // Repeat blocks
536
+ eg?: [event, [[handler, argSpecs, targetPath, usesEvent?]]][]; // Events
462
537
  b?: TemplateBlockMeta[]; // Nested block metadata
463
538
  sa?: string; // Adopted stylesheet specifier
464
539
  sd?: boolean; // Shadow DOM flag for client-created
465
540
  re?: [event, handler, argSpecs][]; // Root-level events
541
+ th?: 1; // Compiler-owned static host
466
542
  }
467
543
  ```
468
544
 
@@ -482,7 +558,7 @@ Compiled metadata:
482
558
  [[[0], 0], [["title"]]], // slot in <h1>, dynamic "title"
483
559
  [[[1], 1], ["Count: ", ["count"]]] // slot in <button>, static + dynamic
484
560
  ],
485
- e: [["click", "increment", [], [1]]] // click -> increment, no event args
561
+ eg: [["click", [["increment", [], [1]]]]] // click -> increment, no event args
486
562
  }
487
563
  ```
488
564
 
@@ -523,15 +599,16 @@ sequenceDiagram
523
599
  ### Why Updates Are O(affected)
524
600
 
525
601
  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.
602
+ DOM node reference stored in a binding array. A per-path index maps each
603
+ decorated property or compiled template root to the subset of bindings that
604
+ reference it.
528
605
 
529
606
  When `this.count = 5` fires, the `@observable` setter calls `$update('count')`,
530
607
  which looks up `'count'` in the index and only patches the bindings that
531
608
  actually depend on `count` — not every binding in the component.
532
609
 
533
- Computed/volatile getters (paths not in the `@observable` set) are stored
534
- under a wildcard key and always included in targeted updates.
610
+ Computed/volatile getters and other paths that are not known state roots are
611
+ stored under a wildcard key and always included in targeted updates.
535
612
 
536
613
  ```typescript
537
614
  // Targeted update (simplified):
@@ -553,27 +630,29 @@ path index ensures only affected pointers are visited.
553
630
 
554
631
  ## SSR State Seeding
555
632
 
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.
633
+ When the server renders `<span>42</span>` for a template binding, the browser
634
+ sees `42` in the DOM before the component's JavaScript state exists. Without
635
+ seeding, the first `$update()` would overwrite the SSR content with the wrong
636
+ value.
560
637
 
561
638
  State seeding uses `window.__webui.state` — a JSON object loaded from the
562
639
  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:
640
+ same data used for SSR rendering to the client. During `$mount()`,
641
+ `$applySSRState()` writes matching decorated keys directly to observable backing
642
+ fields and stores undecorated template roots in hidden framework state before
643
+ any bindings are wired:
566
644
 
567
645
  ```mermaid
568
646
  flowchart LR
569
647
  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'"]
648
+ APPLY --> SEED["Write decorated fields + hidden template state"]
571
649
  SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
572
650
  ```
573
651
 
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.
652
+ `$applySSRState()` only accepts keys that are decorated properties or compiled
653
+ template roots. Unknown keys are ignored. Decorated writes go to the backing
654
+ field (`_prop`) directly, and undecorated template roots stay internal, avoiding
655
+ reactive updates before bindings are wired.
577
656
 
578
657
  ---
579
658
 
@@ -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
+ }
@@ -1,62 +1,11 @@
1
- /**
2
- * Convert a camelCase DOM property name into its kebab-case HTML attribute form.
3
- *
4
- * This function is optimized for framework-level hot paths where attribute
5
- * normalization may run thousands of times per render. It performs three
6
- * progressively cheaper checks:
7
- *
8
- * 1. **Direct lookup for irregular mappings**
9
- * Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
10
- * follow simple camelCase → kebab-case rules. These are resolved through a
11
- * precomputed `propertyToAttribute` map for O(1) returns with no string
12
- * processing.
13
- *
14
- * 2. **Fast path for ARIA attributes**
15
- * ARIA properties always begin with `aria` followed by an uppercase letter
16
- * (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
17
- * This branch avoids the general loop and uses the engine-optimized
18
- * `.toLowerCase()` for the suffix.
19
- *
20
- * 3. **General camelCase → kebab-case conversion**
21
- * For all other inputs, the function performs a tight ASCII-only scan:
22
- * uppercase A–Z (65–90) are converted to lowercase and prefixed with `-`,
23
- * while all other characters are copied as-is. This avoids regex engines,
24
- * callback allocations, and match objects, producing predictable,
25
- * allocation-minimal performance ideal for DOM attribute reflection.
26
- *
27
- * The result is a predictable, JIT-friendly transformation suitable for
28
- * attribute diffing, SSR serialization, and runtime DOM patching.
29
- */
30
1
  export declare function toKebabCase(str: string): string;
31
2
  export declare function getObservableNames(ctor: Function): Set<string>;
32
- /**
33
- * Marks a property as observable. When the value changes the decorator will:
34
- * 1. Call `this.<prop>Changed(oldValue, newValue)` if defined.
35
- * 2. Call `this.$update(name)` if the element is connected, targeting
36
- * only bindings that reference this property.
37
- */
38
- export declare function observable(target: object, name: string): void;
39
3
  export declare function isAttributeProperty(ctor: Function, property: string): boolean;
40
- /**
41
- * Like {@link observable} but also reflects to/from an HTML attribute
42
- * (kebab-case). The decorator patches `observedAttributes` and
43
- * `attributeChangedCallback` on the class so changes flow in both directions.
44
- *
45
- * @example
46
- * ```ts
47
- * class MyEl extends WebUIElement {
48
- * @attr myProp = 'default';
49
- * // syncs with attribute "my-prop"
50
- * }
51
- * ```
52
- */
4
+ export declare function syncAttrProperties(instance: object, ctor: Function): void;
5
+ export declare function observable(target: object, name: string): void;
53
6
  export interface AttrOptions {
54
7
  attribute?: string;
55
- /** When `'boolean'`, the property is `true` when the attribute is present
56
- * and `false` when absent — matching native HTML boolean attribute semantics.
57
- * Default is string mode (property receives the attribute string value). */
58
8
  mode?: 'boolean';
59
9
  }
60
- export declare function syncAttrProperties(instance: object, ctor: Function): void;
61
10
  export declare function attr(target: object, name: string): void;
62
11
  export declare function attr(options: AttrOptions): (target: object, name: string) => void;