@qontinui/ui-bridge-native 0.6.12 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -16
- package/dist/control/action-executor.d.ts +42 -2
- package/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +723 -4
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +723 -5
- package/dist/control/index.mjs.map +1 -1
- package/dist/control/types.d.ts +82 -1
- package/dist/control/types.d.ts.map +1 -1
- package/dist/core/abortable.d.ts +151 -0
- package/dist/core/abortable.d.ts.map +1 -0
- package/dist/core/action-effect.d.ts +74 -0
- package/dist/core/action-effect.d.ts.map +1 -0
- package/dist/core/element-actions.d.ts +38 -0
- package/dist/core/element-actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +756 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +738 -16
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/param-schema.d.ts +282 -0
- package/dist/core/param-schema.d.ts.map +1 -0
- package/dist/core/registry.d.ts +272 -2
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +231 -32
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1568 -135
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1550 -136
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +18 -24
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1385 -130
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1385 -130
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIBridge.d.ts +19 -22
- package/dist/react/useUIBridge.d.ts.map +1 -1
- package/dist/react/useUIComponent.d.ts +113 -8
- package/dist/react/useUIComponent.d.ts.map +1 -1
- package/dist/react/useUIElement.d.ts +34 -8
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +15 -2
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +345 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +345 -44
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/observability.d.ts +40 -0
- package/dist/server/observability.d.ts.map +1 -1
- package/dist/server/types.d.ts +57 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/transport/CloudRelayClient.d.ts.map +1 -1
- package/dist/transport/DeviceAnnouncer.d.ts +35 -7
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/relay-logging.d.ts +50 -0
- package/dist/transport/relay-logging.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -184,7 +184,42 @@ function projectVisionFields(args) {
|
|
|
184
184
|
return fields;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// src/core/element-actions.ts
|
|
188
|
+
function serializeElementCustomActions(customActions) {
|
|
189
|
+
if (!customActions) return void 0;
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const [id, action] of Object.entries(customActions)) {
|
|
192
|
+
if (!action) continue;
|
|
193
|
+
const entry = { id };
|
|
194
|
+
if (action.label !== void 0) entry.label = action.label;
|
|
195
|
+
if (action.description !== void 0) entry.description = action.description;
|
|
196
|
+
if (action.effect !== void 0) entry.effect = action.effect;
|
|
197
|
+
out.push(entry);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
187
202
|
// src/core/registry.ts
|
|
203
|
+
function toRegisteredComponentActions(actions) {
|
|
204
|
+
return actions.map((a) => ({
|
|
205
|
+
id: a.id,
|
|
206
|
+
label: a.label,
|
|
207
|
+
description: a.description,
|
|
208
|
+
paramSchema: a.paramSchema,
|
|
209
|
+
effect: a.effect,
|
|
210
|
+
handler: a.handler
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
function publishedActionSignature(actions) {
|
|
214
|
+
return JSON.stringify(
|
|
215
|
+
actions.map((a) => [a.id, a.label, a.description, a.paramSchema, a.effect])
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function sameElementIds(a, b) {
|
|
219
|
+
if (a === b) return true;
|
|
220
|
+
if (a === void 0 || b === void 0) return false;
|
|
221
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
222
|
+
}
|
|
188
223
|
function extractHandlerNames(props) {
|
|
189
224
|
if (!props) return [];
|
|
190
225
|
return Object.keys(props).filter((k) => typeof props[k] === "function");
|
|
@@ -193,6 +228,22 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
193
228
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
194
229
|
return registrationRoute === currentRoute;
|
|
195
230
|
}
|
|
231
|
+
function deriveActiveTabFromSegments(segments) {
|
|
232
|
+
if (!segments || segments.length === 0) return void 0;
|
|
233
|
+
let innermostGroup = -1;
|
|
234
|
+
for (let i = 0; i < segments.length; i++) {
|
|
235
|
+
const segment = segments[i];
|
|
236
|
+
if (typeof segment === "string" && segment.startsWith("(") && segment.endsWith(")")) {
|
|
237
|
+
innermostGroup = i;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (innermostGroup === -1) return void 0;
|
|
241
|
+
const next = segments[innermostGroup + 1];
|
|
242
|
+
if (next === void 0) return "index";
|
|
243
|
+
if (typeof next !== "string" || next.length === 0) return void 0;
|
|
244
|
+
if (next.startsWith("[") && next.endsWith("]")) return void 0;
|
|
245
|
+
return next;
|
|
246
|
+
}
|
|
196
247
|
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
197
248
|
if (visibility !== "visible") return void 0;
|
|
198
249
|
const layout = state.layout;
|
|
@@ -212,8 +263,58 @@ function projectBbox(state, visibility, pixelRatio = 1) {
|
|
|
212
263
|
h: toPx(layout.height)
|
|
213
264
|
};
|
|
214
265
|
}
|
|
215
|
-
function
|
|
216
|
-
const
|
|
266
|
+
function pageRectOf(state) {
|
|
267
|
+
const layout = state.layout;
|
|
268
|
+
if (!layout) return null;
|
|
269
|
+
if (layout.pageOriginUnmeasured === true) return null;
|
|
270
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
271
|
+
if (!hasPage) return null;
|
|
272
|
+
const left = layout.pageX;
|
|
273
|
+
const top = layout.pageY;
|
|
274
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return { left, top, right: left + layout.width, bottom: top + layout.height };
|
|
278
|
+
}
|
|
279
|
+
function spansOverlap(aMin, aMax, bMin, bMax) {
|
|
280
|
+
if (aMin === aMax) return aMin >= bMin && aMin <= bMax;
|
|
281
|
+
return aMin < bMax && aMax > bMin;
|
|
282
|
+
}
|
|
283
|
+
function isEmptyRect(rect) {
|
|
284
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
285
|
+
}
|
|
286
|
+
function intersectRects(a, b) {
|
|
287
|
+
if (!a) return b;
|
|
288
|
+
if (!b) return a;
|
|
289
|
+
return {
|
|
290
|
+
left: Math.max(a.left, b.left),
|
|
291
|
+
top: Math.max(a.top, b.top),
|
|
292
|
+
right: Math.min(a.right, b.right),
|
|
293
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function computeVisibility(state, clip) {
|
|
297
|
+
if (!state.visible) {
|
|
298
|
+
return { visibility: "hidden", visibilityReason: "hidden" };
|
|
299
|
+
}
|
|
300
|
+
if (state.layout === null) {
|
|
301
|
+
return { visibility: "likely-visible", visibilityReason: "no-layout" };
|
|
302
|
+
}
|
|
303
|
+
if (!clip) {
|
|
304
|
+
return { visibility: "visible" };
|
|
305
|
+
}
|
|
306
|
+
const rect = pageRectOf(state);
|
|
307
|
+
if (!rect) {
|
|
308
|
+
return { visibility: "visible" };
|
|
309
|
+
}
|
|
310
|
+
if (isEmptyRect(clip)) {
|
|
311
|
+
return { visibility: "hidden", visibilityReason: "off-screen" };
|
|
312
|
+
}
|
|
313
|
+
const onScreen = spansOverlap(rect.left, rect.right, clip.left, clip.right) && spansOverlap(rect.top, rect.bottom, clip.top, clip.bottom);
|
|
314
|
+
return onScreen ? { visibility: "visible" } : { visibility: "hidden", visibilityReason: "off-screen" };
|
|
315
|
+
}
|
|
316
|
+
function inferActions(type, scrollAncestorId) {
|
|
317
|
+
const baseActions = scrollAncestorId ? ["focus", "blur", "scrollIntoView"] : ["focus", "blur"];
|
|
217
318
|
switch (type) {
|
|
218
319
|
case "button":
|
|
219
320
|
case "touchable":
|
|
@@ -285,6 +386,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
285
386
|
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
286
387
|
*/
|
|
287
388
|
this.pixelRatio = 1;
|
|
389
|
+
/**
|
|
390
|
+
* Injected getter for the device window size in logical dp, used to clip
|
|
391
|
+
* reported visibility. Injected rather than read here for exactly the reason
|
|
392
|
+
* `pixelRatio` is: a static `import { Dimensions } from 'react-native'` in
|
|
393
|
+
* this non-React core module breaks the vitest suite and risks the
|
|
394
|
+
* Metro/Hermes `unknownModuleError`. It is a PROVIDER, not a value, so a
|
|
395
|
+
* rotation is picked up on the next snapshot with nothing to keep in sync.
|
|
396
|
+
*
|
|
397
|
+
* `null` means the window bounds are UNKNOWN, and {@link computeVisibility}
|
|
398
|
+
* treats unknown bounds as "cannot demote" — never as an empty viewport.
|
|
399
|
+
*/
|
|
400
|
+
this.viewportProvider = null;
|
|
288
401
|
/**
|
|
289
402
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
290
403
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -322,6 +435,30 @@ var NativeUIBridgeRegistry = class {
|
|
|
322
435
|
getRouteProvider() {
|
|
323
436
|
return this.routeProvider;
|
|
324
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the snapshot's `activeTab`: an explicit `getActiveTab()` on the
|
|
440
|
+
* route provider wins, otherwise derive it from the Expo Router segments the
|
|
441
|
+
* provider already exposes.
|
|
442
|
+
*
|
|
443
|
+
* A throwing or empty-string provider degrades to the derivation rather than
|
|
444
|
+
* to `undefined` — the host's opt-in extra should never be able to make the
|
|
445
|
+
* snapshot WORSE than it was without it. A blank derivation stays blank.
|
|
446
|
+
*/
|
|
447
|
+
resolveActiveTab(segments) {
|
|
448
|
+
const explicit = (() => {
|
|
449
|
+
if (!this.routeProvider?.getActiveTab) return void 0;
|
|
450
|
+
try {
|
|
451
|
+
const value = this.routeProvider.getActiveTab();
|
|
452
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
453
|
+
} catch (error3) {
|
|
454
|
+
if (this.config.verbose) {
|
|
455
|
+
console.warn(`[ui-bridge-native] routeProvider getActiveTab threw:`, error3);
|
|
456
|
+
}
|
|
457
|
+
return void 0;
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
return explicit ?? deriveActiveTabFromSegments(segments);
|
|
461
|
+
}
|
|
325
462
|
/**
|
|
326
463
|
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
327
464
|
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
@@ -337,6 +474,56 @@ var NativeUIBridgeRegistry = class {
|
|
|
337
474
|
getPixelRatio() {
|
|
338
475
|
return this.pixelRatio;
|
|
339
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Set the getter for the device window size (logical dp) that reported
|
|
479
|
+
* visibility is clipped against. The React provider passes
|
|
480
|
+
* `() => Dimensions.get('window')` here at construction.
|
|
481
|
+
*
|
|
482
|
+
* Pass `null` to detach. While detached the window bounds are UNKNOWN and no
|
|
483
|
+
* element is ever demoted for being outside them.
|
|
484
|
+
*/
|
|
485
|
+
setViewportProvider(provider) {
|
|
486
|
+
this.viewportProvider = provider ?? null;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The window as a page-space rect, or `null` when it is unknown or the
|
|
490
|
+
* provider returned something unusable (it throws in some RN teardown
|
|
491
|
+
* paths, and a zero-size window would silently hide the entire screen).
|
|
492
|
+
*/
|
|
493
|
+
getViewportRect() {
|
|
494
|
+
if (!this.viewportProvider) return null;
|
|
495
|
+
let size;
|
|
496
|
+
try {
|
|
497
|
+
size = this.viewportProvider();
|
|
498
|
+
} catch (error3) {
|
|
499
|
+
if (this.config.verbose) {
|
|
500
|
+
console.warn(`[ui-bridge-native] viewportProvider threw:`, error3);
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const { width, height } = size ?? {};
|
|
505
|
+
if (typeof width !== "number" || typeof height !== "number" || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return { left: 0, top: 0, right: width, bottom: height };
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The region `element` must reach to count as on screen: the window,
|
|
512
|
+
* intersected with the measured frame of its declared `scrollAncestorId`.
|
|
513
|
+
*
|
|
514
|
+
* Returns `null` when neither bound is known — see {@link computeVisibility}
|
|
515
|
+
* for why that means "do not demote" rather than "nothing is visible".
|
|
516
|
+
* An ancestor that is not registered, or registered but not yet measured,
|
|
517
|
+
* contributes nothing rather than hiding its subtree.
|
|
518
|
+
*/
|
|
519
|
+
getClipRectFor(element, viewport) {
|
|
520
|
+
const bounds = viewport === void 0 ? this.getViewportRect() : viewport;
|
|
521
|
+
const ancestorId = element.scrollAncestorId;
|
|
522
|
+
if (!ancestorId) return bounds;
|
|
523
|
+
const ancestor = this.elements.get(ancestorId);
|
|
524
|
+
if (!ancestor || ancestorId === element.id) return bounds;
|
|
525
|
+
return intersectRects(bounds, pageRectOf(ancestor.getState()));
|
|
526
|
+
}
|
|
340
527
|
// ============================================================================
|
|
341
528
|
// Snapshot Enricher Slots
|
|
342
529
|
// ============================================================================
|
|
@@ -380,16 +567,17 @@ var NativeUIBridgeRegistry = class {
|
|
|
380
567
|
const {
|
|
381
568
|
type = "custom",
|
|
382
569
|
label,
|
|
383
|
-
actions = inferActions(type),
|
|
384
570
|
customActions,
|
|
385
571
|
props,
|
|
386
572
|
treePath = id,
|
|
387
573
|
testId,
|
|
388
574
|
accessibilityLabel,
|
|
389
575
|
registrationRoute,
|
|
576
|
+
scrollAncestorId,
|
|
390
577
|
flatStyle,
|
|
391
578
|
stateStyles
|
|
392
579
|
} = options;
|
|
580
|
+
const actions = options.actions ?? inferActions(type, scrollAncestorId);
|
|
393
581
|
const getState = () => {
|
|
394
582
|
const element = ref.current;
|
|
395
583
|
if (!element) {
|
|
@@ -432,6 +620,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
432
620
|
registeredAt: Date.now(),
|
|
433
621
|
mounted: true,
|
|
434
622
|
registrationRoute: registrationRoute ?? null,
|
|
623
|
+
scrollAncestorId,
|
|
435
624
|
flatStyle,
|
|
436
625
|
stateStyles
|
|
437
626
|
};
|
|
@@ -808,12 +997,8 @@ var NativeUIBridgeRegistry = class {
|
|
|
808
997
|
id,
|
|
809
998
|
name,
|
|
810
999
|
description,
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
label: a.label,
|
|
814
|
-
description: a.description,
|
|
815
|
-
handler: a.handler
|
|
816
|
-
})),
|
|
1000
|
+
// Closed field list, applied once — see `toRegisteredComponentActions`.
|
|
1001
|
+
actions: toRegisteredComponentActions(actions),
|
|
817
1002
|
elementIds,
|
|
818
1003
|
registeredAt: Date.now(),
|
|
819
1004
|
mounted: true
|
|
@@ -825,6 +1010,55 @@ var NativeUIBridgeRegistry = class {
|
|
|
825
1010
|
}
|
|
826
1011
|
return registered;
|
|
827
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Update a registered component's declaration in place — name, description,
|
|
1015
|
+
* actions, elementIds.
|
|
1016
|
+
*
|
|
1017
|
+
* Sister to {@link updateElementMeta}, and it exists for the same reason one
|
|
1018
|
+
* level up. `registerComponent` publishes the declaration exactly once, and
|
|
1019
|
+
* `useUIComponent` registers on mount and never again, so an action whose
|
|
1020
|
+
* `label` carries a count, an action that only exists while a row is
|
|
1021
|
+
* selected, a `paramSchema` that widens once options load, or an `elementIds`
|
|
1022
|
+
* list that grows after mount, all published their mount-time shape to
|
|
1023
|
+
* `/control/components` forever. PR #176 closed that hole for an ELEMENT's
|
|
1024
|
+
* label; this closes it for a COMPONENT's declaration.
|
|
1025
|
+
*
|
|
1026
|
+
* **Idempotent by VALUE over the PUBLISHED shape.** `handler` identity is
|
|
1027
|
+
* excluded from the comparison — call sites pass inline closures that are
|
|
1028
|
+
* freshly allocated every render, so including it would report a change on
|
|
1029
|
+
* every call and make the door useless for a render-path publisher. The
|
|
1030
|
+
* newest handlers are still STORED whenever `actions` is supplied, which is
|
|
1031
|
+
* the other half of the same defect: without it the registry keeps the
|
|
1032
|
+
* closure from the render that registered the component, so a handler reading
|
|
1033
|
+
* component state read the state as it was at mount.
|
|
1034
|
+
*
|
|
1035
|
+
* Returns `true` when the published shape changed and an event was emitted,
|
|
1036
|
+
* `false` otherwise — the same contract as `updateElementMeta`, so a caller
|
|
1037
|
+
* can gate a warning or a state write on a real change.
|
|
1038
|
+
*/
|
|
1039
|
+
updateComponentMeta(id, meta) {
|
|
1040
|
+
const component = this.components.get(id);
|
|
1041
|
+
if (!component) return false;
|
|
1042
|
+
const nextName = meta.name !== void 0 ? meta.name : component.name;
|
|
1043
|
+
const nextDescription = meta.description !== void 0 ? meta.description : component.description;
|
|
1044
|
+
const nextActions = meta.actions !== void 0 ? toRegisteredComponentActions(meta.actions) : component.actions;
|
|
1045
|
+
const nextElementIds = meta.elementIds !== void 0 ? meta.elementIds : component.elementIds;
|
|
1046
|
+
const changed = nextName !== component.name || nextDescription !== component.description || !sameElementIds(nextElementIds, component.elementIds) || publishedActionSignature(nextActions) !== publishedActionSignature(component.actions);
|
|
1047
|
+
if (!changed && meta.actions === void 0) return false;
|
|
1048
|
+
this.components.set(id, {
|
|
1049
|
+
...component,
|
|
1050
|
+
name: nextName,
|
|
1051
|
+
description: nextDescription,
|
|
1052
|
+
actions: nextActions,
|
|
1053
|
+
elementIds: nextElementIds
|
|
1054
|
+
});
|
|
1055
|
+
if (!changed) return false;
|
|
1056
|
+
this.emit("component:registered", { id, name: nextName });
|
|
1057
|
+
if (this.config.verbose) {
|
|
1058
|
+
console.log(`[ui-bridge-native] Updated component: ${id} (${nextName})`);
|
|
1059
|
+
}
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
828
1062
|
/**
|
|
829
1063
|
* Unregister a component
|
|
830
1064
|
*/
|
|
@@ -938,41 +1172,59 @@ var NativeUIBridgeRegistry = class {
|
|
|
938
1172
|
* server-level override (the bare-server / cloud-relay path) still get a
|
|
939
1173
|
* populated `currentRoute` field as long as `setRouteProvider` was called
|
|
940
1174
|
* on the registry.
|
|
1175
|
+
*
|
|
1176
|
+
* `activeTab` resolves through {@link resolveActiveTab} on BOTH branches. An
|
|
1177
|
+
* explicit `routeInfo.activeTab` wins outright; otherwise a registered
|
|
1178
|
+
* provider's `getActiveTab()` answers and the Expo Router derivation is the
|
|
1179
|
+
* fallback. Deriving straight from `routeInfo.segments` here instead — which
|
|
1180
|
+
* is what this branch did when `activeTab` was introduced — silently ignored
|
|
1181
|
+
* `RouteProvider.getActiveTab` on the ONLY path that serves
|
|
1182
|
+
* `control/snapshot` once `setRouteProvider` is wired (the server installs a
|
|
1183
|
+
* `getSnapshot` override that passes `routeInfo`), so the opt-in hook for an
|
|
1184
|
+
* app whose visible pane is decoupled from the router reached no consumer.
|
|
941
1185
|
*/
|
|
942
1186
|
createSnapshot(routeInfo, options) {
|
|
943
1187
|
const resolvedRoute = (() => {
|
|
944
1188
|
if (routeInfo !== void 0) {
|
|
1189
|
+
const explicit = typeof routeInfo.activeTab === "string" && routeInfo.activeTab.length > 0 ? routeInfo.activeTab : void 0;
|
|
945
1190
|
return {
|
|
946
1191
|
currentRoute: routeInfo.currentRoute ?? null,
|
|
947
|
-
segments: routeInfo.segments
|
|
1192
|
+
segments: routeInfo.segments,
|
|
1193
|
+
activeTab: explicit ?? this.resolveActiveTab(routeInfo.segments)
|
|
948
1194
|
};
|
|
949
1195
|
}
|
|
950
1196
|
if (this.routeProvider) {
|
|
951
1197
|
try {
|
|
1198
|
+
const segments = this.routeProvider.getSegments?.();
|
|
952
1199
|
return {
|
|
953
1200
|
currentRoute: this.routeProvider.getCurrentRoute() ?? null,
|
|
954
|
-
segments
|
|
1201
|
+
segments,
|
|
1202
|
+
activeTab: this.resolveActiveTab(segments)
|
|
955
1203
|
};
|
|
956
1204
|
} catch (error3) {
|
|
957
1205
|
if (this.config.verbose) {
|
|
958
1206
|
console.warn(`[ui-bridge-native] routeProvider getCurrentRoute threw:`, error3);
|
|
959
1207
|
}
|
|
960
|
-
return { currentRoute: null, segments: void 0 };
|
|
1208
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
961
1209
|
}
|
|
962
1210
|
}
|
|
963
|
-
return { currentRoute: null, segments: void 0 };
|
|
1211
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
964
1212
|
})();
|
|
965
1213
|
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
966
1214
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
967
1215
|
const currentRoute = resolvedRoute.currentRoute;
|
|
968
1216
|
elements = elements.filter((e) => matchesCurrentRoute(e.registrationRoute, currentRoute));
|
|
969
1217
|
}
|
|
1218
|
+
const viewport = this.getViewportRect();
|
|
970
1219
|
const snapshot = {
|
|
971
1220
|
timestamp: Date.now(),
|
|
972
1221
|
elements: elements.map((e) => {
|
|
973
1222
|
const handlers = extractHandlerNames(e.props);
|
|
974
1223
|
const state = e.getState();
|
|
975
|
-
const visibility
|
|
1224
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1225
|
+
state,
|
|
1226
|
+
this.getClipRectFor(e, viewport)
|
|
1227
|
+
);
|
|
976
1228
|
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
977
1229
|
const vision = projectVisionFields({
|
|
978
1230
|
type: e.type,
|
|
@@ -992,10 +1244,11 @@ var NativeUIBridgeRegistry = class {
|
|
|
992
1244
|
identifier: e.getIdentifier(),
|
|
993
1245
|
state,
|
|
994
1246
|
actions: e.actions,
|
|
995
|
-
customActions:
|
|
1247
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
996
1248
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
997
1249
|
registrationRoute: e.registrationRoute,
|
|
998
1250
|
visibility,
|
|
1251
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
999
1252
|
...bbox ? { bbox } : {},
|
|
1000
1253
|
...vision
|
|
1001
1254
|
};
|
|
@@ -1015,6 +1268,13 @@ var NativeUIBridgeRegistry = class {
|
|
|
1015
1268
|
})),
|
|
1016
1269
|
currentRoute: resolvedRoute.currentRoute,
|
|
1017
1270
|
segments: resolvedRoute.segments,
|
|
1271
|
+
// Canonical aliases, spelled the way the web SDK's `BridgeSnapshot`
|
|
1272
|
+
// spells them so a cross-platform consumer reads one field name on both
|
|
1273
|
+
// platforms. Omitted rather than nulled when unknown — same convention
|
|
1274
|
+
// as the web fields, and it keeps the emitted shape byte-identical for
|
|
1275
|
+
// hosts that wire no route provider.
|
|
1276
|
+
...resolvedRoute.currentRoute ? { route: resolvedRoute.currentRoute } : {},
|
|
1277
|
+
...resolvedRoute.activeTab ? { activeTab: resolvedRoute.activeTab } : {},
|
|
1018
1278
|
registration: this.getRegistrationCoverage()
|
|
1019
1279
|
};
|
|
1020
1280
|
if (this.appInfo) {
|
|
@@ -1117,6 +1377,468 @@ function resetGlobalRegistry() {
|
|
|
1117
1377
|
globalRegistry = null;
|
|
1118
1378
|
}
|
|
1119
1379
|
|
|
1380
|
+
// src/core/action-effect.ts
|
|
1381
|
+
var NATIVE_STANDARD_ACTION_EFFECTS = {
|
|
1382
|
+
// --- Reads ---------------------------------------------------------------
|
|
1383
|
+
focus: "read",
|
|
1384
|
+
blur: "read",
|
|
1385
|
+
scroll: "read",
|
|
1386
|
+
scrollIntoView: "read",
|
|
1387
|
+
longPress: "read",
|
|
1388
|
+
// --- Writes --------------------------------------------------------------
|
|
1389
|
+
press: "write",
|
|
1390
|
+
click: "write",
|
|
1391
|
+
doubleTap: "write",
|
|
1392
|
+
type: "write",
|
|
1393
|
+
setValue: "write",
|
|
1394
|
+
clear: "write",
|
|
1395
|
+
swipe: "write",
|
|
1396
|
+
toggle: "write"
|
|
1397
|
+
};
|
|
1398
|
+
function nativeStandardActionEffect(actionId) {
|
|
1399
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_STANDARD_ACTION_EFFECTS, actionId) ? NATIVE_STANDARD_ACTION_EFFECTS[actionId] : void 0;
|
|
1400
|
+
}
|
|
1401
|
+
function resolveActionEffect(action) {
|
|
1402
|
+
return action.effect ?? nativeStandardActionEffect(action.id);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/core/param-schema.ts
|
|
1406
|
+
var DEFAULT_PARAM_VALIDATION_MODE = "warn";
|
|
1407
|
+
var currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
1408
|
+
function setDefaultParamValidationMode(mode) {
|
|
1409
|
+
currentDefaultMode = mode;
|
|
1410
|
+
}
|
|
1411
|
+
function getDefaultParamValidationMode() {
|
|
1412
|
+
return currentDefaultMode;
|
|
1413
|
+
}
|
|
1414
|
+
function resetDefaultParamValidationMode() {
|
|
1415
|
+
currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
1416
|
+
}
|
|
1417
|
+
var PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
1418
|
+
"string",
|
|
1419
|
+
"number",
|
|
1420
|
+
"integer",
|
|
1421
|
+
"boolean",
|
|
1422
|
+
"object",
|
|
1423
|
+
"array",
|
|
1424
|
+
"null"
|
|
1425
|
+
]);
|
|
1426
|
+
var OBJECT_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1427
|
+
"type",
|
|
1428
|
+
"properties",
|
|
1429
|
+
"required",
|
|
1430
|
+
"additionalProperties",
|
|
1431
|
+
"patternProperties",
|
|
1432
|
+
"propertyNames",
|
|
1433
|
+
"minProperties",
|
|
1434
|
+
"maxProperties",
|
|
1435
|
+
"dependentRequired",
|
|
1436
|
+
"dependentSchemas",
|
|
1437
|
+
"unevaluatedProperties",
|
|
1438
|
+
"allOf",
|
|
1439
|
+
"anyOf",
|
|
1440
|
+
"oneOf",
|
|
1441
|
+
"not",
|
|
1442
|
+
"if",
|
|
1443
|
+
"then",
|
|
1444
|
+
"else",
|
|
1445
|
+
"enum",
|
|
1446
|
+
"const",
|
|
1447
|
+
"default",
|
|
1448
|
+
"examples",
|
|
1449
|
+
"title",
|
|
1450
|
+
"description",
|
|
1451
|
+
"deprecated",
|
|
1452
|
+
"readOnly",
|
|
1453
|
+
"writeOnly",
|
|
1454
|
+
"$schema",
|
|
1455
|
+
"$id",
|
|
1456
|
+
"$ref",
|
|
1457
|
+
"$comment",
|
|
1458
|
+
"$defs",
|
|
1459
|
+
"definitions"
|
|
1460
|
+
]);
|
|
1461
|
+
var MAX_SCHEMA_DEPTH = 32;
|
|
1462
|
+
var MAX_PATTERN_LENGTH = 200;
|
|
1463
|
+
function hasNestedQuantifiers(pattern) {
|
|
1464
|
+
return /(\((?:[^()]*[+*}])[^()]*\))[+*?]|\(\?:[^()]*[+*}][^()]*\)[+*?]/.test(pattern);
|
|
1465
|
+
}
|
|
1466
|
+
function isPlainObject(value) {
|
|
1467
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1468
|
+
}
|
|
1469
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
1470
|
+
function hasOwn(value, key) {
|
|
1471
|
+
return hasOwnProperty.call(value, key);
|
|
1472
|
+
}
|
|
1473
|
+
function render(value) {
|
|
1474
|
+
if (value === null) return "null";
|
|
1475
|
+
if (value === void 0) return "undefined";
|
|
1476
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
1477
|
+
const t = typeof value;
|
|
1478
|
+
if (t === "string") return JSON.stringify(value);
|
|
1479
|
+
if (t === "number" || t === "boolean") return String(value);
|
|
1480
|
+
if (t === "object") return "object";
|
|
1481
|
+
return t;
|
|
1482
|
+
}
|
|
1483
|
+
var MAX_RENDERED_JSON_LENGTH = 120;
|
|
1484
|
+
function renderJson(value) {
|
|
1485
|
+
if (value === void 0) return "undefined";
|
|
1486
|
+
let text;
|
|
1487
|
+
try {
|
|
1488
|
+
text = JSON.stringify(value) ?? String(value);
|
|
1489
|
+
} catch {
|
|
1490
|
+
return render(value);
|
|
1491
|
+
}
|
|
1492
|
+
return text.length > MAX_RENDERED_JSON_LENGTH ? `${text.slice(0, MAX_RENDERED_JSON_LENGTH)}\u2026` : text;
|
|
1493
|
+
}
|
|
1494
|
+
function jsonTypeOf(value) {
|
|
1495
|
+
if (value === null) return "null";
|
|
1496
|
+
if (Array.isArray(value)) return "array";
|
|
1497
|
+
const t = typeof value;
|
|
1498
|
+
if (t === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
1499
|
+
if (t === "string" || t === "boolean" || t === "object") return t;
|
|
1500
|
+
return t;
|
|
1501
|
+
}
|
|
1502
|
+
function matchesType(value, typeName) {
|
|
1503
|
+
switch (typeName) {
|
|
1504
|
+
case "string":
|
|
1505
|
+
return typeof value === "string";
|
|
1506
|
+
case "number":
|
|
1507
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1508
|
+
case "integer":
|
|
1509
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
1510
|
+
case "boolean":
|
|
1511
|
+
return typeof value === "boolean";
|
|
1512
|
+
case "null":
|
|
1513
|
+
return value === null;
|
|
1514
|
+
case "array":
|
|
1515
|
+
return Array.isArray(value);
|
|
1516
|
+
case "object":
|
|
1517
|
+
return isPlainObject(value);
|
|
1518
|
+
default:
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function jsonEquals(a, b, depth = 0) {
|
|
1523
|
+
if (a === b) return true;
|
|
1524
|
+
if (depth > MAX_SCHEMA_DEPTH) return false;
|
|
1525
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
1526
|
+
if (typeof a !== typeof b) return false;
|
|
1527
|
+
if (typeof a !== "object") return false;
|
|
1528
|
+
const aIsArray = Array.isArray(a);
|
|
1529
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
1530
|
+
if (aIsArray) {
|
|
1531
|
+
const bArr = b;
|
|
1532
|
+
const aArr = a;
|
|
1533
|
+
if (aArr.length !== bArr.length) return false;
|
|
1534
|
+
return aArr.every((item, i) => jsonEquals(item, bArr[i], depth + 1));
|
|
1535
|
+
}
|
|
1536
|
+
const aObj = a;
|
|
1537
|
+
const bObj = b;
|
|
1538
|
+
const aKeys = Object.keys(aObj);
|
|
1539
|
+
const bKeys = Object.keys(bObj);
|
|
1540
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1541
|
+
return aKeys.every((key) => hasOwn(bObj, key) && jsonEquals(aObj[key], bObj[key], depth + 1));
|
|
1542
|
+
}
|
|
1543
|
+
function joinPath(base, key) {
|
|
1544
|
+
return base === "" ? key : `${base}.${key}`;
|
|
1545
|
+
}
|
|
1546
|
+
function readTypes(schema) {
|
|
1547
|
+
const raw = schema["type"];
|
|
1548
|
+
if (typeof raw === "string") return [raw];
|
|
1549
|
+
if (Array.isArray(raw)) {
|
|
1550
|
+
const names = raw.filter((t) => typeof t === "string");
|
|
1551
|
+
return names.length > 0 ? names : null;
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
function validateValue(value, schema, path, issues, depth) {
|
|
1556
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
1557
|
+
if (typeof schema === "string") {
|
|
1558
|
+
if (PRIMITIVE_TYPE_NAMES.has(schema) && !matchesType(value, schema)) {
|
|
1559
|
+
issues.push({
|
|
1560
|
+
path,
|
|
1561
|
+
keyword: "type",
|
|
1562
|
+
expected: schema,
|
|
1563
|
+
received: render(value),
|
|
1564
|
+
message: `Parameter "${path}" must be of type ${schema}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (!isPlainObject(schema)) return;
|
|
1570
|
+
const types = readTypes(schema);
|
|
1571
|
+
if (types !== null && !types.some((t) => matchesType(value, t))) {
|
|
1572
|
+
const expected = types.join(" | ");
|
|
1573
|
+
issues.push({
|
|
1574
|
+
path,
|
|
1575
|
+
keyword: "type",
|
|
1576
|
+
expected,
|
|
1577
|
+
received: render(value),
|
|
1578
|
+
message: `Parameter "${path}" must be of type ${expected}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
1579
|
+
});
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (hasOwn(schema, "const") && !jsonEquals(value, schema["const"])) {
|
|
1583
|
+
const expected = renderJson(schema["const"]);
|
|
1584
|
+
const received = renderJson(value);
|
|
1585
|
+
issues.push({
|
|
1586
|
+
path,
|
|
1587
|
+
keyword: "const",
|
|
1588
|
+
expected,
|
|
1589
|
+
received,
|
|
1590
|
+
message: `Parameter "${path}" must equal ${expected}, received ${received}.`
|
|
1591
|
+
});
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const enumValues = schema["enum"];
|
|
1595
|
+
if (Array.isArray(enumValues) && !enumValues.some((allowed) => jsonEquals(value, allowed))) {
|
|
1596
|
+
const expected = enumValues.map((v) => renderJson(v)).join(", ");
|
|
1597
|
+
const received = renderJson(value);
|
|
1598
|
+
issues.push({
|
|
1599
|
+
path,
|
|
1600
|
+
keyword: "enum",
|
|
1601
|
+
expected,
|
|
1602
|
+
received,
|
|
1603
|
+
message: `Parameter "${path}" must be one of [${expected}], received ${received}.`
|
|
1604
|
+
});
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (typeof value === "number") {
|
|
1608
|
+
const min = schema["minimum"];
|
|
1609
|
+
if (typeof min === "number" && value < min) {
|
|
1610
|
+
issues.push({
|
|
1611
|
+
path,
|
|
1612
|
+
keyword: "minimum",
|
|
1613
|
+
expected: `>= ${min}`,
|
|
1614
|
+
received: render(value),
|
|
1615
|
+
message: `Parameter "${path}" must be >= ${min}, received ${value}.`
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
const max = schema["maximum"];
|
|
1619
|
+
if (typeof max === "number" && value > max) {
|
|
1620
|
+
issues.push({
|
|
1621
|
+
path,
|
|
1622
|
+
keyword: "maximum",
|
|
1623
|
+
expected: `<= ${max}`,
|
|
1624
|
+
received: render(value),
|
|
1625
|
+
message: `Parameter "${path}" must be <= ${max}, received ${value}.`
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (typeof value === "string") {
|
|
1630
|
+
const minLength = schema["minLength"];
|
|
1631
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
1632
|
+
issues.push({
|
|
1633
|
+
path,
|
|
1634
|
+
keyword: "minLength",
|
|
1635
|
+
expected: `length >= ${minLength}`,
|
|
1636
|
+
received: `length ${value.length}`,
|
|
1637
|
+
message: `Parameter "${path}" must be at least ${minLength} characters, received ${value.length}.`
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
const maxLength = schema["maxLength"];
|
|
1641
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
1642
|
+
issues.push({
|
|
1643
|
+
path,
|
|
1644
|
+
keyword: "maxLength",
|
|
1645
|
+
expected: `length <= ${maxLength}`,
|
|
1646
|
+
received: `length ${value.length}`,
|
|
1647
|
+
message: `Parameter "${path}" must be at most ${maxLength} characters, received ${value.length}.`
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
const pattern = schema["pattern"];
|
|
1651
|
+
if (typeof pattern === "string" && pattern.length <= MAX_PATTERN_LENGTH && !hasNestedQuantifiers(pattern)) {
|
|
1652
|
+
let re;
|
|
1653
|
+
try {
|
|
1654
|
+
re = new RegExp(pattern);
|
|
1655
|
+
} catch {
|
|
1656
|
+
re = null;
|
|
1657
|
+
}
|
|
1658
|
+
if (re !== null && !re.test(value)) {
|
|
1659
|
+
issues.push({
|
|
1660
|
+
path,
|
|
1661
|
+
keyword: "pattern",
|
|
1662
|
+
expected: `match /${pattern}/`,
|
|
1663
|
+
received: render(value),
|
|
1664
|
+
message: `Parameter "${path}" must match /${pattern}/, received ${render(value)}.`
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (Array.isArray(value) && hasOwn(schema, "items")) {
|
|
1670
|
+
const items = schema["items"];
|
|
1671
|
+
value.forEach((element, index) => {
|
|
1672
|
+
validateValue(element, items, `${path}[${index}]`, issues, depth + 1);
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (isPlainObject(value) && (isPlainObject(schema["properties"]) || hasOwn(schema, "required") || schema["additionalProperties"] === false)) {
|
|
1676
|
+
validateObject(value, schema, path, issues, depth);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function validateObject(value, schema, basePath, issues, depth) {
|
|
1680
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
1681
|
+
const properties = isPlainObject(schema["properties"]) ? schema["properties"] : {};
|
|
1682
|
+
const requiredRaw = schema["required"];
|
|
1683
|
+
const required = Array.isArray(requiredRaw) ? requiredRaw.filter((k) => typeof k === "string") : [];
|
|
1684
|
+
for (const key of required) {
|
|
1685
|
+
if (!hasOwn(value, key) || value[key] === void 0) {
|
|
1686
|
+
const at = joinPath(basePath, key);
|
|
1687
|
+
issues.push({
|
|
1688
|
+
path: at,
|
|
1689
|
+
keyword: "required",
|
|
1690
|
+
expected: "present",
|
|
1691
|
+
message: `Required parameter "${at}" is missing.`
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (schema["additionalProperties"] === false) {
|
|
1696
|
+
const declared = new Set(Object.keys(properties));
|
|
1697
|
+
const expected = declared.size === 0 ? "no parameters" : `one of [${[...declared].join(", ")}]`;
|
|
1698
|
+
for (const key of Object.keys(value)) {
|
|
1699
|
+
if (!declared.has(key)) {
|
|
1700
|
+
const at = joinPath(basePath, key);
|
|
1701
|
+
issues.push({
|
|
1702
|
+
path: at,
|
|
1703
|
+
keyword: "additionalProperties",
|
|
1704
|
+
expected,
|
|
1705
|
+
received: render(value[key]),
|
|
1706
|
+
message: `Parameter "${at}" is not declared by the schema, which sets additionalProperties: false.`
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
1712
|
+
if (!hasOwn(value, key) || value[key] === void 0) continue;
|
|
1713
|
+
validateValue(value[key], propSchema, joinPath(basePath, key), issues, depth + 1);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function isObjectSchemaForm(schema) {
|
|
1717
|
+
if (isPlainObject(schema["properties"])) return true;
|
|
1718
|
+
if (schema["type"] !== "object") return false;
|
|
1719
|
+
return Object.keys(schema).every((key) => OBJECT_SCHEMA_KEYWORDS.has(key));
|
|
1720
|
+
}
|
|
1721
|
+
function normalizeMapForm(schema) {
|
|
1722
|
+
if (isObjectSchemaForm(schema)) return null;
|
|
1723
|
+
const properties = {};
|
|
1724
|
+
for (const [key, raw] of Object.entries(schema)) {
|
|
1725
|
+
if (raw === null || raw === void 0) continue;
|
|
1726
|
+
if (typeof raw === "string") {
|
|
1727
|
+
properties[key] = PRIMITIVE_TYPE_NAMES.has(raw) ? { type: raw } : {};
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (isPlainObject(raw)) {
|
|
1731
|
+
const { optional: _optional, ...rest } = raw;
|
|
1732
|
+
properties[key] = rest;
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return { type: "object", properties };
|
|
1737
|
+
}
|
|
1738
|
+
function validateActionParams(schema, params) {
|
|
1739
|
+
if (!isPlainObject(schema)) return { valid: true, issues: [] };
|
|
1740
|
+
const normalized = normalizeMapForm(schema) ?? schema;
|
|
1741
|
+
const hasProperties = isPlainObject(normalized["properties"]);
|
|
1742
|
+
const hasRequired = Array.isArray(normalized["required"]);
|
|
1743
|
+
const forbidsExtras = normalized["additionalProperties"] === false;
|
|
1744
|
+
if (!hasProperties && !hasRequired && !forbidsExtras) {
|
|
1745
|
+
return { valid: true, issues: [] };
|
|
1746
|
+
}
|
|
1747
|
+
const issues = [];
|
|
1748
|
+
if (params === void 0 || params === null) {
|
|
1749
|
+
validateObject({}, normalized, "", issues, 0);
|
|
1750
|
+
return { valid: issues.length === 0, issues };
|
|
1751
|
+
}
|
|
1752
|
+
if (!isPlainObject(params)) {
|
|
1753
|
+
issues.push({
|
|
1754
|
+
path: "",
|
|
1755
|
+
keyword: "type",
|
|
1756
|
+
expected: "object",
|
|
1757
|
+
received: render(params),
|
|
1758
|
+
message: `Action params must be an object, received ${jsonTypeOf(params)} (${render(params)}).`
|
|
1759
|
+
});
|
|
1760
|
+
return { valid: false, issues };
|
|
1761
|
+
}
|
|
1762
|
+
validateObject(params, normalized, "", issues, 0);
|
|
1763
|
+
return { valid: issues.length === 0, issues };
|
|
1764
|
+
}
|
|
1765
|
+
function formatParamValidationFailure(componentId, actionId, issues) {
|
|
1766
|
+
const detail = issues.map((i) => i.message).join(" ");
|
|
1767
|
+
return `Action "${actionId}" on component "${componentId}" was rejected: params do not match the action's declared paramSchema. ${detail}`;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/core/abortable.ts
|
|
1771
|
+
var MAX_ACTION_TIMEOUT_MS = 864e5;
|
|
1772
|
+
function normalizeActionTimeoutMs(raw) {
|
|
1773
|
+
if (raw === void 0 || raw === null) {
|
|
1774
|
+
return { ok: true, timeoutMs: void 0, clamped: false };
|
|
1775
|
+
}
|
|
1776
|
+
if (typeof raw !== "number") {
|
|
1777
|
+
return {
|
|
1778
|
+
ok: false,
|
|
1779
|
+
reason: `timeoutMs must be a number of milliseconds, received ${typeof raw === "string" ? JSON.stringify(raw) : typeof raw}`
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
if (!Number.isFinite(raw)) {
|
|
1783
|
+
return {
|
|
1784
|
+
ok: false,
|
|
1785
|
+
reason: `timeoutMs must be a finite number of milliseconds, received ${String(raw)}`
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (raw < 0) {
|
|
1789
|
+
return {
|
|
1790
|
+
ok: false,
|
|
1791
|
+
reason: `timeoutMs must not be negative, received ${String(raw)}`
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
if (raw > MAX_ACTION_TIMEOUT_MS) {
|
|
1795
|
+
return { ok: true, timeoutMs: MAX_ACTION_TIMEOUT_MS, clamped: true };
|
|
1796
|
+
}
|
|
1797
|
+
return { ok: true, timeoutMs: Math.floor(raw), clamped: false };
|
|
1798
|
+
}
|
|
1799
|
+
function inertAbortSignal() {
|
|
1800
|
+
return new AbortController().signal;
|
|
1801
|
+
}
|
|
1802
|
+
async function runAbortable(invoke, options = {}) {
|
|
1803
|
+
const callerSignal = options.signal;
|
|
1804
|
+
const timeoutMs = options.timeoutMs;
|
|
1805
|
+
if (callerSignal?.aborted) {
|
|
1806
|
+
return { aborted: true, reason: "signal" };
|
|
1807
|
+
}
|
|
1808
|
+
const controller = new AbortController();
|
|
1809
|
+
let reason = "signal";
|
|
1810
|
+
let timer;
|
|
1811
|
+
const onCallerAbort = () => {
|
|
1812
|
+
reason = "signal";
|
|
1813
|
+
controller.abort();
|
|
1814
|
+
};
|
|
1815
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1816
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0) {
|
|
1817
|
+
timer = setTimeout(
|
|
1818
|
+
() => {
|
|
1819
|
+
reason = "timeout";
|
|
1820
|
+
controller.abort();
|
|
1821
|
+
},
|
|
1822
|
+
Math.min(timeoutMs, MAX_ACTION_TIMEOUT_MS)
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
const abandoned = new Promise((resolve) => {
|
|
1827
|
+
controller.signal.addEventListener("abort", () => resolve({ aborted: true, reason }), {
|
|
1828
|
+
once: true
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
const completed = (async () => ({
|
|
1832
|
+
aborted: false,
|
|
1833
|
+
result: await invoke(controller.signal)
|
|
1834
|
+
}))();
|
|
1835
|
+
return await Promise.race([completed, abandoned]);
|
|
1836
|
+
} finally {
|
|
1837
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1838
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1120
1842
|
// src/core/element-identifier.ts
|
|
1121
1843
|
function createNativeElementIdentifier(id, options = {}) {
|
|
1122
1844
|
return {
|
|
@@ -1221,6 +1943,68 @@ var DEFAULT_WAIT_OPTIONS = {
|
|
|
1221
1943
|
timeout: 1e4,
|
|
1222
1944
|
interval: 100
|
|
1223
1945
|
};
|
|
1946
|
+
var NativeActionError = class extends Error {
|
|
1947
|
+
constructor(message, code) {
|
|
1948
|
+
super(message);
|
|
1949
|
+
this.code = code;
|
|
1950
|
+
this.name = "NativeActionError";
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var DEFAULT_SCROLL_INTO_VIEW_PADDING = 16;
|
|
1954
|
+
var SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS = 1e3;
|
|
1955
|
+
var SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS = 250;
|
|
1956
|
+
function measurePageRectNow(element, preferred) {
|
|
1957
|
+
const preferredNode = preferred;
|
|
1958
|
+
const node = preferredNode && typeof preferredNode.measureInWindow === "function" ? preferredNode : element.ref.current;
|
|
1959
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
1960
|
+
const state = element.getState();
|
|
1961
|
+
return Promise.resolve(state.visible ? pageRectOf(state) : null);
|
|
1962
|
+
}
|
|
1963
|
+
return new Promise((resolve) => {
|
|
1964
|
+
let settled = false;
|
|
1965
|
+
const done = (rect) => {
|
|
1966
|
+
if (settled) return;
|
|
1967
|
+
settled = true;
|
|
1968
|
+
clearTimeout(timer);
|
|
1969
|
+
resolve(rect);
|
|
1970
|
+
};
|
|
1971
|
+
const timer = setTimeout(() => done(null), SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS);
|
|
1972
|
+
try {
|
|
1973
|
+
node.measureInWindow(
|
|
1974
|
+
(x, y, w, h) => done(
|
|
1975
|
+
[x, y, w, h].every(Number.isFinite) && w > 0 && h > 0 ? { left: x, top: y, right: x + w, bottom: y + h } : null
|
|
1976
|
+
)
|
|
1977
|
+
);
|
|
1978
|
+
} catch {
|
|
1979
|
+
done(null);
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
function resolveScrollView(current) {
|
|
1984
|
+
const c = current;
|
|
1985
|
+
if (!c) return null;
|
|
1986
|
+
if (typeof c.scrollTo === "function")
|
|
1987
|
+
return c;
|
|
1988
|
+
for (const unwrap of [c.getNativeScrollRef, c.getScrollResponder]) {
|
|
1989
|
+
if (typeof unwrap !== "function") continue;
|
|
1990
|
+
let inner;
|
|
1991
|
+
try {
|
|
1992
|
+
inner = unwrap.call(c);
|
|
1993
|
+
} catch {
|
|
1994
|
+
inner = null;
|
|
1995
|
+
}
|
|
1996
|
+
if (inner && typeof inner.scrollTo === "function")
|
|
1997
|
+
return inner;
|
|
1998
|
+
}
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
function isDeclaredHorizontal(registeredProps, ...refs) {
|
|
2002
|
+
if (registeredProps?.horizontal === true) return true;
|
|
2003
|
+
return refs.some((r) => {
|
|
2004
|
+
const props = r?.props;
|
|
2005
|
+
return props !== null && typeof props === "object" && props.horizontal === true;
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
1224
2008
|
function sleep(ms) {
|
|
1225
2009
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1226
2010
|
}
|
|
@@ -1340,6 +2124,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
1340
2124
|
return {
|
|
1341
2125
|
success: false,
|
|
1342
2126
|
error: errorMsg,
|
|
2127
|
+
...error3 instanceof NativeActionError ? { code: error3.code } : {},
|
|
1343
2128
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
1344
2129
|
durationMs: failTime - startTime,
|
|
1345
2130
|
timestamp: failTime,
|
|
@@ -1357,7 +2142,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
1357
2142
|
}
|
|
1358
2143
|
const props = element.props || {};
|
|
1359
2144
|
if (element.customActions && action in element.customActions) {
|
|
1360
|
-
return element.customActions[action].handler(params);
|
|
2145
|
+
return element.customActions[action].handler(params, { signal: inertAbortSignal() });
|
|
1361
2146
|
}
|
|
1362
2147
|
switch (action) {
|
|
1363
2148
|
case "click":
|
|
@@ -1380,6 +2165,11 @@ var DefaultNativeActionExecutor = class {
|
|
|
1380
2165
|
return this.performBlur(element);
|
|
1381
2166
|
case "scroll":
|
|
1382
2167
|
return this.performScroll(props, params);
|
|
2168
|
+
case "scrollIntoView":
|
|
2169
|
+
return this.performScrollIntoView(
|
|
2170
|
+
element,
|
|
2171
|
+
params
|
|
2172
|
+
);
|
|
1383
2173
|
case "swipe":
|
|
1384
2174
|
return this.performSwipe(props, params);
|
|
1385
2175
|
case "toggle":
|
|
@@ -1546,6 +2336,138 @@ var DefaultNativeActionExecutor = class {
|
|
|
1546
2336
|
props.onScroll(event);
|
|
1547
2337
|
}
|
|
1548
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Perform scrollIntoView: scroll the element's DECLARED scroll container so
|
|
2341
|
+
* the element sits `padding` dp below the top of its viewport.
|
|
2342
|
+
*
|
|
2343
|
+
* The container comes from `scrollAncestorId` and nowhere else — the
|
|
2344
|
+
* registry keeps no parent chain (`RegisterElementOptions.scrollAncestorId`),
|
|
2345
|
+
* so there is no "nearest ScrollView" to find. Nothing here tracks a
|
|
2346
|
+
* ScrollView's content offset either (`performScroll` only fakes an
|
|
2347
|
+
* `onScroll` event), so the target is measured CONTENT-relative with
|
|
2348
|
+
* `measureLayout` against the container's inner content view: stateless,
|
|
2349
|
+
* and immune to a stale-offset race.
|
|
2350
|
+
*
|
|
2351
|
+
* Result shape mirrors the web executor's `scrollIntoView`
|
|
2352
|
+
* (`@qontinui/ui-bridge` `control/action-executor.ts`):
|
|
2353
|
+
* `{ alreadyVisible, scrolled }`, short-circuiting when the element is
|
|
2354
|
+
* already fully inside its clip region. "Fully visible" is only claimed when
|
|
2355
|
+
* it is MEASURED — a page-space rect for the element and a known,
|
|
2356
|
+
* non-empty clip (window ∩ declared container), both measured at call
|
|
2357
|
+
* time. Anything unknown scrolls.
|
|
2358
|
+
*
|
|
2359
|
+
* Containers: a ScrollView, or a VirtualizedList (FlatList / SectionList)
|
|
2360
|
+
* unwrapped through `getNativeScrollRef()` / `getScrollResponder()`.
|
|
2361
|
+
* Vertical only — a `horizontal` container is NOT_SUPPORTED. The action
|
|
2362
|
+
* returns once `scrollTo` is dispatched; positions settle on the next frame.
|
|
2363
|
+
*
|
|
2364
|
+
* Prerequisites are checked BEFORE the visibility short-circuit, so whether
|
|
2365
|
+
* the element supports the action does not depend on where it happens to be
|
|
2366
|
+
* on screen. Each missing prerequisite is a `NOT_SUPPORTED` failure.
|
|
2367
|
+
*/
|
|
2368
|
+
async performScrollIntoView(element, params) {
|
|
2369
|
+
const padding = params?.padding ?? DEFAULT_SCROLL_INTO_VIEW_PADDING;
|
|
2370
|
+
if (typeof padding !== "number" || !Number.isFinite(padding) || padding < 0) {
|
|
2371
|
+
throw new Error('scrollIntoView "padding" must be a finite, non-negative number');
|
|
2372
|
+
}
|
|
2373
|
+
const ancestorId = element.scrollAncestorId;
|
|
2374
|
+
if (!ancestorId) {
|
|
2375
|
+
throw new NativeActionError(
|
|
2376
|
+
`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`,
|
|
2377
|
+
"NOT_SUPPORTED"
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const ancestor = ancestorId === element.id ? void 0 : this.registry.getElement(ancestorId);
|
|
2381
|
+
if (!ancestor) {
|
|
2382
|
+
throw new NativeActionError(
|
|
2383
|
+
`scrollIntoView is not supported on element "${element.id}": its declared scrollAncestorId "${ancestorId}" is not a registered element`,
|
|
2384
|
+
"NOT_SUPPORTED"
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const container = resolveScrollView(ancestor.ref.current);
|
|
2388
|
+
if (!container) {
|
|
2389
|
+
throw new NativeActionError(
|
|
2390
|
+
`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)`,
|
|
2391
|
+
"NOT_SUPPORTED"
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
if (isDeclaredHorizontal(ancestor.props, ancestor.ref.current, container)) {
|
|
2395
|
+
throw new NativeActionError(
|
|
2396
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" is horizontal (only vertical containers are supported)`,
|
|
2397
|
+
"NOT_SUPPORTED"
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
const innerViewRef = container.getInnerViewRef?.();
|
|
2401
|
+
const innerView = innerViewRef ?? container.getInnerViewNode?.();
|
|
2402
|
+
const innerViewIsTag = innerViewRef == null && typeof innerView === "number";
|
|
2403
|
+
if (innerView === void 0 || innerView === null) {
|
|
2404
|
+
throw new NativeActionError(
|
|
2405
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" exposes no inner content view to measure against`,
|
|
2406
|
+
"NOT_SUPPORTED"
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
const target = element.ref.current;
|
|
2410
|
+
if (!target || typeof target.measureLayout !== "function") {
|
|
2411
|
+
throw new NativeActionError(
|
|
2412
|
+
`scrollIntoView is not supported on element "${element.id}": its ref has no measureLayout (not a mounted host view)`,
|
|
2413
|
+
"NOT_SUPPORTED"
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
const [rect, ancestorRect] = await Promise.all([
|
|
2417
|
+
measurePageRectNow(element),
|
|
2418
|
+
measurePageRectNow(ancestor, container)
|
|
2419
|
+
]);
|
|
2420
|
+
const viewport = this.registry.getViewportRect();
|
|
2421
|
+
const clip = ancestorRect ? intersectRects(viewport, ancestorRect) : null;
|
|
2422
|
+
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) {
|
|
2423
|
+
return { alreadyVisible: true, scrolled: false };
|
|
2424
|
+
}
|
|
2425
|
+
const measured = await new Promise(
|
|
2426
|
+
(resolve, reject) => {
|
|
2427
|
+
let settled = false;
|
|
2428
|
+
const timer = setTimeout(() => {
|
|
2429
|
+
if (settled) return;
|
|
2430
|
+
settled = true;
|
|
2431
|
+
reject(
|
|
2432
|
+
new Error(
|
|
2433
|
+
`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)" : "")
|
|
2434
|
+
)
|
|
2435
|
+
);
|
|
2436
|
+
}, SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS);
|
|
2437
|
+
const finish = (fn) => {
|
|
2438
|
+
if (settled) return;
|
|
2439
|
+
settled = true;
|
|
2440
|
+
clearTimeout(timer);
|
|
2441
|
+
fn();
|
|
2442
|
+
};
|
|
2443
|
+
try {
|
|
2444
|
+
target.measureLayout(
|
|
2445
|
+
innerView,
|
|
2446
|
+
(x, y, width) => finish(
|
|
2447
|
+
() => Number.isFinite(y) ? resolve({ x, y, width }) : reject(new Error("scrollIntoView: measureLayout returned a non-finite y"))
|
|
2448
|
+
),
|
|
2449
|
+
() => finish(
|
|
2450
|
+
() => reject(
|
|
2451
|
+
new Error(
|
|
2452
|
+
`scrollIntoView: measureLayout failed for "${element.id}" relative to "${ancestorId}"`
|
|
2453
|
+
)
|
|
2454
|
+
)
|
|
2455
|
+
)
|
|
2456
|
+
);
|
|
2457
|
+
} catch (error3) {
|
|
2458
|
+
finish(() => reject(error3));
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
if (ancestorRect && Number.isFinite(measured.x) && Number.isFinite(measured.width) && measured.x + measured.width > ancestorRect.right - ancestorRect.left + 1) {
|
|
2463
|
+
throw new NativeActionError(
|
|
2464
|
+
`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`,
|
|
2465
|
+
"NOT_SUPPORTED"
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
container.scrollTo({ y: Math.max(0, measured.y - padding), animated: false });
|
|
2469
|
+
return { alreadyVisible: false, scrolled: true };
|
|
2470
|
+
}
|
|
1549
2471
|
/**
|
|
1550
2472
|
* Perform swipe action
|
|
1551
2473
|
*/
|
|
@@ -1600,7 +2522,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
1600
2522
|
/**
|
|
1601
2523
|
* Execute a component action
|
|
1602
2524
|
*/
|
|
1603
|
-
async executeComponentAction(componentId, request) {
|
|
2525
|
+
async executeComponentAction(componentId, request, options = {}) {
|
|
1604
2526
|
const startTime = Date.now();
|
|
1605
2527
|
try {
|
|
1606
2528
|
const component = this.registry.getComponent(componentId);
|
|
@@ -1623,10 +2545,71 @@ var DefaultNativeActionExecutor = class {
|
|
|
1623
2545
|
requestId: request.requestId
|
|
1624
2546
|
};
|
|
1625
2547
|
}
|
|
1626
|
-
const
|
|
2548
|
+
const timeout = normalizeActionTimeoutMs(request.timeoutMs);
|
|
2549
|
+
if (!timeout.ok) {
|
|
2550
|
+
return {
|
|
2551
|
+
success: false,
|
|
2552
|
+
error: `Action "${request.action}" on component "${componentId}" was rejected: ${timeout.reason}.`,
|
|
2553
|
+
durationMs: Date.now() - startTime,
|
|
2554
|
+
timestamp: Date.now(),
|
|
2555
|
+
requestId: request.requestId
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
const timeoutMs = timeout.timeoutMs;
|
|
2559
|
+
const validationMode = options.paramValidation ?? getDefaultParamValidationMode();
|
|
2560
|
+
if (validationMode !== "off" && action.paramSchema !== void 0) {
|
|
2561
|
+
let validation;
|
|
2562
|
+
try {
|
|
2563
|
+
validation = validateActionParams(action.paramSchema, request.params);
|
|
2564
|
+
} catch (validatorFault) {
|
|
2565
|
+
const detail = validatorFault instanceof Error ? validatorFault.message : String(validatorFault);
|
|
2566
|
+
const faultMessage = `Action "${request.action}" on component "${componentId}": its declared paramSchema could not be evaluated (${detail}).`;
|
|
2567
|
+
if (validationMode === "enforce") {
|
|
2568
|
+
return {
|
|
2569
|
+
success: false,
|
|
2570
|
+
error: faultMessage,
|
|
2571
|
+
durationMs: Date.now() - startTime,
|
|
2572
|
+
timestamp: Date.now(),
|
|
2573
|
+
requestId: request.requestId
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
console.warn(`[ui-bridge-native] ${faultMessage}`);
|
|
2577
|
+
validation = { valid: true, issues: [] };
|
|
2578
|
+
}
|
|
2579
|
+
if (!validation.valid) {
|
|
2580
|
+
const message = formatParamValidationFailure(
|
|
2581
|
+
componentId,
|
|
2582
|
+
request.action,
|
|
2583
|
+
validation.issues
|
|
2584
|
+
);
|
|
2585
|
+
if (validationMode === "enforce") {
|
|
2586
|
+
return {
|
|
2587
|
+
success: false,
|
|
2588
|
+
error: message,
|
|
2589
|
+
durationMs: Date.now() - startTime,
|
|
2590
|
+
timestamp: Date.now(),
|
|
2591
|
+
requestId: request.requestId
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
console.warn(`[ui-bridge-native] ${message}`);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const outcome = await runAbortable((signal) => action.handler(request.params, { signal }), {
|
|
2598
|
+
signal: options.signal,
|
|
2599
|
+
timeoutMs
|
|
2600
|
+
});
|
|
2601
|
+
if (outcome.aborted) {
|
|
2602
|
+
return {
|
|
2603
|
+
success: false,
|
|
2604
|
+
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.`,
|
|
2605
|
+
durationMs: Date.now() - startTime,
|
|
2606
|
+
timestamp: Date.now(),
|
|
2607
|
+
requestId: request.requestId
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
1627
2610
|
return {
|
|
1628
2611
|
success: true,
|
|
1629
|
-
result,
|
|
2612
|
+
result: outcome.result,
|
|
1630
2613
|
durationMs: Date.now() - startTime,
|
|
1631
2614
|
timestamp: Date.now(),
|
|
1632
2615
|
requestId: request.requestId
|
|
@@ -2700,10 +3683,14 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2700
3683
|
if (forRoute) {
|
|
2701
3684
|
allElements = allElements.filter((e) => matchesCurrentRoute(e.registrationRoute, forRoute));
|
|
2702
3685
|
}
|
|
3686
|
+
const viewport = registry.getViewportRect();
|
|
2703
3687
|
const elements = allElements.map((e) => {
|
|
2704
3688
|
const handlers = extractHandlerNames(e.props);
|
|
2705
3689
|
const state = e.getState();
|
|
2706
|
-
const visibility
|
|
3690
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
3691
|
+
state,
|
|
3692
|
+
registry.getClipRectFor(e, viewport)
|
|
3693
|
+
);
|
|
2707
3694
|
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2708
3695
|
const vision = projectVisionFields({
|
|
2709
3696
|
type: e.type,
|
|
@@ -2721,10 +3708,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2721
3708
|
identifier: e.getIdentifier(),
|
|
2722
3709
|
state,
|
|
2723
3710
|
actions: e.actions,
|
|
2724
|
-
customActions:
|
|
3711
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
2725
3712
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2726
3713
|
registrationRoute: e.registrationRoute,
|
|
2727
3714
|
visibility,
|
|
3715
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
2728
3716
|
...bbox ? { bbox } : {},
|
|
2729
3717
|
...vision
|
|
2730
3718
|
};
|
|
@@ -2746,7 +3734,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2746
3734
|
identifier: element.getIdentifier(),
|
|
2747
3735
|
state: resolveElementState(element),
|
|
2748
3736
|
actions: element.actions,
|
|
2749
|
-
customActions:
|
|
3737
|
+
customActions: serializeElementCustomActions(element.customActions),
|
|
2750
3738
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2751
3739
|
registrationRoute: element.registrationRoute
|
|
2752
3740
|
}
|
|
@@ -2767,13 +3755,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2767
3755
|
if (!envelope?.action) {
|
|
2768
3756
|
return error2("Action is required", "INVALID_REQUEST");
|
|
2769
3757
|
}
|
|
2770
|
-
const response = await executor.executeAction(id,
|
|
2771
|
-
action: envelope.action,
|
|
2772
|
-
params: envelope.params,
|
|
2773
|
-
waitOptions: envelope.waitOptions
|
|
2774
|
-
});
|
|
3758
|
+
const response = await executor.executeAction(id, envelope);
|
|
2775
3759
|
if (!response.success) {
|
|
2776
|
-
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
3760
|
+
return error2(response.error || "Action failed", response.code ?? "ACTION_FAILED");
|
|
2777
3761
|
}
|
|
2778
3762
|
return success2(response);
|
|
2779
3763
|
},
|
|
@@ -2783,7 +3767,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2783
3767
|
id: c.id,
|
|
2784
3768
|
name: c.name,
|
|
2785
3769
|
description: c.description,
|
|
2786
|
-
|
|
3770
|
+
// Phase 4 (plan 2026-08-20-ui-bridge-action-declaration-shape):
|
|
3771
|
+
// carry the safety annotation onto the native wire too — a walker
|
|
3772
|
+
// driving a React Native app needs the same `destructive` exclusion
|
|
3773
|
+
// the web channel gets.
|
|
3774
|
+
actions: c.actions.map((a) => ({ id: a.id, label: a.label, effect: a.effect })),
|
|
2787
3775
|
elementIds: c.elementIds
|
|
2788
3776
|
}));
|
|
2789
3777
|
return success2({ components });
|
|
@@ -2802,7 +3790,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2802
3790
|
actions: component.actions.map((a) => ({
|
|
2803
3791
|
id: a.id,
|
|
2804
3792
|
label: a.label,
|
|
2805
|
-
description: a.description
|
|
3793
|
+
description: a.description,
|
|
3794
|
+
// Phase 4 — see the listing twin above.
|
|
3795
|
+
effect: a.effect
|
|
2806
3796
|
})),
|
|
2807
3797
|
elementIds: component.elementIds
|
|
2808
3798
|
}
|
|
@@ -2813,7 +3803,8 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2813
3803
|
const body = ctx.body;
|
|
2814
3804
|
const response = await executor.executeComponentAction(id, {
|
|
2815
3805
|
action: actionId,
|
|
2816
|
-
params: body?.params
|
|
3806
|
+
params: body?.params,
|
|
3807
|
+
timeoutMs: body?.timeoutMs
|
|
2817
3808
|
});
|
|
2818
3809
|
if (!response.success) {
|
|
2819
3810
|
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
@@ -3005,9 +3996,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3005
3996
|
//
|
|
3006
3997
|
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
3007
3998
|
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
3008
|
-
// get a structured envelope explaining the gap rather than a
|
|
3009
|
-
//
|
|
3010
|
-
//
|
|
3999
|
+
// get a structured envelope explaining the gap rather than a bare
|
|
4000
|
+
// "no such route". These answer **HTTP 501** — `httpStatusForResponse` in
|
|
4001
|
+
// server/http-server.ts maps NOT_SUPPORTED → 501, so the status line and
|
|
4002
|
+
// the envelope agree; an unmounted path answers 404 instead. Each error
|
|
4003
|
+
// message names an in-tree mobile replacement so operators aren't left
|
|
4004
|
+
// guessing what to use instead.
|
|
3011
4005
|
aiForms: async () => {
|
|
3012
4006
|
return error2(
|
|
3013
4007
|
'ai/forms (form discovery) is runner-only; mobile React Native has no DOM to walk. Use GET /ui-bridge/control/snapshot and filter for type === "input" instead.',
|
|
@@ -3334,18 +4328,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3334
4328
|
pushModal: async (ctx) => {
|
|
3335
4329
|
const detector = registry.getEnrichers().modalDetector;
|
|
3336
4330
|
if (!detector) {
|
|
3337
|
-
return error2(
|
|
3338
|
-
"modal detector not registered",
|
|
3339
|
-
"ENRICHER_UNAVAILABLE"
|
|
3340
|
-
);
|
|
4331
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
3341
4332
|
}
|
|
3342
4333
|
const body = ctx.body;
|
|
3343
4334
|
const id = body?.id;
|
|
3344
4335
|
if (typeof id !== "string" || id.length === 0) {
|
|
3345
|
-
return error2(
|
|
3346
|
-
'pushModal requires a non-empty "id"',
|
|
3347
|
-
"INVALID_REQUEST"
|
|
3348
|
-
);
|
|
4336
|
+
return error2('pushModal requires a non-empty "id"', "INVALID_REQUEST");
|
|
3349
4337
|
}
|
|
3350
4338
|
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
3351
4339
|
const requestedType = body?.type;
|
|
@@ -3378,10 +4366,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3378
4366
|
dismissModal: async (ctx) => {
|
|
3379
4367
|
const detector = registry.getEnrichers().modalDetector;
|
|
3380
4368
|
if (!detector) {
|
|
3381
|
-
return error2(
|
|
3382
|
-
"modal detector not registered",
|
|
3383
|
-
"ENRICHER_UNAVAILABLE"
|
|
3384
|
-
);
|
|
4369
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
3385
4370
|
}
|
|
3386
4371
|
const { id } = ctx.params;
|
|
3387
4372
|
if (typeof id !== "string" || id.length === 0) {
|
|
@@ -3411,7 +4396,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3411
4396
|
};
|
|
3412
4397
|
if (config?.appInfo) {
|
|
3413
4398
|
response.uiBridge = {
|
|
3414
|
-
version: "0.6.
|
|
4399
|
+
version: "0.6.14",
|
|
3415
4400
|
...config.appInfo,
|
|
3416
4401
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3417
4402
|
elementCount: stats.elements,
|
|
@@ -3559,8 +4544,48 @@ function extractMethod(input, init) {
|
|
|
3559
4544
|
var NetworkRequestBuffer = class {
|
|
3560
4545
|
constructor(capacity = 100) {
|
|
3561
4546
|
this.installed = false;
|
|
4547
|
+
/**
|
|
4548
|
+
* Synchronous re-entrancy depth: non-zero while the ORIGINAL `fetch` is
|
|
4549
|
+
* executing, which is exactly the window in which React Native's whatwg-fetch
|
|
4550
|
+
* polyfill constructs and opens its internal `XMLHttpRequest`.
|
|
4551
|
+
*
|
|
4552
|
+
* A counter rather than a boolean so a `fetch` called from inside a `fetch`
|
|
4553
|
+
* (an interceptor, a retry wrapper) still unwinds to zero. Safe as a plain
|
|
4554
|
+
* number: JS is single-threaded and the only writes are the paired
|
|
4555
|
+
* increment/decrement around one synchronous call.
|
|
4556
|
+
*/
|
|
4557
|
+
this.fetchDepth = { value: 0 };
|
|
3562
4558
|
this.buffer = new RingBuffer(capacity);
|
|
3563
4559
|
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Patch `fetch` and `XMLHttpRequest` so `GET /sdk/network-requests` can
|
|
4562
|
+
* report what the app talked to.
|
|
4563
|
+
*
|
|
4564
|
+
* ONE REQUEST MUST PRODUCE ONE ENTRY. React Native implements `fetch` **on
|
|
4565
|
+
* top of** XHR (whatwg-fetch), so both patches observe the same request and
|
|
4566
|
+
* the buffer used to carry every call twice — same url, timestamps ~1ms
|
|
4567
|
+
* apart, and on failure one `"Network request failed"` beside one
|
|
4568
|
+
* `"XMLHttpRequest error"`. A tester reads that as a double-fetch bug in the
|
|
4569
|
+
* app and chases a defect that does not exist.
|
|
4570
|
+
*
|
|
4571
|
+
* The de-dup: `fetchDepth` is raised across the synchronous call into the
|
|
4572
|
+
* original `fetch`, which is when the polyfill builds its XHR; `open` stamps
|
|
4573
|
+
* any XHR created in that window with `__uiBridgeFromFetch`, and `send`
|
|
4574
|
+
* skips recording those. The `fetch` patch keeps the entry, because it is
|
|
4575
|
+
* the layer the app actually called and the only one that sees `Response.ok`.
|
|
4576
|
+
*
|
|
4577
|
+
* Uninstalling both patches together keeps this sound: an XHR can only be
|
|
4578
|
+
* marked while the fetch patch is live.
|
|
4579
|
+
*
|
|
4580
|
+
* KNOWN LIMIT — it is coupled to the polyfill building its XHR SYNCHRONOUSLY,
|
|
4581
|
+
* which whatwg-fetch does (its work happens in the Promise executor). If the
|
|
4582
|
+
* host's `fetch` is wrapped by an interceptor that defers the real call to a
|
|
4583
|
+
* microtask or `setTimeout`, the depth is back to 0 by the time `open` runs
|
|
4584
|
+
* and every request duplicates again — the original symptom, with no signal
|
|
4585
|
+
* that the mechanism disengaged. Duplicates in `/sdk/network-requests` are
|
|
4586
|
+
* the thing to look for; they mean this assumption stopped holding, not that
|
|
4587
|
+
* the app double-fetched.
|
|
4588
|
+
*/
|
|
3564
4589
|
install() {
|
|
3565
4590
|
if (this.installed) return;
|
|
3566
4591
|
const g = globalThis;
|
|
@@ -3568,12 +4593,20 @@ var NetworkRequestBuffer = class {
|
|
|
3568
4593
|
this.originalFetch = g.fetch;
|
|
3569
4594
|
const buffer = this.buffer;
|
|
3570
4595
|
const original = this.originalFetch;
|
|
4596
|
+
const fetchDepth = this.fetchDepth;
|
|
3571
4597
|
const patched = async (input, init) => {
|
|
3572
4598
|
const start = Date.now();
|
|
3573
4599
|
const url = extractUrl(input);
|
|
3574
4600
|
const method = extractMethod(input, init);
|
|
3575
4601
|
try {
|
|
3576
|
-
|
|
4602
|
+
let pending;
|
|
4603
|
+
fetchDepth.value += 1;
|
|
4604
|
+
try {
|
|
4605
|
+
pending = original.call(globalThis, input, init);
|
|
4606
|
+
} finally {
|
|
4607
|
+
fetchDepth.value -= 1;
|
|
4608
|
+
}
|
|
4609
|
+
const res = await pending;
|
|
3577
4610
|
buffer.push({
|
|
3578
4611
|
timestamp: start,
|
|
3579
4612
|
method,
|
|
@@ -3603,10 +4636,14 @@ var NetworkRequestBuffer = class {
|
|
|
3603
4636
|
this.originalXhrOpen = Xhr.prototype.open;
|
|
3604
4637
|
this.originalXhrSend = Xhr.prototype.send;
|
|
3605
4638
|
const buffer = this.buffer;
|
|
4639
|
+
const fetchDepth = this.fetchDepth;
|
|
3606
4640
|
const origOpen = this.originalXhrOpen;
|
|
3607
4641
|
Xhr.prototype.open = function(method, url, ...rest) {
|
|
3608
4642
|
this.__uiBridgeMethod = method.toUpperCase();
|
|
3609
4643
|
this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
|
|
4644
|
+
if (fetchDepth.value > 0) {
|
|
4645
|
+
this.__uiBridgeFromFetch = true;
|
|
4646
|
+
}
|
|
3610
4647
|
return origOpen.call(
|
|
3611
4648
|
this,
|
|
3612
4649
|
method,
|
|
@@ -3616,6 +4653,9 @@ var NetworkRequestBuffer = class {
|
|
|
3616
4653
|
};
|
|
3617
4654
|
const origSend = this.originalXhrSend;
|
|
3618
4655
|
Xhr.prototype.send = function(body) {
|
|
4656
|
+
if (this.__uiBridgeFromFetch === true) {
|
|
4657
|
+
return origSend.call(this, body);
|
|
4658
|
+
}
|
|
3619
4659
|
const start = Date.now();
|
|
3620
4660
|
const method = this.__uiBridgeMethod ?? "GET";
|
|
3621
4661
|
const url = this.__uiBridgeUrl ?? "";
|
|
@@ -3695,6 +4735,16 @@ function isUnsubscribe(msg) {
|
|
|
3695
4735
|
}
|
|
3696
4736
|
|
|
3697
4737
|
// src/server/http-server.ts
|
|
4738
|
+
var ERROR_CODE_HTTP_STATUS = /* @__PURE__ */ new Map([
|
|
4739
|
+
["NOT_SUPPORTED", 501],
|
|
4740
|
+
["NOT_FOUND", 404],
|
|
4741
|
+
["METHOD_NOT_ALLOWED", 405]
|
|
4742
|
+
]);
|
|
4743
|
+
function httpStatusForResponse(response) {
|
|
4744
|
+
if (response.success) return 200;
|
|
4745
|
+
const mapped = response.code ? ERROR_CODE_HTTP_STATUS.get(response.code) : void 0;
|
|
4746
|
+
return mapped ?? 400;
|
|
4747
|
+
}
|
|
3698
4748
|
function parsePath(pattern, actual) {
|
|
3699
4749
|
const patternParts = pattern.split("/");
|
|
3700
4750
|
const actualParts = actual.split("/");
|
|
@@ -3857,6 +4907,23 @@ var WS_ROUTES = [
|
|
|
3857
4907
|
httpMethods: ["GET"]
|
|
3858
4908
|
}
|
|
3859
4909
|
];
|
|
4910
|
+
var DISCOVERY_PATHS = /* @__PURE__ */ new Set(["_help", "_routes"]);
|
|
4911
|
+
function allowedMethodsForPath(path, testHooks) {
|
|
4912
|
+
const PREFIX = "/ui-bridge/";
|
|
4913
|
+
if (!path.startsWith(PREFIX)) return [];
|
|
4914
|
+
const stripped = path.slice(PREFIX.length);
|
|
4915
|
+
const methods = [];
|
|
4916
|
+
if (DISCOVERY_PATHS.has(stripped)) methods.push("GET");
|
|
4917
|
+
for (const route of WS_ROUTES) {
|
|
4918
|
+
if (!route.httpMethods) continue;
|
|
4919
|
+
if (route.requiresTestHooks && !testHooks) continue;
|
|
4920
|
+
if (parsePath(route.pattern, stripped) === null) continue;
|
|
4921
|
+
for (const verb of route.httpMethods) {
|
|
4922
|
+
if (!methods.includes(verb)) methods.push(verb);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
return methods;
|
|
4926
|
+
}
|
|
3860
4927
|
var HANDLER_DESCRIPTIONS = {
|
|
3861
4928
|
health: "Liveness probe; returns app metadata + element count",
|
|
3862
4929
|
getElements: "Flat list of registered UI elements",
|
|
@@ -3919,6 +4986,10 @@ var ACTION_REFERENCE = [
|
|
|
3919
4986
|
{ name: "focus", params: "none" },
|
|
3920
4987
|
{ name: "blur", params: "none" },
|
|
3921
4988
|
{ name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
|
|
4989
|
+
{
|
|
4990
|
+
name: "scrollIntoView",
|
|
4991
|
+
params: "ScrollIntoViewActionParams { padding? (dp, default 16) } \u2014 scrolls the element's DECLARED scrollAncestorId ScrollView; returns { alreadyVisible, scrolled }; NOT_SUPPORTED (501) without a usable declared ancestor"
|
|
4992
|
+
},
|
|
3922
4993
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
3923
4994
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
3924
4995
|
];
|
|
@@ -4116,7 +5187,9 @@ var NativeUIBridgeServer = class {
|
|
|
4116
5187
|
}
|
|
4117
5188
|
/**
|
|
4118
5189
|
* Set a route provider for reporting the current navigation route.
|
|
4119
|
-
* When set, `control/snapshot` responses include `currentRoute` and
|
|
5190
|
+
* When set, `control/snapshot` responses include `currentRoute` and
|
|
5191
|
+
* `segments`, plus the canonical `route` / `activeTab` aliases the web SDK's
|
|
5192
|
+
* `BridgeSnapshot` spells the same way.
|
|
4120
5193
|
*
|
|
4121
5194
|
* Also forwards the provider into the registry so the *default*
|
|
4122
5195
|
* `getSnapshot` handler (and any other call site of
|
|
@@ -4131,7 +5204,12 @@ var NativeUIBridgeServer = class {
|
|
|
4131
5204
|
this.handlers.getElements = async (ctx) => {
|
|
4132
5205
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
4133
5206
|
if (currentRouteOnly && !ctx.query?.route && !ctx.body?.route) {
|
|
4134
|
-
|
|
5207
|
+
let currentRoute;
|
|
5208
|
+
try {
|
|
5209
|
+
currentRoute = provider.getCurrentRoute();
|
|
5210
|
+
} catch {
|
|
5211
|
+
currentRoute = null;
|
|
5212
|
+
}
|
|
4135
5213
|
if (currentRoute) {
|
|
4136
5214
|
ctx.query = { ...ctx.query, route: currentRoute };
|
|
4137
5215
|
}
|
|
@@ -4141,11 +5219,26 @@ var NativeUIBridgeServer = class {
|
|
|
4141
5219
|
this.handlers.getSnapshot = async (ctx) => {
|
|
4142
5220
|
const visibleOnly = ctx?.query?.visibleOnly === "true" || ctx?.body?.visibleOnly === true;
|
|
4143
5221
|
const currentRouteOnly = ctx?.query?.currentRouteOnly === "true" || ctx?.body?.currentRouteOnly === true;
|
|
5222
|
+
let currentRoute;
|
|
5223
|
+
try {
|
|
5224
|
+
currentRoute = provider.getCurrentRoute() ?? null;
|
|
5225
|
+
} catch {
|
|
5226
|
+
currentRoute = null;
|
|
5227
|
+
}
|
|
5228
|
+
let segments;
|
|
5229
|
+
try {
|
|
5230
|
+
segments = provider.getSegments?.();
|
|
5231
|
+
} catch {
|
|
5232
|
+
segments = void 0;
|
|
5233
|
+
}
|
|
5234
|
+
let activeTab;
|
|
5235
|
+
try {
|
|
5236
|
+
activeTab = provider.getActiveTab?.();
|
|
5237
|
+
} catch {
|
|
5238
|
+
activeTab = void 0;
|
|
5239
|
+
}
|
|
4144
5240
|
const snapshot = this.registry.createSnapshot(
|
|
4145
|
-
{
|
|
4146
|
-
currentRoute: provider.getCurrentRoute(),
|
|
4147
|
-
segments: provider.getSegments?.()
|
|
4148
|
-
},
|
|
5241
|
+
{ currentRoute, segments, activeTab },
|
|
4149
5242
|
{ visibleOnly, currentRouteOnly }
|
|
4150
5243
|
);
|
|
4151
5244
|
return { success: true, data: snapshot, timestamp: Date.now() };
|
|
@@ -4811,8 +5904,15 @@ var NativeUIBridgeServer = class {
|
|
|
4811
5904
|
}
|
|
4812
5905
|
try {
|
|
4813
5906
|
const response = await this.routeRequest(request);
|
|
5907
|
+
if (response.code === "METHOD_NOT_ALLOWED") {
|
|
5908
|
+
const allow = allowedMethodsForPath(request.path, this.config.testHooks === true);
|
|
5909
|
+
if (allow.length > 0) {
|
|
5910
|
+
headers["Allow"] = allow.join(", ");
|
|
5911
|
+
if (this.config.cors) headers["Access-Control-Expose-Headers"] = "Allow";
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
4814
5914
|
return {
|
|
4815
|
-
status: response
|
|
5915
|
+
status: httpStatusForResponse(response),
|
|
4816
5916
|
headers,
|
|
4817
5917
|
body: JSON.stringify(response)
|
|
4818
5918
|
};
|
|
@@ -4869,7 +5969,7 @@ var NativeUIBridgeServer = class {
|
|
|
4869
5969
|
timestamp: Date.now()
|
|
4870
5970
|
};
|
|
4871
5971
|
}
|
|
4872
|
-
let patternMatchedButWrongVerb =
|
|
5972
|
+
let patternMatchedButWrongVerb = DISCOVERY_PATHS.has(stripped);
|
|
4873
5973
|
for (const route of WS_ROUTES) {
|
|
4874
5974
|
const pathParams = parsePath(route.pattern, stripped);
|
|
4875
5975
|
if (!pathParams) continue;
|
|
@@ -4886,9 +5986,10 @@ var NativeUIBridgeServer = class {
|
|
|
4886
5986
|
const ctx = { params: pathParams, query, body };
|
|
4887
5987
|
return this.handlers[route.handler](ctx);
|
|
4888
5988
|
}
|
|
5989
|
+
const allowed = patternMatchedButWrongVerb ? allowedMethodsForPath(path, testHooks) : [];
|
|
4889
5990
|
return {
|
|
4890
5991
|
success: false,
|
|
4891
|
-
error: patternMatchedButWrongVerb ? `Method not allowed: ${method} ${path}` : `Route not found: ${method} ${path}`,
|
|
5992
|
+
error: patternMatchedButWrongVerb ? `Method not allowed: ${method} ${path}${allowed.length > 0 ? ` (allowed: ${allowed.join(", ")})` : ""}` : `Route not found: ${method} ${path}`,
|
|
4892
5993
|
code: patternMatchedButWrongVerb ? "METHOD_NOT_ALLOWED" : "NOT_FOUND",
|
|
4893
5994
|
timestamp: Date.now()
|
|
4894
5995
|
};
|
|
@@ -5192,8 +6293,52 @@ function createTransportLogger() {
|
|
|
5192
6293
|
}
|
|
5193
6294
|
var transportLogger = createTransportLogger();
|
|
5194
6295
|
|
|
6296
|
+
// src/transport/relay-logging.ts
|
|
6297
|
+
var CREDENTIAL_PARAMS = ["token", "access_token", "auth", "authorization", "key", "sig"];
|
|
6298
|
+
var CREDENTIAL_PARAM_RE = new RegExp(`([?&](?:${CREDENTIAL_PARAMS.join("|")})=)[^&#]*`, "gi");
|
|
6299
|
+
var USERINFO_RE = /^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i;
|
|
6300
|
+
function redactRelayUrl(url) {
|
|
6301
|
+
return url.replace(USERINFO_RE, "$1<redacted>@").replace(CREDENTIAL_PARAM_RE, "$1<redacted>");
|
|
6302
|
+
}
|
|
6303
|
+
function describeCloseEvent(event) {
|
|
6304
|
+
const e = event ?? {};
|
|
6305
|
+
const code = typeof e.code === "number" ? String(e.code) : "unknown";
|
|
6306
|
+
const reason = typeof e.reason === "string" && e.reason.length > 0 ? e.reason : "(none)";
|
|
6307
|
+
const wasClean = typeof e.wasClean === "boolean" ? String(e.wasClean) : "unknown";
|
|
6308
|
+
return `code=${code} reason=${reason} wasClean=${wasClean}`;
|
|
6309
|
+
}
|
|
6310
|
+
function describeErrorEvent(event) {
|
|
6311
|
+
const e = event ?? {};
|
|
6312
|
+
const parts = [];
|
|
6313
|
+
if (typeof e.message === "string" && e.message.length > 0) parts.push(`message=${e.message}`);
|
|
6314
|
+
if (e.error instanceof Error) parts.push(`error=${e.error.message}`);
|
|
6315
|
+
else if (typeof e.error === "string" && e.error.length > 0) parts.push(`error=${e.error}`);
|
|
6316
|
+
if (typeof e.code === "number") parts.push(`code=${e.code}`);
|
|
6317
|
+
if (parts.length === 0) parts.push("message=(none reported by the transport)");
|
|
6318
|
+
return parts.join(" ");
|
|
6319
|
+
}
|
|
6320
|
+
|
|
5195
6321
|
// src/transport/DeviceAnnouncer.ts
|
|
6322
|
+
function registeredServiceName(result, requested) {
|
|
6323
|
+
if (result !== null && typeof result === "object") {
|
|
6324
|
+
const name = result.name;
|
|
6325
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
6326
|
+
}
|
|
6327
|
+
return requested;
|
|
6328
|
+
}
|
|
5196
6329
|
var DEFAULT_PORT = 8087;
|
|
6330
|
+
async function releaseZeroconf(zeroconf, name) {
|
|
6331
|
+
if (name !== null) {
|
|
6332
|
+
try {
|
|
6333
|
+
await zeroconf.unpublishService(name);
|
|
6334
|
+
} catch {
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
try {
|
|
6338
|
+
zeroconf.removeDeviceListeners?.();
|
|
6339
|
+
} catch {
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
5197
6342
|
var RECONNECT_INITIAL_DELAY_MS = 2e3;
|
|
5198
6343
|
var RECONNECT_MAX_DELAY_MS = 6e4;
|
|
5199
6344
|
var DeviceAnnouncer = class {
|
|
@@ -5206,8 +6351,16 @@ var DeviceAnnouncer = class {
|
|
|
5206
6351
|
this.reconnectTimer = null;
|
|
5207
6352
|
this.reconnectDelay = RECONNECT_INITIAL_DELAY_MS;
|
|
5208
6353
|
this.stopped = false;
|
|
5209
|
-
/** Zeroconf instance, set when mDNS is
|
|
6354
|
+
/** Zeroconf instance, set when mDNS is started (cleared by `stop()`) */
|
|
5210
6355
|
this.zeroconf = null;
|
|
6356
|
+
/** The name the library registered — the only name this announcer may unpublish */
|
|
6357
|
+
this.publishedName = null;
|
|
6358
|
+
/**
|
|
6359
|
+
* True while a publish is awaiting the library. A `stop()` that lands in that
|
|
6360
|
+
* window leaves the cleanup to the publish path, which is the only side that
|
|
6361
|
+
* will learn the registered name.
|
|
6362
|
+
*/
|
|
6363
|
+
this.publishInFlight = false;
|
|
5211
6364
|
this.config = config;
|
|
5212
6365
|
this.pairingToken = generateHex(16);
|
|
5213
6366
|
}
|
|
@@ -5232,19 +6385,38 @@ var DeviceAnnouncer = class {
|
|
|
5232
6385
|
);
|
|
5233
6386
|
return;
|
|
5234
6387
|
}
|
|
6388
|
+
if (this.stopped) return;
|
|
6389
|
+
let zeroconf = null;
|
|
5235
6390
|
try {
|
|
5236
|
-
|
|
6391
|
+
zeroconf = new ZeroconfCtor();
|
|
6392
|
+
this.zeroconf = zeroconf;
|
|
5237
6393
|
const port = this.config.port ?? DEFAULT_PORT;
|
|
5238
6394
|
const serviceName = `UIBridge-${this.config.deviceId.slice(0, 8)}`;
|
|
5239
|
-
this.
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
6395
|
+
this.publishInFlight = true;
|
|
6396
|
+
let result;
|
|
6397
|
+
try {
|
|
6398
|
+
result = await zeroconf.publishService("_uibridge", "_tcp.", "local.", serviceName, port, {
|
|
6399
|
+
device_id: this.config.deviceId,
|
|
6400
|
+
app_id: this.config.appId,
|
|
6401
|
+
version: this.config.version ?? "unknown",
|
|
6402
|
+
pairing_token: this.pairingToken
|
|
6403
|
+
});
|
|
6404
|
+
} finally {
|
|
6405
|
+
this.publishInFlight = false;
|
|
6406
|
+
}
|
|
6407
|
+
const registered = registeredServiceName(result, serviceName);
|
|
6408
|
+
if (this.stopped) {
|
|
6409
|
+
await releaseZeroconf(zeroconf, registered);
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
this.publishedName = registered;
|
|
5245
6413
|
this.state = { ...this.state, mdnsActive: true };
|
|
5246
|
-
transportLogger.log(`[DeviceAnnouncer] mDNS: advertising "${
|
|
6414
|
+
transportLogger.log(`[DeviceAnnouncer] mDNS: advertising "${registered}" on port ${port}`);
|
|
5247
6415
|
} catch (err) {
|
|
6416
|
+
if (this.stopped) {
|
|
6417
|
+
if (zeroconf) await releaseZeroconf(zeroconf, null);
|
|
6418
|
+
return;
|
|
6419
|
+
}
|
|
5248
6420
|
transportLogger.warn("[DeviceAnnouncer] mDNS start failed:", err);
|
|
5249
6421
|
}
|
|
5250
6422
|
}
|
|
@@ -5261,7 +6433,8 @@ var DeviceAnnouncer = class {
|
|
|
5261
6433
|
}
|
|
5262
6434
|
if (this.stopped) return;
|
|
5263
6435
|
const url = `${this.config.cloudRelayUrl}?token=${encodeURIComponent(this.config.cloudToken)}`;
|
|
5264
|
-
|
|
6436
|
+
const safeUrl = redactRelayUrl(url);
|
|
6437
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connecting to", safeUrl);
|
|
5265
6438
|
const ws = new WebSocket(url);
|
|
5266
6439
|
this.cloudWs = ws;
|
|
5267
6440
|
ws.onopen = () => {
|
|
@@ -5290,14 +6463,22 @@ var DeviceAnnouncer = class {
|
|
|
5290
6463
|
} catch {
|
|
5291
6464
|
}
|
|
5292
6465
|
};
|
|
5293
|
-
ws.onclose = () => {
|
|
6466
|
+
ws.onclose = (event) => {
|
|
5294
6467
|
this.state = { ...this.state, cloudConnected: false };
|
|
5295
|
-
|
|
6468
|
+
const detail = describeCloseEvent(event);
|
|
6469
|
+
if (this.stopped) {
|
|
6470
|
+
transportLogger.log(`[DeviceAnnouncer] Cloud relay closed ${detail} url=${safeUrl}`);
|
|
6471
|
+
} else {
|
|
6472
|
+
transportLogger.warn(
|
|
6473
|
+
`[DeviceAnnouncer] Cloud relay closed unexpectedly ${detail} url=${safeUrl}`
|
|
6474
|
+
);
|
|
5296
6475
|
this.scheduleReconnect();
|
|
5297
6476
|
}
|
|
5298
6477
|
};
|
|
5299
6478
|
ws.onerror = (err) => {
|
|
5300
|
-
transportLogger.warn(
|
|
6479
|
+
transportLogger.warn(
|
|
6480
|
+
`[DeviceAnnouncer] Cloud relay error: ${describeErrorEvent(err)} url=${safeUrl}`
|
|
6481
|
+
);
|
|
5301
6482
|
};
|
|
5302
6483
|
}
|
|
5303
6484
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
@@ -5308,15 +6489,15 @@ var DeviceAnnouncer = class {
|
|
|
5308
6489
|
clearTimeout(this.reconnectTimer);
|
|
5309
6490
|
this.reconnectTimer = null;
|
|
5310
6491
|
}
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
}
|
|
5317
|
-
this.zeroconf = null;
|
|
5318
|
-
}
|
|
6492
|
+
const zeroconf = this.zeroconf;
|
|
6493
|
+
const publishedName = this.publishedName;
|
|
6494
|
+
const inFlight = this.publishInFlight;
|
|
6495
|
+
this.zeroconf = null;
|
|
6496
|
+
this.publishedName = null;
|
|
5319
6497
|
this.state = { ...this.state, mdnsActive: false };
|
|
6498
|
+
if (zeroconf && !inFlight) {
|
|
6499
|
+
await releaseZeroconf(zeroconf, publishedName);
|
|
6500
|
+
}
|
|
5320
6501
|
if (this.cloudWs) {
|
|
5321
6502
|
this.cloudWs.onclose = null;
|
|
5322
6503
|
this.cloudWs.close();
|
|
@@ -5394,7 +6575,8 @@ var CloudRelayClient = class {
|
|
|
5394
6575
|
connect() {
|
|
5395
6576
|
if (this.stopped) return;
|
|
5396
6577
|
const url = `${this.config.relayUrl}?token=${encodeURIComponent(this.config.authToken)}`;
|
|
5397
|
-
|
|
6578
|
+
const safeUrl = redactRelayUrl(url);
|
|
6579
|
+
transportLogger.log("[CloudRelayClient] Connecting to", safeUrl);
|
|
5398
6580
|
const ws = new WebSocket(url);
|
|
5399
6581
|
this.ws = ws;
|
|
5400
6582
|
ws.onopen = () => {
|
|
@@ -5415,14 +6597,22 @@ var CloudRelayClient = class {
|
|
|
5415
6597
|
ws.onmessage = (event) => {
|
|
5416
6598
|
void this.onMessage(event.data);
|
|
5417
6599
|
};
|
|
5418
|
-
ws.onclose = () => {
|
|
6600
|
+
ws.onclose = (event) => {
|
|
5419
6601
|
this._isConnected = false;
|
|
5420
|
-
|
|
6602
|
+
const detail = describeCloseEvent(event);
|
|
6603
|
+
if (this.stopped) {
|
|
6604
|
+
transportLogger.log(`[CloudRelayClient] WebSocket closed ${detail} url=${safeUrl}`);
|
|
6605
|
+
} else {
|
|
6606
|
+
transportLogger.warn(
|
|
6607
|
+
`[CloudRelayClient] WebSocket closed unexpectedly ${detail} url=${safeUrl}`
|
|
6608
|
+
);
|
|
5421
6609
|
this.scheduleReconnect();
|
|
5422
6610
|
}
|
|
5423
6611
|
};
|
|
5424
6612
|
ws.onerror = (err) => {
|
|
5425
|
-
transportLogger.warn(
|
|
6613
|
+
transportLogger.warn(
|
|
6614
|
+
`[CloudRelayClient] WebSocket error: ${describeErrorEvent(err)} url=${safeUrl}`
|
|
6615
|
+
);
|
|
5426
6616
|
};
|
|
5427
6617
|
}
|
|
5428
6618
|
async onMessage(raw) {
|
|
@@ -5701,10 +6891,20 @@ var UndoTracker = class {
|
|
|
5701
6891
|
}
|
|
5702
6892
|
};
|
|
5703
6893
|
var UIBridgeNativeContext = react.createContext(null);
|
|
6894
|
+
var EMPTY_FEATURES = {};
|
|
6895
|
+
var EMPTY_CONFIG = {};
|
|
6896
|
+
function useValueStable(value) {
|
|
6897
|
+
const signature = JSON.stringify(value);
|
|
6898
|
+
return react.useMemo(() => value, [signature]);
|
|
6899
|
+
}
|
|
6900
|
+
function readWindowViewport() {
|
|
6901
|
+
const win = reactNative.Dimensions.get("window");
|
|
6902
|
+
return { width: win.width, height: win.height };
|
|
6903
|
+
}
|
|
5704
6904
|
function UIBridgeNativeProvider({
|
|
5705
6905
|
children,
|
|
5706
|
-
features =
|
|
5707
|
-
config =
|
|
6906
|
+
features: featuresProp = EMPTY_FEATURES,
|
|
6907
|
+
config: configProp = EMPTY_CONFIG,
|
|
5708
6908
|
onEvent,
|
|
5709
6909
|
serverAdapter,
|
|
5710
6910
|
navigationProvider,
|
|
@@ -5713,8 +6913,11 @@ function UIBridgeNativeProvider({
|
|
|
5713
6913
|
routeProvider,
|
|
5714
6914
|
cloudRelayConfig,
|
|
5715
6915
|
enableMdnsAnnounce,
|
|
6916
|
+
zeroconf,
|
|
5716
6917
|
deviceId
|
|
5717
6918
|
}) {
|
|
6919
|
+
const features = useValueStable(featuresProp);
|
|
6920
|
+
const config = useValueStable(configProp);
|
|
5718
6921
|
const registryRef = react.useRef(null);
|
|
5719
6922
|
const executorRef = react.useRef(null);
|
|
5720
6923
|
const modalDetectorRef = react.useRef(null);
|
|
@@ -5727,6 +6930,7 @@ function UIBridgeNativeProvider({
|
|
|
5727
6930
|
onEvent
|
|
5728
6931
|
});
|
|
5729
6932
|
registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
|
|
6933
|
+
registryRef.current.setViewportProvider(readWindowViewport);
|
|
5730
6934
|
setGlobalRegistry(registryRef.current);
|
|
5731
6935
|
}
|
|
5732
6936
|
const registry = registryRef.current;
|
|
@@ -5761,16 +6965,10 @@ function UIBridgeNativeProvider({
|
|
|
5761
6965
|
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5762
6966
|
// keeps existing consumers unchanged.
|
|
5763
6967
|
observability: features.observability,
|
|
5764
|
-
// Injected device-viewport getter for POST /control/page-health
|
|
5765
|
-
//
|
|
5766
|
-
//
|
|
5767
|
-
|
|
5768
|
-
// already has a live react-native import, so it's the safe injection
|
|
5769
|
-
// point. See feedback_metro_require_gotcha for the full incident.
|
|
5770
|
-
viewportProvider: () => {
|
|
5771
|
-
const win = reactNative.Dimensions.get("window");
|
|
5772
|
-
return { width: win.width, height: win.height };
|
|
5773
|
-
}
|
|
6968
|
+
// Injected device-viewport getter for POST /control/page-health and the
|
|
6969
|
+
// design handlers. Same source as the registry's clipping bounds — see
|
|
6970
|
+
// `readWindowViewport`.
|
|
6971
|
+
viewportProvider: readWindowViewport
|
|
5774
6972
|
});
|
|
5775
6973
|
server.setAdapter(serverAdapter);
|
|
5776
6974
|
if (navigationProvider) {
|
|
@@ -5868,10 +7066,7 @@ function UIBridgeNativeProvider({
|
|
|
5868
7066
|
appInfo: config.appInfo,
|
|
5869
7067
|
testHooks: features.testHooks === true,
|
|
5870
7068
|
observability: features.observability,
|
|
5871
|
-
viewportProvider:
|
|
5872
|
-
const win = reactNative.Dimensions.get("window");
|
|
5873
|
-
return { width: win.width, height: win.height };
|
|
5874
|
-
}
|
|
7069
|
+
viewportProvider: readWindowViewport
|
|
5875
7070
|
});
|
|
5876
7071
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
5877
7072
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -5892,7 +7087,7 @@ function UIBridgeNativeProvider({
|
|
|
5892
7087
|
cloudToken: cloudRelayConfig?.authToken
|
|
5893
7088
|
});
|
|
5894
7089
|
announcerRef.current = announcer;
|
|
5895
|
-
void announcer.startMdnsAdvertise().catch((err) => {
|
|
7090
|
+
void announcer.startMdnsAdvertise(zeroconf).catch((err) => {
|
|
5896
7091
|
console.warn("[ui-bridge-native] mDNS advertise failed:", err);
|
|
5897
7092
|
});
|
|
5898
7093
|
} catch (err) {
|
|
@@ -5933,6 +7128,7 @@ function UIBridgeNativeProvider({
|
|
|
5933
7128
|
cloudRelayConfig?.relayUrl,
|
|
5934
7129
|
cloudRelayConfig?.authToken,
|
|
5935
7130
|
enableMdnsAnnounce,
|
|
7131
|
+
zeroconf,
|
|
5936
7132
|
deviceId
|
|
5937
7133
|
]);
|
|
5938
7134
|
react.useEffect(() => {
|
|
@@ -6076,6 +7272,7 @@ function useUIElement(options) {
|
|
|
6076
7272
|
const propsRef = react.useRef({});
|
|
6077
7273
|
const registeredRef = react.useRef(false);
|
|
6078
7274
|
const registeredIdRef = react.useRef(null);
|
|
7275
|
+
const autoPublishedLabelRef = react.useRef(void 0);
|
|
6079
7276
|
const bridgeRef = react.useRef(bridge);
|
|
6080
7277
|
bridgeRef.current = bridge;
|
|
6081
7278
|
const nodeRef = react.useRef(null);
|
|
@@ -6115,6 +7312,7 @@ function useUIElement(options) {
|
|
|
6115
7312
|
autoRegister = true,
|
|
6116
7313
|
onStateChange,
|
|
6117
7314
|
parentPath,
|
|
7315
|
+
scrollAncestorId,
|
|
6118
7316
|
style,
|
|
6119
7317
|
stateStyles: stateStylesProp,
|
|
6120
7318
|
handlers: handlersProp,
|
|
@@ -6139,22 +7337,38 @@ function useUIElement(options) {
|
|
|
6139
7337
|
testId: id,
|
|
6140
7338
|
accessibilityLabel: label,
|
|
6141
7339
|
registrationRoute: bridge.getCurrentRoute(),
|
|
7340
|
+
scrollAncestorId,
|
|
6142
7341
|
flatStyle: flattenStyle(style),
|
|
6143
7342
|
stateStyles: flattenStateStyles(stateStylesProp)
|
|
6144
7343
|
});
|
|
6145
7344
|
registeredRef.current = true;
|
|
6146
7345
|
registeredIdRef.current = id;
|
|
7346
|
+
autoPublishedLabelRef.current = typeof label === "string" ? label : void 0;
|
|
6147
7347
|
if (typeof valueProp === "string") {
|
|
6148
7348
|
bridge.registry.updateElementState(id, { value: valueProp });
|
|
6149
7349
|
}
|
|
6150
7350
|
setRegistered(true);
|
|
6151
7351
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
6152
|
-
}, [
|
|
7352
|
+
}, [
|
|
7353
|
+
bridge,
|
|
7354
|
+
id,
|
|
7355
|
+
type,
|
|
7356
|
+
label,
|
|
7357
|
+
actions,
|
|
7358
|
+
customActions,
|
|
7359
|
+
treePath,
|
|
7360
|
+
scrollAncestorId,
|
|
7361
|
+
style,
|
|
7362
|
+
stateStylesProp,
|
|
7363
|
+
valueProp,
|
|
7364
|
+
ref
|
|
7365
|
+
]);
|
|
6153
7366
|
const unregister = react.useCallback(() => {
|
|
6154
7367
|
if (!bridge || !registeredRef.current) return;
|
|
6155
7368
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
6156
7369
|
registeredRef.current = false;
|
|
6157
7370
|
registeredIdRef.current = null;
|
|
7371
|
+
autoPublishedLabelRef.current = void 0;
|
|
6158
7372
|
setRegistered(false);
|
|
6159
7373
|
}, [bridge, id]);
|
|
6160
7374
|
const onStateChangeRef = react.useRef(onStateChange);
|
|
@@ -6182,7 +7396,7 @@ function useUIElement(options) {
|
|
|
6182
7396
|
writeLayout({ x, y, width, height, pageX, pageY });
|
|
6183
7397
|
});
|
|
6184
7398
|
} else {
|
|
6185
|
-
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
7399
|
+
writeLayout({ x, y, width, height, pageX: x, pageY: y, pageOriginUnmeasured: true });
|
|
6186
7400
|
}
|
|
6187
7401
|
},
|
|
6188
7402
|
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
@@ -6294,6 +7508,16 @@ function useUIElement(options) {
|
|
|
6294
7508
|
if (current === valueProp) return;
|
|
6295
7509
|
bridge.registry.updateElementState(id, { value: valueProp });
|
|
6296
7510
|
}, [bridge, registered, id, valueProp]);
|
|
7511
|
+
react.useEffect(() => {
|
|
7512
|
+
if (!bridge || !registered) return;
|
|
7513
|
+
if (typeof label !== "string") return;
|
|
7514
|
+
autoPublishedLabelRef.current = label;
|
|
7515
|
+
bridge.registry.updateElementMeta(id, {
|
|
7516
|
+
label,
|
|
7517
|
+
accessibilityLabel: label,
|
|
7518
|
+
testId: id
|
|
7519
|
+
});
|
|
7520
|
+
}, [bridge, registered, id, label]);
|
|
6297
7521
|
react.useCallback(
|
|
6298
7522
|
(props) => {
|
|
6299
7523
|
propsRef.current = { ...propsRef.current, ...props };
|
|
@@ -6336,7 +7560,8 @@ function useUIElement(options) {
|
|
|
6336
7560
|
accessibilityLabel: newLabel,
|
|
6337
7561
|
testId: id
|
|
6338
7562
|
});
|
|
6339
|
-
|
|
7563
|
+
const alreadyPublishedBySdk = newLabel === autoPublishedLabelRef.current;
|
|
7564
|
+
if (!changed && !alreadyPublishedBySdk && !warnedRedundantUpdateLabelIds.has(id)) {
|
|
6340
7565
|
warnedRedundantUpdateLabelIds.add(id);
|
|
6341
7566
|
console.warn(
|
|
6342
7567
|
`[UI Bridge] updateLabel("${newLabel}") on element "${id}" was a no-op (label unchanged). Gate the call behind a useEffect with [label] deps to avoid redundant publishes.`
|
|
@@ -6392,36 +7617,56 @@ function useUIElementWithProps(options) {
|
|
|
6392
7617
|
}
|
|
6393
7618
|
function useUIComponent(options) {
|
|
6394
7619
|
const bridge = useUIBridgeNativeOptional();
|
|
7620
|
+
const [registered, setRegistered] = react.useState(false);
|
|
7621
|
+
const [publishVersion, setPublishVersion] = react.useState(0);
|
|
7622
|
+
const publishedIdRef = react.useRef(null);
|
|
6395
7623
|
const registeredRef = react.useRef(false);
|
|
6396
7624
|
const registeredIdRef = react.useRef(null);
|
|
6397
7625
|
const actionsRef = react.useRef(options.actions || []);
|
|
6398
7626
|
const elementIdsRef = react.useRef(options.elementIds || []);
|
|
6399
7627
|
const { id, name, description, autoRegister = true } = options;
|
|
7628
|
+
const actionsSignature = JSON.stringify(
|
|
7629
|
+
(options.actions ?? []).map((a) => [a.id, a.label, a.description, a.paramSchema, a.effect])
|
|
7630
|
+
);
|
|
7631
|
+
const elementIdsSignature = JSON.stringify(options.elementIds ?? []);
|
|
6400
7632
|
react.useEffect(() => {
|
|
6401
|
-
actionsRef.current = options.actions
|
|
6402
|
-
|
|
6403
|
-
|
|
7633
|
+
if (options.actions !== void 0) actionsRef.current = options.actions;
|
|
7634
|
+
}, [options.actions]);
|
|
7635
|
+
react.useEffect(() => {
|
|
7636
|
+
if (options.elementIds !== void 0) elementIdsRef.current = options.elementIds;
|
|
7637
|
+
}, [elementIdsSignature]);
|
|
7638
|
+
const toRegistryActions = react.useCallback(
|
|
7639
|
+
() => actionsRef.current.map(({ handler: declaredHandler, ...rest }) => ({
|
|
7640
|
+
...rest,
|
|
7641
|
+
handler: (params, handlerOptions) => {
|
|
7642
|
+
const latest = actionsRef.current.find((a) => a.id === rest.id);
|
|
7643
|
+
return (latest?.handler ?? declaredHandler)(params, handlerOptions);
|
|
7644
|
+
}
|
|
7645
|
+
})),
|
|
7646
|
+
[]
|
|
7647
|
+
);
|
|
6404
7648
|
const register = react.useCallback(() => {
|
|
6405
7649
|
if (!bridge || registeredRef.current) return;
|
|
6406
7650
|
bridge.registry.registerComponent(id, {
|
|
6407
7651
|
name,
|
|
6408
7652
|
description,
|
|
6409
|
-
actions:
|
|
6410
|
-
id: a.id,
|
|
6411
|
-
label: a.label,
|
|
6412
|
-
description: a.description,
|
|
6413
|
-
handler: a.handler
|
|
6414
|
-
})),
|
|
7653
|
+
actions: toRegistryActions(),
|
|
6415
7654
|
elementIds: elementIdsRef.current
|
|
6416
7655
|
});
|
|
6417
7656
|
registeredRef.current = true;
|
|
6418
7657
|
registeredIdRef.current = id;
|
|
6419
|
-
|
|
7658
|
+
setRegistered(true);
|
|
7659
|
+
if (publishedIdRef.current !== id) {
|
|
7660
|
+
publishedIdRef.current = id;
|
|
7661
|
+
setPublishVersion((v) => v + 1);
|
|
7662
|
+
}
|
|
7663
|
+
}, [bridge, id, name, description, toRegistryActions]);
|
|
6420
7664
|
const unregister = react.useCallback(() => {
|
|
6421
7665
|
if (!bridge || !registeredRef.current) return;
|
|
6422
7666
|
bridge.registry.unregisterComponent(registeredIdRef.current ?? id);
|
|
6423
7667
|
registeredRef.current = false;
|
|
6424
7668
|
registeredIdRef.current = null;
|
|
7669
|
+
setRegistered(false);
|
|
6425
7670
|
}, [bridge, id]);
|
|
6426
7671
|
const executeAction = react.useCallback(
|
|
6427
7672
|
async (actionId, params) => {
|
|
@@ -6439,25 +7684,47 @@ function useUIComponent(options) {
|
|
|
6439
7684
|
},
|
|
6440
7685
|
[bridge, id]
|
|
6441
7686
|
);
|
|
7687
|
+
const publishElementIds = react.useCallback(() => {
|
|
7688
|
+
if (!registeredRef.current || !bridge) return;
|
|
7689
|
+
const changed = bridge.registry.updateComponentMeta(registeredIdRef.current ?? id, {
|
|
7690
|
+
elementIds: elementIdsRef.current
|
|
7691
|
+
});
|
|
7692
|
+
if (changed) {
|
|
7693
|
+
setPublishVersion((v) => v + 1);
|
|
7694
|
+
}
|
|
7695
|
+
}, [bridge, id]);
|
|
6442
7696
|
const updateActions = react.useCallback(
|
|
6443
7697
|
(actions) => {
|
|
6444
7698
|
actionsRef.current = actions;
|
|
6445
7699
|
if (registeredRef.current && bridge) {
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
7700
|
+
const targetId = registeredIdRef.current ?? id;
|
|
7701
|
+
const changed = bridge.registry.updateComponentMeta(targetId, {
|
|
7702
|
+
actions: toRegistryActions()
|
|
7703
|
+
});
|
|
7704
|
+
if (changed) {
|
|
7705
|
+
setPublishVersion((v) => v + 1);
|
|
7706
|
+
}
|
|
6449
7707
|
}
|
|
6450
7708
|
},
|
|
6451
|
-
[bridge, id,
|
|
7709
|
+
[bridge, id, toRegistryActions]
|
|
7710
|
+
);
|
|
7711
|
+
const addElement = react.useCallback(
|
|
7712
|
+
(elementId) => {
|
|
7713
|
+
if (!elementIdsRef.current.includes(elementId)) {
|
|
7714
|
+
elementIdsRef.current = [...elementIdsRef.current, elementId];
|
|
7715
|
+
publishElementIds();
|
|
7716
|
+
}
|
|
7717
|
+
},
|
|
7718
|
+
[publishElementIds]
|
|
7719
|
+
);
|
|
7720
|
+
const removeElement = react.useCallback(
|
|
7721
|
+
(elementId) => {
|
|
7722
|
+
if (!elementIdsRef.current.includes(elementId)) return;
|
|
7723
|
+
elementIdsRef.current = elementIdsRef.current.filter((eid) => eid !== elementId);
|
|
7724
|
+
publishElementIds();
|
|
7725
|
+
},
|
|
7726
|
+
[publishElementIds]
|
|
6452
7727
|
);
|
|
6453
|
-
const addElement = react.useCallback((elementId) => {
|
|
6454
|
-
if (!elementIdsRef.current.includes(elementId)) {
|
|
6455
|
-
elementIdsRef.current = [...elementIdsRef.current, elementId];
|
|
6456
|
-
}
|
|
6457
|
-
}, []);
|
|
6458
|
-
const removeElement = react.useCallback((elementId) => {
|
|
6459
|
-
elementIdsRef.current = elementIdsRef.current.filter((eid) => eid !== elementId);
|
|
6460
|
-
}, []);
|
|
6461
7728
|
const registerRef = react.useRef(register);
|
|
6462
7729
|
const unregisterRef = react.useRef(unregister);
|
|
6463
7730
|
react.useEffect(() => {
|
|
@@ -6473,13 +7740,32 @@ function useUIComponent(options) {
|
|
|
6473
7740
|
unregisterRef.current();
|
|
6474
7741
|
}
|
|
6475
7742
|
};
|
|
6476
|
-
}, [autoRegister, bridge]);
|
|
6477
|
-
|
|
6478
|
-
if (!bridge) return
|
|
6479
|
-
|
|
6480
|
-
|
|
7743
|
+
}, [autoRegister, bridge, id]);
|
|
7744
|
+
react.useEffect(() => {
|
|
7745
|
+
if (!bridge || !registered) return;
|
|
7746
|
+
const targetId = registeredIdRef.current ?? id;
|
|
7747
|
+
const changed = bridge.registry.updateComponentMeta(targetId, {
|
|
7748
|
+
name,
|
|
7749
|
+
description,
|
|
7750
|
+
...options.actions !== void 0 ? { actions: toRegistryActions() } : {},
|
|
7751
|
+
...options.elementIds !== void 0 ? { elementIds: elementIdsRef.current } : {}
|
|
7752
|
+
});
|
|
7753
|
+
if (changed) {
|
|
7754
|
+
setPublishVersion((v) => v + 1);
|
|
7755
|
+
}
|
|
7756
|
+
}, [
|
|
7757
|
+
bridge,
|
|
7758
|
+
registered,
|
|
7759
|
+
id,
|
|
7760
|
+
name,
|
|
7761
|
+
description,
|
|
7762
|
+
actionsSignature,
|
|
7763
|
+
elementIdsSignature,
|
|
7764
|
+
toRegistryActions
|
|
7765
|
+
]);
|
|
7766
|
+
const registeredComponent = registered && bridge ? bridge.registry.getComponent(id) ?? null : null;
|
|
6481
7767
|
return {
|
|
6482
|
-
registered
|
|
7768
|
+
registered,
|
|
6483
7769
|
executeAction,
|
|
6484
7770
|
register,
|
|
6485
7771
|
unregister,
|
|
@@ -6637,12 +7923,31 @@ function TransitionTo(props) {
|
|
|
6637
7923
|
|
|
6638
7924
|
// src/server/types.ts
|
|
6639
7925
|
var UI_BRIDGE_NATIVE_ROUTES = {
|
|
7926
|
+
// Discovery / introspection — the two endpoints an agent hits first to learn
|
|
7927
|
+
// the rest of this table without reading docs. Special-cased in
|
|
7928
|
+
// `routeRequest` rather than living in `WS_ROUTES`, and appended by
|
|
7929
|
+
// `buildRoutesPayload` so `_routes` describes itself.
|
|
7930
|
+
HELP: {
|
|
7931
|
+
method: "GET",
|
|
7932
|
+
path: "/ui-bridge/_help",
|
|
7933
|
+
description: "Route index + action reference + tips (rich discovery payload)"
|
|
7934
|
+
},
|
|
7935
|
+
ROUTES: {
|
|
7936
|
+
method: "GET",
|
|
7937
|
+
path: "/ui-bridge/_routes",
|
|
7938
|
+
description: "Minimal {method, path} route list, matching the runner /_routes shape"
|
|
7939
|
+
},
|
|
6640
7940
|
// Control - Elements
|
|
6641
7941
|
GET_ELEMENTS: {
|
|
6642
7942
|
method: "GET",
|
|
6643
7943
|
path: "/ui-bridge/control/elements",
|
|
6644
7944
|
description: "List all registered elements"
|
|
6645
7945
|
},
|
|
7946
|
+
GET_ELEMENTS_FLAT: {
|
|
7947
|
+
method: "GET",
|
|
7948
|
+
path: "/ui-bridge/elements",
|
|
7949
|
+
description: "List all registered elements (flat alias matching the runner path)"
|
|
7950
|
+
},
|
|
6646
7951
|
GET_ELEMENT: {
|
|
6647
7952
|
method: "GET",
|
|
6648
7953
|
path: "/ui-bridge/control/element/:id",
|
|
@@ -6680,6 +7985,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6680
7985
|
path: "/ui-bridge/control/find",
|
|
6681
7986
|
description: "Find elements matching criteria"
|
|
6682
7987
|
},
|
|
7988
|
+
AI_FIND: {
|
|
7989
|
+
method: "POST",
|
|
7990
|
+
path: "/ui-bridge/ai/find",
|
|
7991
|
+
description: "Find elements matching criteria (runner-parity alias for /control/find)"
|
|
7992
|
+
},
|
|
6683
7993
|
GET_SNAPSHOT: {
|
|
6684
7994
|
method: "GET",
|
|
6685
7995
|
path: "/ui-bridge/control/snapshot",
|
|
@@ -6690,11 +8000,78 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6690
8000
|
path: "/ui-bridge/control/discover",
|
|
6691
8001
|
description: "Discover elements (alias for snapshot; web/runner parity)"
|
|
6692
8002
|
},
|
|
8003
|
+
DISCOVER_POST: {
|
|
8004
|
+
method: "POST",
|
|
8005
|
+
path: "/ui-bridge/control/discover",
|
|
8006
|
+
description: "Discover elements over POST (same handler as the GET form; web/runner parity)"
|
|
8007
|
+
},
|
|
6693
8008
|
PAGE_HEALTH: {
|
|
6694
8009
|
method: "POST",
|
|
6695
8010
|
path: "/ui-bridge/control/page-health",
|
|
6696
8011
|
description: "Holistic page health diagnostic: spatial coverage, layout regions, text signals, interactive readiness, anomalies + ASCII heatmap"
|
|
6697
8012
|
},
|
|
8013
|
+
PAGE_HEALTH_GET: {
|
|
8014
|
+
method: "GET",
|
|
8015
|
+
path: "/ui-bridge/control/page-health",
|
|
8016
|
+
description: "Page health diagnostic over GET, for ad-hoc curls with no body"
|
|
8017
|
+
},
|
|
8018
|
+
// Screenshots — GET and POST both accepted so a curl works either way.
|
|
8019
|
+
//
|
|
8020
|
+
// Both keys carry an explicit verb suffix. The pre-existing multi-verb pairs
|
|
8021
|
+
// do not agree on what an UNSUFFIXED key means — `DISCOVER` is the GET and
|
|
8022
|
+
// `PAGE_HEALTH` is the POST — so a consumer reading `ROUTES.<NAME>.method`
|
|
8023
|
+
// off a bare name gets opposite verbs from equally unmarked keys. Those two
|
|
8024
|
+
// names are load-bearing for existing consumers and are left alone; new
|
|
8025
|
+
// multi-verb entries name their verb rather than entrenching the ambiguity.
|
|
8026
|
+
SCREENSHOT_GET: {
|
|
8027
|
+
method: "GET",
|
|
8028
|
+
path: "/ui-bridge/control/screenshot",
|
|
8029
|
+
description: "PNG screenshot of the current screen"
|
|
8030
|
+
},
|
|
8031
|
+
SCREENSHOT_POST: {
|
|
8032
|
+
method: "POST",
|
|
8033
|
+
path: "/ui-bridge/control/screenshot",
|
|
8034
|
+
description: "PNG screenshot of the current screen (POST form)"
|
|
8035
|
+
},
|
|
8036
|
+
// Coordinate-based interaction
|
|
8037
|
+
CONTROL_TAP: {
|
|
8038
|
+
method: "POST",
|
|
8039
|
+
path: "/ui-bridge/control/tap",
|
|
8040
|
+
description: "Synthesize a press at screen coords by matching registered element layout rects: body { x, y, action? }"
|
|
8041
|
+
},
|
|
8042
|
+
// App-agnostic interaction parity with the web/runner surface. The
|
|
8043
|
+
// selector-based pair has no RN analog and answers NOT_SUPPORTED (501).
|
|
8044
|
+
PAGE_CLICK_BY_TEXT: {
|
|
8045
|
+
method: "POST",
|
|
8046
|
+
path: "/ui-bridge/control/page/click-by-text",
|
|
8047
|
+
description: "Press the first element matching free text: body { text, visibleOnly? }"
|
|
8048
|
+
},
|
|
8049
|
+
PAGE_CLICK_BY_SELECTOR: {
|
|
8050
|
+
method: "POST",
|
|
8051
|
+
path: "/ui-bridge/control/page/click-by-selector",
|
|
8052
|
+
description: "Selector-based press (web-only; mobile returns NOT_SUPPORTED)"
|
|
8053
|
+
},
|
|
8054
|
+
PAGE_TYPE_INTO: {
|
|
8055
|
+
method: "POST",
|
|
8056
|
+
path: "/ui-bridge/control/page/type-into",
|
|
8057
|
+
description: "Type into the input matching a label: body { label, text, clear?, visibleOnly? }"
|
|
8058
|
+
},
|
|
8059
|
+
PAGE_READ_VALUE: {
|
|
8060
|
+
method: "POST",
|
|
8061
|
+
path: "/ui-bridge/control/page/read-value",
|
|
8062
|
+
description: "Selector-based value read (web-only; mobile returns NOT_SUPPORTED)"
|
|
8063
|
+
},
|
|
8064
|
+
PAGE_FIND_BY_TEXT: {
|
|
8065
|
+
method: "POST",
|
|
8066
|
+
path: "/ui-bridge/control/page/find-by-text",
|
|
8067
|
+
description: "List elements matching free text without pressing: body { text, visibleOnly? }"
|
|
8068
|
+
},
|
|
8069
|
+
// Meta
|
|
8070
|
+
META_METHODS: {
|
|
8071
|
+
method: "GET",
|
|
8072
|
+
path: "/ui-bridge/meta/methods",
|
|
8073
|
+
description: "List every handler name (legacy introspection; prefer /_routes)"
|
|
8074
|
+
},
|
|
6698
8075
|
// Workflows
|
|
6699
8076
|
GET_WORKFLOWS: {
|
|
6700
8077
|
method: "GET",
|
|
@@ -6773,6 +8150,27 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6773
8150
|
path: "/ui-bridge/design/style-guide",
|
|
6774
8151
|
description: "Clear loaded style guide"
|
|
6775
8152
|
},
|
|
8153
|
+
// Quality Evaluation
|
|
8154
|
+
DESIGN_EVALUATE: {
|
|
8155
|
+
method: "POST",
|
|
8156
|
+
path: "/ui-bridge/design/evaluate",
|
|
8157
|
+
description: "Score the current UI against a quality context"
|
|
8158
|
+
},
|
|
8159
|
+
DESIGN_EVALUATE_CONTEXTS: {
|
|
8160
|
+
method: "GET",
|
|
8161
|
+
path: "/ui-bridge/design/evaluate/contexts",
|
|
8162
|
+
description: "List available quality evaluation contexts"
|
|
8163
|
+
},
|
|
8164
|
+
DESIGN_EVALUATE_BASELINE: {
|
|
8165
|
+
method: "POST",
|
|
8166
|
+
path: "/ui-bridge/design/evaluate/baseline",
|
|
8167
|
+
description: "Save a quality baseline snapshot"
|
|
8168
|
+
},
|
|
8169
|
+
DESIGN_EVALUATE_DIFF: {
|
|
8170
|
+
method: "POST",
|
|
8171
|
+
path: "/ui-bridge/design/evaluate/diff",
|
|
8172
|
+
description: "Diff the current UI against a saved baseline"
|
|
8173
|
+
},
|
|
6776
8174
|
// AI helpers
|
|
6777
8175
|
AI_FILL_FORM: {
|
|
6778
8176
|
method: "POST",
|
|
@@ -6804,18 +8202,34 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6804
8202
|
path: "/ui-bridge/ai/wait-for-element",
|
|
6805
8203
|
description: "Wait for element predicate via HTTP (runner-only; mobile returns NOT_SUPPORTED \u2014 use WS waitForElement instead)"
|
|
6806
8204
|
},
|
|
6807
|
-
// Test hooks — drive ModalDetector state
|
|
6808
|
-
// (gated by `features.testHooks`). Mirrors `pushModal` /
|
|
6809
|
-
// calls that components normally make in-process.
|
|
8205
|
+
// Test hooks — drive ModalDetector state and the screen lock from outside
|
|
8206
|
+
// the React tree (gated by `features.testHooks`). Mirrors `pushModal` /
|
|
8207
|
+
// `dismissModal` calls that components normally make in-process.
|
|
8208
|
+
//
|
|
8209
|
+
// These are LISTED unconditionally — this table is a static constant with no
|
|
8210
|
+
// config to consult — but each carries `requiresTestHooks: true`, which is
|
|
8211
|
+
// the machine-readable form of the `(testHooks)` in its description. With the
|
|
8212
|
+
// flag off `routeRequest` and `buildRoutesPayload` both drop them and the
|
|
8213
|
+
// route answers a bare 404. `route-table-parity.test.ts` compares the whole
|
|
8214
|
+
// table against `_routes` at `testHooks: true`, and the flagged subset
|
|
8215
|
+
// against `_routes` at `testHooks: false`, so both surfaces are pinned.
|
|
6810
8216
|
PUSH_MODAL: {
|
|
6811
8217
|
method: "POST",
|
|
6812
8218
|
path: "/ui-bridge/control/modal/push",
|
|
6813
|
-
description: "Push a modal onto the modal stack (testHooks)"
|
|
8219
|
+
description: "Push a modal onto the modal stack (testHooks)",
|
|
8220
|
+
requiresTestHooks: true
|
|
6814
8221
|
},
|
|
6815
8222
|
DISMISS_MODAL: {
|
|
6816
8223
|
method: "POST",
|
|
6817
8224
|
path: "/ui-bridge/control/modal/dismiss/:id",
|
|
6818
|
-
description: "Dismiss a modal by id (testHooks)"
|
|
8225
|
+
description: "Dismiss a modal by id (testHooks)",
|
|
8226
|
+
requiresTestHooks: true
|
|
8227
|
+
},
|
|
8228
|
+
KEEP_AWAKE: {
|
|
8229
|
+
method: "POST",
|
|
8230
|
+
path: "/ui-bridge/control/keep-awake",
|
|
8231
|
+
description: "Keep the device screen awake via the injected keepAwakeProvider: body { enabled, durationMs? } (testHooks)",
|
|
8232
|
+
requiresTestHooks: true
|
|
6819
8233
|
},
|
|
6820
8234
|
// Observability — last-N ring buffers (always mounted; `installed:false`
|
|
6821
8235
|
// when the console/network patches are off, i.e. no testHooks).
|
|
@@ -7642,9 +9056,13 @@ var styles = reactNative.StyleSheet.create({
|
|
|
7642
9056
|
});
|
|
7643
9057
|
|
|
7644
9058
|
exports.CloudRelayClient = CloudRelayClient;
|
|
9059
|
+
exports.DEFAULT_PARAM_VALIDATION_MODE = DEFAULT_PARAM_VALIDATION_MODE;
|
|
7645
9060
|
exports.DefaultNativeActionExecutor = DefaultNativeActionExecutor;
|
|
7646
9061
|
exports.DeviceAnnouncer = DeviceAnnouncer;
|
|
9062
|
+
exports.MAX_ACTION_TIMEOUT_MS = MAX_ACTION_TIMEOUT_MS;
|
|
7647
9063
|
exports.ModalDetector = ModalDetector;
|
|
9064
|
+
exports.NATIVE_STANDARD_ACTION_EFFECTS = NATIVE_STANDARD_ACTION_EFFECTS;
|
|
9065
|
+
exports.NativeActionError = NativeActionError;
|
|
7648
9066
|
exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
|
|
7649
9067
|
exports.NativeUIBridgeServer = NativeUIBridgeServer;
|
|
7650
9068
|
exports.State = State;
|
|
@@ -7662,6 +9080,7 @@ exports.buildUpgradeResponse = buildUpgradeResponse;
|
|
|
7662
9080
|
exports.captureNativeResponsiveSnapshot = captureNativeResponsiveSnapshot;
|
|
7663
9081
|
exports.captureNativeStateVariations = captureNativeStateVariations;
|
|
7664
9082
|
exports.computeAcceptKey = computeAcceptKey;
|
|
9083
|
+
exports.computeVisibility = computeVisibility;
|
|
7665
9084
|
exports.createDesignHandlers = createDesignHandlers;
|
|
7666
9085
|
exports.createNativeActionExecutor = createNativeActionExecutor;
|
|
7667
9086
|
exports.createNativeElementIdentifier = createNativeElementIdentifier;
|
|
@@ -7672,17 +9091,30 @@ exports.deriveText = deriveText;
|
|
|
7672
9091
|
exports.extractHandlerNames = extractHandlerNames;
|
|
7673
9092
|
exports.findAllByPattern = findAllByPattern;
|
|
7674
9093
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
9094
|
+
exports.formatParamValidationFailure = formatParamValidationFailure;
|
|
9095
|
+
exports.getDefaultParamValidationMode = getDefaultParamValidationMode;
|
|
7675
9096
|
exports.getGlobalRegistry = getGlobalRegistry;
|
|
7676
9097
|
exports.getNativeElementDesignData = getNativeElementDesignData;
|
|
9098
|
+
exports.httpStatusForResponse = httpStatusForResponse;
|
|
9099
|
+
exports.inertAbortSignal = inertAbortSignal;
|
|
9100
|
+
exports.intersectRects = intersectRects;
|
|
7677
9101
|
exports.isAddrInUseError = isAddrInUseError;
|
|
9102
|
+
exports.isEmptyRect = isEmptyRect;
|
|
7678
9103
|
exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
|
|
7679
9104
|
exports.matchesIdentifier = matchesIdentifier;
|
|
9105
|
+
exports.nativeStandardActionEffect = nativeStandardActionEffect;
|
|
9106
|
+
exports.normalizeActionTimeoutMs = normalizeActionTimeoutMs;
|
|
9107
|
+
exports.pageRectOf = pageRectOf;
|
|
7680
9108
|
exports.parseColorToRgb = parseColorToRgb;
|
|
7681
9109
|
exports.parseFontSizePx = parseFontSizePx;
|
|
7682
9110
|
exports.parseTreePath = parseTreePath;
|
|
7683
9111
|
exports.projectVisionFields = projectVisionFields;
|
|
9112
|
+
exports.resetDefaultParamValidationMode = resetDefaultParamValidationMode;
|
|
7684
9113
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
9114
|
+
exports.resolveActionEffect = resolveActionEffect;
|
|
7685
9115
|
exports.roleForType = roleForType;
|
|
9116
|
+
exports.runAbortable = runAbortable;
|
|
9117
|
+
exports.setDefaultParamValidationMode = setDefaultParamValidationMode;
|
|
7686
9118
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
7687
9119
|
exports.useToastRecorder = useToastRecorder;
|
|
7688
9120
|
exports.useUIBridge = useUIBridge;
|
|
@@ -7696,5 +9128,6 @@ exports.useUIComponent = useUIComponent;
|
|
|
7696
9128
|
exports.useUIComponentAction = useUIComponentAction;
|
|
7697
9129
|
exports.useUIElement = useUIElement;
|
|
7698
9130
|
exports.useUIElementWithProps = useUIElementWithProps;
|
|
9131
|
+
exports.validateActionParams = validateActionParams;
|
|
7699
9132
|
//# sourceMappingURL=index.js.map
|
|
7700
9133
|
//# sourceMappingURL=index.js.map
|