@lingxia/elements 0.14.0 → 0.16.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.
Files changed (69) hide show
  1. package/dist/index.d.ts +3 -2
  2. package/dist/index.js +1 -0
  3. package/dist/inline-native/commit.d.ts +39 -0
  4. package/dist/inline-native/commit.js +94 -0
  5. package/dist/inline-native/elements.d.ts +104 -0
  6. package/dist/inline-native/elements.js +749 -0
  7. package/dist/inline-native/errors.d.ts +8 -0
  8. package/dist/inline-native/errors.js +23 -0
  9. package/dist/inline-native/events.d.ts +10 -0
  10. package/dist/inline-native/events.js +16 -0
  11. package/dist/inline-native/geometry.d.ts +78 -0
  12. package/dist/inline-native/geometry.js +40 -0
  13. package/dist/inline-native/identity.d.ts +28 -0
  14. package/dist/inline-native/identity.js +51 -0
  15. package/dist/inline-native/index.d.ts +14 -0
  16. package/dist/inline-native/index.js +19 -0
  17. package/dist/inline-native/layout.d.ts +2 -0
  18. package/dist/inline-native/layout.js +58 -0
  19. package/dist/inline-native/lease.d.ts +13 -0
  20. package/dist/inline-native/lease.js +31 -0
  21. package/dist/inline-native/runtime.d.ts +64 -0
  22. package/dist/inline-native/runtime.js +119 -0
  23. package/dist/inline-native/schema.d.ts +111 -0
  24. package/dist/inline-native/schema.js +210 -0
  25. package/dist/inline-native/structure.d.ts +22 -0
  26. package/dist/inline-native/structure.js +706 -0
  27. package/dist/inline-native/style.d.ts +7 -0
  28. package/dist/inline-native/style.js +119 -0
  29. package/dist/inline-native/types.d.ts +82 -0
  30. package/dist/inline-native/types.js +7 -0
  31. package/dist/inline-native/unwrap.d.ts +10 -0
  32. package/dist/inline-native/unwrap.js +32 -0
  33. package/dist/inline-native/video.d.ts +65 -0
  34. package/dist/inline-native/video.js +69 -0
  35. package/dist/native_component_wrapper_shared.d.ts +3 -2
  36. package/dist/native_component_wrapper_shared.js +6 -3
  37. package/dist/nativecomponent.d.ts +1 -1
  38. package/dist/nativecomponent.js +8 -11
  39. package/dist/navigator.d.ts +2 -2
  40. package/dist/navigator.js +5 -5
  41. package/dist/picker.js +12 -4
  42. package/dist/video-events.d.ts +2 -0
  43. package/dist/video-events.js +18 -0
  44. package/dist/video.d.ts +98 -79
  45. package/dist/video.js +182 -757
  46. package/package.json +2 -1
  47. package/src/index.ts +58 -1
  48. package/src/inline-native/commit.ts +141 -0
  49. package/src/inline-native/elements.ts +829 -0
  50. package/src/inline-native/errors.ts +35 -0
  51. package/src/inline-native/events.ts +31 -0
  52. package/src/inline-native/geometry.ts +86 -0
  53. package/src/inline-native/identity.ts +102 -0
  54. package/src/inline-native/index.ts +103 -0
  55. package/src/inline-native/layout.ts +58 -0
  56. package/src/inline-native/lease.ts +48 -0
  57. package/src/inline-native/runtime.ts +185 -0
  58. package/src/inline-native/schema.ts +247 -0
  59. package/src/inline-native/structure.ts +823 -0
  60. package/src/inline-native/style.ts +111 -0
  61. package/src/inline-native/types.ts +105 -0
  62. package/src/inline-native/unwrap.ts +34 -0
  63. package/src/inline-native/video.ts +125 -0
  64. package/src/native_component_wrapper_shared.ts +8 -6
  65. package/src/nativecomponent.ts +8 -13
  66. package/src/navigator.ts +9 -9
  67. package/src/picker.ts +13 -4
  68. package/src/video-events.ts +16 -0
  69. package/src/video.ts +245 -839
@@ -0,0 +1,829 @@
1
+ import {
2
+ collectAuthorTreeFromElement,
3
+ compileInlineNativeRoot,
4
+ parseBooleanAttr,
5
+ } from "./structure.js";
6
+ import type { CompileInlineNativeResult, NativeError, RootRef } from "./types.js";
7
+ import { nativeError } from "./errors.js";
8
+ import { nextOpaqueKey } from "./identity.js";
9
+ import {
10
+ applyHostLeaseMessage,
11
+ createRootRuntimeState,
12
+ publishCompiledRoot,
13
+ rootFallbackAllowed,
14
+ type RootRuntimeState,
15
+ } from "./runtime.js";
16
+ import { isFallbackElement } from "./structure.js";
17
+ import { applyIslandHostEvent } from "./events.js";
18
+ import { effectiveOpacity } from "./style.js";
19
+ import { observeNativeLayout } from "./layout.js";
20
+ import { ensureComponentId } from "../component.js";
21
+ import {
22
+ registerNativeComponentHandler,
23
+ sendNativeComponentMessage,
24
+ } from "../nativecomponent.js";
25
+
26
+ const ROOT_INVALID_EVENT = "error";
27
+ const STRUCTURE_COMPILED_EVENT = "lxnativecompiled";
28
+
29
+ type PendingCompile = {
30
+ frame: number | null;
31
+ timer: ReturnType<typeof setTimeout> | null;
32
+ };
33
+
34
+ function asRecord(el: HTMLElement): HTMLElement & Record<string, unknown> {
35
+ return el as HTMLElement & Record<string, unknown>;
36
+ }
37
+
38
+ function reflectBoolean(el: HTMLElement, name: string, value: unknown): void {
39
+ if (value === false || value === "false") {
40
+ el.setAttribute(name, "false");
41
+ return;
42
+ }
43
+ if (value === true || value === "" || value === "true") {
44
+ el.setAttribute(name, "");
45
+ return;
46
+ }
47
+ if (value == null) {
48
+ el.removeAttribute(name);
49
+ return;
50
+ }
51
+ el.setAttribute(name, String(value));
52
+ }
53
+
54
+ function reflectString(el: HTMLElement, name: string, value: unknown): void {
55
+ if (value == null || value === "") {
56
+ el.removeAttribute(name);
57
+ return;
58
+ }
59
+ el.setAttribute(name, String(value));
60
+ }
61
+
62
+ class LxNativeBaseElement extends HTMLElement {
63
+ static get observedAttributes(): string[] {
64
+ return [
65
+ "id",
66
+ "automation-id",
67
+ "hidden",
68
+ "pointer-events",
69
+ "aria-label",
70
+ "aria-description",
71
+ "aria-hidden",
72
+ ];
73
+ }
74
+
75
+ /// Geometry is measured per element id, so every island node needs one —
76
+ /// without it the host would place this node at the root's rect.
77
+ connectedCallback(): void {
78
+ ensureComponentId(this, this.localName);
79
+ }
80
+
81
+ get automationId(): string | null {
82
+ return this.getAttribute("automation-id");
83
+ }
84
+ set automationId(value: string | null | undefined) {
85
+ reflectString(this, "automation-id", value);
86
+ }
87
+
88
+ get pointerEvents(): string | null {
89
+ return this.getAttribute("pointer-events");
90
+ }
91
+ set pointerEvents(value: string | null | undefined) {
92
+ reflectString(this, "pointer-events", value);
93
+ }
94
+ }
95
+
96
+ export class LxNativeRootElement extends LxNativeBaseElement {
97
+ static get observedAttributes(): string[] {
98
+ return LxNativeBaseElement.observedAttributes;
99
+ }
100
+
101
+ private pending: PendingCompile = { frame: null, timer: null };
102
+ private observer?: MutationObserver;
103
+ private documentObserver?: MutationObserver;
104
+ private resizeObserver?: ResizeObserver;
105
+ private stopLayoutObservation?: () => void;
106
+ private lastResult: CompileInlineNativeResult | null = null;
107
+ private styleDiagnostics = new Set<string>();
108
+ private rootKey = nextOpaqueKey("root");
109
+ private rootEpoch = 1;
110
+ private runtime: RootRuntimeState = createRootRuntimeState();
111
+ private unregisterHost?: () => void;
112
+ private fallbackTimer: ReturnType<typeof setTimeout> | null = null;
113
+
114
+ lastCompileResult(): CompileInlineNativeResult | null {
115
+ return this.lastResult;
116
+ }
117
+
118
+ retry(): Promise<void> {
119
+ this.compileNow();
120
+ return Promise.resolve();
121
+ }
122
+
123
+ connectedCallback(): void {
124
+ if (!this.shadowRoot) {
125
+ const shadow = this.attachShadow({ mode: "open" });
126
+ shadow.innerHTML = '<style>:host([data-lx-native-paint-hidden]:not([data-lx-native-measuring])) { opacity: 0 !important; }</style><slot></slot>';
127
+ }
128
+ this.style.display = this.style.display || "block";
129
+ this.style.position = this.style.position || "relative";
130
+ this.unregisterHost = registerNativeComponentHandler(this.rootKey, (message) => {
131
+ this.onHostMessage(message as {
132
+ action?: string;
133
+ root?: RootRef;
134
+ leaseId?: string;
135
+ sequence?: number;
136
+ leaseDurationMs?: number;
137
+ message?: string;
138
+ });
139
+ });
140
+ this.updateFallbackVisibility(true);
141
+ sendNativeComponentMessage({ id: this.rootKey, action: "component.ready" });
142
+ this.observer = new MutationObserver((records) => {
143
+ if (records.every(record => record.target === this &&
144
+ ["data-lx-native-measuring", "data-lx-native-paint-hidden"].includes(record.attributeName ?? ""))) return;
145
+ this.syncResizeObservation();
146
+ this.scheduleCompile();
147
+ });
148
+ this.observer.observe(this, {
149
+ childList: true,
150
+ subtree: true,
151
+ attributes: true,
152
+ characterData: true,
153
+ });
154
+ this.documentObserver = new MutationObserver((records) => {
155
+ if (records.some((record) => Array.from(record.addedNodes).concat(Array.from(record.removedNodes)).some(containsNativeRoot))) {
156
+ this.scheduleCompile();
157
+ }
158
+ });
159
+ this.documentObserver.observe(document.documentElement, { childList: true, subtree: true });
160
+ if (typeof ResizeObserver !== "undefined") {
161
+ this.resizeObserver = new ResizeObserver(() => this.scheduleCompile());
162
+ this.syncResizeObservation();
163
+ }
164
+ document.addEventListener("scroll", this.onViewportGeometryChanged, true);
165
+ window.addEventListener("resize", this.onViewportGeometryChanged);
166
+ this.stopLayoutObservation = observeNativeLayout(this, () => this.scheduleCompile());
167
+ this.scheduleCompile();
168
+ }
169
+
170
+ disconnectedCallback(): void {
171
+ const root = this.runtime.identified?.rootRef ?? this.rootRef();
172
+ sendNativeComponentMessage({ id: this.rootKey, action: "root.destroy", root });
173
+ this.runtime = createRootRuntimeState();
174
+ this.rootEpoch += 1;
175
+ this.observer?.disconnect();
176
+ this.observer = undefined;
177
+ this.documentObserver?.disconnect();
178
+ this.documentObserver = undefined;
179
+ this.unregisterHost?.();
180
+ this.unregisterHost = undefined;
181
+ this.resizeObserver?.disconnect();
182
+ this.resizeObserver = undefined;
183
+ this.stopLayoutObservation?.();
184
+ this.stopLayoutObservation = undefined;
185
+ document.removeEventListener("scroll", this.onViewportGeometryChanged, true);
186
+ window.removeEventListener("resize", this.onViewportGeometryChanged);
187
+ if (this.pending.frame != null && typeof cancelAnimationFrame === "function") {
188
+ cancelAnimationFrame(this.pending.frame);
189
+ this.pending.frame = null;
190
+ }
191
+ if (this.pending.timer != null) {
192
+ clearTimeout(this.pending.timer);
193
+ this.pending.timer = null;
194
+ }
195
+ if (this.fallbackTimer != null) {
196
+ clearTimeout(this.fallbackTimer);
197
+ this.fallbackTimer = null;
198
+ }
199
+ }
200
+
201
+ private scheduleCompile(): void {
202
+ if (typeof requestAnimationFrame !== "function") {
203
+ this.compileNow();
204
+ return;
205
+ }
206
+ // A hidden document never services animation frames — an occluded or
207
+ // backgrounded window would leave the host without a tree until it is
208
+ // revealed. Commit on a timer instead; the host owns what it paints.
209
+ if (typeof document !== "undefined" && document.hidden) {
210
+ if (this.pending.timer != null) return;
211
+ this.pending.timer = setTimeout(() => {
212
+ this.pending.timer = null;
213
+ this.compileNow();
214
+ }, 0);
215
+ return;
216
+ }
217
+ if (this.pending.frame != null || this.pending.timer != null) return;
218
+ this.pending.frame = requestAnimationFrame(() => {
219
+ this.pending.frame = null;
220
+ if (this.pending.timer != null) {
221
+ clearTimeout(this.pending.timer);
222
+ this.pending.timer = null;
223
+ }
224
+ this.compileNow();
225
+ });
226
+ // Browsers may defer animation frames while a window is occluded or while
227
+ // automation owns the frame clock. Geometry must still reach the native
228
+ // island or its visual and hit target remain at the pre-scroll position.
229
+ this.pending.timer = setTimeout(() => {
230
+ this.pending.timer = null;
231
+ if (this.pending.frame != null) {
232
+ cancelAnimationFrame(this.pending.frame);
233
+ this.pending.frame = null;
234
+ }
235
+ this.compileNow();
236
+ }, 50);
237
+ }
238
+
239
+ compileNow(): CompileInlineNativeResult {
240
+ // Measure author CSS synchronously, then suppress its duplicate paint before the browser frame.
241
+ const measuring = this.hasAttribute("data-lx-native-measuring");
242
+ this.setAttribute("data-lx-native-measuring", "");
243
+ try {
244
+ return this.compileMeasured();
245
+ } finally {
246
+ if (!measuring) this.removeAttribute("data-lx-native-measuring");
247
+ }
248
+ }
249
+
250
+ private compileMeasured(): CompileInlineNativeResult {
251
+ const author = collectAuthorTreeFromElement(this);
252
+ const result = compileInlineNativeRoot(author, { rootRef: this.rootRef() });
253
+ this.lastResult = result;
254
+ const nextDiagnostics = new Set(result.diagnostics.map((diagnostic) => diagnostic.message));
255
+ const previousDiagnostics = this.styleDiagnostics;
256
+ this.styleDiagnostics = nextDiagnostics;
257
+ for (const diagnostic of result.diagnostics) {
258
+ if (previousDiagnostics.has(diagnostic.message)) continue;
259
+ console.warn(`[LingXia] ${diagnostic.code}: ${diagnostic.message}`);
260
+ this.dispatchEvent(new CustomEvent(ROOT_INVALID_EVENT, { detail: diagnostic }));
261
+ }
262
+ if (!result.ok) {
263
+ this.destroyPublishedRoot();
264
+ this.updateFallbackVisibility(true);
265
+ this.dispatchEvent(
266
+ new CustomEvent(ROOT_INVALID_EVENT, {
267
+ detail: result.error,
268
+ bubbles: false,
269
+ })
270
+ );
271
+ } else {
272
+ const geometry = measureNativeNodeGeometry(this);
273
+ const published = publishCompiledRoot({
274
+ compiled: result.root,
275
+ rootRef: this.rootRef(),
276
+ state: this.runtime,
277
+ rootRect: elementContentRect(this),
278
+ rootVisible: elementIsVisible(this),
279
+ nodeRects: geometry.rects,
280
+ nodeVisibility: geometry.visibility,
281
+ nodeClipStacks: geometry.clipStacks,
282
+ rootOrder: nativeRootOrder(this),
283
+ viewportOffset: pageViewportOffset(),
284
+ });
285
+ // Adopt the published state only once the host has actually been told about
286
+ // it: a dropped first commit carries every mount operation, and the next
287
+ // compile would only produce a diff against a tree the host never saw.
288
+ if (published.messages.commit) {
289
+ if (!sendNativeComponentMessage({ id: this.rootKey, ...published.messages.commit })) {
290
+ this.scheduleCompile();
291
+ return result;
292
+ }
293
+ }
294
+ this.runtime = published.state;
295
+ sendNativeComponentMessage({ id: this.rootKey, ...published.messages.geometry });
296
+ this.dispatchEvent(
297
+ new CustomEvent(STRUCTURE_COMPILED_EVENT, {
298
+ detail: result.root,
299
+ bubbles: false,
300
+ })
301
+ );
302
+ }
303
+ return result;
304
+ }
305
+
306
+ private onHostMessage(message: {
307
+ action?: string;
308
+ root?: RootRef;
309
+ leaseId?: string;
310
+ sequence?: number;
311
+ leaseDurationMs?: number;
312
+ message?: string;
313
+ }): void {
314
+ const currentRoot = this.runtime.identified?.rootRef ?? this.rootRef();
315
+ if (message.root && !sameRootGeneration(message.root, currentRoot)) {
316
+ return;
317
+ }
318
+ if (applyIslandHostEvent(this, message)) {
319
+ return;
320
+ }
321
+ if (message.action === "root.resyncRequired") {
322
+ this.runtime = createRootRuntimeState();
323
+ this.updateFallbackVisibility(true);
324
+ this.scheduleCompile();
325
+ return;
326
+ }
327
+ if (message.action === "root.error") {
328
+ this.destroyPublishedRoot();
329
+ this.updateFallbackVisibility(true);
330
+ this.dispatchEvent(
331
+ new CustomEvent(ROOT_INVALID_EVENT, {
332
+ detail: nativeError("NATIVE_ROOT_FAILED", message.message ?? "native Root rejected"),
333
+ })
334
+ );
335
+ return;
336
+ }
337
+ const wasReady = this.runtime.lease.phase === "active";
338
+ const applied = applyHostLeaseMessage(this.runtime, message, Date.now());
339
+ this.runtime = applied.state;
340
+ if (applied.leaseAccept) {
341
+ sendNativeComponentMessage(applied.leaseAccept);
342
+ }
343
+ if (applied.ready) {
344
+ if (this.fallbackTimer != null) {
345
+ clearTimeout(this.fallbackTimer);
346
+ this.fallbackTimer = null;
347
+ }
348
+ this.updateFallbackVisibility(false);
349
+ if (!wasReady) this.dispatchEvent(new CustomEvent("ready", { detail: {} }));
350
+ } else {
351
+ this.armFallbackDeadline();
352
+ }
353
+ }
354
+
355
+ private armFallbackDeadline(): void {
356
+ if (this.fallbackTimer != null) clearTimeout(this.fallbackTimer);
357
+ const deadline = this.runtime.lease.deadlineTickMs;
358
+ if (deadline === undefined) return;
359
+ this.fallbackTimer = setTimeout(() => {
360
+ this.fallbackTimer = null;
361
+ if (rootFallbackAllowed(this.runtime, Date.now())) {
362
+ this.updateFallbackVisibility(true);
363
+ }
364
+ }, Math.max(0, deadline - Date.now()));
365
+ }
366
+
367
+ private updateFallbackVisibility(show: boolean): void {
368
+ if (this.hasAttribute("data-lx-native-paint-hidden") === show) {
369
+ this.toggleAttribute("data-lx-native-paint-hidden", !show);
370
+ }
371
+ for (const fallback of Array.from(this.querySelectorAll("[data-lx-native-fallback]"))) {
372
+ if (fallback instanceof HTMLElement && fallback.closest("lx-native-root") === this) {
373
+ if (fallback.hidden === show) fallback.hidden = !show;
374
+ if (show && fallback.hasAttribute("aria-hidden")) fallback.removeAttribute("aria-hidden");
375
+ else if (!show && fallback.getAttribute("aria-hidden") !== "true") fallback.setAttribute("aria-hidden", "true");
376
+ }
377
+ }
378
+ }
379
+
380
+ private rootRef(): RootRef {
381
+ return pageRootScope(this.rootKey, this.rootEpoch);
382
+ }
383
+
384
+ private destroyPublishedRoot(): void {
385
+ const root = this.runtime.identified?.rootRef;
386
+ if (!root) return;
387
+ sendNativeComponentMessage({ id: this.rootKey, action: "root.destroy", root });
388
+ this.runtime = createRootRuntimeState();
389
+ this.rootEpoch += 1;
390
+ }
391
+
392
+ private onViewportGeometryChanged = (): void => this.scheduleCompile();
393
+
394
+ private syncResizeObservation(): void {
395
+ if (!this.resizeObserver) return;
396
+ this.resizeObserver.disconnect();
397
+ this.resizeObserver.observe(this);
398
+ for (const element of nativeDescendants(this)) {
399
+ this.resizeObserver.observe(element);
400
+ }
401
+ }
402
+ }
403
+
404
+ export class LxNativeViewElement extends LxNativeBaseElement {
405
+ static get observedAttributes(): string[] {
406
+ return [...LxNativeBaseElement.observedAttributes, "role"];
407
+ }
408
+ }
409
+
410
+ export class LxNativeCoverElement extends LxNativeBaseElement {
411
+ static get observedAttributes(): string[] {
412
+ return [...LxNativeBaseElement.observedAttributes, "scrim", "scrim-opacity", "role"];
413
+ }
414
+
415
+ connectedCallback(): void {
416
+ super.connectedCallback();
417
+ if (!this.style.position) this.style.position = "absolute";
418
+ if (!this.style.inset && !this.style.top && !this.style.left) {
419
+ this.style.inset = "0";
420
+ }
421
+ if (!this.style.pointerEvents) {
422
+ this.style.pointerEvents = "none";
423
+ }
424
+ }
425
+
426
+ get scrim(): string {
427
+ return this.getAttribute("scrim") ?? "none";
428
+ }
429
+ set scrim(value: string | null | undefined) {
430
+ reflectString(this, "scrim", value);
431
+ }
432
+
433
+ get scrimOpacity(): number {
434
+ const raw = this.getAttribute("scrim-opacity");
435
+ const parsed = raw == null ? 0.6 : Number(raw);
436
+ return Number.isFinite(parsed) ? parsed : 0.6;
437
+ }
438
+ set scrimOpacity(value: number | string | null | undefined) {
439
+ if (value == null) {
440
+ this.removeAttribute("scrim-opacity");
441
+ return;
442
+ }
443
+ this.setAttribute("scrim-opacity", String(value));
444
+ }
445
+ }
446
+
447
+ export class LxNativeTextElement extends LxNativeBaseElement {
448
+ private typography?: HTMLStyleElement;
449
+ static get observedAttributes(): string[] {
450
+ return [
451
+ ...LxNativeBaseElement.observedAttributes,
452
+ "max-lines",
453
+ "dir",
454
+ "font-size",
455
+ "font-weight",
456
+ "line-height",
457
+ "text-align",
458
+ "color",
459
+ ];
460
+ }
461
+
462
+ connectedCallback(): void {
463
+ super.connectedCallback();
464
+ if (!this.shadowRoot) {
465
+ const shadow = this.attachShadow({ mode: "open" });
466
+ this.typography = document.createElement("style");
467
+ shadow.append(this.typography, document.createElement("slot"));
468
+ }
469
+ this.syncTypography();
470
+ if (!this.style.pointerEvents) {
471
+ this.style.pointerEvents = "none";
472
+ }
473
+ }
474
+
475
+ attributeChangedCallback(): void {
476
+ this.syncTypography();
477
+ }
478
+
479
+ private syncTypography(): void {
480
+ if (!this.typography) return;
481
+ const style = document.createElement("span").style;
482
+ for (const name of ["font-size", "font-weight", "line-height", "text-align", "color"]) {
483
+ const value = this.getAttribute(name);
484
+ if (!value) continue;
485
+ const cssValue = (name === "font-size" || name === "line-height") && /^\d+(\.\d+)?$/.test(value)
486
+ ? `${value}px` : value;
487
+ style.setProperty(name, cssValue);
488
+ }
489
+ // Keep DOM measurement and accessible text without painting a second native label.
490
+ this.typography.textContent = `:host { ${style.cssText} } slot { opacity: 0; }`;
491
+ }
492
+
493
+ get maxLines(): number | null {
494
+ const raw = this.getAttribute("max-lines");
495
+ if (raw == null) return null;
496
+ const parsed = Number(raw);
497
+ return Number.isFinite(parsed) ? parsed : null;
498
+ }
499
+ set maxLines(value: number | string | null | undefined) {
500
+ reflectString(this, "max-lines", value == null ? null : String(value));
501
+ }
502
+ }
503
+
504
+ export class LxNativeButtonElement extends LxNativeBaseElement {
505
+ private unregisterHost?: () => void;
506
+ private registeredId?: string;
507
+ private measuredLabel?: HTMLSpanElement;
508
+ private measuredIcon?: HTMLSpanElement;
509
+ private contentSlot?: HTMLSlotElement;
510
+ private dispatchAccessiblePress(source: "keyboard" | "pointer"): void {
511
+ if (this.disabled || this.loading) return;
512
+ this.dispatchEvent(new CustomEvent("press", {
513
+ bubbles: true,
514
+ detail: { source },
515
+ }));
516
+ }
517
+ private readonly onAccessibleClick = (event: MouseEvent): void => {
518
+ this.dispatchAccessiblePress(event.detail === 0 ? "keyboard" : "pointer");
519
+ };
520
+ private readonly onAccessibleKeyDown = (event: KeyboardEvent): void => {
521
+ if (event.key !== "Enter" && event.key !== " ") return;
522
+ event.preventDefault();
523
+ this.dispatchAccessiblePress("keyboard");
524
+ };
525
+
526
+ connectedCallback(): void {
527
+ super.connectedCallback();
528
+ if (!this.shadowRoot) {
529
+ const shadow = this.attachShadow({ mode: "open" });
530
+ const style = document.createElement("style");
531
+ style.textContent = `
532
+ :host { display: inline-flex; box-sizing: border-box; align-items: center;
533
+ justify-content: center; gap: .4em; min-height: 32px; padding: 6px 12px; border-radius: 10px; }
534
+ :host([hidden]:not([hidden="false"])) { display: none; }
535
+ :host([size="compact"]) { min-height: 26px; padding: 4px 8px; }
536
+ :host([icon-position="end"]) { flex-direction: row-reverse; }
537
+ .icon { width: 1em; height: 1em; flex: none; }
538
+ span, slot { opacity: 0; }
539
+ [hidden] { display: none; }
540
+ `;
541
+ this.measuredLabel = document.createElement("span");
542
+ this.measuredIcon = document.createElement("span");
543
+ this.measuredIcon.className = "icon";
544
+ this.measuredIcon.setAttribute("aria-hidden", "true");
545
+ this.contentSlot = document.createElement("slot");
546
+ shadow.append(style, this.measuredIcon, this.measuredLabel, this.contentSlot);
547
+ }
548
+ this.syncMeasurement();
549
+ if (!this.hasAttribute("role")) this.setAttribute("role", "button");
550
+ if (!this.hasAttribute("tabindex")) this.tabIndex = 0;
551
+ this.syncAccessibility();
552
+ this.addEventListener("click", this.onAccessibleClick);
553
+ this.addEventListener("keydown", this.onAccessibleKeyDown);
554
+ this.registerEventBridge();
555
+ }
556
+
557
+ private registerEventBridge(): void {
558
+ const id = ensureComponentId(this, "lx-native-button");
559
+ if (this.registeredId === id) return;
560
+ this.unregisterHost?.();
561
+ this.registeredId = id;
562
+ this.unregisterHost = registerNativeComponentHandler(id, (message) => {
563
+ applyIslandHostEvent(this, message as { event?: string; action?: string; detail?: unknown });
564
+ });
565
+ }
566
+
567
+ disconnectedCallback(): void {
568
+ this.removeEventListener("click", this.onAccessibleClick);
569
+ this.removeEventListener("keydown", this.onAccessibleKeyDown);
570
+ this.unregisterHost?.();
571
+ this.unregisterHost = undefined;
572
+ this.registeredId = undefined;
573
+ }
574
+
575
+ attributeChangedCallback(name: string): void {
576
+ if (name === "id" && this.isConnected) this.registerEventBridge();
577
+ this.syncMeasurement();
578
+ this.syncAccessibility();
579
+ }
580
+
581
+ private syncMeasurement(): void {
582
+ if (!this.measuredLabel || !this.measuredIcon || !this.contentSlot) return;
583
+ this.measuredLabel.textContent = this.label ?? "";
584
+ this.measuredLabel.hidden = !this.label;
585
+ this.contentSlot.hidden = Boolean(this.label);
586
+ this.measuredIcon.hidden = !this.icon;
587
+ }
588
+
589
+ private syncAccessibility(): void {
590
+ this.setAttribute("aria-disabled", String(this.disabled || this.loading));
591
+ this.setAttribute("aria-pressed", String(this.pressed));
592
+ this.setAttribute("aria-expanded", String(this.expanded));
593
+ }
594
+
595
+ static get observedAttributes(): string[] {
596
+ return [
597
+ ...LxNativeBaseElement.observedAttributes,
598
+ "label",
599
+ "icon",
600
+ "icon-position",
601
+ "intent",
602
+ "emphasis",
603
+ "size",
604
+ "hit-slop",
605
+ "disabled",
606
+ "pressed",
607
+ "expanded",
608
+ "loading",
609
+ ];
610
+ }
611
+
612
+ get label(): string | null {
613
+ return this.getAttribute("label");
614
+ }
615
+ set label(value: string | null | undefined) {
616
+ reflectString(this, "label", value);
617
+ }
618
+
619
+ get icon(): string | null {
620
+ return this.getAttribute("icon");
621
+ }
622
+ set icon(value: string | null | undefined) {
623
+ // Invalid JS values remain visible to the compiler instead of disappearing.
624
+ reflectString(this, "icon", value);
625
+ }
626
+
627
+ get disabled(): boolean {
628
+ return parseBooleanAttr(this.getAttribute("disabled"), false);
629
+ }
630
+ set disabled(value: unknown) {
631
+ reflectBoolean(this, "disabled", value);
632
+ }
633
+
634
+ get pressed(): boolean {
635
+ return parseBooleanAttr(this.getAttribute("pressed"), false);
636
+ }
637
+ set pressed(value: unknown) {
638
+ reflectBoolean(this, "pressed", value);
639
+ }
640
+
641
+ get expanded(): boolean {
642
+ return parseBooleanAttr(this.getAttribute("expanded"), false);
643
+ }
644
+ set expanded(value: unknown) {
645
+ reflectBoolean(this, "expanded", value);
646
+ }
647
+
648
+ get loading(): boolean {
649
+ return parseBooleanAttr(this.getAttribute("loading"), false);
650
+ }
651
+ set loading(value: unknown) {
652
+ reflectBoolean(this, "loading", value);
653
+ }
654
+ }
655
+
656
+ function elementContentRect(el: Element): { x: number; y: number; width: number; height: number } {
657
+ if (typeof (el as HTMLElement).getBoundingClientRect !== "function") {
658
+ return { x: 0, y: 0, width: 0, height: 0 };
659
+ }
660
+ const box = (el as HTMLElement).getBoundingClientRect();
661
+ const scrollX = typeof window !== "undefined" ? window.scrollX || 0 : 0;
662
+ const scrollY = typeof window !== "undefined" ? window.scrollY || 0 : 0;
663
+ return {
664
+ x: box.left + scrollX,
665
+ y: box.top + scrollY,
666
+ width: box.width,
667
+ height: box.height,
668
+ };
669
+ }
670
+
671
+ function pageViewportOffset(): { x: number; y: number } {
672
+ if (typeof window === "undefined") return { x: 0, y: 0 };
673
+ return { x: window.scrollX || 0, y: window.scrollY || 0 };
674
+ }
675
+
676
+ function measureNativeNodeGeometry(root: Element): {
677
+ rects: Record<string, { x: number; y: number; width: number; height: number }>;
678
+ visibility: Record<string, boolean>;
679
+ clipStacks: Record<string, unknown[]>;
680
+ } {
681
+ const rects: Record<string, { x: number; y: number; width: number; height: number }> = {};
682
+ const visibility: Record<string, boolean> = {};
683
+ const clipStacks: Record<string, unknown[]> = {};
684
+ for (const child of nativeDescendants(root)) {
685
+ const tag = child.tagName.toLowerCase();
686
+ const id = ensureComponentId(child as HTMLElement, tag);
687
+ const rect = elementContentRect(child);
688
+ const clips = overflowClipStack(child);
689
+ rects[id] = rect;
690
+ visibility[id] = elementIsVisible(child) && clips.every((clip) => rectsIntersect(rect, clip));
691
+ clipStacks[id] = clips;
692
+ }
693
+ return { rects, visibility, clipStacks };
694
+ }
695
+
696
+ function nativeDescendants(root: Element): Element[] {
697
+ return Array.from(root.querySelectorAll("lx-native-view,lx-native-cover,lx-native-text,lx-native-button,lx-video"))
698
+ .filter((element) => !isFallbackElement(element) && element.closest("lx-native-root") === root);
699
+ }
700
+
701
+ function elementIsVisible(element: Element): boolean {
702
+ const html = element as HTMLElement;
703
+ if (typeof getComputedStyle !== "function") return true;
704
+ const style = getComputedStyle(html);
705
+ if (style.display === "none" || style.visibility === "hidden" || effectiveOpacity(element) === 0) {
706
+ return false;
707
+ }
708
+ const rect = html.getBoundingClientRect();
709
+ if (rect.width <= 0 || rect.height <= 0 || html.getClientRects().length === 0) {
710
+ return false;
711
+ }
712
+ if (typeof window === "undefined") return true;
713
+ return rect.right > 0 && rect.bottom > 0 && rect.left < window.innerWidth && rect.top < window.innerHeight;
714
+ }
715
+
716
+ function overflowClipStack(
717
+ element: Element
718
+ ): Array<{ x: number; y: number; width: number; height: number }> {
719
+ if (typeof getComputedStyle !== "function") return [];
720
+ const clips: Array<{ x: number; y: number; width: number; height: number }> = [];
721
+ for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
722
+ const style = getComputedStyle(ancestor);
723
+ const clipsX = style.overflowX !== "visible";
724
+ const clipsY = style.overflowY !== "visible";
725
+ if (clipsX || clipsY) {
726
+ const box = elementContentRect(ancestor);
727
+ const own = elementContentRect(element);
728
+ const html = ancestor as HTMLElement;
729
+ clips.push({
730
+ x: clipsX ? box.x + html.clientLeft : own.x,
731
+ y: clipsY ? box.y + html.clientTop : own.y,
732
+ width: clipsX ? html.clientWidth : own.width,
733
+ height: clipsY ? html.clientHeight : own.height,
734
+ });
735
+ }
736
+ }
737
+ return clips;
738
+ }
739
+
740
+ function rectsIntersect(
741
+ a: { x: number; y: number; width: number; height: number },
742
+ b: { x: number; y: number; width: number; height: number }
743
+ ): boolean {
744
+ return a.x < b.x + b.width &&
745
+ a.x + a.width > b.x &&
746
+ a.y < b.y + b.height &&
747
+ a.y + a.height > b.y;
748
+ }
749
+
750
+ function nativeRootOrder(root: LxNativeRootElement): number {
751
+ if (typeof document === "undefined") return 0;
752
+ const roots = Array.from(document.querySelectorAll("lx-native-root"));
753
+ const index = roots.indexOf(root);
754
+ return index < 0 ? 0 : index;
755
+ }
756
+
757
+ function containsNativeRoot(node: Node): boolean {
758
+ return node instanceof Element &&
759
+ (node.localName === "lx-native-root" || node.querySelector("lx-native-root") !== null);
760
+ }
761
+
762
+ function sameRootGeneration(a: RootRef, b: RootRef): boolean {
763
+ return a.surfaceInstanceId === b.surfaceInstanceId &&
764
+ a.pageInstanceId === b.pageInstanceId &&
765
+ a.documentInstanceId === b.documentInstanceId &&
766
+ a.rootKey === b.rootKey &&
767
+ a.rootEpoch === b.rootEpoch;
768
+ }
769
+
770
+ function pageRootScope(rootKey: string, rootEpoch: number): RootRef {
771
+ let surfaceInstanceId = "surface";
772
+ let pageInstanceId = "page";
773
+ let documentInstanceId = "doc";
774
+ if (typeof window !== "undefined") {
775
+ const cfg = (window as Window & { __LX_BRIDGE_CFG?: Record<string, string> }).__LX_BRIDGE_CFG;
776
+ if (cfg?.surfaceInstanceId) surfaceInstanceId = cfg.surfaceInstanceId;
777
+ if (cfg?.pageInstanceId) pageInstanceId = cfg.pageInstanceId;
778
+ const w = window as Window & { __LX_DOCUMENT_INSTANCE_ID?: string };
779
+ if (!w.__LX_DOCUMENT_INSTANCE_ID) {
780
+ w.__LX_DOCUMENT_INSTANCE_ID = nextOpaqueKey("doc");
781
+ }
782
+ documentInstanceId = w.__LX_DOCUMENT_INSTANCE_ID;
783
+ }
784
+ return { surfaceInstanceId, pageInstanceId, documentInstanceId, rootKey, rootEpoch };
785
+ }
786
+
787
+ function defineOnce(tag: string, ctor: CustomElementConstructor): void {
788
+ if (typeof customElements === "undefined") return;
789
+ if (customElements.get(tag)) return;
790
+ customElements.define(tag, ctor);
791
+ }
792
+
793
+ export function registerNativeRootComponent(): void {
794
+ defineOnce("lx-native-root", LxNativeRootElement);
795
+ }
796
+ export function registerNativeViewComponent(): void {
797
+ defineOnce("lx-native-view", LxNativeViewElement);
798
+ }
799
+ export function registerNativeCoverComponent(): void {
800
+ defineOnce("lx-native-cover", LxNativeCoverElement);
801
+ }
802
+ export function registerNativeTextComponent(): void {
803
+ defineOnce("lx-native-text", LxNativeTextElement);
804
+ }
805
+ export function registerNativeButtonComponent(): void {
806
+ defineOnce("lx-native-button", LxNativeButtonElement);
807
+ }
808
+
809
+ export function registerInlineNativeAuthorComponents(): void {
810
+ registerNativeRootComponent();
811
+ registerNativeViewComponent();
812
+ registerNativeCoverComponent();
813
+ registerNativeTextComponent();
814
+ registerNativeButtonComponent();
815
+ }
816
+
817
+ export function structureErrorFromUnknown(message: string): NativeError {
818
+ return nativeError("NATIVE_ROOT_INVALID_STRUCTURE", message);
819
+ }
820
+
821
+ declare global {
822
+ interface HTMLElementTagNameMap {
823
+ "lx-native-root": LxNativeRootElement;
824
+ "lx-native-view": LxNativeViewElement;
825
+ "lx-native-cover": LxNativeCoverElement;
826
+ "lx-native-text": LxNativeTextElement;
827
+ "lx-native-button": LxNativeButtonElement;
828
+ }
829
+ }