@qontinui/ui-bridge-native 0.6.12 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +36 -16
  2. package/dist/control/action-executor.d.ts +42 -2
  3. package/dist/control/action-executor.d.ts.map +1 -1
  4. package/dist/control/index.js +723 -4
  5. package/dist/control/index.js.map +1 -1
  6. package/dist/control/index.mjs +723 -5
  7. package/dist/control/index.mjs.map +1 -1
  8. package/dist/control/types.d.ts +82 -1
  9. package/dist/control/types.d.ts.map +1 -1
  10. package/dist/core/abortable.d.ts +151 -0
  11. package/dist/core/abortable.d.ts.map +1 -0
  12. package/dist/core/action-effect.d.ts +74 -0
  13. package/dist/core/action-effect.d.ts.map +1 -0
  14. package/dist/core/element-actions.d.ts +38 -0
  15. package/dist/core/element-actions.d.ts.map +1 -0
  16. package/dist/core/index.d.ts +4 -0
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +756 -15
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/index.mjs +738 -16
  21. package/dist/core/index.mjs.map +1 -1
  22. package/dist/core/param-schema.d.ts +282 -0
  23. package/dist/core/param-schema.d.ts.map +1 -0
  24. package/dist/core/registry.d.ts +272 -2
  25. package/dist/core/registry.d.ts.map +1 -1
  26. package/dist/core/types.d.ts +231 -32
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/debug/index.js.map +1 -1
  29. package/dist/debug/index.mjs.map +1 -1
  30. package/dist/index.d.ts +10 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1568 -135
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +1550 -136
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/react/UIBridgeNativeProvider.d.ts +18 -24
  37. package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
  38. package/dist/react/index.js +1385 -130
  39. package/dist/react/index.js.map +1 -1
  40. package/dist/react/index.mjs +1385 -130
  41. package/dist/react/index.mjs.map +1 -1
  42. package/dist/react/useUIBridge.d.ts +19 -22
  43. package/dist/react/useUIBridge.d.ts.map +1 -1
  44. package/dist/react/useUIComponent.d.ts +113 -8
  45. package/dist/react/useUIComponent.d.ts.map +1 -1
  46. package/dist/react/useUIElement.d.ts +34 -8
  47. package/dist/react/useUIElement.d.ts.map +1 -1
  48. package/dist/server/handlers.d.ts.map +1 -1
  49. package/dist/server/http-server.d.ts +15 -2
  50. package/dist/server/http-server.d.ts.map +1 -1
  51. package/dist/server/index.js +345 -43
  52. package/dist/server/index.js.map +1 -1
  53. package/dist/server/index.mjs +345 -44
  54. package/dist/server/index.mjs.map +1 -1
  55. package/dist/server/observability.d.ts +40 -0
  56. package/dist/server/observability.d.ts.map +1 -1
  57. package/dist/server/types.d.ts +57 -1
  58. package/dist/server/types.d.ts.map +1 -1
  59. package/dist/transport/CloudRelayClient.d.ts.map +1 -1
  60. package/dist/transport/DeviceAnnouncer.d.ts +35 -7
  61. package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
  62. package/dist/transport/index.d.ts +1 -1
  63. package/dist/transport/index.d.ts.map +1 -1
  64. package/dist/transport/relay-logging.d.ts +50 -0
  65. package/dist/transport/relay-logging.d.ts.map +1 -0
  66. package/package.json +2 -2
@@ -1,12 +1,469 @@
1
1
  'use strict';
2
2
 
3
3
  // src/core/registry.ts
4
+ function pageRectOf(state) {
5
+ const layout = state.layout;
6
+ if (!layout) return null;
7
+ if (layout.pageOriginUnmeasured === true) return null;
8
+ const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
9
+ if (!hasPage) return null;
10
+ const left = layout.pageX;
11
+ const top = layout.pageY;
12
+ if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
13
+ return null;
14
+ }
15
+ return { left, top, right: left + layout.width, bottom: top + layout.height };
16
+ }
17
+ function isEmptyRect(rect) {
18
+ return rect.right <= rect.left || rect.bottom <= rect.top;
19
+ }
20
+ function intersectRects(a, b) {
21
+ if (!a) return b;
22
+ if (!b) return a;
23
+ return {
24
+ left: Math.max(a.left, b.left),
25
+ top: Math.max(a.top, b.top),
26
+ right: Math.min(a.right, b.right),
27
+ bottom: Math.min(a.bottom, b.bottom)
28
+ };
29
+ }
4
30
 
5
31
  // src/core/element-identifier.ts
6
32
  function findElementByIdentifier(identifier) {
7
33
  return null;
8
34
  }
9
35
 
36
+ // src/core/abortable.ts
37
+ var MAX_ACTION_TIMEOUT_MS = 864e5;
38
+ function normalizeActionTimeoutMs(raw) {
39
+ if (raw === void 0 || raw === null) {
40
+ return { ok: true, timeoutMs: void 0, clamped: false };
41
+ }
42
+ if (typeof raw !== "number") {
43
+ return {
44
+ ok: false,
45
+ reason: `timeoutMs must be a number of milliseconds, received ${typeof raw === "string" ? JSON.stringify(raw) : typeof raw}`
46
+ };
47
+ }
48
+ if (!Number.isFinite(raw)) {
49
+ return {
50
+ ok: false,
51
+ reason: `timeoutMs must be a finite number of milliseconds, received ${String(raw)}`
52
+ };
53
+ }
54
+ if (raw < 0) {
55
+ return {
56
+ ok: false,
57
+ reason: `timeoutMs must not be negative, received ${String(raw)}`
58
+ };
59
+ }
60
+ if (raw > MAX_ACTION_TIMEOUT_MS) {
61
+ return { ok: true, timeoutMs: MAX_ACTION_TIMEOUT_MS, clamped: true };
62
+ }
63
+ return { ok: true, timeoutMs: Math.floor(raw), clamped: false };
64
+ }
65
+ function inertAbortSignal() {
66
+ return new AbortController().signal;
67
+ }
68
+ async function runAbortable(invoke, options = {}) {
69
+ const callerSignal = options.signal;
70
+ const timeoutMs = options.timeoutMs;
71
+ if (callerSignal?.aborted) {
72
+ return { aborted: true, reason: "signal" };
73
+ }
74
+ const controller = new AbortController();
75
+ let reason = "signal";
76
+ let timer;
77
+ const onCallerAbort = () => {
78
+ reason = "signal";
79
+ controller.abort();
80
+ };
81
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
82
+ if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0) {
83
+ timer = setTimeout(
84
+ () => {
85
+ reason = "timeout";
86
+ controller.abort();
87
+ },
88
+ Math.min(timeoutMs, MAX_ACTION_TIMEOUT_MS)
89
+ );
90
+ }
91
+ try {
92
+ const abandoned = new Promise((resolve) => {
93
+ controller.signal.addEventListener("abort", () => resolve({ aborted: true, reason }), {
94
+ once: true
95
+ });
96
+ });
97
+ const completed = (async () => ({
98
+ aborted: false,
99
+ result: await invoke(controller.signal)
100
+ }))();
101
+ return await Promise.race([completed, abandoned]);
102
+ } finally {
103
+ if (timer !== void 0) clearTimeout(timer);
104
+ callerSignal?.removeEventListener("abort", onCallerAbort);
105
+ }
106
+ }
107
+
108
+ // src/core/param-schema.ts
109
+ var DEFAULT_PARAM_VALIDATION_MODE = "warn";
110
+ var currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
111
+ function getDefaultParamValidationMode() {
112
+ return currentDefaultMode;
113
+ }
114
+ var PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
115
+ "string",
116
+ "number",
117
+ "integer",
118
+ "boolean",
119
+ "object",
120
+ "array",
121
+ "null"
122
+ ]);
123
+ var OBJECT_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
124
+ "type",
125
+ "properties",
126
+ "required",
127
+ "additionalProperties",
128
+ "patternProperties",
129
+ "propertyNames",
130
+ "minProperties",
131
+ "maxProperties",
132
+ "dependentRequired",
133
+ "dependentSchemas",
134
+ "unevaluatedProperties",
135
+ "allOf",
136
+ "anyOf",
137
+ "oneOf",
138
+ "not",
139
+ "if",
140
+ "then",
141
+ "else",
142
+ "enum",
143
+ "const",
144
+ "default",
145
+ "examples",
146
+ "title",
147
+ "description",
148
+ "deprecated",
149
+ "readOnly",
150
+ "writeOnly",
151
+ "$schema",
152
+ "$id",
153
+ "$ref",
154
+ "$comment",
155
+ "$defs",
156
+ "definitions"
157
+ ]);
158
+ var MAX_SCHEMA_DEPTH = 32;
159
+ var MAX_PATTERN_LENGTH = 200;
160
+ function hasNestedQuantifiers(pattern) {
161
+ return /(\((?:[^()]*[+*}])[^()]*\))[+*?]|\(\?:[^()]*[+*}][^()]*\)[+*?]/.test(pattern);
162
+ }
163
+ function isPlainObject(value) {
164
+ return typeof value === "object" && value !== null && !Array.isArray(value);
165
+ }
166
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
167
+ function hasOwn(value, key) {
168
+ return hasOwnProperty.call(value, key);
169
+ }
170
+ function render(value) {
171
+ if (value === null) return "null";
172
+ if (value === void 0) return "undefined";
173
+ if (Array.isArray(value)) return `array(${value.length})`;
174
+ const t = typeof value;
175
+ if (t === "string") return JSON.stringify(value);
176
+ if (t === "number" || t === "boolean") return String(value);
177
+ if (t === "object") return "object";
178
+ return t;
179
+ }
180
+ var MAX_RENDERED_JSON_LENGTH = 120;
181
+ function renderJson(value) {
182
+ if (value === void 0) return "undefined";
183
+ let text;
184
+ try {
185
+ text = JSON.stringify(value) ?? String(value);
186
+ } catch {
187
+ return render(value);
188
+ }
189
+ return text.length > MAX_RENDERED_JSON_LENGTH ? `${text.slice(0, MAX_RENDERED_JSON_LENGTH)}\u2026` : text;
190
+ }
191
+ function jsonTypeOf(value) {
192
+ if (value === null) return "null";
193
+ if (Array.isArray(value)) return "array";
194
+ const t = typeof value;
195
+ if (t === "number") return Number.isInteger(value) ? "integer" : "number";
196
+ if (t === "string" || t === "boolean" || t === "object") return t;
197
+ return t;
198
+ }
199
+ function matchesType(value, typeName) {
200
+ switch (typeName) {
201
+ case "string":
202
+ return typeof value === "string";
203
+ case "number":
204
+ return typeof value === "number" && Number.isFinite(value);
205
+ case "integer":
206
+ return typeof value === "number" && Number.isInteger(value);
207
+ case "boolean":
208
+ return typeof value === "boolean";
209
+ case "null":
210
+ return value === null;
211
+ case "array":
212
+ return Array.isArray(value);
213
+ case "object":
214
+ return isPlainObject(value);
215
+ default:
216
+ return true;
217
+ }
218
+ }
219
+ function jsonEquals(a, b, depth = 0) {
220
+ if (a === b) return true;
221
+ if (depth > MAX_SCHEMA_DEPTH) return false;
222
+ if (a === null || b === null || a === void 0 || b === void 0) return false;
223
+ if (typeof a !== typeof b) return false;
224
+ if (typeof a !== "object") return false;
225
+ const aIsArray = Array.isArray(a);
226
+ if (aIsArray !== Array.isArray(b)) return false;
227
+ if (aIsArray) {
228
+ const bArr = b;
229
+ const aArr = a;
230
+ if (aArr.length !== bArr.length) return false;
231
+ return aArr.every((item, i) => jsonEquals(item, bArr[i], depth + 1));
232
+ }
233
+ const aObj = a;
234
+ const bObj = b;
235
+ const aKeys = Object.keys(aObj);
236
+ const bKeys = Object.keys(bObj);
237
+ if (aKeys.length !== bKeys.length) return false;
238
+ return aKeys.every((key) => hasOwn(bObj, key) && jsonEquals(aObj[key], bObj[key], depth + 1));
239
+ }
240
+ function joinPath(base, key) {
241
+ return base === "" ? key : `${base}.${key}`;
242
+ }
243
+ function readTypes(schema) {
244
+ const raw = schema["type"];
245
+ if (typeof raw === "string") return [raw];
246
+ if (Array.isArray(raw)) {
247
+ const names = raw.filter((t) => typeof t === "string");
248
+ return names.length > 0 ? names : null;
249
+ }
250
+ return null;
251
+ }
252
+ function validateValue(value, schema, path, issues, depth) {
253
+ if (depth > MAX_SCHEMA_DEPTH) return;
254
+ if (typeof schema === "string") {
255
+ if (PRIMITIVE_TYPE_NAMES.has(schema) && !matchesType(value, schema)) {
256
+ issues.push({
257
+ path,
258
+ keyword: "type",
259
+ expected: schema,
260
+ received: render(value),
261
+ message: `Parameter "${path}" must be of type ${schema}, received ${jsonTypeOf(value)} (${render(value)}).`
262
+ });
263
+ }
264
+ return;
265
+ }
266
+ if (!isPlainObject(schema)) return;
267
+ const types = readTypes(schema);
268
+ if (types !== null && !types.some((t) => matchesType(value, t))) {
269
+ const expected = types.join(" | ");
270
+ issues.push({
271
+ path,
272
+ keyword: "type",
273
+ expected,
274
+ received: render(value),
275
+ message: `Parameter "${path}" must be of type ${expected}, received ${jsonTypeOf(value)} (${render(value)}).`
276
+ });
277
+ return;
278
+ }
279
+ if (hasOwn(schema, "const") && !jsonEquals(value, schema["const"])) {
280
+ const expected = renderJson(schema["const"]);
281
+ const received = renderJson(value);
282
+ issues.push({
283
+ path,
284
+ keyword: "const",
285
+ expected,
286
+ received,
287
+ message: `Parameter "${path}" must equal ${expected}, received ${received}.`
288
+ });
289
+ return;
290
+ }
291
+ const enumValues = schema["enum"];
292
+ if (Array.isArray(enumValues) && !enumValues.some((allowed) => jsonEquals(value, allowed))) {
293
+ const expected = enumValues.map((v) => renderJson(v)).join(", ");
294
+ const received = renderJson(value);
295
+ issues.push({
296
+ path,
297
+ keyword: "enum",
298
+ expected,
299
+ received,
300
+ message: `Parameter "${path}" must be one of [${expected}], received ${received}.`
301
+ });
302
+ return;
303
+ }
304
+ if (typeof value === "number") {
305
+ const min = schema["minimum"];
306
+ if (typeof min === "number" && value < min) {
307
+ issues.push({
308
+ path,
309
+ keyword: "minimum",
310
+ expected: `>= ${min}`,
311
+ received: render(value),
312
+ message: `Parameter "${path}" must be >= ${min}, received ${value}.`
313
+ });
314
+ }
315
+ const max = schema["maximum"];
316
+ if (typeof max === "number" && value > max) {
317
+ issues.push({
318
+ path,
319
+ keyword: "maximum",
320
+ expected: `<= ${max}`,
321
+ received: render(value),
322
+ message: `Parameter "${path}" must be <= ${max}, received ${value}.`
323
+ });
324
+ }
325
+ }
326
+ if (typeof value === "string") {
327
+ const minLength = schema["minLength"];
328
+ if (typeof minLength === "number" && value.length < minLength) {
329
+ issues.push({
330
+ path,
331
+ keyword: "minLength",
332
+ expected: `length >= ${minLength}`,
333
+ received: `length ${value.length}`,
334
+ message: `Parameter "${path}" must be at least ${minLength} characters, received ${value.length}.`
335
+ });
336
+ }
337
+ const maxLength = schema["maxLength"];
338
+ if (typeof maxLength === "number" && value.length > maxLength) {
339
+ issues.push({
340
+ path,
341
+ keyword: "maxLength",
342
+ expected: `length <= ${maxLength}`,
343
+ received: `length ${value.length}`,
344
+ message: `Parameter "${path}" must be at most ${maxLength} characters, received ${value.length}.`
345
+ });
346
+ }
347
+ const pattern = schema["pattern"];
348
+ if (typeof pattern === "string" && pattern.length <= MAX_PATTERN_LENGTH && !hasNestedQuantifiers(pattern)) {
349
+ let re;
350
+ try {
351
+ re = new RegExp(pattern);
352
+ } catch {
353
+ re = null;
354
+ }
355
+ if (re !== null && !re.test(value)) {
356
+ issues.push({
357
+ path,
358
+ keyword: "pattern",
359
+ expected: `match /${pattern}/`,
360
+ received: render(value),
361
+ message: `Parameter "${path}" must match /${pattern}/, received ${render(value)}.`
362
+ });
363
+ }
364
+ }
365
+ }
366
+ if (Array.isArray(value) && hasOwn(schema, "items")) {
367
+ const items = schema["items"];
368
+ value.forEach((element, index) => {
369
+ validateValue(element, items, `${path}[${index}]`, issues, depth + 1);
370
+ });
371
+ }
372
+ if (isPlainObject(value) && (isPlainObject(schema["properties"]) || hasOwn(schema, "required") || schema["additionalProperties"] === false)) {
373
+ validateObject(value, schema, path, issues, depth);
374
+ }
375
+ }
376
+ function validateObject(value, schema, basePath, issues, depth) {
377
+ if (depth > MAX_SCHEMA_DEPTH) return;
378
+ const properties = isPlainObject(schema["properties"]) ? schema["properties"] : {};
379
+ const requiredRaw = schema["required"];
380
+ const required = Array.isArray(requiredRaw) ? requiredRaw.filter((k) => typeof k === "string") : [];
381
+ for (const key of required) {
382
+ if (!hasOwn(value, key) || value[key] === void 0) {
383
+ const at = joinPath(basePath, key);
384
+ issues.push({
385
+ path: at,
386
+ keyword: "required",
387
+ expected: "present",
388
+ message: `Required parameter "${at}" is missing.`
389
+ });
390
+ }
391
+ }
392
+ if (schema["additionalProperties"] === false) {
393
+ const declared = new Set(Object.keys(properties));
394
+ const expected = declared.size === 0 ? "no parameters" : `one of [${[...declared].join(", ")}]`;
395
+ for (const key of Object.keys(value)) {
396
+ if (!declared.has(key)) {
397
+ const at = joinPath(basePath, key);
398
+ issues.push({
399
+ path: at,
400
+ keyword: "additionalProperties",
401
+ expected,
402
+ received: render(value[key]),
403
+ message: `Parameter "${at}" is not declared by the schema, which sets additionalProperties: false.`
404
+ });
405
+ }
406
+ }
407
+ }
408
+ for (const [key, propSchema] of Object.entries(properties)) {
409
+ if (!hasOwn(value, key) || value[key] === void 0) continue;
410
+ validateValue(value[key], propSchema, joinPath(basePath, key), issues, depth + 1);
411
+ }
412
+ }
413
+ function isObjectSchemaForm(schema) {
414
+ if (isPlainObject(schema["properties"])) return true;
415
+ if (schema["type"] !== "object") return false;
416
+ return Object.keys(schema).every((key) => OBJECT_SCHEMA_KEYWORDS.has(key));
417
+ }
418
+ function normalizeMapForm(schema) {
419
+ if (isObjectSchemaForm(schema)) return null;
420
+ const properties = {};
421
+ for (const [key, raw] of Object.entries(schema)) {
422
+ if (raw === null || raw === void 0) continue;
423
+ if (typeof raw === "string") {
424
+ properties[key] = PRIMITIVE_TYPE_NAMES.has(raw) ? { type: raw } : {};
425
+ continue;
426
+ }
427
+ if (isPlainObject(raw)) {
428
+ const { optional: _optional, ...rest } = raw;
429
+ properties[key] = rest;
430
+ continue;
431
+ }
432
+ }
433
+ return { type: "object", properties };
434
+ }
435
+ function validateActionParams(schema, params) {
436
+ if (!isPlainObject(schema)) return { valid: true, issues: [] };
437
+ const normalized = normalizeMapForm(schema) ?? schema;
438
+ const hasProperties = isPlainObject(normalized["properties"]);
439
+ const hasRequired = Array.isArray(normalized["required"]);
440
+ const forbidsExtras = normalized["additionalProperties"] === false;
441
+ if (!hasProperties && !hasRequired && !forbidsExtras) {
442
+ return { valid: true, issues: [] };
443
+ }
444
+ const issues = [];
445
+ if (params === void 0 || params === null) {
446
+ validateObject({}, normalized, "", issues, 0);
447
+ return { valid: issues.length === 0, issues };
448
+ }
449
+ if (!isPlainObject(params)) {
450
+ issues.push({
451
+ path: "",
452
+ keyword: "type",
453
+ expected: "object",
454
+ received: render(params),
455
+ message: `Action params must be an object, received ${jsonTypeOf(params)} (${render(params)}).`
456
+ });
457
+ return { valid: false, issues };
458
+ }
459
+ validateObject(params, normalized, "", issues, 0);
460
+ return { valid: issues.length === 0, issues };
461
+ }
462
+ function formatParamValidationFailure(componentId, actionId, issues) {
463
+ const detail = issues.map((i) => i.message).join(" ");
464
+ return `Action "${actionId}" on component "${componentId}" was rejected: params do not match the action's declared paramSchema. ${detail}`;
465
+ }
466
+
10
467
  // src/control/action-executor.ts
11
468
  var DEFAULT_WAIT_OPTIONS = {
12
469
  visible: true,
@@ -16,6 +473,68 @@ var DEFAULT_WAIT_OPTIONS = {
16
473
  timeout: 1e4,
17
474
  interval: 100
18
475
  };
476
+ var NativeActionError = class extends Error {
477
+ constructor(message, code) {
478
+ super(message);
479
+ this.code = code;
480
+ this.name = "NativeActionError";
481
+ }
482
+ };
483
+ var DEFAULT_SCROLL_INTO_VIEW_PADDING = 16;
484
+ var SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS = 1e3;
485
+ var SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS = 250;
486
+ function measurePageRectNow(element, preferred) {
487
+ const preferredNode = preferred;
488
+ const node = preferredNode && typeof preferredNode.measureInWindow === "function" ? preferredNode : element.ref.current;
489
+ if (!node || typeof node.measureInWindow !== "function") {
490
+ const state = element.getState();
491
+ return Promise.resolve(state.visible ? pageRectOf(state) : null);
492
+ }
493
+ return new Promise((resolve) => {
494
+ let settled = false;
495
+ const done = (rect) => {
496
+ if (settled) return;
497
+ settled = true;
498
+ clearTimeout(timer);
499
+ resolve(rect);
500
+ };
501
+ const timer = setTimeout(() => done(null), SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS);
502
+ try {
503
+ node.measureInWindow(
504
+ (x, y, w, h) => done(
505
+ [x, y, w, h].every(Number.isFinite) && w > 0 && h > 0 ? { left: x, top: y, right: x + w, bottom: y + h } : null
506
+ )
507
+ );
508
+ } catch {
509
+ done(null);
510
+ }
511
+ });
512
+ }
513
+ function resolveScrollView(current) {
514
+ const c = current;
515
+ if (!c) return null;
516
+ if (typeof c.scrollTo === "function")
517
+ return c;
518
+ for (const unwrap of [c.getNativeScrollRef, c.getScrollResponder]) {
519
+ if (typeof unwrap !== "function") continue;
520
+ let inner;
521
+ try {
522
+ inner = unwrap.call(c);
523
+ } catch {
524
+ inner = null;
525
+ }
526
+ if (inner && typeof inner.scrollTo === "function")
527
+ return inner;
528
+ }
529
+ return null;
530
+ }
531
+ function isDeclaredHorizontal(registeredProps, ...refs) {
532
+ if (registeredProps?.horizontal === true) return true;
533
+ return refs.some((r) => {
534
+ const props = r?.props;
535
+ return props !== null && typeof props === "object" && props.horizontal === true;
536
+ });
537
+ }
19
538
  function sleep(ms) {
20
539
  return new Promise((resolve) => setTimeout(resolve, ms));
21
540
  }
@@ -135,6 +654,7 @@ var DefaultNativeActionExecutor = class {
135
654
  return {
136
655
  success: false,
137
656
  error: errorMsg,
657
+ ...error instanceof NativeActionError ? { code: error.code } : {},
138
658
  stack: error instanceof Error ? error.stack : void 0,
139
659
  durationMs: failTime - startTime,
140
660
  timestamp: failTime,
@@ -152,7 +672,7 @@ var DefaultNativeActionExecutor = class {
152
672
  }
153
673
  const props = element.props || {};
154
674
  if (element.customActions && action in element.customActions) {
155
- return element.customActions[action].handler(params);
675
+ return element.customActions[action].handler(params, { signal: inertAbortSignal() });
156
676
  }
157
677
  switch (action) {
158
678
  case "click":
@@ -175,6 +695,11 @@ var DefaultNativeActionExecutor = class {
175
695
  return this.performBlur(element);
176
696
  case "scroll":
177
697
  return this.performScroll(props, params);
698
+ case "scrollIntoView":
699
+ return this.performScrollIntoView(
700
+ element,
701
+ params
702
+ );
178
703
  case "swipe":
179
704
  return this.performSwipe(props, params);
180
705
  case "toggle":
@@ -341,6 +866,138 @@ var DefaultNativeActionExecutor = class {
341
866
  props.onScroll(event);
342
867
  }
343
868
  }
869
+ /**
870
+ * Perform scrollIntoView: scroll the element's DECLARED scroll container so
871
+ * the element sits `padding` dp below the top of its viewport.
872
+ *
873
+ * The container comes from `scrollAncestorId` and nowhere else — the
874
+ * registry keeps no parent chain (`RegisterElementOptions.scrollAncestorId`),
875
+ * so there is no "nearest ScrollView" to find. Nothing here tracks a
876
+ * ScrollView's content offset either (`performScroll` only fakes an
877
+ * `onScroll` event), so the target is measured CONTENT-relative with
878
+ * `measureLayout` against the container's inner content view: stateless,
879
+ * and immune to a stale-offset race.
880
+ *
881
+ * Result shape mirrors the web executor's `scrollIntoView`
882
+ * (`@qontinui/ui-bridge` `control/action-executor.ts`):
883
+ * `{ alreadyVisible, scrolled }`, short-circuiting when the element is
884
+ * already fully inside its clip region. "Fully visible" is only claimed when
885
+ * it is MEASURED — a page-space rect for the element and a known,
886
+ * non-empty clip (window ∩ declared container), both measured at call
887
+ * time. Anything unknown scrolls.
888
+ *
889
+ * Containers: a ScrollView, or a VirtualizedList (FlatList / SectionList)
890
+ * unwrapped through `getNativeScrollRef()` / `getScrollResponder()`.
891
+ * Vertical only — a `horizontal` container is NOT_SUPPORTED. The action
892
+ * returns once `scrollTo` is dispatched; positions settle on the next frame.
893
+ *
894
+ * Prerequisites are checked BEFORE the visibility short-circuit, so whether
895
+ * the element supports the action does not depend on where it happens to be
896
+ * on screen. Each missing prerequisite is a `NOT_SUPPORTED` failure.
897
+ */
898
+ async performScrollIntoView(element, params) {
899
+ const padding = params?.padding ?? DEFAULT_SCROLL_INTO_VIEW_PADDING;
900
+ if (typeof padding !== "number" || !Number.isFinite(padding) || padding < 0) {
901
+ throw new Error('scrollIntoView "padding" must be a finite, non-negative number');
902
+ }
903
+ const ancestorId = element.scrollAncestorId;
904
+ if (!ancestorId) {
905
+ throw new NativeActionError(
906
+ `scrollIntoView is not supported on element "${element.id}": it declares no scrollAncestorId, and the registry keeps no parent chain to find its scroll container from`,
907
+ "NOT_SUPPORTED"
908
+ );
909
+ }
910
+ const ancestor = ancestorId === element.id ? void 0 : this.registry.getElement(ancestorId);
911
+ if (!ancestor) {
912
+ throw new NativeActionError(
913
+ `scrollIntoView is not supported on element "${element.id}": its declared scrollAncestorId "${ancestorId}" is not a registered element`,
914
+ "NOT_SUPPORTED"
915
+ );
916
+ }
917
+ const container = resolveScrollView(ancestor.ref.current);
918
+ if (!container) {
919
+ throw new NativeActionError(
920
+ `scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" has no scrollTo (not a mounted ScrollView, and no getNativeScrollRef/getScrollResponder yielding one)`,
921
+ "NOT_SUPPORTED"
922
+ );
923
+ }
924
+ if (isDeclaredHorizontal(ancestor.props, ancestor.ref.current, container)) {
925
+ throw new NativeActionError(
926
+ `scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" is horizontal (only vertical containers are supported)`,
927
+ "NOT_SUPPORTED"
928
+ );
929
+ }
930
+ const innerViewRef = container.getInnerViewRef?.();
931
+ const innerView = innerViewRef ?? container.getInnerViewNode?.();
932
+ const innerViewIsTag = innerViewRef == null && typeof innerView === "number";
933
+ if (innerView === void 0 || innerView === null) {
934
+ throw new NativeActionError(
935
+ `scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" exposes no inner content view to measure against`,
936
+ "NOT_SUPPORTED"
937
+ );
938
+ }
939
+ const target = element.ref.current;
940
+ if (!target || typeof target.measureLayout !== "function") {
941
+ throw new NativeActionError(
942
+ `scrollIntoView is not supported on element "${element.id}": its ref has no measureLayout (not a mounted host view)`,
943
+ "NOT_SUPPORTED"
944
+ );
945
+ }
946
+ const [rect, ancestorRect] = await Promise.all([
947
+ measurePageRectNow(element),
948
+ measurePageRectNow(ancestor, container)
949
+ ]);
950
+ const viewport = this.registry.getViewportRect();
951
+ const clip = ancestorRect ? intersectRects(viewport, ancestorRect) : null;
952
+ if (rect && clip && !isEmptyRect(clip) && rect.right > rect.left && rect.bottom > rect.top && rect.top >= clip.top && rect.left >= clip.left && rect.bottom <= clip.bottom && rect.right <= clip.right) {
953
+ return { alreadyVisible: true, scrolled: false };
954
+ }
955
+ const measured = await new Promise(
956
+ (resolve, reject) => {
957
+ let settled = false;
958
+ const timer = setTimeout(() => {
959
+ if (settled) return;
960
+ settled = true;
961
+ reject(
962
+ new Error(
963
+ `scrollIntoView: measureLayout did not call back within ${SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS}ms` + (innerViewIsTag ? " (measured against a numeric inner-view tag from getInnerViewNode, which the new architecture does not support)" : "")
964
+ )
965
+ );
966
+ }, SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS);
967
+ const finish = (fn) => {
968
+ if (settled) return;
969
+ settled = true;
970
+ clearTimeout(timer);
971
+ fn();
972
+ };
973
+ try {
974
+ target.measureLayout(
975
+ innerView,
976
+ (x, y, width) => finish(
977
+ () => Number.isFinite(y) ? resolve({ x, y, width }) : reject(new Error("scrollIntoView: measureLayout returned a non-finite y"))
978
+ ),
979
+ () => finish(
980
+ () => reject(
981
+ new Error(
982
+ `scrollIntoView: measureLayout failed for "${element.id}" relative to "${ancestorId}"`
983
+ )
984
+ )
985
+ )
986
+ );
987
+ } catch (error) {
988
+ finish(() => reject(error));
989
+ }
990
+ }
991
+ );
992
+ if (ancestorRect && Number.isFinite(measured.x) && Number.isFinite(measured.width) && measured.x + measured.width > ancestorRect.right - ancestorRect.left + 1) {
993
+ throw new NativeActionError(
994
+ `scrollIntoView is not supported on element "${element.id}": it is laid out beyond the horizontal extent of its scroll ancestor "${ancestorId}" (sideways overflow, a horizontal container, or a nested horizontal container \u2014 declare that inner container as its scrollAncestorId); only vertical scrolling is supported`,
995
+ "NOT_SUPPORTED"
996
+ );
997
+ }
998
+ container.scrollTo({ y: Math.max(0, measured.y - padding), animated: false });
999
+ return { alreadyVisible: false, scrolled: true };
1000
+ }
344
1001
  /**
345
1002
  * Perform swipe action
346
1003
  */
@@ -395,7 +1052,7 @@ var DefaultNativeActionExecutor = class {
395
1052
  /**
396
1053
  * Execute a component action
397
1054
  */
398
- async executeComponentAction(componentId, request) {
1055
+ async executeComponentAction(componentId, request, options = {}) {
399
1056
  const startTime = Date.now();
400
1057
  try {
401
1058
  const component = this.registry.getComponent(componentId);
@@ -418,10 +1075,71 @@ var DefaultNativeActionExecutor = class {
418
1075
  requestId: request.requestId
419
1076
  };
420
1077
  }
421
- const result = await action.handler(request.params);
1078
+ const timeout = normalizeActionTimeoutMs(request.timeoutMs);
1079
+ if (!timeout.ok) {
1080
+ return {
1081
+ success: false,
1082
+ error: `Action "${request.action}" on component "${componentId}" was rejected: ${timeout.reason}.`,
1083
+ durationMs: Date.now() - startTime,
1084
+ timestamp: Date.now(),
1085
+ requestId: request.requestId
1086
+ };
1087
+ }
1088
+ const timeoutMs = timeout.timeoutMs;
1089
+ const validationMode = options.paramValidation ?? getDefaultParamValidationMode();
1090
+ if (validationMode !== "off" && action.paramSchema !== void 0) {
1091
+ let validation;
1092
+ try {
1093
+ validation = validateActionParams(action.paramSchema, request.params);
1094
+ } catch (validatorFault) {
1095
+ const detail = validatorFault instanceof Error ? validatorFault.message : String(validatorFault);
1096
+ const faultMessage = `Action "${request.action}" on component "${componentId}": its declared paramSchema could not be evaluated (${detail}).`;
1097
+ if (validationMode === "enforce") {
1098
+ return {
1099
+ success: false,
1100
+ error: faultMessage,
1101
+ durationMs: Date.now() - startTime,
1102
+ timestamp: Date.now(),
1103
+ requestId: request.requestId
1104
+ };
1105
+ }
1106
+ console.warn(`[ui-bridge-native] ${faultMessage}`);
1107
+ validation = { valid: true, issues: [] };
1108
+ }
1109
+ if (!validation.valid) {
1110
+ const message = formatParamValidationFailure(
1111
+ componentId,
1112
+ request.action,
1113
+ validation.issues
1114
+ );
1115
+ if (validationMode === "enforce") {
1116
+ return {
1117
+ success: false,
1118
+ error: message,
1119
+ durationMs: Date.now() - startTime,
1120
+ timestamp: Date.now(),
1121
+ requestId: request.requestId
1122
+ };
1123
+ }
1124
+ console.warn(`[ui-bridge-native] ${message}`);
1125
+ }
1126
+ }
1127
+ const outcome = await runAbortable((signal) => action.handler(request.params, { signal }), {
1128
+ signal: options.signal,
1129
+ timeoutMs
1130
+ });
1131
+ if (outcome.aborted) {
1132
+ return {
1133
+ success: false,
1134
+ error: outcome.reason === "timeout" ? `Action "${request.action}" on component "${componentId}" was abandoned after its ${timeoutMs}ms timeout elapsed.` : `Action "${request.action}" on component "${componentId}" was cancelled by the caller's abort signal.`,
1135
+ durationMs: Date.now() - startTime,
1136
+ timestamp: Date.now(),
1137
+ requestId: request.requestId
1138
+ };
1139
+ }
422
1140
  return {
423
1141
  success: true,
424
- result,
1142
+ result: outcome.result,
425
1143
  durationMs: Date.now() - startTime,
426
1144
  timestamp: Date.now(),
427
1145
  requestId: request.requestId
@@ -543,6 +1261,7 @@ function createNativeActionExecutor(registry) {
543
1261
  }
544
1262
 
545
1263
  exports.DefaultNativeActionExecutor = DefaultNativeActionExecutor;
1264
+ exports.NativeActionError = NativeActionError;
546
1265
  exports.createNativeActionExecutor = createNativeActionExecutor;
547
1266
  //# sourceMappingURL=index.js.map
548
1267
  //# sourceMappingURL=index.js.map