@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.
- 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/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/video-events.ts +16 -0
- package/src/video.ts +245 -839
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import { collectNativeStyleIssues, effectiveOpacity, normalizeNativeColor } from "./style.js";
|
|
2
|
+
import { nativeError } from "./errors.js";
|
|
3
|
+
import {
|
|
4
|
+
NATIVE_ACTION_ICONS,
|
|
5
|
+
PUBLIC_COMPONENT_NAMES,
|
|
6
|
+
TAG_TO_AUTHOR_COMPONENT,
|
|
7
|
+
type PublicComponentName,
|
|
8
|
+
} from "./schema.js";
|
|
9
|
+
import {
|
|
10
|
+
EMPTY_ROOT_REF,
|
|
11
|
+
type AuthorChild,
|
|
12
|
+
type AuthorNode,
|
|
13
|
+
type CompileInlineNativeOptions,
|
|
14
|
+
type CompileInlineNativeResult,
|
|
15
|
+
type CompiledNativeRoot,
|
|
16
|
+
type CoreNode,
|
|
17
|
+
type NativeError,
|
|
18
|
+
type RootRef,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
const PUBLIC_NAME_SET = new Set<string>(PUBLIC_COMPONENT_NAMES);
|
|
22
|
+
const ACTION_ICON_SET = new Set<string>(NATIVE_ACTION_ICONS);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile one author Root into the host core tree. Cover/Button expand here;
|
|
26
|
+
* illegal children never become a host commit.
|
|
27
|
+
*/
|
|
28
|
+
export function compileInlineNativeRoot(
|
|
29
|
+
author: AuthorNode,
|
|
30
|
+
options: CompileInlineNativeOptions = {}
|
|
31
|
+
): CompileInlineNativeResult {
|
|
32
|
+
const rootRef = options.rootRef ?? EMPTY_ROOT_REF;
|
|
33
|
+
const diagnostics = collectStyleDiagnostics(author, rootRef);
|
|
34
|
+
const unsupported = unsupportedAuthorProp(author);
|
|
35
|
+
if (unsupported) return fail(nativeError("NATIVE_COMPONENT_INVALID_PROPS",
|
|
36
|
+
`${unsupported} is not supported; fullscreen belongs to LxVideo and visibility changes are immediate`,
|
|
37
|
+
{ root: rootRef }), diagnostics);
|
|
38
|
+
const authorType = normalizeAuthorType(author.type);
|
|
39
|
+
|
|
40
|
+
if (authorType !== "LxNativeRoot") {
|
|
41
|
+
return fail(
|
|
42
|
+
nativeError(
|
|
43
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
44
|
+
authorType === "LxVideo"
|
|
45
|
+
? "LxVideo must be a direct child of an explicit LxNativeRoot; implicit roots are not created"
|
|
46
|
+
: `inline native trees must start at LxNativeRoot, got ${author.type || "<unknown>"}`,
|
|
47
|
+
{ root: rootRef }
|
|
48
|
+
),
|
|
49
|
+
diagnostics
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const flattened = flattenAuthorChildren(author.children);
|
|
54
|
+
const textChild = flattened.find((child) => child.kind === "text");
|
|
55
|
+
if (textChild) {
|
|
56
|
+
return fail(
|
|
57
|
+
nativeError(
|
|
58
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
59
|
+
"LxNativeRoot cannot contain bare text; wrap copy in LxNativeText",
|
|
60
|
+
{ root: rootRef }
|
|
61
|
+
),
|
|
62
|
+
diagnostics
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const authorChildren: AuthorNode[] = [];
|
|
67
|
+
for (const child of flattened) {
|
|
68
|
+
if (child.kind === "text") {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (child.kind === "unknown") {
|
|
72
|
+
return fail(
|
|
73
|
+
nativeError(
|
|
74
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
75
|
+
`LxNativeRoot cannot contain DOM or unregistered children (${child.label})`,
|
|
76
|
+
{ root: rootRef }
|
|
77
|
+
),
|
|
78
|
+
diagnostics
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
authorChildren.push(child.node);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const props = { ...(author.props ?? {}) };
|
|
85
|
+
const defaultVideoControls = options.defaultVideoControls !== false;
|
|
86
|
+
const expanded: CoreNode[] = [];
|
|
87
|
+
for (const child of authorChildren) {
|
|
88
|
+
const result = expandAuthorNode(child, rootRef, {
|
|
89
|
+
allowVideo: true,
|
|
90
|
+
parent: "LxNativeRoot",
|
|
91
|
+
defaultVideoControls,
|
|
92
|
+
});
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
return fail(result.error, diagnostics);
|
|
95
|
+
}
|
|
96
|
+
expanded.push(result.node);
|
|
97
|
+
diagnostics.push(...result.diagnostics);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const root: CompiledNativeRoot = {
|
|
101
|
+
kind: "root",
|
|
102
|
+
authorType: "LxNativeRoot",
|
|
103
|
+
authorId: author.authorId,
|
|
104
|
+
automationId: author.automationId,
|
|
105
|
+
props,
|
|
106
|
+
children: expanded,
|
|
107
|
+
};
|
|
108
|
+
return { ok: true, root, diagnostics };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function unsupportedAuthorProp(author: AuthorNode): string | undefined {
|
|
112
|
+
for (const key of ["fullscreenScope", "hiddenTransition"]) {
|
|
113
|
+
if (author.props?.[key] !== undefined) return key;
|
|
114
|
+
}
|
|
115
|
+
for (const child of flattenAuthorChildren(author.children)) {
|
|
116
|
+
if (child.kind !== "node") continue;
|
|
117
|
+
const found = unsupportedAuthorProp(child.node);
|
|
118
|
+
if (found) return found;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Scan a page-level author forest. A bare LxVideo (or a Video nested in a
|
|
125
|
+
* non-Root) is `NATIVE_ROOT_INVALID_STRUCTURE`.
|
|
126
|
+
*/
|
|
127
|
+
export function compileInlineNativeForest(
|
|
128
|
+
nodes: readonly AuthorNode[],
|
|
129
|
+
options: CompileInlineNativeOptions = {}
|
|
130
|
+
): CompileInlineNativeResult | { ok: true; roots: CompiledNativeRoot[]; diagnostics: NativeError[] } {
|
|
131
|
+
const roots: CompiledNativeRoot[] = [];
|
|
132
|
+
const diagnostics: NativeError[] = [];
|
|
133
|
+
for (const node of nodes) {
|
|
134
|
+
const type = normalizeAuthorType(node.type);
|
|
135
|
+
if (type === "LxVideo") {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: nativeError(
|
|
139
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
140
|
+
"LxVideo must be a direct child of an explicit LxNativeRoot; implicit roots are not created",
|
|
141
|
+
{ root: options.rootRef ?? EMPTY_ROOT_REF }
|
|
142
|
+
),
|
|
143
|
+
diagnostics,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (type === "LxNativeRoot") {
|
|
147
|
+
const compiled = compileInlineNativeRoot(node, options);
|
|
148
|
+
if (!compiled.ok) {
|
|
149
|
+
return compiled;
|
|
150
|
+
}
|
|
151
|
+
roots.push(compiled.root);
|
|
152
|
+
diagnostics.push(...compiled.diagnostics);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (type) {
|
|
156
|
+
const nestedVideo = findFirstVideo(node);
|
|
157
|
+
if (nestedVideo) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: nativeError(
|
|
161
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
162
|
+
"LxVideo must be a direct child of an explicit LxNativeRoot; it cannot live in View/Cover or outside a Root",
|
|
163
|
+
{ root: options.rootRef ?? EMPTY_ROOT_REF }
|
|
164
|
+
),
|
|
165
|
+
diagnostics,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, roots, diagnostics };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function fail(
|
|
174
|
+
error: NativeError,
|
|
175
|
+
diagnostics: NativeError[]
|
|
176
|
+
): CompileInlineNativeResult {
|
|
177
|
+
return { ok: false, error, diagnostics };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type FlatChild =
|
|
181
|
+
| { kind: "node"; node: AuthorNode }
|
|
182
|
+
| { kind: "text"; value: string }
|
|
183
|
+
| { kind: "unknown"; label: string };
|
|
184
|
+
|
|
185
|
+
function flattenAuthorChildren(children: AuthorChild | undefined): FlatChild[] {
|
|
186
|
+
const out: FlatChild[] = [];
|
|
187
|
+
walk(children);
|
|
188
|
+
return out;
|
|
189
|
+
|
|
190
|
+
function walk(value: AuthorChild | undefined): void {
|
|
191
|
+
if (value === false || value === null || value === undefined) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
for (const entry of value) {
|
|
196
|
+
walk(entry);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
201
|
+
out.push({ kind: "text", value: String(value) });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (typeof value === "object" && value && typeof (value as AuthorNode).type === "string") {
|
|
205
|
+
const node = value as AuthorNode;
|
|
206
|
+
if (node.type === "#text") {
|
|
207
|
+
const text = node.textContent == null ? "" : String(node.textContent);
|
|
208
|
+
if (text) {
|
|
209
|
+
out.push({ kind: "text", value: text });
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
out.push({ kind: "node", node });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
out.push({ kind: "unknown", label: describeUnknown(value) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function describeUnknown(value: unknown): string {
|
|
221
|
+
if (value && typeof value === "object") {
|
|
222
|
+
const record = value as { type?: unknown; tagName?: unknown };
|
|
223
|
+
if (typeof record.type === "string") return record.type;
|
|
224
|
+
if (typeof record.tagName === "string") return record.tagName;
|
|
225
|
+
return Object.prototype.toString.call(value);
|
|
226
|
+
}
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function normalizeAuthorType(type: string | undefined): PublicComponentName | undefined {
|
|
231
|
+
if (!type) return undefined;
|
|
232
|
+
const trimmed = type.trim();
|
|
233
|
+
if (PUBLIC_NAME_SET.has(trimmed)) {
|
|
234
|
+
return trimmed as PublicComponentName;
|
|
235
|
+
}
|
|
236
|
+
const lower = trimmed.toLowerCase();
|
|
237
|
+
if (lower in TAG_TO_AUTHOR_COMPONENT) {
|
|
238
|
+
return TAG_TO_AUTHOR_COMPONENT[lower as keyof typeof TAG_TO_AUTHOR_COMPONENT];
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function parseBooleanAttr(value: unknown, defaultValue = false): boolean {
|
|
244
|
+
if (value === true || value === "") return true;
|
|
245
|
+
if (value === false) return false;
|
|
246
|
+
if (typeof value === "string") {
|
|
247
|
+
const normalized = value.trim().toLowerCase();
|
|
248
|
+
if (normalized === "true" || normalized === "") return true;
|
|
249
|
+
if (normalized === "false") return false;
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === "number") {
|
|
252
|
+
return value !== 0;
|
|
253
|
+
}
|
|
254
|
+
return defaultValue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface ExpandContext {
|
|
258
|
+
allowVideo: boolean;
|
|
259
|
+
parent: PublicComponentName;
|
|
260
|
+
defaultVideoControls: boolean;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
type ExpandResult =
|
|
264
|
+
| { ok: true; node: CoreNode; diagnostics: NativeError[] }
|
|
265
|
+
| { ok: false; error: NativeError; diagnostics: NativeError[] };
|
|
266
|
+
|
|
267
|
+
function expandAuthorNode(
|
|
268
|
+
author: AuthorNode,
|
|
269
|
+
rootRef: RootRef,
|
|
270
|
+
context: ExpandContext
|
|
271
|
+
): ExpandResult {
|
|
272
|
+
const type = normalizeAuthorType(author.type);
|
|
273
|
+
if (!type) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
error: nativeError(
|
|
277
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
278
|
+
`LxNativeRoot cannot contain DOM or unregistered children (${author.type || "<unknown>"})`,
|
|
279
|
+
{ root: rootRef }
|
|
280
|
+
),
|
|
281
|
+
diagnostics: [],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (type === "LxNativeRoot") {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: nativeError(
|
|
289
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
290
|
+
"LxNativeRoot cannot nest another LxNativeRoot",
|
|
291
|
+
{ root: rootRef }
|
|
292
|
+
),
|
|
293
|
+
diagnostics: [],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (type === "LxVideo") {
|
|
298
|
+
if (!context.allowVideo || context.parent !== "LxNativeRoot") {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
error: nativeError(
|
|
302
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
303
|
+
"LxVideo must be a direct child of LxNativeRoot; View/Cover cannot wrap it",
|
|
304
|
+
{ root: rootRef }
|
|
305
|
+
),
|
|
306
|
+
diagnostics: [],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const nested = flattenAuthorChildren(author.children);
|
|
310
|
+
if (nested.length > 0) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
error: nativeError(
|
|
314
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
315
|
+
"LxVideo cannot have children",
|
|
316
|
+
{ root: rootRef }
|
|
317
|
+
),
|
|
318
|
+
diagnostics: [],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const props = { ...(author.props ?? {}) };
|
|
322
|
+
if (!("controls" in props) || props.controls === undefined) {
|
|
323
|
+
props.controls = context.defaultVideoControls;
|
|
324
|
+
} else {
|
|
325
|
+
props.controls = parseBooleanAttr(props.controls, context.defaultVideoControls);
|
|
326
|
+
}
|
|
327
|
+
if ("autoplay" in props) props.autoplay = parseBooleanAttr(props.autoplay, false);
|
|
328
|
+
if ("loop" in props) props.loop = parseBooleanAttr(props.loop, false);
|
|
329
|
+
if ("muted" in props) props.muted = parseBooleanAttr(props.muted, false);
|
|
330
|
+
if ("live" in props) props.live = parseBooleanAttr(props.live, false);
|
|
331
|
+
if (typeof props.volume === "string") {
|
|
332
|
+
const parsed = Number(props.volume);
|
|
333
|
+
if (Number.isFinite(parsed)) props.volume = parsed;
|
|
334
|
+
}
|
|
335
|
+
if (typeof props.qualities === "string") {
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(props.qualities);
|
|
338
|
+
if (Array.isArray(parsed)) props.qualities = parsed;
|
|
339
|
+
} catch {
|
|
340
|
+
/* keep the raw string; host validation skips non-arrays */
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (typeof props.playbackRates === "string") {
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(props.playbackRates);
|
|
346
|
+
if (Array.isArray(parsed)) props.playbackRates = parsed;
|
|
347
|
+
} catch {
|
|
348
|
+
/* keep the raw string */
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const pressHandler = props.onPress;
|
|
352
|
+
if (
|
|
353
|
+
props.controls === false &&
|
|
354
|
+
typeof pressHandler === "function" &&
|
|
355
|
+
!nonEmptyAriaLabel(props)
|
|
356
|
+
) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
error: nativeError(
|
|
360
|
+
"NATIVE_COMPONENT_INVALID_PROPS",
|
|
361
|
+
"LxVideo with controls={false} and onPress requires a non-empty aria-label",
|
|
362
|
+
{ root: rootRef, scope: "node", recoverable: true }
|
|
363
|
+
),
|
|
364
|
+
diagnostics: [],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
node: {
|
|
370
|
+
kind: "video",
|
|
371
|
+
authorType: "LxVideo",
|
|
372
|
+
authorId: author.authorId,
|
|
373
|
+
automationId: author.automationId,
|
|
374
|
+
props,
|
|
375
|
+
children: [],
|
|
376
|
+
},
|
|
377
|
+
diagnostics: [],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (type === "LxNativeText") {
|
|
382
|
+
const text = readTextContent(author);
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
node: {
|
|
386
|
+
kind: "text",
|
|
387
|
+
authorType: "LxNativeText",
|
|
388
|
+
authorId: author.authorId,
|
|
389
|
+
automationId: author.automationId,
|
|
390
|
+
props: { ...(author.props ?? {}), pointerEvents: author.props?.pointerEvents ?? "none" },
|
|
391
|
+
children: [],
|
|
392
|
+
text,
|
|
393
|
+
},
|
|
394
|
+
diagnostics: [],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (type === "LxNativeButton") {
|
|
399
|
+
const nested = flattenAuthorChildren(author.children);
|
|
400
|
+
if (nested.some((entry) => entry.kind === "node")) {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
error: nativeError(
|
|
404
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
405
|
+
"LxNativeButton cannot wrap interactive or native children; use label/icon",
|
|
406
|
+
{ root: rootRef }
|
|
407
|
+
),
|
|
408
|
+
diagnostics: [],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const label = readButtonLabel(author, nested);
|
|
412
|
+
const icon = author.props?.icon;
|
|
413
|
+
if (!label && !icon && !nonEmptyAriaLabel(author.props)) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: nativeError(
|
|
417
|
+
"NATIVE_COMPONENT_INVALID_PROPS",
|
|
418
|
+
"LxNativeButton without label must provide a non-empty aria-label",
|
|
419
|
+
{ root: rootRef, scope: "node", recoverable: true }
|
|
420
|
+
),
|
|
421
|
+
diagnostics: [],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (icon != null && icon !== "" && (typeof icon !== "string" || !ACTION_ICON_SET.has(icon))) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: nativeError(
|
|
428
|
+
"NATIVE_COMPONENT_INVALID_PROPS",
|
|
429
|
+
"LxNativeButton icon must be a supported NativeActionIcon; resource icons are not supported",
|
|
430
|
+
{ root: rootRef, scope: "node", recoverable: true }
|
|
431
|
+
),
|
|
432
|
+
diagnostics: [],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ok: true,
|
|
437
|
+
node: {
|
|
438
|
+
kind: "tappable",
|
|
439
|
+
authorType: "LxNativeButton",
|
|
440
|
+
authorId: author.authorId,
|
|
441
|
+
automationId: author.automationId,
|
|
442
|
+
props: {
|
|
443
|
+
...normalizeButtonProps(author.props),
|
|
444
|
+
content: buildButtonContent(label, icon),
|
|
445
|
+
},
|
|
446
|
+
children: [],
|
|
447
|
+
},
|
|
448
|
+
diagnostics: [],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (type === "LxNativeCover" || type === "LxNativeView") {
|
|
453
|
+
const childContext: ExpandContext = {
|
|
454
|
+
allowVideo: false,
|
|
455
|
+
parent: type,
|
|
456
|
+
defaultVideoControls: context.defaultVideoControls,
|
|
457
|
+
};
|
|
458
|
+
const expandedChildren: CoreNode[] = [];
|
|
459
|
+
const diagnostics: NativeError[] = [];
|
|
460
|
+
const flattened = flattenAuthorChildren(author.children);
|
|
461
|
+
for (const child of flattened) {
|
|
462
|
+
if (child.kind === "text") {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
error: nativeError(
|
|
466
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
467
|
+
`${type} cannot contain bare text; wrap copy in LxNativeText`,
|
|
468
|
+
{ root: rootRef }
|
|
469
|
+
),
|
|
470
|
+
diagnostics,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (child.kind === "unknown") {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
error: nativeError(
|
|
477
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
478
|
+
`${type} cannot contain DOM or unregistered children (${child.label})`,
|
|
479
|
+
{ root: rootRef }
|
|
480
|
+
),
|
|
481
|
+
diagnostics,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const result = expandAuthorNode(child.node, rootRef, childContext);
|
|
485
|
+
if (!result.ok) {
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
expandedChildren.push(result.node);
|
|
489
|
+
diagnostics.push(...result.diagnostics);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (type === "LxNativeCover") {
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
node: {
|
|
496
|
+
kind: "view",
|
|
497
|
+
authorType: "LxNativeCover",
|
|
498
|
+
authorId: author.authorId,
|
|
499
|
+
automationId: author.automationId,
|
|
500
|
+
props: normalizeCoverProps(author.props),
|
|
501
|
+
children: expandedChildren,
|
|
502
|
+
},
|
|
503
|
+
diagnostics,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
ok: true,
|
|
509
|
+
node: {
|
|
510
|
+
kind: "view",
|
|
511
|
+
authorType: "LxNativeView",
|
|
512
|
+
authorId: author.authorId,
|
|
513
|
+
automationId: author.automationId,
|
|
514
|
+
props: {
|
|
515
|
+
pointerEvents: author.props?.pointerEvents ?? "auto",
|
|
516
|
+
...(author.props ?? {}),
|
|
517
|
+
},
|
|
518
|
+
children: expandedChildren,
|
|
519
|
+
},
|
|
520
|
+
diagnostics,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
ok: false,
|
|
526
|
+
error: nativeError(
|
|
527
|
+
"NATIVE_ROOT_INVALID_STRUCTURE",
|
|
528
|
+
`unsupported inline native component ${author.type}`,
|
|
529
|
+
{ root: rootRef }
|
|
530
|
+
),
|
|
531
|
+
diagnostics: [],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function findFirstVideo(node: AuthorNode): AuthorNode | undefined {
|
|
536
|
+
if (normalizeAuthorType(node.type) === "LxVideo") {
|
|
537
|
+
return node;
|
|
538
|
+
}
|
|
539
|
+
for (const child of flattenAuthorChildren(node.children)) {
|
|
540
|
+
if (child.kind === "node") {
|
|
541
|
+
const found = findFirstVideo(child.node);
|
|
542
|
+
if (found) return found;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function readTextContent(author: AuthorNode): string {
|
|
549
|
+
if (author.textContent !== undefined && author.textContent !== null) {
|
|
550
|
+
return String(author.textContent);
|
|
551
|
+
}
|
|
552
|
+
const parts: string[] = [];
|
|
553
|
+
for (const child of flattenAuthorChildren(author.children)) {
|
|
554
|
+
if (child.kind === "text") {
|
|
555
|
+
parts.push(child.value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return parts.join("");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function readButtonLabel(author: AuthorNode, nested: FlatChild[]): string {
|
|
562
|
+
const labeled = author.props?.label;
|
|
563
|
+
if (typeof labeled === "string" && labeled.trim()) {
|
|
564
|
+
return labeled;
|
|
565
|
+
}
|
|
566
|
+
return nested
|
|
567
|
+
.filter((entry): entry is { kind: "text"; value: string } => entry.kind === "text")
|
|
568
|
+
.map((entry) => entry.value)
|
|
569
|
+
.join("");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function nonEmptyAriaLabel(props: Record<string, unknown> | undefined): boolean {
|
|
573
|
+
if (!props) return false;
|
|
574
|
+
const label = props["aria-label"] ?? props.ariaLabel;
|
|
575
|
+
return typeof label === "string" && label.trim().length > 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function buildButtonContent(label: string, icon: unknown): Record<string, unknown> {
|
|
579
|
+
const content: Record<string, unknown> = {};
|
|
580
|
+
if (label) content.text = label;
|
|
581
|
+
if (typeof icon === "string" && icon) {
|
|
582
|
+
content.icon = { kind: "semantic", name: icon };
|
|
583
|
+
}
|
|
584
|
+
return content;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function normalizeCoverProps(props: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
588
|
+
const next = { ...(props ?? {}) };
|
|
589
|
+
if (next.pointerEvents === undefined) {
|
|
590
|
+
next.pointerEvents = "box-none";
|
|
591
|
+
}
|
|
592
|
+
const scrim = typeof next.scrim === "string" ? next.scrim : "none";
|
|
593
|
+
const scrimOpacity =
|
|
594
|
+
typeof next.scrimOpacity === "number"
|
|
595
|
+
? next.scrimOpacity
|
|
596
|
+
: typeof next.scrimOpacity === "string"
|
|
597
|
+
? Number(next.scrimOpacity)
|
|
598
|
+
: 0.6;
|
|
599
|
+
next.scrimPaint = {
|
|
600
|
+
scrim,
|
|
601
|
+
opacity: Number.isFinite(scrimOpacity) ? scrimOpacity : 0.6,
|
|
602
|
+
};
|
|
603
|
+
next.coverPreset = { position: "absolute", inset: 0 };
|
|
604
|
+
return next;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function normalizeButtonProps(props: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
608
|
+
const next = { ...(props ?? {}) };
|
|
609
|
+
normalizeNumberField(next, "hitSlop");
|
|
610
|
+
if (next.intent === undefined) next.intent = "neutral";
|
|
611
|
+
if (next.emphasis === undefined) next.emphasis = "secondary";
|
|
612
|
+
if (next.size === undefined) next.size = "regular";
|
|
613
|
+
if (next.iconPosition === undefined) next.iconPosition = "start";
|
|
614
|
+
if (next.pointerEvents === undefined) next.pointerEvents = "auto";
|
|
615
|
+
return next;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function normalizeNumberField(props: Record<string, unknown>, key: string): void {
|
|
619
|
+
if (typeof props[key] !== "string" || props[key] === "") return;
|
|
620
|
+
const parsed = Number(props[key]);
|
|
621
|
+
if (Number.isFinite(parsed)) props[key] = parsed;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Collect author nodes from a Root element's light DOM (skips fallback). */
|
|
625
|
+
export function collectAuthorTreeFromElement(root: Element): AuthorNode {
|
|
626
|
+
return readElementAsAuthor(root);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function readElementAsAuthor(element: Element): AuthorNode {
|
|
630
|
+
const tag = element.tagName.toLowerCase();
|
|
631
|
+
const type = normalizeAuthorType(tag) ?? tag;
|
|
632
|
+
const props = readElementProps(element);
|
|
633
|
+
const styleIssues = collectNativeStyleIssues(element);
|
|
634
|
+
const authorId = element.getAttribute("id") ?? undefined;
|
|
635
|
+
const automationId = element.getAttribute("automation-id") ?? undefined;
|
|
636
|
+
if (type === "LxNativeText") {
|
|
637
|
+
return {
|
|
638
|
+
type,
|
|
639
|
+
authorId,
|
|
640
|
+
automationId,
|
|
641
|
+
props,
|
|
642
|
+
textContent: element.textContent ?? "",
|
|
643
|
+
styleIssues,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const children: AuthorNode[] = [];
|
|
647
|
+
for (const child of Array.from(element.childNodes)) {
|
|
648
|
+
if (child.nodeType === 3) {
|
|
649
|
+
const text = (child.textContent ?? "").trim();
|
|
650
|
+
if (text) {
|
|
651
|
+
children.push({ type: "#text", textContent: text, props: {} });
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (child.nodeType !== 1) continue;
|
|
656
|
+
const el = child as Element;
|
|
657
|
+
if (isFallbackElement(el)) continue;
|
|
658
|
+
children.push(readElementAsAuthor(el));
|
|
659
|
+
}
|
|
660
|
+
return { type, authorId, automationId, props, children, styleIssues };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function findInlineNativeRoot(element: Element): Element | null {
|
|
664
|
+
if (typeof element.closest === "function") {
|
|
665
|
+
return element.closest("lx-native-root");
|
|
666
|
+
}
|
|
667
|
+
let current: Element | null = element.parentElement;
|
|
668
|
+
while (current) {
|
|
669
|
+
if (current.tagName.toLowerCase() === "lx-native-root") {
|
|
670
|
+
return current;
|
|
671
|
+
}
|
|
672
|
+
current = current.parentElement;
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function isFallbackElement(element: Element): boolean {
|
|
678
|
+
if (element.hasAttribute("data-lx-native-fallback")) {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
const tag = element.tagName.toLowerCase();
|
|
682
|
+
if (tag === "template" && element.getAttribute("slot") === "fallback") {
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function readElementProps(element: Element): Record<string, unknown> {
|
|
689
|
+
const props: Record<string, unknown> = {};
|
|
690
|
+
const dataset: Record<string, string> = {};
|
|
691
|
+
for (const attr of Array.from(element.attributes)) {
|
|
692
|
+
if (attr.name === "data-lx-native-measuring" || attr.name === "data-lx-native-paint-hidden") continue;
|
|
693
|
+
const name = attr.name;
|
|
694
|
+
if (name === "id" || name === "automation-id" || name === "class" || name === "style") {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
props[camelize(name)] = attr.value;
|
|
698
|
+
if (name.startsWith("data-") && name !== "data-lx-component-id") {
|
|
699
|
+
const key = camelize(name.slice(5));
|
|
700
|
+
if (key) dataset[key] = attr.value;
|
|
701
|
+
}
|
|
702
|
+
if (name.startsWith("aria-")) {
|
|
703
|
+
props[name] = attr.value;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const anyEl = element as HTMLElement & Record<string, unknown>;
|
|
707
|
+
const propertyKeys = [
|
|
708
|
+
"src",
|
|
709
|
+
"poster",
|
|
710
|
+
"controls",
|
|
711
|
+
"scrim",
|
|
712
|
+
"scrimOpacity",
|
|
713
|
+
"icon",
|
|
714
|
+
"label",
|
|
715
|
+
"pointerEvents",
|
|
716
|
+
"hidden",
|
|
717
|
+
"hiddenTransition",
|
|
718
|
+
"fullscreenScope",
|
|
719
|
+
"onPress",
|
|
720
|
+
"watermark",
|
|
721
|
+
"qualities",
|
|
722
|
+
"quality",
|
|
723
|
+
"playbackRates",
|
|
724
|
+
"rate",
|
|
725
|
+
"objectFit",
|
|
726
|
+
"contentRotate",
|
|
727
|
+
"live",
|
|
728
|
+
"autoplay",
|
|
729
|
+
"loop",
|
|
730
|
+
"muted",
|
|
731
|
+
"volume",
|
|
732
|
+
"progressBar",
|
|
733
|
+
"pageFuncBindings",
|
|
734
|
+
"pageFuncBindingsJson",
|
|
735
|
+
"intent",
|
|
736
|
+
"emphasis",
|
|
737
|
+
"size",
|
|
738
|
+
"hitSlop",
|
|
739
|
+
"disabled",
|
|
740
|
+
"pressed",
|
|
741
|
+
"expanded",
|
|
742
|
+
"loading",
|
|
743
|
+
"maxLines",
|
|
744
|
+
"fontSize",
|
|
745
|
+
"fontWeight",
|
|
746
|
+
"lineHeight",
|
|
747
|
+
"textAlign",
|
|
748
|
+
"color",
|
|
749
|
+
"dir",
|
|
750
|
+
"role",
|
|
751
|
+
];
|
|
752
|
+
for (const key of propertyKeys) {
|
|
753
|
+
if (key in anyEl && anyEl[key] !== undefined && anyEl[key] !== null) {
|
|
754
|
+
props[key] = anyEl[key];
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (Object.keys(dataset).length > 0) {
|
|
758
|
+
props.dataset = dataset;
|
|
759
|
+
props.datasetJson = JSON.stringify(dataset);
|
|
760
|
+
}
|
|
761
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
762
|
+
if (ariaLabel) {
|
|
763
|
+
props["aria-label"] = ariaLabel;
|
|
764
|
+
props.ariaLabel = ariaLabel;
|
|
765
|
+
}
|
|
766
|
+
const nativeStyle = readComputedNativeStyle(element);
|
|
767
|
+
if (Object.keys(nativeStyle).length > 0) {
|
|
768
|
+
props.nativeStyle = nativeStyle;
|
|
769
|
+
}
|
|
770
|
+
const type = normalizeAuthorType(element.tagName.toLowerCase());
|
|
771
|
+
if (type === "LxNativeText") {
|
|
772
|
+
for (const key of ["fontSize", "fontWeight", "lineHeight", "textAlign", "color", "dir"] as const) {
|
|
773
|
+
if (nativeStyle[key] !== undefined) {
|
|
774
|
+
props[key] = nativeStyle[key];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return props;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function readComputedNativeStyle(element: Element): Record<string, unknown> {
|
|
782
|
+
const view = element.ownerDocument?.defaultView;
|
|
783
|
+
if (!view || typeof view.getComputedStyle !== "function") return {};
|
|
784
|
+
const style = view.getComputedStyle(element);
|
|
785
|
+
const inherited = element.parentElement ? view.getComputedStyle(element.parentElement) : null;
|
|
786
|
+
const out: Record<string, unknown> = {};
|
|
787
|
+
const put = (key: string, value: string, skip?: (value: string) => boolean) => {
|
|
788
|
+
const normalized = value.trim();
|
|
789
|
+
if (!normalized || skip?.(normalized)) return;
|
|
790
|
+
out[key] = key.endsWith("Color") || key === "color"
|
|
791
|
+
? normalizeNativeColor(element.ownerDocument, normalized) ?? normalized : normalized;
|
|
792
|
+
};
|
|
793
|
+
put("backgroundColor", style.backgroundColor, (value) => value === "transparent" || value === "rgba(0, 0, 0, 0)");
|
|
794
|
+
put("color", style.color);
|
|
795
|
+
put("accentColor", style.accentColor, (value) => value === "auto");
|
|
796
|
+
put("opacity", String(effectiveOpacity(element)), (value) => value === "1");
|
|
797
|
+
put("borderColor", style.borderTopColor, (value) => style.borderTopWidth === "0px" && value === inherited?.color);
|
|
798
|
+
put("borderWidth", style.borderTopWidth, (value) => value === "0px");
|
|
799
|
+
put("borderStyle", style.borderTopStyle, (value) => value === "none");
|
|
800
|
+
put("borderRadius", style.borderTopLeftRadius);
|
|
801
|
+
put("fontSize", style.fontSize);
|
|
802
|
+
put("fontWeight", style.fontWeight);
|
|
803
|
+
put("lineHeight", style.lineHeight, (value) => value === "normal");
|
|
804
|
+
put("textAlign", style.textAlign, (value) => value === "start");
|
|
805
|
+
put("dir", style.direction, (value) => value === "ltr");
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function camelize(name: string): string {
|
|
810
|
+
return name.replace(/-([a-z])/g, (_, ch: string) => ch.toUpperCase());
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function collectStyleDiagnostics(author: AuthorNode, root: RootRef): NativeError[] {
|
|
814
|
+
const diagnostics = (author.styleIssues ?? []).map(({ property, value, layout }) =>
|
|
815
|
+
nativeError(layout ? "NATIVE_ROOT_UNSUPPORTED_LAYOUT" : "NATIVE_ROOT_UNSUPPORTED_STYLE",
|
|
816
|
+
`${author.type}${author.authorId ? `#${author.authorId}` : ""}: ${property}: ${value} is not supported by native rendering`,
|
|
817
|
+
{ root, scope: author.type === "LxNativeRoot" ? "root" : "node", recoverable: true })
|
|
818
|
+
);
|
|
819
|
+
for (const child of flattenAuthorChildren(author.children)) {
|
|
820
|
+
if (child.kind === "node") diagnostics.push(...collectStyleDiagnostics(child.node, root));
|
|
821
|
+
}
|
|
822
|
+
return diagnostics;
|
|
823
|
+
}
|