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