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