@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.
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 +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 +1502 -117
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +1484 -118
  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 +1319 -112
  39. package/dist/react/index.js.map +1 -1
  40. package/dist/react/index.mjs +1319 -112
  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.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,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 inferActions(type) {
205
- const baseActions = ["focus", "blur"];
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
- actions: actions.map((a) => ({
801
- id: a.id,
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: this.routeProvider.getSegments?.()
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 = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
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: e.customActions ? Object.keys(e.customActions) : void 0,
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