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