@qontinui/ui-bridge-native 0.6.12 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -16
- package/dist/control/action-executor.d.ts +42 -2
- package/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +723 -4
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +723 -5
- package/dist/control/index.mjs.map +1 -1
- package/dist/control/types.d.ts +82 -1
- package/dist/control/types.d.ts.map +1 -1
- package/dist/core/abortable.d.ts +151 -0
- package/dist/core/abortable.d.ts.map +1 -0
- package/dist/core/action-effect.d.ts +74 -0
- package/dist/core/action-effect.d.ts.map +1 -0
- package/dist/core/element-actions.d.ts +38 -0
- package/dist/core/element-actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +756 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +738 -16
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/param-schema.d.ts +282 -0
- package/dist/core/param-schema.d.ts.map +1 -0
- package/dist/core/registry.d.ts +272 -2
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +231 -32
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1502 -117
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1484 -118
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +1 -23
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1319 -112
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1319 -112
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIBridge.d.ts +19 -22
- package/dist/react/useUIBridge.d.ts.map +1 -1
- package/dist/react/useUIComponent.d.ts +113 -8
- package/dist/react/useUIComponent.d.ts.map +1 -1
- package/dist/react/useUIElement.d.ts +34 -8
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +15 -2
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +345 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +345 -44
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/observability.d.ts +40 -0
- package/dist/server/observability.d.ts.map +1 -1
- package/dist/server/types.d.ts +57 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/transport/CloudRelayClient.d.ts.map +1 -1
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/dist/transport/relay-logging.d.ts +50 -0
- package/dist/transport/relay-logging.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/core/index.js
CHANGED
|
@@ -1,5 +1,467 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/core/abortable.ts
|
|
4
|
+
var MAX_ACTION_TIMEOUT_MS = 864e5;
|
|
5
|
+
function normalizeActionTimeoutMs(raw) {
|
|
6
|
+
if (raw === void 0 || raw === null) {
|
|
7
|
+
return { ok: true, timeoutMs: void 0, clamped: false };
|
|
8
|
+
}
|
|
9
|
+
if (typeof raw !== "number") {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
reason: `timeoutMs must be a number of milliseconds, received ${typeof raw === "string" ? JSON.stringify(raw) : typeof raw}`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (!Number.isFinite(raw)) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `timeoutMs must be a finite number of milliseconds, received ${String(raw)}`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (raw < 0) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
reason: `timeoutMs must not be negative, received ${String(raw)}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (raw > MAX_ACTION_TIMEOUT_MS) {
|
|
28
|
+
return { ok: true, timeoutMs: MAX_ACTION_TIMEOUT_MS, clamped: true };
|
|
29
|
+
}
|
|
30
|
+
return { ok: true, timeoutMs: Math.floor(raw), clamped: false };
|
|
31
|
+
}
|
|
32
|
+
function inertAbortSignal() {
|
|
33
|
+
return new AbortController().signal;
|
|
34
|
+
}
|
|
35
|
+
async function runAbortable(invoke, options = {}) {
|
|
36
|
+
const callerSignal = options.signal;
|
|
37
|
+
const timeoutMs = options.timeoutMs;
|
|
38
|
+
if (callerSignal?.aborted) {
|
|
39
|
+
return { aborted: true, reason: "signal" };
|
|
40
|
+
}
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
let reason = "signal";
|
|
43
|
+
let timer;
|
|
44
|
+
const onCallerAbort = () => {
|
|
45
|
+
reason = "signal";
|
|
46
|
+
controller.abort();
|
|
47
|
+
};
|
|
48
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
49
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0) {
|
|
50
|
+
timer = setTimeout(
|
|
51
|
+
() => {
|
|
52
|
+
reason = "timeout";
|
|
53
|
+
controller.abort();
|
|
54
|
+
},
|
|
55
|
+
Math.min(timeoutMs, MAX_ACTION_TIMEOUT_MS)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const abandoned = new Promise((resolve) => {
|
|
60
|
+
controller.signal.addEventListener("abort", () => resolve({ aborted: true, reason }), {
|
|
61
|
+
once: true
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const completed = (async () => ({
|
|
65
|
+
aborted: false,
|
|
66
|
+
result: await invoke(controller.signal)
|
|
67
|
+
}))();
|
|
68
|
+
return await Promise.race([completed, abandoned]);
|
|
69
|
+
} finally {
|
|
70
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
71
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/core/param-schema.ts
|
|
76
|
+
var DEFAULT_PARAM_VALIDATION_MODE = "warn";
|
|
77
|
+
var currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
78
|
+
function setDefaultParamValidationMode(mode) {
|
|
79
|
+
currentDefaultMode = mode;
|
|
80
|
+
}
|
|
81
|
+
function getDefaultParamValidationMode() {
|
|
82
|
+
return currentDefaultMode;
|
|
83
|
+
}
|
|
84
|
+
function resetDefaultParamValidationMode() {
|
|
85
|
+
currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
86
|
+
}
|
|
87
|
+
var PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
88
|
+
"string",
|
|
89
|
+
"number",
|
|
90
|
+
"integer",
|
|
91
|
+
"boolean",
|
|
92
|
+
"object",
|
|
93
|
+
"array",
|
|
94
|
+
"null"
|
|
95
|
+
]);
|
|
96
|
+
var OBJECT_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
97
|
+
"type",
|
|
98
|
+
"properties",
|
|
99
|
+
"required",
|
|
100
|
+
"additionalProperties",
|
|
101
|
+
"patternProperties",
|
|
102
|
+
"propertyNames",
|
|
103
|
+
"minProperties",
|
|
104
|
+
"maxProperties",
|
|
105
|
+
"dependentRequired",
|
|
106
|
+
"dependentSchemas",
|
|
107
|
+
"unevaluatedProperties",
|
|
108
|
+
"allOf",
|
|
109
|
+
"anyOf",
|
|
110
|
+
"oneOf",
|
|
111
|
+
"not",
|
|
112
|
+
"if",
|
|
113
|
+
"then",
|
|
114
|
+
"else",
|
|
115
|
+
"enum",
|
|
116
|
+
"const",
|
|
117
|
+
"default",
|
|
118
|
+
"examples",
|
|
119
|
+
"title",
|
|
120
|
+
"description",
|
|
121
|
+
"deprecated",
|
|
122
|
+
"readOnly",
|
|
123
|
+
"writeOnly",
|
|
124
|
+
"$schema",
|
|
125
|
+
"$id",
|
|
126
|
+
"$ref",
|
|
127
|
+
"$comment",
|
|
128
|
+
"$defs",
|
|
129
|
+
"definitions"
|
|
130
|
+
]);
|
|
131
|
+
var MAX_SCHEMA_DEPTH = 32;
|
|
132
|
+
var MAX_PATTERN_LENGTH = 200;
|
|
133
|
+
function hasNestedQuantifiers(pattern) {
|
|
134
|
+
return /(\((?:[^()]*[+*}])[^()]*\))[+*?]|\(\?:[^()]*[+*}][^()]*\)[+*?]/.test(pattern);
|
|
135
|
+
}
|
|
136
|
+
function isPlainObject(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
138
|
+
}
|
|
139
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
140
|
+
function hasOwn(value, key) {
|
|
141
|
+
return hasOwnProperty.call(value, key);
|
|
142
|
+
}
|
|
143
|
+
function render(value) {
|
|
144
|
+
if (value === null) return "null";
|
|
145
|
+
if (value === void 0) return "undefined";
|
|
146
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
147
|
+
const t = typeof value;
|
|
148
|
+
if (t === "string") return JSON.stringify(value);
|
|
149
|
+
if (t === "number" || t === "boolean") return String(value);
|
|
150
|
+
if (t === "object") return "object";
|
|
151
|
+
return t;
|
|
152
|
+
}
|
|
153
|
+
var MAX_RENDERED_JSON_LENGTH = 120;
|
|
154
|
+
function renderJson(value) {
|
|
155
|
+
if (value === void 0) return "undefined";
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = JSON.stringify(value) ?? String(value);
|
|
159
|
+
} catch {
|
|
160
|
+
return render(value);
|
|
161
|
+
}
|
|
162
|
+
return text.length > MAX_RENDERED_JSON_LENGTH ? `${text.slice(0, MAX_RENDERED_JSON_LENGTH)}\u2026` : text;
|
|
163
|
+
}
|
|
164
|
+
function jsonTypeOf(value) {
|
|
165
|
+
if (value === null) return "null";
|
|
166
|
+
if (Array.isArray(value)) return "array";
|
|
167
|
+
const t = typeof value;
|
|
168
|
+
if (t === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
169
|
+
if (t === "string" || t === "boolean" || t === "object") return t;
|
|
170
|
+
return t;
|
|
171
|
+
}
|
|
172
|
+
function matchesType(value, typeName) {
|
|
173
|
+
switch (typeName) {
|
|
174
|
+
case "string":
|
|
175
|
+
return typeof value === "string";
|
|
176
|
+
case "number":
|
|
177
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
178
|
+
case "integer":
|
|
179
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
180
|
+
case "boolean":
|
|
181
|
+
return typeof value === "boolean";
|
|
182
|
+
case "null":
|
|
183
|
+
return value === null;
|
|
184
|
+
case "array":
|
|
185
|
+
return Array.isArray(value);
|
|
186
|
+
case "object":
|
|
187
|
+
return isPlainObject(value);
|
|
188
|
+
default:
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function jsonEquals(a, b, depth = 0) {
|
|
193
|
+
if (a === b) return true;
|
|
194
|
+
if (depth > MAX_SCHEMA_DEPTH) return false;
|
|
195
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
196
|
+
if (typeof a !== typeof b) return false;
|
|
197
|
+
if (typeof a !== "object") return false;
|
|
198
|
+
const aIsArray = Array.isArray(a);
|
|
199
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
200
|
+
if (aIsArray) {
|
|
201
|
+
const bArr = b;
|
|
202
|
+
const aArr = a;
|
|
203
|
+
if (aArr.length !== bArr.length) return false;
|
|
204
|
+
return aArr.every((item, i) => jsonEquals(item, bArr[i], depth + 1));
|
|
205
|
+
}
|
|
206
|
+
const aObj = a;
|
|
207
|
+
const bObj = b;
|
|
208
|
+
const aKeys = Object.keys(aObj);
|
|
209
|
+
const bKeys = Object.keys(bObj);
|
|
210
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
211
|
+
return aKeys.every((key) => hasOwn(bObj, key) && jsonEquals(aObj[key], bObj[key], depth + 1));
|
|
212
|
+
}
|
|
213
|
+
function joinPath(base, key) {
|
|
214
|
+
return base === "" ? key : `${base}.${key}`;
|
|
215
|
+
}
|
|
216
|
+
function readTypes(schema) {
|
|
217
|
+
const raw = schema["type"];
|
|
218
|
+
if (typeof raw === "string") return [raw];
|
|
219
|
+
if (Array.isArray(raw)) {
|
|
220
|
+
const names = raw.filter((t) => typeof t === "string");
|
|
221
|
+
return names.length > 0 ? names : null;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function validateValue(value, schema, path, issues, depth) {
|
|
226
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
227
|
+
if (typeof schema === "string") {
|
|
228
|
+
if (PRIMITIVE_TYPE_NAMES.has(schema) && !matchesType(value, schema)) {
|
|
229
|
+
issues.push({
|
|
230
|
+
path,
|
|
231
|
+
keyword: "type",
|
|
232
|
+
expected: schema,
|
|
233
|
+
received: render(value),
|
|
234
|
+
message: `Parameter "${path}" must be of type ${schema}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!isPlainObject(schema)) return;
|
|
240
|
+
const types = readTypes(schema);
|
|
241
|
+
if (types !== null && !types.some((t) => matchesType(value, t))) {
|
|
242
|
+
const expected = types.join(" | ");
|
|
243
|
+
issues.push({
|
|
244
|
+
path,
|
|
245
|
+
keyword: "type",
|
|
246
|
+
expected,
|
|
247
|
+
received: render(value),
|
|
248
|
+
message: `Parameter "${path}" must be of type ${expected}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (hasOwn(schema, "const") && !jsonEquals(value, schema["const"])) {
|
|
253
|
+
const expected = renderJson(schema["const"]);
|
|
254
|
+
const received = renderJson(value);
|
|
255
|
+
issues.push({
|
|
256
|
+
path,
|
|
257
|
+
keyword: "const",
|
|
258
|
+
expected,
|
|
259
|
+
received,
|
|
260
|
+
message: `Parameter "${path}" must equal ${expected}, received ${received}.`
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const enumValues = schema["enum"];
|
|
265
|
+
if (Array.isArray(enumValues) && !enumValues.some((allowed) => jsonEquals(value, allowed))) {
|
|
266
|
+
const expected = enumValues.map((v) => renderJson(v)).join(", ");
|
|
267
|
+
const received = renderJson(value);
|
|
268
|
+
issues.push({
|
|
269
|
+
path,
|
|
270
|
+
keyword: "enum",
|
|
271
|
+
expected,
|
|
272
|
+
received,
|
|
273
|
+
message: `Parameter "${path}" must be one of [${expected}], received ${received}.`
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (typeof value === "number") {
|
|
278
|
+
const min = schema["minimum"];
|
|
279
|
+
if (typeof min === "number" && value < min) {
|
|
280
|
+
issues.push({
|
|
281
|
+
path,
|
|
282
|
+
keyword: "minimum",
|
|
283
|
+
expected: `>= ${min}`,
|
|
284
|
+
received: render(value),
|
|
285
|
+
message: `Parameter "${path}" must be >= ${min}, received ${value}.`
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const max = schema["maximum"];
|
|
289
|
+
if (typeof max === "number" && value > max) {
|
|
290
|
+
issues.push({
|
|
291
|
+
path,
|
|
292
|
+
keyword: "maximum",
|
|
293
|
+
expected: `<= ${max}`,
|
|
294
|
+
received: render(value),
|
|
295
|
+
message: `Parameter "${path}" must be <= ${max}, received ${value}.`
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (typeof value === "string") {
|
|
300
|
+
const minLength = schema["minLength"];
|
|
301
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
302
|
+
issues.push({
|
|
303
|
+
path,
|
|
304
|
+
keyword: "minLength",
|
|
305
|
+
expected: `length >= ${minLength}`,
|
|
306
|
+
received: `length ${value.length}`,
|
|
307
|
+
message: `Parameter "${path}" must be at least ${minLength} characters, received ${value.length}.`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const maxLength = schema["maxLength"];
|
|
311
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
312
|
+
issues.push({
|
|
313
|
+
path,
|
|
314
|
+
keyword: "maxLength",
|
|
315
|
+
expected: `length <= ${maxLength}`,
|
|
316
|
+
received: `length ${value.length}`,
|
|
317
|
+
message: `Parameter "${path}" must be at most ${maxLength} characters, received ${value.length}.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const pattern = schema["pattern"];
|
|
321
|
+
if (typeof pattern === "string" && pattern.length <= MAX_PATTERN_LENGTH && !hasNestedQuantifiers(pattern)) {
|
|
322
|
+
let re;
|
|
323
|
+
try {
|
|
324
|
+
re = new RegExp(pattern);
|
|
325
|
+
} catch {
|
|
326
|
+
re = null;
|
|
327
|
+
}
|
|
328
|
+
if (re !== null && !re.test(value)) {
|
|
329
|
+
issues.push({
|
|
330
|
+
path,
|
|
331
|
+
keyword: "pattern",
|
|
332
|
+
expected: `match /${pattern}/`,
|
|
333
|
+
received: render(value),
|
|
334
|
+
message: `Parameter "${path}" must match /${pattern}/, received ${render(value)}.`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(value) && hasOwn(schema, "items")) {
|
|
340
|
+
const items = schema["items"];
|
|
341
|
+
value.forEach((element, index) => {
|
|
342
|
+
validateValue(element, items, `${path}[${index}]`, issues, depth + 1);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (isPlainObject(value) && (isPlainObject(schema["properties"]) || hasOwn(schema, "required") || schema["additionalProperties"] === false)) {
|
|
346
|
+
validateObject(value, schema, path, issues, depth);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function validateObject(value, schema, basePath, issues, depth) {
|
|
350
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
351
|
+
const properties = isPlainObject(schema["properties"]) ? schema["properties"] : {};
|
|
352
|
+
const requiredRaw = schema["required"];
|
|
353
|
+
const required = Array.isArray(requiredRaw) ? requiredRaw.filter((k) => typeof k === "string") : [];
|
|
354
|
+
for (const key of required) {
|
|
355
|
+
if (!hasOwn(value, key) || value[key] === void 0) {
|
|
356
|
+
const at = joinPath(basePath, key);
|
|
357
|
+
issues.push({
|
|
358
|
+
path: at,
|
|
359
|
+
keyword: "required",
|
|
360
|
+
expected: "present",
|
|
361
|
+
message: `Required parameter "${at}" is missing.`
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (schema["additionalProperties"] === false) {
|
|
366
|
+
const declared = new Set(Object.keys(properties));
|
|
367
|
+
const expected = declared.size === 0 ? "no parameters" : `one of [${[...declared].join(", ")}]`;
|
|
368
|
+
for (const key of Object.keys(value)) {
|
|
369
|
+
if (!declared.has(key)) {
|
|
370
|
+
const at = joinPath(basePath, key);
|
|
371
|
+
issues.push({
|
|
372
|
+
path: at,
|
|
373
|
+
keyword: "additionalProperties",
|
|
374
|
+
expected,
|
|
375
|
+
received: render(value[key]),
|
|
376
|
+
message: `Parameter "${at}" is not declared by the schema, which sets additionalProperties: false.`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
382
|
+
if (!hasOwn(value, key) || value[key] === void 0) continue;
|
|
383
|
+
validateValue(value[key], propSchema, joinPath(basePath, key), issues, depth + 1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function isObjectSchemaForm(schema) {
|
|
387
|
+
if (isPlainObject(schema["properties"])) return true;
|
|
388
|
+
if (schema["type"] !== "object") return false;
|
|
389
|
+
return Object.keys(schema).every((key) => OBJECT_SCHEMA_KEYWORDS.has(key));
|
|
390
|
+
}
|
|
391
|
+
function normalizeMapForm(schema) {
|
|
392
|
+
if (isObjectSchemaForm(schema)) return null;
|
|
393
|
+
const properties = {};
|
|
394
|
+
for (const [key, raw] of Object.entries(schema)) {
|
|
395
|
+
if (raw === null || raw === void 0) continue;
|
|
396
|
+
if (typeof raw === "string") {
|
|
397
|
+
properties[key] = PRIMITIVE_TYPE_NAMES.has(raw) ? { type: raw } : {};
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (isPlainObject(raw)) {
|
|
401
|
+
const { optional: _optional, ...rest } = raw;
|
|
402
|
+
properties[key] = rest;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { type: "object", properties };
|
|
407
|
+
}
|
|
408
|
+
function validateActionParams(schema, params) {
|
|
409
|
+
if (!isPlainObject(schema)) return { valid: true, issues: [] };
|
|
410
|
+
const normalized = normalizeMapForm(schema) ?? schema;
|
|
411
|
+
const hasProperties = isPlainObject(normalized["properties"]);
|
|
412
|
+
const hasRequired = Array.isArray(normalized["required"]);
|
|
413
|
+
const forbidsExtras = normalized["additionalProperties"] === false;
|
|
414
|
+
if (!hasProperties && !hasRequired && !forbidsExtras) {
|
|
415
|
+
return { valid: true, issues: [] };
|
|
416
|
+
}
|
|
417
|
+
const issues = [];
|
|
418
|
+
if (params === void 0 || params === null) {
|
|
419
|
+
validateObject({}, normalized, "", issues, 0);
|
|
420
|
+
return { valid: issues.length === 0, issues };
|
|
421
|
+
}
|
|
422
|
+
if (!isPlainObject(params)) {
|
|
423
|
+
issues.push({
|
|
424
|
+
path: "",
|
|
425
|
+
keyword: "type",
|
|
426
|
+
expected: "object",
|
|
427
|
+
received: render(params),
|
|
428
|
+
message: `Action params must be an object, received ${jsonTypeOf(params)} (${render(params)}).`
|
|
429
|
+
});
|
|
430
|
+
return { valid: false, issues };
|
|
431
|
+
}
|
|
432
|
+
validateObject(params, normalized, "", issues, 0);
|
|
433
|
+
return { valid: issues.length === 0, issues };
|
|
434
|
+
}
|
|
435
|
+
function formatParamValidationFailure(componentId, actionId, issues) {
|
|
436
|
+
const detail = issues.map((i) => i.message).join(" ");
|
|
437
|
+
return `Action "${actionId}" on component "${componentId}" was rejected: params do not match the action's declared paramSchema. ${detail}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/core/action-effect.ts
|
|
441
|
+
var NATIVE_STANDARD_ACTION_EFFECTS = {
|
|
442
|
+
// --- Reads ---------------------------------------------------------------
|
|
443
|
+
focus: "read",
|
|
444
|
+
blur: "read",
|
|
445
|
+
scroll: "read",
|
|
446
|
+
scrollIntoView: "read",
|
|
447
|
+
longPress: "read",
|
|
448
|
+
// --- Writes --------------------------------------------------------------
|
|
449
|
+
press: "write",
|
|
450
|
+
click: "write",
|
|
451
|
+
doubleTap: "write",
|
|
452
|
+
type: "write",
|
|
453
|
+
setValue: "write",
|
|
454
|
+
clear: "write",
|
|
455
|
+
swipe: "write",
|
|
456
|
+
toggle: "write"
|
|
457
|
+
};
|
|
458
|
+
function nativeStandardActionEffect(actionId) {
|
|
459
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_STANDARD_ACTION_EFFECTS, actionId) ? NATIVE_STANDARD_ACTION_EFFECTS[actionId] : void 0;
|
|
460
|
+
}
|
|
461
|
+
function resolveActionEffect(action) {
|
|
462
|
+
return action.effect ?? nativeStandardActionEffect(action.id);
|
|
463
|
+
}
|
|
464
|
+
|
|
3
465
|
// src/core/vision-fields.ts
|
|
4
466
|
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
5
467
|
"onPress",
|
|
@@ -173,7 +635,42 @@ function projectVisionFields(args) {
|
|
|
173
635
|
return fields;
|
|
174
636
|
}
|
|
175
637
|
|
|
638
|
+
// src/core/element-actions.ts
|
|
639
|
+
function serializeElementCustomActions(customActions) {
|
|
640
|
+
if (!customActions) return void 0;
|
|
641
|
+
const out = [];
|
|
642
|
+
for (const [id, action] of Object.entries(customActions)) {
|
|
643
|
+
if (!action) continue;
|
|
644
|
+
const entry = { id };
|
|
645
|
+
if (action.label !== void 0) entry.label = action.label;
|
|
646
|
+
if (action.description !== void 0) entry.description = action.description;
|
|
647
|
+
if (action.effect !== void 0) entry.effect = action.effect;
|
|
648
|
+
out.push(entry);
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
|
|
176
653
|
// src/core/registry.ts
|
|
654
|
+
function toRegisteredComponentActions(actions) {
|
|
655
|
+
return actions.map((a) => ({
|
|
656
|
+
id: a.id,
|
|
657
|
+
label: a.label,
|
|
658
|
+
description: a.description,
|
|
659
|
+
paramSchema: a.paramSchema,
|
|
660
|
+
effect: a.effect,
|
|
661
|
+
handler: a.handler
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
function publishedActionSignature(actions) {
|
|
665
|
+
return JSON.stringify(
|
|
666
|
+
actions.map((a) => [a.id, a.label, a.description, a.paramSchema, a.effect])
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
function sameElementIds(a, b) {
|
|
670
|
+
if (a === b) return true;
|
|
671
|
+
if (a === void 0 || b === void 0) return false;
|
|
672
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
673
|
+
}
|
|
177
674
|
function extractHandlerNames(props) {
|
|
178
675
|
if (!props) return [];
|
|
179
676
|
return Object.keys(props).filter((k) => typeof props[k] === "function");
|
|
@@ -182,6 +679,22 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
182
679
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
183
680
|
return registrationRoute === currentRoute;
|
|
184
681
|
}
|
|
682
|
+
function deriveActiveTabFromSegments(segments) {
|
|
683
|
+
if (!segments || segments.length === 0) return void 0;
|
|
684
|
+
let innermostGroup = -1;
|
|
685
|
+
for (let i = 0; i < segments.length; i++) {
|
|
686
|
+
const segment = segments[i];
|
|
687
|
+
if (typeof segment === "string" && segment.startsWith("(") && segment.endsWith(")")) {
|
|
688
|
+
innermostGroup = i;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (innermostGroup === -1) return void 0;
|
|
692
|
+
const next = segments[innermostGroup + 1];
|
|
693
|
+
if (next === void 0) return "index";
|
|
694
|
+
if (typeof next !== "string" || next.length === 0) return void 0;
|
|
695
|
+
if (next.startsWith("[") && next.endsWith("]")) return void 0;
|
|
696
|
+
return next;
|
|
697
|
+
}
|
|
185
698
|
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
186
699
|
if (visibility !== "visible") return void 0;
|
|
187
700
|
const layout = state.layout;
|
|
@@ -201,8 +714,58 @@ function projectBbox(state, visibility, pixelRatio = 1) {
|
|
|
201
714
|
h: toPx(layout.height)
|
|
202
715
|
};
|
|
203
716
|
}
|
|
204
|
-
function
|
|
205
|
-
const
|
|
717
|
+
function pageRectOf(state) {
|
|
718
|
+
const layout = state.layout;
|
|
719
|
+
if (!layout) return null;
|
|
720
|
+
if (layout.pageOriginUnmeasured === true) return null;
|
|
721
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
722
|
+
if (!hasPage) return null;
|
|
723
|
+
const left = layout.pageX;
|
|
724
|
+
const top = layout.pageY;
|
|
725
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return { left, top, right: left + layout.width, bottom: top + layout.height };
|
|
729
|
+
}
|
|
730
|
+
function spansOverlap(aMin, aMax, bMin, bMax) {
|
|
731
|
+
if (aMin === aMax) return aMin >= bMin && aMin <= bMax;
|
|
732
|
+
return aMin < bMax && aMax > bMin;
|
|
733
|
+
}
|
|
734
|
+
function isEmptyRect(rect) {
|
|
735
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
736
|
+
}
|
|
737
|
+
function intersectRects(a, b) {
|
|
738
|
+
if (!a) return b;
|
|
739
|
+
if (!b) return a;
|
|
740
|
+
return {
|
|
741
|
+
left: Math.max(a.left, b.left),
|
|
742
|
+
top: Math.max(a.top, b.top),
|
|
743
|
+
right: Math.min(a.right, b.right),
|
|
744
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function computeVisibility(state, clip) {
|
|
748
|
+
if (!state.visible) {
|
|
749
|
+
return { visibility: "hidden", visibilityReason: "hidden" };
|
|
750
|
+
}
|
|
751
|
+
if (state.layout === null) {
|
|
752
|
+
return { visibility: "likely-visible", visibilityReason: "no-layout" };
|
|
753
|
+
}
|
|
754
|
+
if (!clip) {
|
|
755
|
+
return { visibility: "visible" };
|
|
756
|
+
}
|
|
757
|
+
const rect = pageRectOf(state);
|
|
758
|
+
if (!rect) {
|
|
759
|
+
return { visibility: "visible" };
|
|
760
|
+
}
|
|
761
|
+
if (isEmptyRect(clip)) {
|
|
762
|
+
return { visibility: "hidden", visibilityReason: "off-screen" };
|
|
763
|
+
}
|
|
764
|
+
const onScreen = spansOverlap(rect.left, rect.right, clip.left, clip.right) && spansOverlap(rect.top, rect.bottom, clip.top, clip.bottom);
|
|
765
|
+
return onScreen ? { visibility: "visible" } : { visibility: "hidden", visibilityReason: "off-screen" };
|
|
766
|
+
}
|
|
767
|
+
function inferActions(type, scrollAncestorId) {
|
|
768
|
+
const baseActions = scrollAncestorId ? ["focus", "blur", "scrollIntoView"] : ["focus", "blur"];
|
|
206
769
|
switch (type) {
|
|
207
770
|
case "button":
|
|
208
771
|
case "touchable":
|
|
@@ -274,6 +837,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
274
837
|
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
275
838
|
*/
|
|
276
839
|
this.pixelRatio = 1;
|
|
840
|
+
/**
|
|
841
|
+
* Injected getter for the device window size in logical dp, used to clip
|
|
842
|
+
* reported visibility. Injected rather than read here for exactly the reason
|
|
843
|
+
* `pixelRatio` is: a static `import { Dimensions } from 'react-native'` in
|
|
844
|
+
* this non-React core module breaks the vitest suite and risks the
|
|
845
|
+
* Metro/Hermes `unknownModuleError`. It is a PROVIDER, not a value, so a
|
|
846
|
+
* rotation is picked up on the next snapshot with nothing to keep in sync.
|
|
847
|
+
*
|
|
848
|
+
* `null` means the window bounds are UNKNOWN, and {@link computeVisibility}
|
|
849
|
+
* treats unknown bounds as "cannot demote" — never as an empty viewport.
|
|
850
|
+
*/
|
|
851
|
+
this.viewportProvider = null;
|
|
277
852
|
/**
|
|
278
853
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
279
854
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -311,6 +886,30 @@ var NativeUIBridgeRegistry = class {
|
|
|
311
886
|
getRouteProvider() {
|
|
312
887
|
return this.routeProvider;
|
|
313
888
|
}
|
|
889
|
+
/**
|
|
890
|
+
* Resolve the snapshot's `activeTab`: an explicit `getActiveTab()` on the
|
|
891
|
+
* route provider wins, otherwise derive it from the Expo Router segments the
|
|
892
|
+
* provider already exposes.
|
|
893
|
+
*
|
|
894
|
+
* A throwing or empty-string provider degrades to the derivation rather than
|
|
895
|
+
* to `undefined` — the host's opt-in extra should never be able to make the
|
|
896
|
+
* snapshot WORSE than it was without it. A blank derivation stays blank.
|
|
897
|
+
*/
|
|
898
|
+
resolveActiveTab(segments) {
|
|
899
|
+
const explicit = (() => {
|
|
900
|
+
if (!this.routeProvider?.getActiveTab) return void 0;
|
|
901
|
+
try {
|
|
902
|
+
const value = this.routeProvider.getActiveTab();
|
|
903
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if (this.config.verbose) {
|
|
906
|
+
console.warn(`[ui-bridge-native] routeProvider getActiveTab threw:`, error);
|
|
907
|
+
}
|
|
908
|
+
return void 0;
|
|
909
|
+
}
|
|
910
|
+
})();
|
|
911
|
+
return explicit ?? deriveActiveTabFromSegments(segments);
|
|
912
|
+
}
|
|
314
913
|
/**
|
|
315
914
|
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
316
915
|
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
@@ -326,6 +925,56 @@ var NativeUIBridgeRegistry = class {
|
|
|
326
925
|
getPixelRatio() {
|
|
327
926
|
return this.pixelRatio;
|
|
328
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* Set the getter for the device window size (logical dp) that reported
|
|
930
|
+
* visibility is clipped against. The React provider passes
|
|
931
|
+
* `() => Dimensions.get('window')` here at construction.
|
|
932
|
+
*
|
|
933
|
+
* Pass `null` to detach. While detached the window bounds are UNKNOWN and no
|
|
934
|
+
* element is ever demoted for being outside them.
|
|
935
|
+
*/
|
|
936
|
+
setViewportProvider(provider) {
|
|
937
|
+
this.viewportProvider = provider ?? null;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* The window as a page-space rect, or `null` when it is unknown or the
|
|
941
|
+
* provider returned something unusable (it throws in some RN teardown
|
|
942
|
+
* paths, and a zero-size window would silently hide the entire screen).
|
|
943
|
+
*/
|
|
944
|
+
getViewportRect() {
|
|
945
|
+
if (!this.viewportProvider) return null;
|
|
946
|
+
let size;
|
|
947
|
+
try {
|
|
948
|
+
size = this.viewportProvider();
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (this.config.verbose) {
|
|
951
|
+
console.warn(`[ui-bridge-native] viewportProvider threw:`, error);
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
const { width, height } = size ?? {};
|
|
956
|
+
if (typeof width !== "number" || typeof height !== "number" || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
return { left: 0, top: 0, right: width, bottom: height };
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The region `element` must reach to count as on screen: the window,
|
|
963
|
+
* intersected with the measured frame of its declared `scrollAncestorId`.
|
|
964
|
+
*
|
|
965
|
+
* Returns `null` when neither bound is known — see {@link computeVisibility}
|
|
966
|
+
* for why that means "do not demote" rather than "nothing is visible".
|
|
967
|
+
* An ancestor that is not registered, or registered but not yet measured,
|
|
968
|
+
* contributes nothing rather than hiding its subtree.
|
|
969
|
+
*/
|
|
970
|
+
getClipRectFor(element, viewport) {
|
|
971
|
+
const bounds = viewport === void 0 ? this.getViewportRect() : viewport;
|
|
972
|
+
const ancestorId = element.scrollAncestorId;
|
|
973
|
+
if (!ancestorId) return bounds;
|
|
974
|
+
const ancestor = this.elements.get(ancestorId);
|
|
975
|
+
if (!ancestor || ancestorId === element.id) return bounds;
|
|
976
|
+
return intersectRects(bounds, pageRectOf(ancestor.getState()));
|
|
977
|
+
}
|
|
329
978
|
// ============================================================================
|
|
330
979
|
// Snapshot Enricher Slots
|
|
331
980
|
// ============================================================================
|
|
@@ -369,16 +1018,17 @@ var NativeUIBridgeRegistry = class {
|
|
|
369
1018
|
const {
|
|
370
1019
|
type = "custom",
|
|
371
1020
|
label,
|
|
372
|
-
actions = inferActions(type),
|
|
373
1021
|
customActions,
|
|
374
1022
|
props,
|
|
375
1023
|
treePath = id,
|
|
376
1024
|
testId,
|
|
377
1025
|
accessibilityLabel,
|
|
378
1026
|
registrationRoute,
|
|
1027
|
+
scrollAncestorId,
|
|
379
1028
|
flatStyle,
|
|
380
1029
|
stateStyles
|
|
381
1030
|
} = options;
|
|
1031
|
+
const actions = options.actions ?? inferActions(type, scrollAncestorId);
|
|
382
1032
|
const getState = () => {
|
|
383
1033
|
const element = ref.current;
|
|
384
1034
|
if (!element) {
|
|
@@ -421,6 +1071,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
421
1071
|
registeredAt: Date.now(),
|
|
422
1072
|
mounted: true,
|
|
423
1073
|
registrationRoute: registrationRoute ?? null,
|
|
1074
|
+
scrollAncestorId,
|
|
424
1075
|
flatStyle,
|
|
425
1076
|
stateStyles
|
|
426
1077
|
};
|
|
@@ -797,12 +1448,8 @@ var NativeUIBridgeRegistry = class {
|
|
|
797
1448
|
id,
|
|
798
1449
|
name,
|
|
799
1450
|
description,
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
label: a.label,
|
|
803
|
-
description: a.description,
|
|
804
|
-
handler: a.handler
|
|
805
|
-
})),
|
|
1451
|
+
// Closed field list, applied once — see `toRegisteredComponentActions`.
|
|
1452
|
+
actions: toRegisteredComponentActions(actions),
|
|
806
1453
|
elementIds,
|
|
807
1454
|
registeredAt: Date.now(),
|
|
808
1455
|
mounted: true
|
|
@@ -814,6 +1461,55 @@ var NativeUIBridgeRegistry = class {
|
|
|
814
1461
|
}
|
|
815
1462
|
return registered;
|
|
816
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Update a registered component's declaration in place — name, description,
|
|
1466
|
+
* actions, elementIds.
|
|
1467
|
+
*
|
|
1468
|
+
* Sister to {@link updateElementMeta}, and it exists for the same reason one
|
|
1469
|
+
* level up. `registerComponent` publishes the declaration exactly once, and
|
|
1470
|
+
* `useUIComponent` registers on mount and never again, so an action whose
|
|
1471
|
+
* `label` carries a count, an action that only exists while a row is
|
|
1472
|
+
* selected, a `paramSchema` that widens once options load, or an `elementIds`
|
|
1473
|
+
* list that grows after mount, all published their mount-time shape to
|
|
1474
|
+
* `/control/components` forever. PR #176 closed that hole for an ELEMENT's
|
|
1475
|
+
* label; this closes it for a COMPONENT's declaration.
|
|
1476
|
+
*
|
|
1477
|
+
* **Idempotent by VALUE over the PUBLISHED shape.** `handler` identity is
|
|
1478
|
+
* excluded from the comparison — call sites pass inline closures that are
|
|
1479
|
+
* freshly allocated every render, so including it would report a change on
|
|
1480
|
+
* every call and make the door useless for a render-path publisher. The
|
|
1481
|
+
* newest handlers are still STORED whenever `actions` is supplied, which is
|
|
1482
|
+
* the other half of the same defect: without it the registry keeps the
|
|
1483
|
+
* closure from the render that registered the component, so a handler reading
|
|
1484
|
+
* component state read the state as it was at mount.
|
|
1485
|
+
*
|
|
1486
|
+
* Returns `true` when the published shape changed and an event was emitted,
|
|
1487
|
+
* `false` otherwise — the same contract as `updateElementMeta`, so a caller
|
|
1488
|
+
* can gate a warning or a state write on a real change.
|
|
1489
|
+
*/
|
|
1490
|
+
updateComponentMeta(id, meta) {
|
|
1491
|
+
const component = this.components.get(id);
|
|
1492
|
+
if (!component) return false;
|
|
1493
|
+
const nextName = meta.name !== void 0 ? meta.name : component.name;
|
|
1494
|
+
const nextDescription = meta.description !== void 0 ? meta.description : component.description;
|
|
1495
|
+
const nextActions = meta.actions !== void 0 ? toRegisteredComponentActions(meta.actions) : component.actions;
|
|
1496
|
+
const nextElementIds = meta.elementIds !== void 0 ? meta.elementIds : component.elementIds;
|
|
1497
|
+
const changed = nextName !== component.name || nextDescription !== component.description || !sameElementIds(nextElementIds, component.elementIds) || publishedActionSignature(nextActions) !== publishedActionSignature(component.actions);
|
|
1498
|
+
if (!changed && meta.actions === void 0) return false;
|
|
1499
|
+
this.components.set(id, {
|
|
1500
|
+
...component,
|
|
1501
|
+
name: nextName,
|
|
1502
|
+
description: nextDescription,
|
|
1503
|
+
actions: nextActions,
|
|
1504
|
+
elementIds: nextElementIds
|
|
1505
|
+
});
|
|
1506
|
+
if (!changed) return false;
|
|
1507
|
+
this.emit("component:registered", { id, name: nextName });
|
|
1508
|
+
if (this.config.verbose) {
|
|
1509
|
+
console.log(`[ui-bridge-native] Updated component: ${id} (${nextName})`);
|
|
1510
|
+
}
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
817
1513
|
/**
|
|
818
1514
|
* Unregister a component
|
|
819
1515
|
*/
|
|
@@ -927,41 +1623,59 @@ var NativeUIBridgeRegistry = class {
|
|
|
927
1623
|
* server-level override (the bare-server / cloud-relay path) still get a
|
|
928
1624
|
* populated `currentRoute` field as long as `setRouteProvider` was called
|
|
929
1625
|
* on the registry.
|
|
1626
|
+
*
|
|
1627
|
+
* `activeTab` resolves through {@link resolveActiveTab} on BOTH branches. An
|
|
1628
|
+
* explicit `routeInfo.activeTab` wins outright; otherwise a registered
|
|
1629
|
+
* provider's `getActiveTab()` answers and the Expo Router derivation is the
|
|
1630
|
+
* fallback. Deriving straight from `routeInfo.segments` here instead — which
|
|
1631
|
+
* is what this branch did when `activeTab` was introduced — silently ignored
|
|
1632
|
+
* `RouteProvider.getActiveTab` on the ONLY path that serves
|
|
1633
|
+
* `control/snapshot` once `setRouteProvider` is wired (the server installs a
|
|
1634
|
+
* `getSnapshot` override that passes `routeInfo`), so the opt-in hook for an
|
|
1635
|
+
* app whose visible pane is decoupled from the router reached no consumer.
|
|
930
1636
|
*/
|
|
931
1637
|
createSnapshot(routeInfo, options) {
|
|
932
1638
|
const resolvedRoute = (() => {
|
|
933
1639
|
if (routeInfo !== void 0) {
|
|
1640
|
+
const explicit = typeof routeInfo.activeTab === "string" && routeInfo.activeTab.length > 0 ? routeInfo.activeTab : void 0;
|
|
934
1641
|
return {
|
|
935
1642
|
currentRoute: routeInfo.currentRoute ?? null,
|
|
936
|
-
segments: routeInfo.segments
|
|
1643
|
+
segments: routeInfo.segments,
|
|
1644
|
+
activeTab: explicit ?? this.resolveActiveTab(routeInfo.segments)
|
|
937
1645
|
};
|
|
938
1646
|
}
|
|
939
1647
|
if (this.routeProvider) {
|
|
940
1648
|
try {
|
|
1649
|
+
const segments = this.routeProvider.getSegments?.();
|
|
941
1650
|
return {
|
|
942
1651
|
currentRoute: this.routeProvider.getCurrentRoute() ?? null,
|
|
943
|
-
segments
|
|
1652
|
+
segments,
|
|
1653
|
+
activeTab: this.resolveActiveTab(segments)
|
|
944
1654
|
};
|
|
945
1655
|
} catch (error) {
|
|
946
1656
|
if (this.config.verbose) {
|
|
947
1657
|
console.warn(`[ui-bridge-native] routeProvider getCurrentRoute threw:`, error);
|
|
948
1658
|
}
|
|
949
|
-
return { currentRoute: null, segments: void 0 };
|
|
1659
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
950
1660
|
}
|
|
951
1661
|
}
|
|
952
|
-
return { currentRoute: null, segments: void 0 };
|
|
1662
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
953
1663
|
})();
|
|
954
1664
|
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
955
1665
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
956
1666
|
const currentRoute = resolvedRoute.currentRoute;
|
|
957
1667
|
elements = elements.filter((e) => matchesCurrentRoute(e.registrationRoute, currentRoute));
|
|
958
1668
|
}
|
|
1669
|
+
const viewport = this.getViewportRect();
|
|
959
1670
|
const snapshot = {
|
|
960
1671
|
timestamp: Date.now(),
|
|
961
1672
|
elements: elements.map((e) => {
|
|
962
1673
|
const handlers = extractHandlerNames(e.props);
|
|
963
1674
|
const state = e.getState();
|
|
964
|
-
const visibility
|
|
1675
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1676
|
+
state,
|
|
1677
|
+
this.getClipRectFor(e, viewport)
|
|
1678
|
+
);
|
|
965
1679
|
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
966
1680
|
const vision = projectVisionFields({
|
|
967
1681
|
type: e.type,
|
|
@@ -981,10 +1695,11 @@ var NativeUIBridgeRegistry = class {
|
|
|
981
1695
|
identifier: e.getIdentifier(),
|
|
982
1696
|
state,
|
|
983
1697
|
actions: e.actions,
|
|
984
|
-
customActions:
|
|
1698
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
985
1699
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
986
1700
|
registrationRoute: e.registrationRoute,
|
|
987
1701
|
visibility,
|
|
1702
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
988
1703
|
...bbox ? { bbox } : {},
|
|
989
1704
|
...vision
|
|
990
1705
|
};
|
|
@@ -1004,6 +1719,13 @@ var NativeUIBridgeRegistry = class {
|
|
|
1004
1719
|
})),
|
|
1005
1720
|
currentRoute: resolvedRoute.currentRoute,
|
|
1006
1721
|
segments: resolvedRoute.segments,
|
|
1722
|
+
// Canonical aliases, spelled the way the web SDK's `BridgeSnapshot`
|
|
1723
|
+
// spells them so a cross-platform consumer reads one field name on both
|
|
1724
|
+
// platforms. Omitted rather than nulled when unknown — same convention
|
|
1725
|
+
// as the web fields, and it keeps the emitted shape byte-identical for
|
|
1726
|
+
// hosts that wire no route provider.
|
|
1727
|
+
...resolvedRoute.currentRoute ? { route: resolvedRoute.currentRoute } : {},
|
|
1728
|
+
...resolvedRoute.activeTab ? { activeTab: resolvedRoute.activeTab } : {},
|
|
1007
1729
|
registration: this.getRegistrationCoverage()
|
|
1008
1730
|
};
|
|
1009
1731
|
if (this.appInfo) {
|
|
@@ -1201,18 +1923,37 @@ function matchesIdentifier(identifier, criteria) {
|
|
|
1201
1923
|
return true;
|
|
1202
1924
|
}
|
|
1203
1925
|
|
|
1926
|
+
exports.DEFAULT_PARAM_VALIDATION_MODE = DEFAULT_PARAM_VALIDATION_MODE;
|
|
1927
|
+
exports.MAX_ACTION_TIMEOUT_MS = MAX_ACTION_TIMEOUT_MS;
|
|
1928
|
+
exports.NATIVE_STANDARD_ACTION_EFFECTS = NATIVE_STANDARD_ACTION_EFFECTS;
|
|
1204
1929
|
exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
|
|
1205
1930
|
exports.buildTreePath = buildTreePath;
|
|
1931
|
+
exports.computeVisibility = computeVisibility;
|
|
1206
1932
|
exports.createNativeElementIdentifier = createNativeElementIdentifier;
|
|
1933
|
+
exports.deriveActiveTabFromSegments = deriveActiveTabFromSegments;
|
|
1207
1934
|
exports.extractHandlerNames = extractHandlerNames;
|
|
1208
1935
|
exports.findAllByPattern = findAllByPattern;
|
|
1209
1936
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
1937
|
+
exports.formatParamValidationFailure = formatParamValidationFailure;
|
|
1938
|
+
exports.getDefaultParamValidationMode = getDefaultParamValidationMode;
|
|
1210
1939
|
exports.getGlobalRegistry = getGlobalRegistry;
|
|
1940
|
+
exports.inertAbortSignal = inertAbortSignal;
|
|
1941
|
+
exports.intersectRects = intersectRects;
|
|
1942
|
+
exports.isEmptyRect = isEmptyRect;
|
|
1211
1943
|
exports.matchesCurrentRoute = matchesCurrentRoute;
|
|
1212
1944
|
exports.matchesIdentifier = matchesIdentifier;
|
|
1945
|
+
exports.nativeStandardActionEffect = nativeStandardActionEffect;
|
|
1946
|
+
exports.normalizeActionTimeoutMs = normalizeActionTimeoutMs;
|
|
1947
|
+
exports.pageRectOf = pageRectOf;
|
|
1213
1948
|
exports.parseTreePath = parseTreePath;
|
|
1214
1949
|
exports.projectBbox = projectBbox;
|
|
1950
|
+
exports.resetDefaultParamValidationMode = resetDefaultParamValidationMode;
|
|
1215
1951
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
1952
|
+
exports.resolveActionEffect = resolveActionEffect;
|
|
1953
|
+
exports.runAbortable = runAbortable;
|
|
1954
|
+
exports.serializeElementCustomActions = serializeElementCustomActions;
|
|
1955
|
+
exports.setDefaultParamValidationMode = setDefaultParamValidationMode;
|
|
1216
1956
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
1957
|
+
exports.validateActionParams = validateActionParams;
|
|
1217
1958
|
//# sourceMappingURL=index.js.map
|
|
1218
1959
|
//# sourceMappingURL=index.js.map
|