@lingxia/elements 0.15.0 → 0.17.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 (67) 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/video-events.d.ts +2 -0
  42. package/dist/video-events.js +18 -0
  43. package/dist/video.d.ts +98 -79
  44. package/dist/video.js +182 -757
  45. package/package.json +2 -1
  46. package/src/index.ts +58 -1
  47. package/src/inline-native/commit.ts +141 -0
  48. package/src/inline-native/elements.ts +829 -0
  49. package/src/inline-native/errors.ts +35 -0
  50. package/src/inline-native/events.ts +31 -0
  51. package/src/inline-native/geometry.ts +86 -0
  52. package/src/inline-native/identity.ts +102 -0
  53. package/src/inline-native/index.ts +103 -0
  54. package/src/inline-native/layout.ts +58 -0
  55. package/src/inline-native/lease.ts +48 -0
  56. package/src/inline-native/runtime.ts +185 -0
  57. package/src/inline-native/schema.ts +247 -0
  58. package/src/inline-native/structure.ts +823 -0
  59. package/src/inline-native/style.ts +111 -0
  60. package/src/inline-native/types.ts +105 -0
  61. package/src/inline-native/unwrap.ts +34 -0
  62. package/src/inline-native/video.ts +125 -0
  63. package/src/native_component_wrapper_shared.ts +8 -6
  64. package/src/nativecomponent.ts +8 -13
  65. package/src/navigator.ts +9 -9
  66. package/src/video-events.ts +16 -0
  67. package/src/video.ts +245 -839
@@ -0,0 +1,111 @@
1
+ import type { AuthorNode } from "./types.js";
2
+
3
+ // Computed values catch unsupported styles from classes as well as inline CSS.
4
+ const unsupportedDefaults: Record<string, readonly string[]> = {
5
+ transform: ["none"], translate: ["none"], rotate: ["none"], scale: ["none"],
6
+ perspective: ["none"], clipPath: ["none"], maskImage: ["none"],
7
+ boxShadow: ["none"], filter: ["none"], backdropFilter: ["none"],
8
+ mixBlendMode: ["normal"], backgroundImage: ["none"],
9
+ animationName: ["none"], transitionDuration: ["0s"],
10
+ textDecorationLine: ["none"], fontStyle: ["normal"],
11
+ letterSpacing: ["normal", "0px"], textShadow: ["none"],
12
+ };
13
+ const layoutProperties = new Set([
14
+ "transform", "translate", "rotate", "scale", "perspective", "clipPath", "maskImage",
15
+ ]);
16
+
17
+ export const OBSERVED_STYLE_FIELDS = [
18
+ ...Object.keys(unsupportedDefaults), "display", "visibility", "opacity", "overflowX", "overflowY",
19
+ "color", "backgroundColor", "accentColor", "fontSize", "fontWeight", "lineHeight", "textAlign", "direction",
20
+ ...["Top", "Right", "Bottom", "Left"].flatMap(side => ["Width", "Style", "Color"].map(suffix => `border${side}${suffix}`)),
21
+ "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius",
22
+ ];
23
+
24
+ /** Native nodes are flat siblings, so each receives its cumulative alpha. */
25
+ export function effectiveOpacity(element: Element): number {
26
+ let opacity = 1;
27
+ for (let node: Element | null = element; node; node = node.parentElement) {
28
+ opacity *= Number(node.ownerDocument.defaultView?.getComputedStyle(node).opacity ?? 1);
29
+ }
30
+ return opacity;
31
+ }
32
+
33
+ const colorContexts = new WeakMap<Document, CanvasRenderingContext2D>();
34
+ const colors = new Map<string, string>();
35
+
36
+ /** Canvas resolves browser-supported CSS colors into the hosts' sRGB wire format. */
37
+ export function normalizeNativeColor(document: Document, value: string): string | undefined {
38
+ if (/^rgba?\([\d.,\s]+\)$/.test(value) || /^#[\da-f]{6}([\da-f]{2})?$/i.test(value)) return value;
39
+ const cached = colors.get(value);
40
+ if (cached) return cached;
41
+ let context = colorContexts.get(document);
42
+ if (!context) {
43
+ const canvas = document.createElement("canvas");
44
+ canvas.width = canvas.height = 1;
45
+ context = canvas.getContext("2d", { willReadFrequently: true, colorSpace: "srgb" }) ?? undefined;
46
+ if (!context) return undefined;
47
+ colorContexts.set(document, context);
48
+ }
49
+ context.fillStyle = "#010203";
50
+ context.fillStyle = value;
51
+ const first = context.fillStyle;
52
+ context.fillStyle = "#040506";
53
+ context.fillStyle = value;
54
+ if (context.fillStyle !== first) return undefined;
55
+ context.clearRect(0, 0, 1, 1);
56
+ context.fillRect(0, 0, 1, 1);
57
+ const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
58
+ const result = `rgba(${r}, ${g}, ${b}, ${a / 255})`;
59
+ if (colors.size >= 256) colors.clear();
60
+ colors.set(value, result);
61
+ return result;
62
+ }
63
+
64
+ export function collectNativeStyleIssues(element: Element): NonNullable<AuthorNode["styleIssues"]> {
65
+ const view = element.ownerDocument?.defaultView;
66
+ if (!view?.getComputedStyle) return [];
67
+ const style = view.getComputedStyle(element);
68
+ const issues: NonNullable<AuthorNode["styleIssues"]> = [];
69
+ for (const property of ["color", "backgroundColor", "borderTopColor", "accentColor"] as const) {
70
+ const value = style[property];
71
+ if (value && value !== "auto" && !normalizeNativeColor(element.ownerDocument, value)) {
72
+ issues.push({ property, value, layout: false });
73
+ }
74
+ }
75
+ for (const [property, defaults] of Object.entries(unsupportedDefaults)) {
76
+ const value = String(style[property as keyof CSSStyleDeclaration] ?? "").trim();
77
+ if (!value || defaults.includes(value)) continue;
78
+ if (property === "backgroundImage" && isVideoPoster(element, value)) continue;
79
+ if (property === "transitionDuration" && value.split(",").every((duration) => parseFloat(duration) === 0)) continue;
80
+ issues.push({ property, value, layout: layoutProperties.has(property) });
81
+ }
82
+ const corners = [style.borderTopLeftRadius, style.borderTopRightRadius,
83
+ style.borderBottomRightRadius, style.borderBottomLeftRadius];
84
+ if (corners.some((value) => value && (value !== corners[0] || !/^\d+(\.\d+)?px$/.test(value)))) {
85
+ issues.push({ property: "borderRadius", value: corners.join(" / "), layout: false });
86
+ }
87
+ for (const suffix of ["Width", "Color", "Style"] as const) {
88
+ const values = [style[`borderTop${suffix}`], style[`borderRight${suffix}`],
89
+ style[`borderBottom${suffix}`], style[`borderLeft${suffix}`]];
90
+ const hasBorder = [style.borderTopWidth, style.borderRightWidth,
91
+ style.borderBottomWidth, style.borderLeftWidth].some((width) => parseFloat(width) > 0);
92
+ if (hasBorder && (values.some((value) => value !== values[0]) ||
93
+ (suffix === "Style" && values.some((value) => value !== "solid" && value !== "none")))) {
94
+ issues.push({ property: `border${suffix}`, value: values.join(" / "), layout: false });
95
+ }
96
+ }
97
+ return issues;
98
+ }
99
+
100
+ function isVideoPoster(element: Element, background: string): boolean {
101
+ if (element.tagName.toLowerCase() !== "lx-video") return false;
102
+ const poster = element.getAttribute("poster");
103
+ const url = /^url\("(.*)"\)$/.exec(background)?.[1];
104
+ if (!poster || !url) return false;
105
+ // LxVideo owns this DOM placeholder; the native backend paints its poster prop.
106
+ try {
107
+ return new URL(url.replace(/\\(["\\])/g, "$1"), element.baseURI).href === new URL(poster, element.baseURI).href;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
@@ -0,0 +1,105 @@
1
+ import type {
2
+ CoreKind,
3
+ HostFactoryKind,
4
+ NativeErrorCode,
5
+ PublicComponentName,
6
+ } from "./schema.js";
7
+
8
+ export interface RootRef {
9
+ surfaceInstanceId: string;
10
+ pageInstanceId: string;
11
+ documentInstanceId: string;
12
+ rootKey: string;
13
+ rootEpoch: number;
14
+ }
15
+
16
+ export interface NodeRef extends RootRef {
17
+ nodeKey: string;
18
+ nodeEpoch: number;
19
+ }
20
+
21
+ export interface NativeError {
22
+ code: NativeErrorCode;
23
+ scope: "node" | "root";
24
+ recoverable: boolean;
25
+ root: RootRef;
26
+ node?: NodeRef;
27
+ message: string;
28
+ }
29
+
30
+ export interface PressPayload {
31
+ source: "pointer" | "keyboard" | "accessibility";
32
+ }
33
+
34
+ export interface ValuePayload {
35
+ value: number;
36
+ }
37
+
38
+ export interface FocusPayload {
39
+ source: "pointer" | "keyboard" | "accessibility";
40
+ }
41
+
42
+ export interface PointerPayload {
43
+ source: "pointer";
44
+ }
45
+
46
+ export type NativeHandler<TPayload> = (payload: TPayload) => void;
47
+
48
+ export type AuthorChild =
49
+ | AuthorNode
50
+ | string
51
+ | number
52
+ | false
53
+ | null
54
+ | undefined
55
+ | readonly AuthorChild[];
56
+
57
+ export interface AuthorNode {
58
+ type: string;
59
+ authorId?: string;
60
+ automationId?: string;
61
+ props?: Record<string, unknown>;
62
+ children?: AuthorChild;
63
+ textContent?: string | number;
64
+ styleIssues?: Array<{ property: string; value: string; layout: boolean }>;
65
+ }
66
+
67
+ export interface CoreNode {
68
+ kind: HostFactoryKind;
69
+ authorType: PublicComponentName;
70
+ authorId?: string;
71
+ automationId?: string;
72
+ props: Record<string, unknown>;
73
+ children: CoreNode[];
74
+ text?: string;
75
+ }
76
+
77
+ export interface CompiledNativeRoot {
78
+ authorType: "LxNativeRoot";
79
+ authorId?: string;
80
+ automationId?: string;
81
+ props: Record<string, unknown>;
82
+ children: CoreNode[];
83
+ kind: Extract<CoreKind, "root">;
84
+ }
85
+
86
+ export type CompileInlineNativeResult =
87
+ | { ok: true; root: CompiledNativeRoot; diagnostics: NativeError[] }
88
+ | { ok: false; error: NativeError; diagnostics: NativeError[] };
89
+
90
+ export interface CompileInlineNativeOptions {
91
+ rootRef?: RootRef;
92
+ /**
93
+ * When true, `controls` defaults to `true` (author contract). Tests and
94
+ * wrappers should leave this on; only a host fixture may override it.
95
+ */
96
+ defaultVideoControls?: boolean;
97
+ }
98
+
99
+ export const EMPTY_ROOT_REF: RootRef = {
100
+ surfaceInstanceId: "",
101
+ pageInstanceId: "",
102
+ documentInstanceId: "",
103
+ rootKey: "",
104
+ rootEpoch: 0,
105
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Payload-first unwrap used by React/Vue wrappers. HTML keeps CustomEvent.
3
+ *
4
+ * A real DOM Event (or an event-like `{ type, detail }` used by unit tests)
5
+ * yields `detail`. A bare payload object is returned as-is so wrappers can
6
+ * also be called with already-unwrapped values.
7
+ */
8
+ export function unwrapNativeEventPayload<TPayload>(event: unknown): TPayload {
9
+ if (isDomEvent(event) || isEventLike(event)) {
10
+ const detail = (event as { detail?: TPayload }).detail;
11
+ return (detail === undefined ? ({} as TPayload) : detail) as TPayload;
12
+ }
13
+ return event as TPayload;
14
+ }
15
+
16
+ export function isDomEvent(value: unknown): value is Event {
17
+ return typeof Event !== "undefined" && value instanceof Event;
18
+ }
19
+
20
+ function isEventLike(value: unknown): value is { detail?: unknown; type?: unknown } {
21
+ if (!value || typeof value !== "object") return false;
22
+ const record = value as { detail?: unknown; type?: unknown; target?: unknown };
23
+ if (!("detail" in record)) return false;
24
+ return "type" in record || "target" in record || "currentTarget" in record;
25
+ }
26
+
27
+ export function bindPayloadHandler<TPayload>(
28
+ handler: ((payload: TPayload) => void) | undefined
29
+ ): EventListener {
30
+ return (event: Event) => {
31
+ if (typeof handler !== "function") return;
32
+ handler(unwrapNativeEventPayload<TPayload>(event));
33
+ };
34
+ }
@@ -0,0 +1,125 @@
1
+ import type { NodeRef } from "./types.js";
2
+
3
+ export const VIDEO_COMMANDS = [
4
+ "play",
5
+ "pause",
6
+ "stop",
7
+ "seek",
8
+ "enterFullscreen",
9
+ "exitFullscreen",
10
+ "setStreamSource",
11
+ ] as const;
12
+
13
+ export type VideoCommandName = (typeof VIDEO_COMMANDS)[number];
14
+
15
+ export type VideoCommand =
16
+ | { name: "play" }
17
+ | { name: "pause" }
18
+ | { name: "stop" }
19
+ | { name: "seek"; seconds: number }
20
+ | { name: "enterFullscreen" }
21
+ | { name: "exitFullscreen" }
22
+ | { name: "setStreamSource"; options: Record<string, unknown> };
23
+
24
+ export interface VideoCommandRequest {
25
+ action: "video.command";
26
+ owner: NodeRef;
27
+ requestId: string;
28
+ command: VideoCommand;
29
+ }
30
+
31
+ export interface VideoControlDescriptorBase {
32
+ controlId: string;
33
+ label: string;
34
+ frame: { x: number; y: number; width: number; height: number };
35
+ visible: boolean;
36
+ disabled?: boolean;
37
+ }
38
+
39
+ export type VideoControlDescriptor =
40
+ | (VideoControlDescriptorBase & {
41
+ role: "button";
42
+ pressed?: boolean;
43
+ expanded?: boolean;
44
+ actions: ReadonlyArray<"focus" | "blur" | "activate">;
45
+ })
46
+ | (VideoControlDescriptorBase & {
47
+ role: "slider";
48
+ value: number;
49
+ min: number;
50
+ max: number;
51
+ actions: ReadonlyArray<"focus" | "blur" | "increment" | "decrement" | "setValue">;
52
+ });
53
+
54
+ export interface VideoControlsSemanticSnapshot {
55
+ action: "video.controlsSemanticSnapshot";
56
+ owner: NodeRef;
57
+ revision: number;
58
+ controls: readonly VideoControlDescriptor[];
59
+ }
60
+
61
+ export function buildVideoCommandRequest(
62
+ owner: NodeRef,
63
+ command: VideoCommand,
64
+ requestId: string
65
+ ): VideoCommandRequest {
66
+ return { action: "video.command", owner, requestId, command };
67
+ }
68
+
69
+ export function videoCommandUrls(command: VideoCommand): string[] {
70
+ if (command.name !== "setStreamSource") return [];
71
+ const options = command.options ?? {};
72
+ const urls: string[] = [];
73
+ for (const key of ["url", "src", "uri"]) {
74
+ const value = options[key];
75
+ if (typeof value === "string" && value) urls.push(value);
76
+ }
77
+ return urls;
78
+ }
79
+
80
+ export function collectVideoResourceUrls(props: Record<string, unknown>): string[] {
81
+ const urls: string[] = [];
82
+ for (const key of ["src", "poster"]) {
83
+ const value = props[key];
84
+ if (typeof value === "string" && value) urls.push(value);
85
+ }
86
+ const watermark = props.watermark as { resource?: { url?: string } } | undefined;
87
+ if (watermark?.resource && typeof watermark.resource === "object") {
88
+ const url = (watermark.resource as { url?: string }).url;
89
+ if (typeof url === "string" && url) urls.push(url);
90
+ }
91
+ const qualities = props.qualities;
92
+ if (Array.isArray(qualities)) {
93
+ for (const item of qualities) {
94
+ if (item && typeof item === "object" && typeof (item as { url?: string }).url === "string") {
95
+ urls.push((item as { url: string }).url);
96
+ }
97
+ }
98
+ }
99
+ return urls;
100
+ }
101
+
102
+ export function validateControlsSnapshot(
103
+ snapshot: VideoControlsSemanticSnapshot,
104
+ lastRevision: number
105
+ ): { ok: true } | { ok: false; message: string } {
106
+ if (snapshot.revision <= lastRevision) {
107
+ return { ok: false, message: "controls snapshot revision must increase" };
108
+ }
109
+ const seen = new Set<string>();
110
+ for (const control of snapshot.controls) {
111
+ if (!control.controlId || seen.has(control.controlId)) {
112
+ return { ok: false, message: "controlId must be unique and non-empty" };
113
+ }
114
+ seen.add(control.controlId);
115
+ if (control.role === "slider") {
116
+ if (![control.min, control.max, control.value].every(Number.isFinite)) {
117
+ return { ok: false, message: "slider descriptor values must be finite" };
118
+ }
119
+ if (control.min > control.value || control.value > control.max) {
120
+ return { ok: false, message: "slider descriptor value is out of range" };
121
+ }
122
+ }
123
+ }
124
+ return { ok: true };
125
+ }
@@ -1,7 +1,7 @@
1
- import type { NavigatorEnvVersion, NavigatorOpenType, NavigatorTarget } from "./navigator.js";
1
+ import type { NavigatorChannel, NavigatorOpenType, NavigatorTarget } from "./navigator.js";
2
2
  import type { NavigatorQuery } from "./navigator.js";
3
3
  import type { LxMediaSwiperItem } from "./media_swiper.js";
4
- import type { LxVideoQuality } from "./video.js";
4
+ import type { LxVideoQuality, LxVideoEventPayloads } from "./video.js";
5
5
 
6
6
  export function appendDataAttrs(
7
7
  attrs: Record<string, unknown>,
@@ -39,7 +39,7 @@ export interface NavigatorNativeAttrOptions {
39
39
  delta?: number;
40
40
  appId?: string;
41
41
  query?: NavigatorQuery;
42
- envVersion?: NavigatorEnvVersion;
42
+ channel?: NavigatorChannel;
43
43
  targetVersion?: string;
44
44
  phoneNumber?: string;
45
45
  hoverClass?: string;
@@ -66,7 +66,7 @@ export function buildNavigatorNativeAttrs(
66
66
  if (options.target) result.target = options.target;
67
67
  if (options.appId) result["app-id"] = options.appId;
68
68
  if (options.query) result.query = JSON.stringify(options.query);
69
- if (options.envVersion) result["env-version"] = options.envVersion;
69
+ if (options.channel) result.channel = options.channel;
70
70
  if (options.targetVersion) result["target-version"] = options.targetVersion;
71
71
  if (options.phoneNumber) result["phone-number"] = options.phoneNumber;
72
72
 
@@ -208,7 +208,8 @@ export const VIDEO_DOM_EVENT_MAP = {
208
208
  onWaiting: "waiting",
209
209
  onQualityChange: "qualitychange",
210
210
  onRateChange: "ratechange",
211
- } as const;
211
+ onVolumeChange: "volumechange",
212
+ } as const satisfies Record<keyof LxVideoEventPayloads, string>;
212
213
 
213
214
  export interface VideoNativeAttrOptions {
214
215
  id?: string;
@@ -238,7 +239,8 @@ export function buildVideoNativeAttrs(
238
239
  if (options.autoplay) result.autoplay = "";
239
240
  if (options.loop) result.loop = "";
240
241
  if (options.muted) result.muted = "";
241
- if (options.controls) result.controls = "";
242
+ if (options.controls === false) result.controls = "false";
243
+ else if (options.controls) result.controls = "";
242
244
  if (options.progressBar === false) result["progress-bar"] = "false";
243
245
  if (options.live) result.live = "";
244
246
  if (options.volume !== undefined) result.volume = options.volume;
@@ -7,7 +7,9 @@ const NATIVE_COMPONENT_LAYOUT_INVALIDATED_EVENT = "lingxia:native-component-layo
7
7
  let pendingLayoutInvalidationFrame: number | null = null;
8
8
  const pendingLayoutInvalidationTimers = new Map<number, number>();
9
9
 
10
- export function sendNativeComponentMessage(message: NativeComponentMessage) {
10
+ /// Returns false when the native channel is not up yet and the message was
11
+ /// dropped, so callers that carry state (an island's first commit) can retry.
12
+ export function sendNativeComponentMessage(message: NativeComponentMessage): boolean {
11
13
  const sender =
12
14
  typeof window !== "undefined"
13
15
  ? window.LingXiaBridge?.nativeComponents?.send
@@ -17,9 +19,10 @@ export function sendNativeComponentMessage(message: NativeComponentMessage) {
17
19
  warnedNoHandler = true;
18
20
  console.warn("[LingXia NativeComponent] message handler not available");
19
21
  }
20
- return;
22
+ return false;
21
23
  }
22
24
  sender(message);
25
+ return true;
23
26
  }
24
27
 
25
28
  export function registerNativeComponentHandler(
@@ -33,19 +36,11 @@ export function registerNativeComponentHandler(
33
36
  const registerFn = nativeComponents?.register;
34
37
  if (typeof registerFn === "function") {
35
38
  const unregister = registerFn(id, handler as Parameters<typeof registerFn>[1]);
36
- const platform = window.LingXiaBridge?.platform;
37
- const requiresReadyHandshake = !!(
38
- platform?.isIOS?.() ||
39
- platform?.isAndroid?.() ||
40
- platform?.isMacOS?.()
41
- );
42
- const hasHandler = nativeComponents?.hasHandler;
43
39
  const send = nativeComponents?.send;
40
+ // Always announce ready after the leaf handler is registered, including
41
+ // Windows: island video queues play/playing until this id's handshake.
44
42
  if (typeof send === "function") {
45
- const nativeReady = typeof hasHandler === "function" ? hasHandler() : true;
46
- if (requiresReadyHandshake || nativeReady) {
47
- send({ action: "component.ready", id });
48
- }
43
+ send({ action: "component.ready", id });
49
44
  }
50
45
  return unregister;
51
46
  }
package/src/navigator.ts CHANGED
@@ -18,7 +18,7 @@ export type NavigatorTarget =
18
18
 
19
19
  export type NavigatorQueryValue = string | number | boolean | null | undefined;
20
20
  export type NavigatorQuery = Record<string, NavigatorQueryValue>;
21
- export type NavigatorEnvVersion = 'release' | 'preview' | 'developer';
21
+ export type NavigatorChannel = 'release' | 'draft';
22
22
 
23
23
  export interface LxNavigatorEventDetail {
24
24
  success?: boolean;
@@ -44,7 +44,7 @@ export type LxNavigatorAttributes = {
44
44
 
45
45
  // Open external lxapp
46
46
  'app-id'?: string; // Target lxapp ID
47
- 'env-version'?: NavigatorEnvVersion; // Target lxapp envVersion
47
+ channel?: NavigatorChannel; // Target lxapp channel
48
48
  'target-version'?: string; // Exact target lxapp version
49
49
 
50
50
  // Phone call
@@ -89,7 +89,7 @@ export class LxNavigatorElement extends HTMLElement {
89
89
  "target",
90
90
  "delta",
91
91
  "app-id",
92
- "env-version",
92
+ "channel",
93
93
  "target-version",
94
94
  "phone-number",
95
95
  "hover-class",
@@ -263,7 +263,7 @@ export class LxNavigatorElement extends HTMLElement {
263
263
  const explicitTarget = this.getAttribute('target') as NavigatorTarget | null;
264
264
  const delta = parseInt(this.getAttribute('delta') || '1', 10);
265
265
  const appId = this.getAttribute('app-id');
266
- const envVersion = this.getAttribute('env-version') as NavigatorEnvVersion | null;
266
+ const channel = this.getAttribute('channel') as NavigatorChannel | null;
267
267
  const targetVersion = this.getAttribute('target-version');
268
268
  const phoneNumber = this.getAttribute('phone-number');
269
269
 
@@ -279,7 +279,7 @@ export class LxNavigatorElement extends HTMLElement {
279
279
  target,
280
280
  delta,
281
281
  appId,
282
- envVersion,
282
+ channel,
283
283
  targetVersion,
284
284
  phoneNumber
285
285
  });
@@ -322,7 +322,7 @@ export class LxNavigatorElement extends HTMLElement {
322
322
  target: NavigatorTarget;
323
323
  delta: number;
324
324
  appId?: string | null;
325
- envVersion?: NavigatorEnvVersion | null;
325
+ channel?: NavigatorChannel | null;
326
326
  targetVersion?: string | null;
327
327
  phoneNumber?: string | null;
328
328
  }) {
@@ -415,7 +415,7 @@ export class LxNavigatorElement extends HTMLElement {
415
415
  target: NavigatorTarget;
416
416
  delta: number;
417
417
  appId?: string | null;
418
- envVersion?: NavigatorEnvVersion | null;
418
+ channel?: NavigatorChannel | null;
419
419
  targetVersion?: string | null;
420
420
  phoneNumber?: string | null;
421
421
  }) {
@@ -530,13 +530,13 @@ export class LxNavigatorElement extends HTMLElement {
530
530
  appId?: string | null;
531
531
  page?: string | null;
532
532
  query?: string | null;
533
- envVersion?: NavigatorEnvVersion | null;
533
+ channel?: NavigatorChannel | null;
534
534
  targetVersion?: string | null;
535
535
  }): Record<string, unknown> {
536
536
  const target: Record<string, unknown> = { appId: options.appId };
537
537
  const pageTarget = this.buildPageTarget(options);
538
538
  if (pageTarget) Object.assign(target, pageTarget);
539
- if (options.envVersion) target.envVersion = options.envVersion;
539
+ if (options.channel) target.channel = options.channel;
540
540
  if (options.targetVersion) target.targetVersion = options.targetVersion;
541
541
  return target;
542
542
  }
@@ -0,0 +1,16 @@
1
+ /** Keep platform spelling differences out of the public DOM/payload contract. */
2
+ export function normalizeVideoEventDetail(event: string, payload: unknown): Record<string, unknown> {
3
+ const detail = payload && typeof payload === "object" ? payload as Record<string, unknown> : {};
4
+ if (["playrequest", "play", "playing", "pause", "stop", "ended", "waiting"].includes(event)) return {};
5
+ if (event === "fullscreenchange") {
6
+ return { ...detail, fullscreen: detail.fullscreen ?? detail.fullScreen ?? false };
7
+ }
8
+ if (event === "error") {
9
+ return {
10
+ ...detail,
11
+ code: String(detail.code ?? "NATIVE_COMPONENT_COMMAND_FAILED"),
12
+ message: String(detail.message ?? detail.errMsg ?? "Native video failed"),
13
+ };
14
+ }
15
+ return detail;
16
+ }