@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.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/inline-native/commit.d.ts +39 -0
- package/dist/inline-native/commit.js +94 -0
- package/dist/inline-native/elements.d.ts +104 -0
- package/dist/inline-native/elements.js +749 -0
- package/dist/inline-native/errors.d.ts +8 -0
- package/dist/inline-native/errors.js +23 -0
- package/dist/inline-native/events.d.ts +10 -0
- package/dist/inline-native/events.js +16 -0
- package/dist/inline-native/geometry.d.ts +78 -0
- package/dist/inline-native/geometry.js +40 -0
- package/dist/inline-native/identity.d.ts +28 -0
- package/dist/inline-native/identity.js +51 -0
- package/dist/inline-native/index.d.ts +14 -0
- package/dist/inline-native/index.js +19 -0
- package/dist/inline-native/layout.d.ts +2 -0
- package/dist/inline-native/layout.js +58 -0
- package/dist/inline-native/lease.d.ts +13 -0
- package/dist/inline-native/lease.js +31 -0
- package/dist/inline-native/runtime.d.ts +64 -0
- package/dist/inline-native/runtime.js +119 -0
- package/dist/inline-native/schema.d.ts +111 -0
- package/dist/inline-native/schema.js +210 -0
- package/dist/inline-native/structure.d.ts +22 -0
- package/dist/inline-native/structure.js +706 -0
- package/dist/inline-native/style.d.ts +7 -0
- package/dist/inline-native/style.js +119 -0
- package/dist/inline-native/types.d.ts +82 -0
- package/dist/inline-native/types.js +7 -0
- package/dist/inline-native/unwrap.d.ts +10 -0
- package/dist/inline-native/unwrap.js +32 -0
- package/dist/inline-native/video.d.ts +65 -0
- package/dist/inline-native/video.js +69 -0
- package/dist/native_component_wrapper_shared.d.ts +3 -2
- package/dist/native_component_wrapper_shared.js +6 -3
- package/dist/nativecomponent.d.ts +1 -1
- package/dist/nativecomponent.js +8 -11
- package/dist/navigator.d.ts +2 -2
- package/dist/navigator.js +5 -5
- package/dist/picker.js +12 -4
- package/dist/video-events.d.ts +2 -0
- package/dist/video-events.js +18 -0
- package/dist/video.d.ts +98 -79
- package/dist/video.js +182 -757
- package/package.json +2 -1
- package/src/index.ts +58 -1
- package/src/inline-native/commit.ts +141 -0
- package/src/inline-native/elements.ts +829 -0
- package/src/inline-native/errors.ts +35 -0
- package/src/inline-native/events.ts +31 -0
- package/src/inline-native/geometry.ts +86 -0
- package/src/inline-native/identity.ts +102 -0
- package/src/inline-native/index.ts +103 -0
- package/src/inline-native/layout.ts +58 -0
- package/src/inline-native/lease.ts +48 -0
- package/src/inline-native/runtime.ts +185 -0
- package/src/inline-native/schema.ts +247 -0
- package/src/inline-native/structure.ts +823 -0
- package/src/inline-native/style.ts +111 -0
- package/src/inline-native/types.ts +105 -0
- package/src/inline-native/unwrap.ts +34 -0
- package/src/inline-native/video.ts +125 -0
- package/src/native_component_wrapper_shared.ts +8 -6
- package/src/nativecomponent.ts +8 -13
- package/src/navigator.ts +9 -9
- package/src/picker.ts +13 -4
- package/src/video-events.ts +16 -0
- 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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|
package/src/nativecomponent.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
21
|
+
export type NavigatorChannel = 'release' | 'preview' | '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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
539
|
+
if (options.channel) target.channel = options.channel;
|
|
540
540
|
if (options.targetVersion) target.targetVersion = options.targetVersion;
|
|
541
541
|
return target;
|
|
542
542
|
}
|
package/src/picker.ts
CHANGED
|
@@ -5,7 +5,16 @@ import {
|
|
|
5
5
|
} from "./nativecomponent.js";
|
|
6
6
|
import { ensureComponentId } from "./component.js";
|
|
7
7
|
import { measureElement } from "./dom.js";
|
|
8
|
-
import { isAndroid, isHarmony,
|
|
8
|
+
import { isAndroid, isHarmony, isIOS } from "./platform.js";
|
|
9
|
+
|
|
10
|
+
// Which hosts register a `picker.native` component. A capability question, not
|
|
11
|
+
// a form-factor one: macOS and Windows have no picker factory, so they draw the
|
|
12
|
+
// web picker even in the Runner's phone frame, where the host class is mobile
|
|
13
|
+
// but the machine underneath is still a Mac or a PC. Harmony has its own embed
|
|
14
|
+
// path and never reaches either branch.
|
|
15
|
+
function hasNativePicker(): boolean {
|
|
16
|
+
return isIOS() || isAndroid();
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
const HARMONY_PROPS_PREFIX = "data:application/json,";
|
|
11
20
|
|
|
@@ -90,7 +99,7 @@ export class LxPickerElement extends HTMLElement {
|
|
|
90
99
|
private removeLayoutInvalidationListener?: () => void;
|
|
91
100
|
private readonly layoutInvalidationListener: EventListenerObject = {
|
|
92
101
|
handleEvent: () => {
|
|
93
|
-
if (!this.isConnected || isHarmony() ||
|
|
102
|
+
if (!this.isConnected || isHarmony() || !hasNativePicker()) return;
|
|
94
103
|
void this.mountPicker();
|
|
95
104
|
}
|
|
96
105
|
};
|
|
@@ -146,7 +155,7 @@ export class LxPickerElement extends HTMLElement {
|
|
|
146
155
|
this.removeLayoutInvalidationListener?.();
|
|
147
156
|
this.removeLayoutInvalidationListener = undefined;
|
|
148
157
|
|
|
149
|
-
if (this.componentId && !isHarmony() &&
|
|
158
|
+
if (this.componentId && !isHarmony() && hasNativePicker()) {
|
|
150
159
|
sendNativeComponentMessage({
|
|
151
160
|
action: "component.unmount",
|
|
152
161
|
id: this.componentId
|
|
@@ -301,7 +310,7 @@ export class LxPickerElement extends HTMLElement {
|
|
|
301
310
|
return;
|
|
302
311
|
}
|
|
303
312
|
|
|
304
|
-
if (
|
|
313
|
+
if (!hasNativePicker()) {
|
|
305
314
|
if (this.webCleanup) return;
|
|
306
315
|
const { renderWebPicker } = await import('./picker-web.js');
|
|
307
316
|
if (!this.isConnected || !this.componentId || this.webCleanup) return;
|
|
@@ -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
|
+
}
|