@qontinui/ui-bridge-native 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +853 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +835 -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 +317 -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 +1603 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1585 -119
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +1 -23
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1420 -113
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1420 -113
- 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 +348 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +348 -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.map +1 -1
- package/dist/transport/relay-logging.d.ts +50 -0
- package/dist/transport/relay-logging.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -184,7 +184,42 @@ function projectVisionFields(args) {
|
|
|
184
184
|
return fields;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// src/core/element-actions.ts
|
|
188
|
+
function serializeElementCustomActions(customActions) {
|
|
189
|
+
if (!customActions) return void 0;
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const [id, action] of Object.entries(customActions)) {
|
|
192
|
+
if (!action) continue;
|
|
193
|
+
const entry = { id };
|
|
194
|
+
if (action.label !== void 0) entry.label = action.label;
|
|
195
|
+
if (action.description !== void 0) entry.description = action.description;
|
|
196
|
+
if (action.effect !== void 0) entry.effect = action.effect;
|
|
197
|
+
out.push(entry);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
187
202
|
// src/core/registry.ts
|
|
203
|
+
function toRegisteredComponentActions(actions) {
|
|
204
|
+
return actions.map((a) => ({
|
|
205
|
+
id: a.id,
|
|
206
|
+
label: a.label,
|
|
207
|
+
description: a.description,
|
|
208
|
+
paramSchema: a.paramSchema,
|
|
209
|
+
effect: a.effect,
|
|
210
|
+
handler: a.handler
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
function publishedActionSignature(actions) {
|
|
214
|
+
return JSON.stringify(
|
|
215
|
+
actions.map((a) => [a.id, a.label, a.description, a.paramSchema, a.effect])
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function sameElementIds(a, b) {
|
|
219
|
+
if (a === b) return true;
|
|
220
|
+
if (a === void 0 || b === void 0) return false;
|
|
221
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
222
|
+
}
|
|
188
223
|
function extractHandlerNames(props) {
|
|
189
224
|
if (!props) return [];
|
|
190
225
|
return Object.keys(props).filter((k) => typeof props[k] === "function");
|
|
@@ -193,6 +228,22 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
193
228
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
194
229
|
return registrationRoute === currentRoute;
|
|
195
230
|
}
|
|
231
|
+
function deriveActiveTabFromSegments(segments) {
|
|
232
|
+
if (!segments || segments.length === 0) return void 0;
|
|
233
|
+
let innermostGroup = -1;
|
|
234
|
+
for (let i = 0; i < segments.length; i++) {
|
|
235
|
+
const segment = segments[i];
|
|
236
|
+
if (typeof segment === "string" && segment.startsWith("(") && segment.endsWith(")")) {
|
|
237
|
+
innermostGroup = i;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (innermostGroup === -1) return void 0;
|
|
241
|
+
const next = segments[innermostGroup + 1];
|
|
242
|
+
if (next === void 0) return "index";
|
|
243
|
+
if (typeof next !== "string" || next.length === 0) return void 0;
|
|
244
|
+
if (next.startsWith("[") && next.endsWith("]")) return void 0;
|
|
245
|
+
return next;
|
|
246
|
+
}
|
|
196
247
|
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
197
248
|
if (visibility !== "visible") return void 0;
|
|
198
249
|
const layout = state.layout;
|
|
@@ -212,8 +263,58 @@ function projectBbox(state, visibility, pixelRatio = 1) {
|
|
|
212
263
|
h: toPx(layout.height)
|
|
213
264
|
};
|
|
214
265
|
}
|
|
215
|
-
function
|
|
216
|
-
const
|
|
266
|
+
function pageRectOf(state) {
|
|
267
|
+
const layout = state.layout;
|
|
268
|
+
if (!layout) return null;
|
|
269
|
+
if (layout.pageOriginUnmeasured === true) return null;
|
|
270
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
271
|
+
if (!hasPage) return null;
|
|
272
|
+
const left = layout.pageX;
|
|
273
|
+
const top = layout.pageY;
|
|
274
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return { left, top, right: left + layout.width, bottom: top + layout.height };
|
|
278
|
+
}
|
|
279
|
+
function spansOverlap(aMin, aMax, bMin, bMax) {
|
|
280
|
+
if (aMin === aMax) return aMin >= bMin && aMin <= bMax;
|
|
281
|
+
return aMin < bMax && aMax > bMin;
|
|
282
|
+
}
|
|
283
|
+
function isEmptyRect(rect) {
|
|
284
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
285
|
+
}
|
|
286
|
+
function intersectRects(a, b) {
|
|
287
|
+
if (!a) return b;
|
|
288
|
+
if (!b) return a;
|
|
289
|
+
return {
|
|
290
|
+
left: Math.max(a.left, b.left),
|
|
291
|
+
top: Math.max(a.top, b.top),
|
|
292
|
+
right: Math.min(a.right, b.right),
|
|
293
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function computeVisibility(state, clip) {
|
|
297
|
+
if (!state.visible) {
|
|
298
|
+
return { visibility: "hidden", visibilityReason: "hidden" };
|
|
299
|
+
}
|
|
300
|
+
if (state.layout === null) {
|
|
301
|
+
return { visibility: "likely-visible", visibilityReason: "no-layout" };
|
|
302
|
+
}
|
|
303
|
+
if (!clip) {
|
|
304
|
+
return { visibility: "visible" };
|
|
305
|
+
}
|
|
306
|
+
const rect = pageRectOf(state);
|
|
307
|
+
if (!rect) {
|
|
308
|
+
return { visibility: "visible" };
|
|
309
|
+
}
|
|
310
|
+
if (isEmptyRect(clip)) {
|
|
311
|
+
return { visibility: "hidden", visibilityReason: "off-screen" };
|
|
312
|
+
}
|
|
313
|
+
const onScreen = spansOverlap(rect.left, rect.right, clip.left, clip.right) && spansOverlap(rect.top, rect.bottom, clip.top, clip.bottom);
|
|
314
|
+
return onScreen ? { visibility: "visible" } : { visibility: "hidden", visibilityReason: "off-screen" };
|
|
315
|
+
}
|
|
316
|
+
function inferActions(type, scrollAncestorId) {
|
|
317
|
+
const baseActions = scrollAncestorId ? ["focus", "blur", "scrollIntoView"] : ["focus", "blur"];
|
|
217
318
|
switch (type) {
|
|
218
319
|
case "button":
|
|
219
320
|
case "touchable":
|
|
@@ -285,6 +386,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
285
386
|
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
286
387
|
*/
|
|
287
388
|
this.pixelRatio = 1;
|
|
389
|
+
/**
|
|
390
|
+
* Injected getter for the device window size in logical dp, used to clip
|
|
391
|
+
* reported visibility. Injected rather than read here for exactly the reason
|
|
392
|
+
* `pixelRatio` is: a static `import { Dimensions } from 'react-native'` in
|
|
393
|
+
* this non-React core module breaks the vitest suite and risks the
|
|
394
|
+
* Metro/Hermes `unknownModuleError`. It is a PROVIDER, not a value, so a
|
|
395
|
+
* rotation is picked up on the next snapshot with nothing to keep in sync.
|
|
396
|
+
*
|
|
397
|
+
* `null` means the window bounds are UNKNOWN, and {@link computeVisibility}
|
|
398
|
+
* treats unknown bounds as "cannot demote" — never as an empty viewport.
|
|
399
|
+
*/
|
|
400
|
+
this.viewportProvider = null;
|
|
288
401
|
/**
|
|
289
402
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
290
403
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -322,6 +435,30 @@ var NativeUIBridgeRegistry = class {
|
|
|
322
435
|
getRouteProvider() {
|
|
323
436
|
return this.routeProvider;
|
|
324
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the snapshot's `activeTab`: an explicit `getActiveTab()` on the
|
|
440
|
+
* route provider wins, otherwise derive it from the Expo Router segments the
|
|
441
|
+
* provider already exposes.
|
|
442
|
+
*
|
|
443
|
+
* A throwing or empty-string provider degrades to the derivation rather than
|
|
444
|
+
* to `undefined` — the host's opt-in extra should never be able to make the
|
|
445
|
+
* snapshot WORSE than it was without it. A blank derivation stays blank.
|
|
446
|
+
*/
|
|
447
|
+
resolveActiveTab(segments) {
|
|
448
|
+
const explicit = (() => {
|
|
449
|
+
if (!this.routeProvider?.getActiveTab) return void 0;
|
|
450
|
+
try {
|
|
451
|
+
const value = this.routeProvider.getActiveTab();
|
|
452
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
453
|
+
} catch (error3) {
|
|
454
|
+
if (this.config.verbose) {
|
|
455
|
+
console.warn(`[ui-bridge-native] routeProvider getActiveTab threw:`, error3);
|
|
456
|
+
}
|
|
457
|
+
return void 0;
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
return explicit ?? deriveActiveTabFromSegments(segments);
|
|
461
|
+
}
|
|
325
462
|
/**
|
|
326
463
|
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
327
464
|
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
@@ -337,6 +474,56 @@ var NativeUIBridgeRegistry = class {
|
|
|
337
474
|
getPixelRatio() {
|
|
338
475
|
return this.pixelRatio;
|
|
339
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Set the getter for the device window size (logical dp) that reported
|
|
479
|
+
* visibility is clipped against. The React provider passes
|
|
480
|
+
* `() => Dimensions.get('window')` here at construction.
|
|
481
|
+
*
|
|
482
|
+
* Pass `null` to detach. While detached the window bounds are UNKNOWN and no
|
|
483
|
+
* element is ever demoted for being outside them.
|
|
484
|
+
*/
|
|
485
|
+
setViewportProvider(provider) {
|
|
486
|
+
this.viewportProvider = provider ?? null;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The window as a page-space rect, or `null` when it is unknown or the
|
|
490
|
+
* provider returned something unusable (it throws in some RN teardown
|
|
491
|
+
* paths, and a zero-size window would silently hide the entire screen).
|
|
492
|
+
*/
|
|
493
|
+
getViewportRect() {
|
|
494
|
+
if (!this.viewportProvider) return null;
|
|
495
|
+
let size;
|
|
496
|
+
try {
|
|
497
|
+
size = this.viewportProvider();
|
|
498
|
+
} catch (error3) {
|
|
499
|
+
if (this.config.verbose) {
|
|
500
|
+
console.warn(`[ui-bridge-native] viewportProvider threw:`, error3);
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const { width, height } = size ?? {};
|
|
505
|
+
if (typeof width !== "number" || typeof height !== "number" || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return { left: 0, top: 0, right: width, bottom: height };
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The region `element` must reach to count as on screen: the window,
|
|
512
|
+
* intersected with the measured frame of its declared `scrollAncestorId`.
|
|
513
|
+
*
|
|
514
|
+
* Returns `null` when neither bound is known — see {@link computeVisibility}
|
|
515
|
+
* for why that means "do not demote" rather than "nothing is visible".
|
|
516
|
+
* An ancestor that is not registered, or registered but not yet measured,
|
|
517
|
+
* contributes nothing rather than hiding its subtree.
|
|
518
|
+
*/
|
|
519
|
+
getClipRectFor(element, viewport) {
|
|
520
|
+
const bounds = viewport === void 0 ? this.getViewportRect() : viewport;
|
|
521
|
+
const ancestorId = element.scrollAncestorId;
|
|
522
|
+
if (!ancestorId) return bounds;
|
|
523
|
+
const ancestor = this.elements.get(ancestorId);
|
|
524
|
+
if (!ancestor || ancestorId === element.id) return bounds;
|
|
525
|
+
return intersectRects(bounds, pageRectOf(ancestor.getState()));
|
|
526
|
+
}
|
|
340
527
|
// ============================================================================
|
|
341
528
|
// Snapshot Enricher Slots
|
|
342
529
|
// ============================================================================
|
|
@@ -380,16 +567,17 @@ var NativeUIBridgeRegistry = class {
|
|
|
380
567
|
const {
|
|
381
568
|
type = "custom",
|
|
382
569
|
label,
|
|
383
|
-
actions = inferActions(type),
|
|
384
570
|
customActions,
|
|
385
571
|
props,
|
|
386
572
|
treePath = id,
|
|
387
573
|
testId,
|
|
388
574
|
accessibilityLabel,
|
|
389
575
|
registrationRoute,
|
|
576
|
+
scrollAncestorId,
|
|
390
577
|
flatStyle,
|
|
391
578
|
stateStyles
|
|
392
579
|
} = options;
|
|
580
|
+
const actions = options.actions ?? inferActions(type, scrollAncestorId);
|
|
393
581
|
const getState = () => {
|
|
394
582
|
const element = ref.current;
|
|
395
583
|
if (!element) {
|
|
@@ -432,6 +620,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
432
620
|
registeredAt: Date.now(),
|
|
433
621
|
mounted: true,
|
|
434
622
|
registrationRoute: registrationRoute ?? null,
|
|
623
|
+
scrollAncestorId,
|
|
435
624
|
flatStyle,
|
|
436
625
|
stateStyles
|
|
437
626
|
};
|
|
@@ -562,6 +751,103 @@ var NativeUIBridgeRegistry = class {
|
|
|
562
751
|
console.log(`[ui-bridge-native] Marked ${cleared} elements offscreen for route: ${route}`);
|
|
563
752
|
}
|
|
564
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* Re-measure every mounted-visible element's geometry via its stored React
|
|
756
|
+
* ref and write the fresh coordinates into the registry ("measure-on-
|
|
757
|
+
* snapshot").
|
|
758
|
+
*
|
|
759
|
+
* Why: element geometry is otherwise captured ONCE at mount time
|
|
760
|
+
* (`onLayout` + `measureInWindow` in `useUIElement`) and RN never re-fires
|
|
761
|
+
* `onLayout` on pure translation — scrolling a virtualized list
|
|
762
|
+
* (FlashList), recycling a cell, a keyboard shift or an orientation change
|
|
763
|
+
* all move elements WITHOUT updating the stored `state.layout`. Snapshot
|
|
764
|
+
* consumers (vision analyzers hit-testing `bbox`, coord-based tap) then
|
|
765
|
+
* operate on stale positions. Re-measuring at READ time is correct
|
|
766
|
+
* regardless of what moved the element and needs zero per-consumer wiring:
|
|
767
|
+
* every registry entry already stores its ref (see `registerElement`).
|
|
768
|
+
*
|
|
769
|
+
* Per-element outcomes:
|
|
770
|
+
* - non-zero dims → write a fresh full layout (same state shape as the
|
|
771
|
+
* mount-time measure loop in `useUIElement`); counted as `measured`.
|
|
772
|
+
* - zero dims AND the element previously had a non-null `state.layout` →
|
|
773
|
+
* the view collapsed / left the screen; clear to
|
|
774
|
+
* `{ visible: false, layout: null }` (same shape `markRouteOffscreen`
|
|
775
|
+
* writes); counted as `cleared`.
|
|
776
|
+
* - zero dims and it never had a layout → leave state untouched (element
|
|
777
|
+
* in the mount→first-onLayout gap; don't demote `likely-visible`);
|
|
778
|
+
* counted as `skipped`.
|
|
779
|
+
* - no ref / no callable `measureInWindow` (test fixtures, web) → leave
|
|
780
|
+
* untouched; counted as `skipped`.
|
|
781
|
+
*
|
|
782
|
+
* The whole sweep races an overall timeout (default 250 ms) so a dead ref
|
|
783
|
+
* whose `measureInWindow` never calls back can never hang a snapshot: the
|
|
784
|
+
* returned promise resolves at the deadline with whatever counts have
|
|
785
|
+
* accumulated. Late callbacks may still write state afterwards — harmless,
|
|
786
|
+
* freshest data wins. Never throws.
|
|
787
|
+
*
|
|
788
|
+
* Non-React core module on purpose — `measureInWindow` is duck-typed off
|
|
789
|
+
* the stored ref rather than imported from `react-native` (see the
|
|
790
|
+
* `projectBbox` doc comment for why this file must not import RN).
|
|
791
|
+
*/
|
|
792
|
+
async refreshMeasurements(options) {
|
|
793
|
+
const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 250;
|
|
794
|
+
const counts = { measured: 0, cleared: 0, skipped: 0 };
|
|
795
|
+
const pending = [];
|
|
796
|
+
for (const element of this.getMountedVisibleElements()) {
|
|
797
|
+
const node = element.ref?.current;
|
|
798
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
799
|
+
counts.skipped++;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const id = element.id;
|
|
803
|
+
pending.push(
|
|
804
|
+
new Promise((resolve) => {
|
|
805
|
+
let settled = false;
|
|
806
|
+
try {
|
|
807
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
808
|
+
if (settled) return;
|
|
809
|
+
settled = true;
|
|
810
|
+
try {
|
|
811
|
+
if (w > 0 && h > 0) {
|
|
812
|
+
this.updateElementState(id, {
|
|
813
|
+
mounted: true,
|
|
814
|
+
visible: true,
|
|
815
|
+
enabled: true,
|
|
816
|
+
focused: false,
|
|
817
|
+
layout: { x: pageX, y: pageY, width: w, height: h, pageX, pageY }
|
|
818
|
+
});
|
|
819
|
+
counts.measured++;
|
|
820
|
+
} else if (this.elements.get(id)?.getState().layout != null) {
|
|
821
|
+
this.updateElementState(id, { visible: false, layout: null });
|
|
822
|
+
counts.cleared++;
|
|
823
|
+
} else {
|
|
824
|
+
counts.skipped++;
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
resolve();
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
} catch {
|
|
831
|
+
if (!settled) {
|
|
832
|
+
settled = true;
|
|
833
|
+
counts.skipped++;
|
|
834
|
+
resolve();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
})
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (pending.length > 0) {
|
|
841
|
+
await new Promise((resolve) => {
|
|
842
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
843
|
+
void Promise.all(pending).then(() => {
|
|
844
|
+
clearTimeout(timer);
|
|
845
|
+
resolve();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return counts;
|
|
850
|
+
}
|
|
565
851
|
/**
|
|
566
852
|
* Update element state
|
|
567
853
|
*/
|
|
@@ -711,12 +997,8 @@ var NativeUIBridgeRegistry = class {
|
|
|
711
997
|
id,
|
|
712
998
|
name,
|
|
713
999
|
description,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
label: a.label,
|
|
717
|
-
description: a.description,
|
|
718
|
-
handler: a.handler
|
|
719
|
-
})),
|
|
1000
|
+
// Closed field list, applied once — see `toRegisteredComponentActions`.
|
|
1001
|
+
actions: toRegisteredComponentActions(actions),
|
|
720
1002
|
elementIds,
|
|
721
1003
|
registeredAt: Date.now(),
|
|
722
1004
|
mounted: true
|
|
@@ -728,6 +1010,55 @@ var NativeUIBridgeRegistry = class {
|
|
|
728
1010
|
}
|
|
729
1011
|
return registered;
|
|
730
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Update a registered component's declaration in place — name, description,
|
|
1015
|
+
* actions, elementIds.
|
|
1016
|
+
*
|
|
1017
|
+
* Sister to {@link updateElementMeta}, and it exists for the same reason one
|
|
1018
|
+
* level up. `registerComponent` publishes the declaration exactly once, and
|
|
1019
|
+
* `useUIComponent` registers on mount and never again, so an action whose
|
|
1020
|
+
* `label` carries a count, an action that only exists while a row is
|
|
1021
|
+
* selected, a `paramSchema` that widens once options load, or an `elementIds`
|
|
1022
|
+
* list that grows after mount, all published their mount-time shape to
|
|
1023
|
+
* `/control/components` forever. PR #176 closed that hole for an ELEMENT's
|
|
1024
|
+
* label; this closes it for a COMPONENT's declaration.
|
|
1025
|
+
*
|
|
1026
|
+
* **Idempotent by VALUE over the PUBLISHED shape.** `handler` identity is
|
|
1027
|
+
* excluded from the comparison — call sites pass inline closures that are
|
|
1028
|
+
* freshly allocated every render, so including it would report a change on
|
|
1029
|
+
* every call and make the door useless for a render-path publisher. The
|
|
1030
|
+
* newest handlers are still STORED whenever `actions` is supplied, which is
|
|
1031
|
+
* the other half of the same defect: without it the registry keeps the
|
|
1032
|
+
* closure from the render that registered the component, so a handler reading
|
|
1033
|
+
* component state read the state as it was at mount.
|
|
1034
|
+
*
|
|
1035
|
+
* Returns `true` when the published shape changed and an event was emitted,
|
|
1036
|
+
* `false` otherwise — the same contract as `updateElementMeta`, so a caller
|
|
1037
|
+
* can gate a warning or a state write on a real change.
|
|
1038
|
+
*/
|
|
1039
|
+
updateComponentMeta(id, meta) {
|
|
1040
|
+
const component = this.components.get(id);
|
|
1041
|
+
if (!component) return false;
|
|
1042
|
+
const nextName = meta.name !== void 0 ? meta.name : component.name;
|
|
1043
|
+
const nextDescription = meta.description !== void 0 ? meta.description : component.description;
|
|
1044
|
+
const nextActions = meta.actions !== void 0 ? toRegisteredComponentActions(meta.actions) : component.actions;
|
|
1045
|
+
const nextElementIds = meta.elementIds !== void 0 ? meta.elementIds : component.elementIds;
|
|
1046
|
+
const changed = nextName !== component.name || nextDescription !== component.description || !sameElementIds(nextElementIds, component.elementIds) || publishedActionSignature(nextActions) !== publishedActionSignature(component.actions);
|
|
1047
|
+
if (!changed && meta.actions === void 0) return false;
|
|
1048
|
+
this.components.set(id, {
|
|
1049
|
+
...component,
|
|
1050
|
+
name: nextName,
|
|
1051
|
+
description: nextDescription,
|
|
1052
|
+
actions: nextActions,
|
|
1053
|
+
elementIds: nextElementIds
|
|
1054
|
+
});
|
|
1055
|
+
if (!changed) return false;
|
|
1056
|
+
this.emit("component:registered", { id, name: nextName });
|
|
1057
|
+
if (this.config.verbose) {
|
|
1058
|
+
console.log(`[ui-bridge-native] Updated component: ${id} (${nextName})`);
|
|
1059
|
+
}
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
731
1062
|
/**
|
|
732
1063
|
* Unregister a component
|
|
733
1064
|
*/
|
|
@@ -841,41 +1172,59 @@ var NativeUIBridgeRegistry = class {
|
|
|
841
1172
|
* server-level override (the bare-server / cloud-relay path) still get a
|
|
842
1173
|
* populated `currentRoute` field as long as `setRouteProvider` was called
|
|
843
1174
|
* on the registry.
|
|
1175
|
+
*
|
|
1176
|
+
* `activeTab` resolves through {@link resolveActiveTab} on BOTH branches. An
|
|
1177
|
+
* explicit `routeInfo.activeTab` wins outright; otherwise a registered
|
|
1178
|
+
* provider's `getActiveTab()` answers and the Expo Router derivation is the
|
|
1179
|
+
* fallback. Deriving straight from `routeInfo.segments` here instead — which
|
|
1180
|
+
* is what this branch did when `activeTab` was introduced — silently ignored
|
|
1181
|
+
* `RouteProvider.getActiveTab` on the ONLY path that serves
|
|
1182
|
+
* `control/snapshot` once `setRouteProvider` is wired (the server installs a
|
|
1183
|
+
* `getSnapshot` override that passes `routeInfo`), so the opt-in hook for an
|
|
1184
|
+
* app whose visible pane is decoupled from the router reached no consumer.
|
|
844
1185
|
*/
|
|
845
1186
|
createSnapshot(routeInfo, options) {
|
|
846
1187
|
const resolvedRoute = (() => {
|
|
847
1188
|
if (routeInfo !== void 0) {
|
|
1189
|
+
const explicit = typeof routeInfo.activeTab === "string" && routeInfo.activeTab.length > 0 ? routeInfo.activeTab : void 0;
|
|
848
1190
|
return {
|
|
849
1191
|
currentRoute: routeInfo.currentRoute ?? null,
|
|
850
|
-
segments: routeInfo.segments
|
|
1192
|
+
segments: routeInfo.segments,
|
|
1193
|
+
activeTab: explicit ?? this.resolveActiveTab(routeInfo.segments)
|
|
851
1194
|
};
|
|
852
1195
|
}
|
|
853
1196
|
if (this.routeProvider) {
|
|
854
1197
|
try {
|
|
1198
|
+
const segments = this.routeProvider.getSegments?.();
|
|
855
1199
|
return {
|
|
856
1200
|
currentRoute: this.routeProvider.getCurrentRoute() ?? null,
|
|
857
|
-
segments
|
|
1201
|
+
segments,
|
|
1202
|
+
activeTab: this.resolveActiveTab(segments)
|
|
858
1203
|
};
|
|
859
1204
|
} catch (error3) {
|
|
860
1205
|
if (this.config.verbose) {
|
|
861
1206
|
console.warn(`[ui-bridge-native] routeProvider getCurrentRoute threw:`, error3);
|
|
862
1207
|
}
|
|
863
|
-
return { currentRoute: null, segments: void 0 };
|
|
1208
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
864
1209
|
}
|
|
865
1210
|
}
|
|
866
|
-
return { currentRoute: null, segments: void 0 };
|
|
1211
|
+
return { currentRoute: null, segments: void 0, activeTab: void 0 };
|
|
867
1212
|
})();
|
|
868
1213
|
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
869
1214
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
870
1215
|
const currentRoute = resolvedRoute.currentRoute;
|
|
871
1216
|
elements = elements.filter((e) => matchesCurrentRoute(e.registrationRoute, currentRoute));
|
|
872
1217
|
}
|
|
1218
|
+
const viewport = this.getViewportRect();
|
|
873
1219
|
const snapshot = {
|
|
874
1220
|
timestamp: Date.now(),
|
|
875
1221
|
elements: elements.map((e) => {
|
|
876
1222
|
const handlers = extractHandlerNames(e.props);
|
|
877
1223
|
const state = e.getState();
|
|
878
|
-
const visibility
|
|
1224
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1225
|
+
state,
|
|
1226
|
+
this.getClipRectFor(e, viewport)
|
|
1227
|
+
);
|
|
879
1228
|
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
880
1229
|
const vision = projectVisionFields({
|
|
881
1230
|
type: e.type,
|
|
@@ -895,10 +1244,11 @@ var NativeUIBridgeRegistry = class {
|
|
|
895
1244
|
identifier: e.getIdentifier(),
|
|
896
1245
|
state,
|
|
897
1246
|
actions: e.actions,
|
|
898
|
-
customActions:
|
|
1247
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
899
1248
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
900
1249
|
registrationRoute: e.registrationRoute,
|
|
901
1250
|
visibility,
|
|
1251
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
902
1252
|
...bbox ? { bbox } : {},
|
|
903
1253
|
...vision
|
|
904
1254
|
};
|
|
@@ -918,6 +1268,13 @@ var NativeUIBridgeRegistry = class {
|
|
|
918
1268
|
})),
|
|
919
1269
|
currentRoute: resolvedRoute.currentRoute,
|
|
920
1270
|
segments: resolvedRoute.segments,
|
|
1271
|
+
// Canonical aliases, spelled the way the web SDK's `BridgeSnapshot`
|
|
1272
|
+
// spells them so a cross-platform consumer reads one field name on both
|
|
1273
|
+
// platforms. Omitted rather than nulled when unknown — same convention
|
|
1274
|
+
// as the web fields, and it keeps the emitted shape byte-identical for
|
|
1275
|
+
// hosts that wire no route provider.
|
|
1276
|
+
...resolvedRoute.currentRoute ? { route: resolvedRoute.currentRoute } : {},
|
|
1277
|
+
...resolvedRoute.activeTab ? { activeTab: resolvedRoute.activeTab } : {},
|
|
921
1278
|
registration: this.getRegistrationCoverage()
|
|
922
1279
|
};
|
|
923
1280
|
if (this.appInfo) {
|
|
@@ -1020,6 +1377,468 @@ function resetGlobalRegistry() {
|
|
|
1020
1377
|
globalRegistry = null;
|
|
1021
1378
|
}
|
|
1022
1379
|
|
|
1380
|
+
// src/core/action-effect.ts
|
|
1381
|
+
var NATIVE_STANDARD_ACTION_EFFECTS = {
|
|
1382
|
+
// --- Reads ---------------------------------------------------------------
|
|
1383
|
+
focus: "read",
|
|
1384
|
+
blur: "read",
|
|
1385
|
+
scroll: "read",
|
|
1386
|
+
scrollIntoView: "read",
|
|
1387
|
+
longPress: "read",
|
|
1388
|
+
// --- Writes --------------------------------------------------------------
|
|
1389
|
+
press: "write",
|
|
1390
|
+
click: "write",
|
|
1391
|
+
doubleTap: "write",
|
|
1392
|
+
type: "write",
|
|
1393
|
+
setValue: "write",
|
|
1394
|
+
clear: "write",
|
|
1395
|
+
swipe: "write",
|
|
1396
|
+
toggle: "write"
|
|
1397
|
+
};
|
|
1398
|
+
function nativeStandardActionEffect(actionId) {
|
|
1399
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_STANDARD_ACTION_EFFECTS, actionId) ? NATIVE_STANDARD_ACTION_EFFECTS[actionId] : void 0;
|
|
1400
|
+
}
|
|
1401
|
+
function resolveActionEffect(action) {
|
|
1402
|
+
return action.effect ?? nativeStandardActionEffect(action.id);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/core/param-schema.ts
|
|
1406
|
+
var DEFAULT_PARAM_VALIDATION_MODE = "warn";
|
|
1407
|
+
var currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
1408
|
+
function setDefaultParamValidationMode(mode) {
|
|
1409
|
+
currentDefaultMode = mode;
|
|
1410
|
+
}
|
|
1411
|
+
function getDefaultParamValidationMode() {
|
|
1412
|
+
return currentDefaultMode;
|
|
1413
|
+
}
|
|
1414
|
+
function resetDefaultParamValidationMode() {
|
|
1415
|
+
currentDefaultMode = DEFAULT_PARAM_VALIDATION_MODE;
|
|
1416
|
+
}
|
|
1417
|
+
var PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
1418
|
+
"string",
|
|
1419
|
+
"number",
|
|
1420
|
+
"integer",
|
|
1421
|
+
"boolean",
|
|
1422
|
+
"object",
|
|
1423
|
+
"array",
|
|
1424
|
+
"null"
|
|
1425
|
+
]);
|
|
1426
|
+
var OBJECT_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1427
|
+
"type",
|
|
1428
|
+
"properties",
|
|
1429
|
+
"required",
|
|
1430
|
+
"additionalProperties",
|
|
1431
|
+
"patternProperties",
|
|
1432
|
+
"propertyNames",
|
|
1433
|
+
"minProperties",
|
|
1434
|
+
"maxProperties",
|
|
1435
|
+
"dependentRequired",
|
|
1436
|
+
"dependentSchemas",
|
|
1437
|
+
"unevaluatedProperties",
|
|
1438
|
+
"allOf",
|
|
1439
|
+
"anyOf",
|
|
1440
|
+
"oneOf",
|
|
1441
|
+
"not",
|
|
1442
|
+
"if",
|
|
1443
|
+
"then",
|
|
1444
|
+
"else",
|
|
1445
|
+
"enum",
|
|
1446
|
+
"const",
|
|
1447
|
+
"default",
|
|
1448
|
+
"examples",
|
|
1449
|
+
"title",
|
|
1450
|
+
"description",
|
|
1451
|
+
"deprecated",
|
|
1452
|
+
"readOnly",
|
|
1453
|
+
"writeOnly",
|
|
1454
|
+
"$schema",
|
|
1455
|
+
"$id",
|
|
1456
|
+
"$ref",
|
|
1457
|
+
"$comment",
|
|
1458
|
+
"$defs",
|
|
1459
|
+
"definitions"
|
|
1460
|
+
]);
|
|
1461
|
+
var MAX_SCHEMA_DEPTH = 32;
|
|
1462
|
+
var MAX_PATTERN_LENGTH = 200;
|
|
1463
|
+
function hasNestedQuantifiers(pattern) {
|
|
1464
|
+
return /(\((?:[^()]*[+*}])[^()]*\))[+*?]|\(\?:[^()]*[+*}][^()]*\)[+*?]/.test(pattern);
|
|
1465
|
+
}
|
|
1466
|
+
function isPlainObject(value) {
|
|
1467
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1468
|
+
}
|
|
1469
|
+
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
1470
|
+
function hasOwn(value, key) {
|
|
1471
|
+
return hasOwnProperty.call(value, key);
|
|
1472
|
+
}
|
|
1473
|
+
function render(value) {
|
|
1474
|
+
if (value === null) return "null";
|
|
1475
|
+
if (value === void 0) return "undefined";
|
|
1476
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
1477
|
+
const t = typeof value;
|
|
1478
|
+
if (t === "string") return JSON.stringify(value);
|
|
1479
|
+
if (t === "number" || t === "boolean") return String(value);
|
|
1480
|
+
if (t === "object") return "object";
|
|
1481
|
+
return t;
|
|
1482
|
+
}
|
|
1483
|
+
var MAX_RENDERED_JSON_LENGTH = 120;
|
|
1484
|
+
function renderJson(value) {
|
|
1485
|
+
if (value === void 0) return "undefined";
|
|
1486
|
+
let text;
|
|
1487
|
+
try {
|
|
1488
|
+
text = JSON.stringify(value) ?? String(value);
|
|
1489
|
+
} catch {
|
|
1490
|
+
return render(value);
|
|
1491
|
+
}
|
|
1492
|
+
return text.length > MAX_RENDERED_JSON_LENGTH ? `${text.slice(0, MAX_RENDERED_JSON_LENGTH)}\u2026` : text;
|
|
1493
|
+
}
|
|
1494
|
+
function jsonTypeOf(value) {
|
|
1495
|
+
if (value === null) return "null";
|
|
1496
|
+
if (Array.isArray(value)) return "array";
|
|
1497
|
+
const t = typeof value;
|
|
1498
|
+
if (t === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
1499
|
+
if (t === "string" || t === "boolean" || t === "object") return t;
|
|
1500
|
+
return t;
|
|
1501
|
+
}
|
|
1502
|
+
function matchesType(value, typeName) {
|
|
1503
|
+
switch (typeName) {
|
|
1504
|
+
case "string":
|
|
1505
|
+
return typeof value === "string";
|
|
1506
|
+
case "number":
|
|
1507
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1508
|
+
case "integer":
|
|
1509
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
1510
|
+
case "boolean":
|
|
1511
|
+
return typeof value === "boolean";
|
|
1512
|
+
case "null":
|
|
1513
|
+
return value === null;
|
|
1514
|
+
case "array":
|
|
1515
|
+
return Array.isArray(value);
|
|
1516
|
+
case "object":
|
|
1517
|
+
return isPlainObject(value);
|
|
1518
|
+
default:
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function jsonEquals(a, b, depth = 0) {
|
|
1523
|
+
if (a === b) return true;
|
|
1524
|
+
if (depth > MAX_SCHEMA_DEPTH) return false;
|
|
1525
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
1526
|
+
if (typeof a !== typeof b) return false;
|
|
1527
|
+
if (typeof a !== "object") return false;
|
|
1528
|
+
const aIsArray = Array.isArray(a);
|
|
1529
|
+
if (aIsArray !== Array.isArray(b)) return false;
|
|
1530
|
+
if (aIsArray) {
|
|
1531
|
+
const bArr = b;
|
|
1532
|
+
const aArr = a;
|
|
1533
|
+
if (aArr.length !== bArr.length) return false;
|
|
1534
|
+
return aArr.every((item, i) => jsonEquals(item, bArr[i], depth + 1));
|
|
1535
|
+
}
|
|
1536
|
+
const aObj = a;
|
|
1537
|
+
const bObj = b;
|
|
1538
|
+
const aKeys = Object.keys(aObj);
|
|
1539
|
+
const bKeys = Object.keys(bObj);
|
|
1540
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1541
|
+
return aKeys.every((key) => hasOwn(bObj, key) && jsonEquals(aObj[key], bObj[key], depth + 1));
|
|
1542
|
+
}
|
|
1543
|
+
function joinPath(base, key) {
|
|
1544
|
+
return base === "" ? key : `${base}.${key}`;
|
|
1545
|
+
}
|
|
1546
|
+
function readTypes(schema) {
|
|
1547
|
+
const raw = schema["type"];
|
|
1548
|
+
if (typeof raw === "string") return [raw];
|
|
1549
|
+
if (Array.isArray(raw)) {
|
|
1550
|
+
const names = raw.filter((t) => typeof t === "string");
|
|
1551
|
+
return names.length > 0 ? names : null;
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
function validateValue(value, schema, path, issues, depth) {
|
|
1556
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
1557
|
+
if (typeof schema === "string") {
|
|
1558
|
+
if (PRIMITIVE_TYPE_NAMES.has(schema) && !matchesType(value, schema)) {
|
|
1559
|
+
issues.push({
|
|
1560
|
+
path,
|
|
1561
|
+
keyword: "type",
|
|
1562
|
+
expected: schema,
|
|
1563
|
+
received: render(value),
|
|
1564
|
+
message: `Parameter "${path}" must be of type ${schema}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (!isPlainObject(schema)) return;
|
|
1570
|
+
const types = readTypes(schema);
|
|
1571
|
+
if (types !== null && !types.some((t) => matchesType(value, t))) {
|
|
1572
|
+
const expected = types.join(" | ");
|
|
1573
|
+
issues.push({
|
|
1574
|
+
path,
|
|
1575
|
+
keyword: "type",
|
|
1576
|
+
expected,
|
|
1577
|
+
received: render(value),
|
|
1578
|
+
message: `Parameter "${path}" must be of type ${expected}, received ${jsonTypeOf(value)} (${render(value)}).`
|
|
1579
|
+
});
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (hasOwn(schema, "const") && !jsonEquals(value, schema["const"])) {
|
|
1583
|
+
const expected = renderJson(schema["const"]);
|
|
1584
|
+
const received = renderJson(value);
|
|
1585
|
+
issues.push({
|
|
1586
|
+
path,
|
|
1587
|
+
keyword: "const",
|
|
1588
|
+
expected,
|
|
1589
|
+
received,
|
|
1590
|
+
message: `Parameter "${path}" must equal ${expected}, received ${received}.`
|
|
1591
|
+
});
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const enumValues = schema["enum"];
|
|
1595
|
+
if (Array.isArray(enumValues) && !enumValues.some((allowed) => jsonEquals(value, allowed))) {
|
|
1596
|
+
const expected = enumValues.map((v) => renderJson(v)).join(", ");
|
|
1597
|
+
const received = renderJson(value);
|
|
1598
|
+
issues.push({
|
|
1599
|
+
path,
|
|
1600
|
+
keyword: "enum",
|
|
1601
|
+
expected,
|
|
1602
|
+
received,
|
|
1603
|
+
message: `Parameter "${path}" must be one of [${expected}], received ${received}.`
|
|
1604
|
+
});
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (typeof value === "number") {
|
|
1608
|
+
const min = schema["minimum"];
|
|
1609
|
+
if (typeof min === "number" && value < min) {
|
|
1610
|
+
issues.push({
|
|
1611
|
+
path,
|
|
1612
|
+
keyword: "minimum",
|
|
1613
|
+
expected: `>= ${min}`,
|
|
1614
|
+
received: render(value),
|
|
1615
|
+
message: `Parameter "${path}" must be >= ${min}, received ${value}.`
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
const max = schema["maximum"];
|
|
1619
|
+
if (typeof max === "number" && value > max) {
|
|
1620
|
+
issues.push({
|
|
1621
|
+
path,
|
|
1622
|
+
keyword: "maximum",
|
|
1623
|
+
expected: `<= ${max}`,
|
|
1624
|
+
received: render(value),
|
|
1625
|
+
message: `Parameter "${path}" must be <= ${max}, received ${value}.`
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (typeof value === "string") {
|
|
1630
|
+
const minLength = schema["minLength"];
|
|
1631
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
1632
|
+
issues.push({
|
|
1633
|
+
path,
|
|
1634
|
+
keyword: "minLength",
|
|
1635
|
+
expected: `length >= ${minLength}`,
|
|
1636
|
+
received: `length ${value.length}`,
|
|
1637
|
+
message: `Parameter "${path}" must be at least ${minLength} characters, received ${value.length}.`
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
const maxLength = schema["maxLength"];
|
|
1641
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
1642
|
+
issues.push({
|
|
1643
|
+
path,
|
|
1644
|
+
keyword: "maxLength",
|
|
1645
|
+
expected: `length <= ${maxLength}`,
|
|
1646
|
+
received: `length ${value.length}`,
|
|
1647
|
+
message: `Parameter "${path}" must be at most ${maxLength} characters, received ${value.length}.`
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
const pattern = schema["pattern"];
|
|
1651
|
+
if (typeof pattern === "string" && pattern.length <= MAX_PATTERN_LENGTH && !hasNestedQuantifiers(pattern)) {
|
|
1652
|
+
let re;
|
|
1653
|
+
try {
|
|
1654
|
+
re = new RegExp(pattern);
|
|
1655
|
+
} catch {
|
|
1656
|
+
re = null;
|
|
1657
|
+
}
|
|
1658
|
+
if (re !== null && !re.test(value)) {
|
|
1659
|
+
issues.push({
|
|
1660
|
+
path,
|
|
1661
|
+
keyword: "pattern",
|
|
1662
|
+
expected: `match /${pattern}/`,
|
|
1663
|
+
received: render(value),
|
|
1664
|
+
message: `Parameter "${path}" must match /${pattern}/, received ${render(value)}.`
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (Array.isArray(value) && hasOwn(schema, "items")) {
|
|
1670
|
+
const items = schema["items"];
|
|
1671
|
+
value.forEach((element, index) => {
|
|
1672
|
+
validateValue(element, items, `${path}[${index}]`, issues, depth + 1);
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (isPlainObject(value) && (isPlainObject(schema["properties"]) || hasOwn(schema, "required") || schema["additionalProperties"] === false)) {
|
|
1676
|
+
validateObject(value, schema, path, issues, depth);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function validateObject(value, schema, basePath, issues, depth) {
|
|
1680
|
+
if (depth > MAX_SCHEMA_DEPTH) return;
|
|
1681
|
+
const properties = isPlainObject(schema["properties"]) ? schema["properties"] : {};
|
|
1682
|
+
const requiredRaw = schema["required"];
|
|
1683
|
+
const required = Array.isArray(requiredRaw) ? requiredRaw.filter((k) => typeof k === "string") : [];
|
|
1684
|
+
for (const key of required) {
|
|
1685
|
+
if (!hasOwn(value, key) || value[key] === void 0) {
|
|
1686
|
+
const at = joinPath(basePath, key);
|
|
1687
|
+
issues.push({
|
|
1688
|
+
path: at,
|
|
1689
|
+
keyword: "required",
|
|
1690
|
+
expected: "present",
|
|
1691
|
+
message: `Required parameter "${at}" is missing.`
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (schema["additionalProperties"] === false) {
|
|
1696
|
+
const declared = new Set(Object.keys(properties));
|
|
1697
|
+
const expected = declared.size === 0 ? "no parameters" : `one of [${[...declared].join(", ")}]`;
|
|
1698
|
+
for (const key of Object.keys(value)) {
|
|
1699
|
+
if (!declared.has(key)) {
|
|
1700
|
+
const at = joinPath(basePath, key);
|
|
1701
|
+
issues.push({
|
|
1702
|
+
path: at,
|
|
1703
|
+
keyword: "additionalProperties",
|
|
1704
|
+
expected,
|
|
1705
|
+
received: render(value[key]),
|
|
1706
|
+
message: `Parameter "${at}" is not declared by the schema, which sets additionalProperties: false.`
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
1712
|
+
if (!hasOwn(value, key) || value[key] === void 0) continue;
|
|
1713
|
+
validateValue(value[key], propSchema, joinPath(basePath, key), issues, depth + 1);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function isObjectSchemaForm(schema) {
|
|
1717
|
+
if (isPlainObject(schema["properties"])) return true;
|
|
1718
|
+
if (schema["type"] !== "object") return false;
|
|
1719
|
+
return Object.keys(schema).every((key) => OBJECT_SCHEMA_KEYWORDS.has(key));
|
|
1720
|
+
}
|
|
1721
|
+
function normalizeMapForm(schema) {
|
|
1722
|
+
if (isObjectSchemaForm(schema)) return null;
|
|
1723
|
+
const properties = {};
|
|
1724
|
+
for (const [key, raw] of Object.entries(schema)) {
|
|
1725
|
+
if (raw === null || raw === void 0) continue;
|
|
1726
|
+
if (typeof raw === "string") {
|
|
1727
|
+
properties[key] = PRIMITIVE_TYPE_NAMES.has(raw) ? { type: raw } : {};
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (isPlainObject(raw)) {
|
|
1731
|
+
const { optional: _optional, ...rest } = raw;
|
|
1732
|
+
properties[key] = rest;
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return { type: "object", properties };
|
|
1737
|
+
}
|
|
1738
|
+
function validateActionParams(schema, params) {
|
|
1739
|
+
if (!isPlainObject(schema)) return { valid: true, issues: [] };
|
|
1740
|
+
const normalized = normalizeMapForm(schema) ?? schema;
|
|
1741
|
+
const hasProperties = isPlainObject(normalized["properties"]);
|
|
1742
|
+
const hasRequired = Array.isArray(normalized["required"]);
|
|
1743
|
+
const forbidsExtras = normalized["additionalProperties"] === false;
|
|
1744
|
+
if (!hasProperties && !hasRequired && !forbidsExtras) {
|
|
1745
|
+
return { valid: true, issues: [] };
|
|
1746
|
+
}
|
|
1747
|
+
const issues = [];
|
|
1748
|
+
if (params === void 0 || params === null) {
|
|
1749
|
+
validateObject({}, normalized, "", issues, 0);
|
|
1750
|
+
return { valid: issues.length === 0, issues };
|
|
1751
|
+
}
|
|
1752
|
+
if (!isPlainObject(params)) {
|
|
1753
|
+
issues.push({
|
|
1754
|
+
path: "",
|
|
1755
|
+
keyword: "type",
|
|
1756
|
+
expected: "object",
|
|
1757
|
+
received: render(params),
|
|
1758
|
+
message: `Action params must be an object, received ${jsonTypeOf(params)} (${render(params)}).`
|
|
1759
|
+
});
|
|
1760
|
+
return { valid: false, issues };
|
|
1761
|
+
}
|
|
1762
|
+
validateObject(params, normalized, "", issues, 0);
|
|
1763
|
+
return { valid: issues.length === 0, issues };
|
|
1764
|
+
}
|
|
1765
|
+
function formatParamValidationFailure(componentId, actionId, issues) {
|
|
1766
|
+
const detail = issues.map((i) => i.message).join(" ");
|
|
1767
|
+
return `Action "${actionId}" on component "${componentId}" was rejected: params do not match the action's declared paramSchema. ${detail}`;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/core/abortable.ts
|
|
1771
|
+
var MAX_ACTION_TIMEOUT_MS = 864e5;
|
|
1772
|
+
function normalizeActionTimeoutMs(raw) {
|
|
1773
|
+
if (raw === void 0 || raw === null) {
|
|
1774
|
+
return { ok: true, timeoutMs: void 0, clamped: false };
|
|
1775
|
+
}
|
|
1776
|
+
if (typeof raw !== "number") {
|
|
1777
|
+
return {
|
|
1778
|
+
ok: false,
|
|
1779
|
+
reason: `timeoutMs must be a number of milliseconds, received ${typeof raw === "string" ? JSON.stringify(raw) : typeof raw}`
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
if (!Number.isFinite(raw)) {
|
|
1783
|
+
return {
|
|
1784
|
+
ok: false,
|
|
1785
|
+
reason: `timeoutMs must be a finite number of milliseconds, received ${String(raw)}`
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (raw < 0) {
|
|
1789
|
+
return {
|
|
1790
|
+
ok: false,
|
|
1791
|
+
reason: `timeoutMs must not be negative, received ${String(raw)}`
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
if (raw > MAX_ACTION_TIMEOUT_MS) {
|
|
1795
|
+
return { ok: true, timeoutMs: MAX_ACTION_TIMEOUT_MS, clamped: true };
|
|
1796
|
+
}
|
|
1797
|
+
return { ok: true, timeoutMs: Math.floor(raw), clamped: false };
|
|
1798
|
+
}
|
|
1799
|
+
function inertAbortSignal() {
|
|
1800
|
+
return new AbortController().signal;
|
|
1801
|
+
}
|
|
1802
|
+
async function runAbortable(invoke, options = {}) {
|
|
1803
|
+
const callerSignal = options.signal;
|
|
1804
|
+
const timeoutMs = options.timeoutMs;
|
|
1805
|
+
if (callerSignal?.aborted) {
|
|
1806
|
+
return { aborted: true, reason: "signal" };
|
|
1807
|
+
}
|
|
1808
|
+
const controller = new AbortController();
|
|
1809
|
+
let reason = "signal";
|
|
1810
|
+
let timer;
|
|
1811
|
+
const onCallerAbort = () => {
|
|
1812
|
+
reason = "signal";
|
|
1813
|
+
controller.abort();
|
|
1814
|
+
};
|
|
1815
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1816
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0) {
|
|
1817
|
+
timer = setTimeout(
|
|
1818
|
+
() => {
|
|
1819
|
+
reason = "timeout";
|
|
1820
|
+
controller.abort();
|
|
1821
|
+
},
|
|
1822
|
+
Math.min(timeoutMs, MAX_ACTION_TIMEOUT_MS)
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
const abandoned = new Promise((resolve) => {
|
|
1827
|
+
controller.signal.addEventListener("abort", () => resolve({ aborted: true, reason }), {
|
|
1828
|
+
once: true
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
const completed = (async () => ({
|
|
1832
|
+
aborted: false,
|
|
1833
|
+
result: await invoke(controller.signal)
|
|
1834
|
+
}))();
|
|
1835
|
+
return await Promise.race([completed, abandoned]);
|
|
1836
|
+
} finally {
|
|
1837
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1838
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1023
1842
|
// src/core/element-identifier.ts
|
|
1024
1843
|
function createNativeElementIdentifier(id, options = {}) {
|
|
1025
1844
|
return {
|
|
@@ -1124,6 +1943,68 @@ var DEFAULT_WAIT_OPTIONS = {
|
|
|
1124
1943
|
timeout: 1e4,
|
|
1125
1944
|
interval: 100
|
|
1126
1945
|
};
|
|
1946
|
+
var NativeActionError = class extends Error {
|
|
1947
|
+
constructor(message, code) {
|
|
1948
|
+
super(message);
|
|
1949
|
+
this.code = code;
|
|
1950
|
+
this.name = "NativeActionError";
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var DEFAULT_SCROLL_INTO_VIEW_PADDING = 16;
|
|
1954
|
+
var SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS = 1e3;
|
|
1955
|
+
var SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS = 250;
|
|
1956
|
+
function measurePageRectNow(element, preferred) {
|
|
1957
|
+
const preferredNode = preferred;
|
|
1958
|
+
const node = preferredNode && typeof preferredNode.measureInWindow === "function" ? preferredNode : element.ref.current;
|
|
1959
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
1960
|
+
const state = element.getState();
|
|
1961
|
+
return Promise.resolve(state.visible ? pageRectOf(state) : null);
|
|
1962
|
+
}
|
|
1963
|
+
return new Promise((resolve) => {
|
|
1964
|
+
let settled = false;
|
|
1965
|
+
const done = (rect) => {
|
|
1966
|
+
if (settled) return;
|
|
1967
|
+
settled = true;
|
|
1968
|
+
clearTimeout(timer);
|
|
1969
|
+
resolve(rect);
|
|
1970
|
+
};
|
|
1971
|
+
const timer = setTimeout(() => done(null), SCROLL_INTO_VIEW_WINDOW_MEASURE_TIMEOUT_MS);
|
|
1972
|
+
try {
|
|
1973
|
+
node.measureInWindow(
|
|
1974
|
+
(x, y, w, h) => done(
|
|
1975
|
+
[x, y, w, h].every(Number.isFinite) && w > 0 && h > 0 ? { left: x, top: y, right: x + w, bottom: y + h } : null
|
|
1976
|
+
)
|
|
1977
|
+
);
|
|
1978
|
+
} catch {
|
|
1979
|
+
done(null);
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
function resolveScrollView(current) {
|
|
1984
|
+
const c = current;
|
|
1985
|
+
if (!c) return null;
|
|
1986
|
+
if (typeof c.scrollTo === "function")
|
|
1987
|
+
return c;
|
|
1988
|
+
for (const unwrap of [c.getNativeScrollRef, c.getScrollResponder]) {
|
|
1989
|
+
if (typeof unwrap !== "function") continue;
|
|
1990
|
+
let inner;
|
|
1991
|
+
try {
|
|
1992
|
+
inner = unwrap.call(c);
|
|
1993
|
+
} catch {
|
|
1994
|
+
inner = null;
|
|
1995
|
+
}
|
|
1996
|
+
if (inner && typeof inner.scrollTo === "function")
|
|
1997
|
+
return inner;
|
|
1998
|
+
}
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
function isDeclaredHorizontal(registeredProps, ...refs) {
|
|
2002
|
+
if (registeredProps?.horizontal === true) return true;
|
|
2003
|
+
return refs.some((r) => {
|
|
2004
|
+
const props = r?.props;
|
|
2005
|
+
return props !== null && typeof props === "object" && props.horizontal === true;
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
1127
2008
|
function sleep(ms) {
|
|
1128
2009
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1129
2010
|
}
|
|
@@ -1243,6 +2124,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
1243
2124
|
return {
|
|
1244
2125
|
success: false,
|
|
1245
2126
|
error: errorMsg,
|
|
2127
|
+
...error3 instanceof NativeActionError ? { code: error3.code } : {},
|
|
1246
2128
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
1247
2129
|
durationMs: failTime - startTime,
|
|
1248
2130
|
timestamp: failTime,
|
|
@@ -1260,7 +2142,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
1260
2142
|
}
|
|
1261
2143
|
const props = element.props || {};
|
|
1262
2144
|
if (element.customActions && action in element.customActions) {
|
|
1263
|
-
return element.customActions[action].handler(params);
|
|
2145
|
+
return element.customActions[action].handler(params, { signal: inertAbortSignal() });
|
|
1264
2146
|
}
|
|
1265
2147
|
switch (action) {
|
|
1266
2148
|
case "click":
|
|
@@ -1283,6 +2165,11 @@ var DefaultNativeActionExecutor = class {
|
|
|
1283
2165
|
return this.performBlur(element);
|
|
1284
2166
|
case "scroll":
|
|
1285
2167
|
return this.performScroll(props, params);
|
|
2168
|
+
case "scrollIntoView":
|
|
2169
|
+
return this.performScrollIntoView(
|
|
2170
|
+
element,
|
|
2171
|
+
params
|
|
2172
|
+
);
|
|
1286
2173
|
case "swipe":
|
|
1287
2174
|
return this.performSwipe(props, params);
|
|
1288
2175
|
case "toggle":
|
|
@@ -1449,6 +2336,138 @@ var DefaultNativeActionExecutor = class {
|
|
|
1449
2336
|
props.onScroll(event);
|
|
1450
2337
|
}
|
|
1451
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Perform scrollIntoView: scroll the element's DECLARED scroll container so
|
|
2341
|
+
* the element sits `padding` dp below the top of its viewport.
|
|
2342
|
+
*
|
|
2343
|
+
* The container comes from `scrollAncestorId` and nowhere else — the
|
|
2344
|
+
* registry keeps no parent chain (`RegisterElementOptions.scrollAncestorId`),
|
|
2345
|
+
* so there is no "nearest ScrollView" to find. Nothing here tracks a
|
|
2346
|
+
* ScrollView's content offset either (`performScroll` only fakes an
|
|
2347
|
+
* `onScroll` event), so the target is measured CONTENT-relative with
|
|
2348
|
+
* `measureLayout` against the container's inner content view: stateless,
|
|
2349
|
+
* and immune to a stale-offset race.
|
|
2350
|
+
*
|
|
2351
|
+
* Result shape mirrors the web executor's `scrollIntoView`
|
|
2352
|
+
* (`@qontinui/ui-bridge` `control/action-executor.ts`):
|
|
2353
|
+
* `{ alreadyVisible, scrolled }`, short-circuiting when the element is
|
|
2354
|
+
* already fully inside its clip region. "Fully visible" is only claimed when
|
|
2355
|
+
* it is MEASURED — a page-space rect for the element and a known,
|
|
2356
|
+
* non-empty clip (window ∩ declared container), both measured at call
|
|
2357
|
+
* time. Anything unknown scrolls.
|
|
2358
|
+
*
|
|
2359
|
+
* Containers: a ScrollView, or a VirtualizedList (FlatList / SectionList)
|
|
2360
|
+
* unwrapped through `getNativeScrollRef()` / `getScrollResponder()`.
|
|
2361
|
+
* Vertical only — a `horizontal` container is NOT_SUPPORTED. The action
|
|
2362
|
+
* returns once `scrollTo` is dispatched; positions settle on the next frame.
|
|
2363
|
+
*
|
|
2364
|
+
* Prerequisites are checked BEFORE the visibility short-circuit, so whether
|
|
2365
|
+
* the element supports the action does not depend on where it happens to be
|
|
2366
|
+
* on screen. Each missing prerequisite is a `NOT_SUPPORTED` failure.
|
|
2367
|
+
*/
|
|
2368
|
+
async performScrollIntoView(element, params) {
|
|
2369
|
+
const padding = params?.padding ?? DEFAULT_SCROLL_INTO_VIEW_PADDING;
|
|
2370
|
+
if (typeof padding !== "number" || !Number.isFinite(padding) || padding < 0) {
|
|
2371
|
+
throw new Error('scrollIntoView "padding" must be a finite, non-negative number');
|
|
2372
|
+
}
|
|
2373
|
+
const ancestorId = element.scrollAncestorId;
|
|
2374
|
+
if (!ancestorId) {
|
|
2375
|
+
throw new NativeActionError(
|
|
2376
|
+
`scrollIntoView is not supported on element "${element.id}": it declares no scrollAncestorId, and the registry keeps no parent chain to find its scroll container from`,
|
|
2377
|
+
"NOT_SUPPORTED"
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const ancestor = ancestorId === element.id ? void 0 : this.registry.getElement(ancestorId);
|
|
2381
|
+
if (!ancestor) {
|
|
2382
|
+
throw new NativeActionError(
|
|
2383
|
+
`scrollIntoView is not supported on element "${element.id}": its declared scrollAncestorId "${ancestorId}" is not a registered element`,
|
|
2384
|
+
"NOT_SUPPORTED"
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const container = resolveScrollView(ancestor.ref.current);
|
|
2388
|
+
if (!container) {
|
|
2389
|
+
throw new NativeActionError(
|
|
2390
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" has no scrollTo (not a mounted ScrollView, and no getNativeScrollRef/getScrollResponder yielding one)`,
|
|
2391
|
+
"NOT_SUPPORTED"
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
if (isDeclaredHorizontal(ancestor.props, ancestor.ref.current, container)) {
|
|
2395
|
+
throw new NativeActionError(
|
|
2396
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" is horizontal (only vertical containers are supported)`,
|
|
2397
|
+
"NOT_SUPPORTED"
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
const innerViewRef = container.getInnerViewRef?.();
|
|
2401
|
+
const innerView = innerViewRef ?? container.getInnerViewNode?.();
|
|
2402
|
+
const innerViewIsTag = innerViewRef == null && typeof innerView === "number";
|
|
2403
|
+
if (innerView === void 0 || innerView === null) {
|
|
2404
|
+
throw new NativeActionError(
|
|
2405
|
+
`scrollIntoView is not supported on element "${element.id}": its scroll ancestor "${ancestorId}" exposes no inner content view to measure against`,
|
|
2406
|
+
"NOT_SUPPORTED"
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
const target = element.ref.current;
|
|
2410
|
+
if (!target || typeof target.measureLayout !== "function") {
|
|
2411
|
+
throw new NativeActionError(
|
|
2412
|
+
`scrollIntoView is not supported on element "${element.id}": its ref has no measureLayout (not a mounted host view)`,
|
|
2413
|
+
"NOT_SUPPORTED"
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
const [rect, ancestorRect] = await Promise.all([
|
|
2417
|
+
measurePageRectNow(element),
|
|
2418
|
+
measurePageRectNow(ancestor, container)
|
|
2419
|
+
]);
|
|
2420
|
+
const viewport = this.registry.getViewportRect();
|
|
2421
|
+
const clip = ancestorRect ? intersectRects(viewport, ancestorRect) : null;
|
|
2422
|
+
if (rect && clip && !isEmptyRect(clip) && rect.right > rect.left && rect.bottom > rect.top && rect.top >= clip.top && rect.left >= clip.left && rect.bottom <= clip.bottom && rect.right <= clip.right) {
|
|
2423
|
+
return { alreadyVisible: true, scrolled: false };
|
|
2424
|
+
}
|
|
2425
|
+
const measured = await new Promise(
|
|
2426
|
+
(resolve, reject) => {
|
|
2427
|
+
let settled = false;
|
|
2428
|
+
const timer = setTimeout(() => {
|
|
2429
|
+
if (settled) return;
|
|
2430
|
+
settled = true;
|
|
2431
|
+
reject(
|
|
2432
|
+
new Error(
|
|
2433
|
+
`scrollIntoView: measureLayout did not call back within ${SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS}ms` + (innerViewIsTag ? " (measured against a numeric inner-view tag from getInnerViewNode, which the new architecture does not support)" : "")
|
|
2434
|
+
)
|
|
2435
|
+
);
|
|
2436
|
+
}, SCROLL_INTO_VIEW_MEASURE_TIMEOUT_MS);
|
|
2437
|
+
const finish = (fn) => {
|
|
2438
|
+
if (settled) return;
|
|
2439
|
+
settled = true;
|
|
2440
|
+
clearTimeout(timer);
|
|
2441
|
+
fn();
|
|
2442
|
+
};
|
|
2443
|
+
try {
|
|
2444
|
+
target.measureLayout(
|
|
2445
|
+
innerView,
|
|
2446
|
+
(x, y, width) => finish(
|
|
2447
|
+
() => Number.isFinite(y) ? resolve({ x, y, width }) : reject(new Error("scrollIntoView: measureLayout returned a non-finite y"))
|
|
2448
|
+
),
|
|
2449
|
+
() => finish(
|
|
2450
|
+
() => reject(
|
|
2451
|
+
new Error(
|
|
2452
|
+
`scrollIntoView: measureLayout failed for "${element.id}" relative to "${ancestorId}"`
|
|
2453
|
+
)
|
|
2454
|
+
)
|
|
2455
|
+
)
|
|
2456
|
+
);
|
|
2457
|
+
} catch (error3) {
|
|
2458
|
+
finish(() => reject(error3));
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
if (ancestorRect && Number.isFinite(measured.x) && Number.isFinite(measured.width) && measured.x + measured.width > ancestorRect.right - ancestorRect.left + 1) {
|
|
2463
|
+
throw new NativeActionError(
|
|
2464
|
+
`scrollIntoView is not supported on element "${element.id}": it is laid out beyond the horizontal extent of its scroll ancestor "${ancestorId}" (sideways overflow, a horizontal container, or a nested horizontal container \u2014 declare that inner container as its scrollAncestorId); only vertical scrolling is supported`,
|
|
2465
|
+
"NOT_SUPPORTED"
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
container.scrollTo({ y: Math.max(0, measured.y - padding), animated: false });
|
|
2469
|
+
return { alreadyVisible: false, scrolled: true };
|
|
2470
|
+
}
|
|
1452
2471
|
/**
|
|
1453
2472
|
* Perform swipe action
|
|
1454
2473
|
*/
|
|
@@ -1503,7 +2522,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
1503
2522
|
/**
|
|
1504
2523
|
* Execute a component action
|
|
1505
2524
|
*/
|
|
1506
|
-
async executeComponentAction(componentId, request) {
|
|
2525
|
+
async executeComponentAction(componentId, request, options = {}) {
|
|
1507
2526
|
const startTime = Date.now();
|
|
1508
2527
|
try {
|
|
1509
2528
|
const component = this.registry.getComponent(componentId);
|
|
@@ -1526,10 +2545,71 @@ var DefaultNativeActionExecutor = class {
|
|
|
1526
2545
|
requestId: request.requestId
|
|
1527
2546
|
};
|
|
1528
2547
|
}
|
|
1529
|
-
const
|
|
2548
|
+
const timeout = normalizeActionTimeoutMs(request.timeoutMs);
|
|
2549
|
+
if (!timeout.ok) {
|
|
2550
|
+
return {
|
|
2551
|
+
success: false,
|
|
2552
|
+
error: `Action "${request.action}" on component "${componentId}" was rejected: ${timeout.reason}.`,
|
|
2553
|
+
durationMs: Date.now() - startTime,
|
|
2554
|
+
timestamp: Date.now(),
|
|
2555
|
+
requestId: request.requestId
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
const timeoutMs = timeout.timeoutMs;
|
|
2559
|
+
const validationMode = options.paramValidation ?? getDefaultParamValidationMode();
|
|
2560
|
+
if (validationMode !== "off" && action.paramSchema !== void 0) {
|
|
2561
|
+
let validation;
|
|
2562
|
+
try {
|
|
2563
|
+
validation = validateActionParams(action.paramSchema, request.params);
|
|
2564
|
+
} catch (validatorFault) {
|
|
2565
|
+
const detail = validatorFault instanceof Error ? validatorFault.message : String(validatorFault);
|
|
2566
|
+
const faultMessage = `Action "${request.action}" on component "${componentId}": its declared paramSchema could not be evaluated (${detail}).`;
|
|
2567
|
+
if (validationMode === "enforce") {
|
|
2568
|
+
return {
|
|
2569
|
+
success: false,
|
|
2570
|
+
error: faultMessage,
|
|
2571
|
+
durationMs: Date.now() - startTime,
|
|
2572
|
+
timestamp: Date.now(),
|
|
2573
|
+
requestId: request.requestId
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
console.warn(`[ui-bridge-native] ${faultMessage}`);
|
|
2577
|
+
validation = { valid: true, issues: [] };
|
|
2578
|
+
}
|
|
2579
|
+
if (!validation.valid) {
|
|
2580
|
+
const message = formatParamValidationFailure(
|
|
2581
|
+
componentId,
|
|
2582
|
+
request.action,
|
|
2583
|
+
validation.issues
|
|
2584
|
+
);
|
|
2585
|
+
if (validationMode === "enforce") {
|
|
2586
|
+
return {
|
|
2587
|
+
success: false,
|
|
2588
|
+
error: message,
|
|
2589
|
+
durationMs: Date.now() - startTime,
|
|
2590
|
+
timestamp: Date.now(),
|
|
2591
|
+
requestId: request.requestId
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
console.warn(`[ui-bridge-native] ${message}`);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const outcome = await runAbortable((signal) => action.handler(request.params, { signal }), {
|
|
2598
|
+
signal: options.signal,
|
|
2599
|
+
timeoutMs
|
|
2600
|
+
});
|
|
2601
|
+
if (outcome.aborted) {
|
|
2602
|
+
return {
|
|
2603
|
+
success: false,
|
|
2604
|
+
error: outcome.reason === "timeout" ? `Action "${request.action}" on component "${componentId}" was abandoned after its ${timeoutMs}ms timeout elapsed.` : `Action "${request.action}" on component "${componentId}" was cancelled by the caller's abort signal.`,
|
|
2605
|
+
durationMs: Date.now() - startTime,
|
|
2606
|
+
timestamp: Date.now(),
|
|
2607
|
+
requestId: request.requestId
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
1530
2610
|
return {
|
|
1531
2611
|
success: true,
|
|
1532
|
-
result,
|
|
2612
|
+
result: outcome.result,
|
|
1533
2613
|
durationMs: Date.now() - startTime,
|
|
1534
2614
|
timestamp: Date.now(),
|
|
1535
2615
|
requestId: request.requestId
|
|
@@ -2596,16 +3676,21 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2596
3676
|
return {
|
|
2597
3677
|
// Elements
|
|
2598
3678
|
getElements: async (ctx) => {
|
|
3679
|
+
await registry.refreshMeasurements();
|
|
2599
3680
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2600
3681
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
2601
3682
|
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
2602
3683
|
if (forRoute) {
|
|
2603
3684
|
allElements = allElements.filter((e) => matchesCurrentRoute(e.registrationRoute, forRoute));
|
|
2604
3685
|
}
|
|
3686
|
+
const viewport = registry.getViewportRect();
|
|
2605
3687
|
const elements = allElements.map((e) => {
|
|
2606
3688
|
const handlers = extractHandlerNames(e.props);
|
|
2607
3689
|
const state = e.getState();
|
|
2608
|
-
const visibility
|
|
3690
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
3691
|
+
state,
|
|
3692
|
+
registry.getClipRectFor(e, viewport)
|
|
3693
|
+
);
|
|
2609
3694
|
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2610
3695
|
const vision = projectVisionFields({
|
|
2611
3696
|
type: e.type,
|
|
@@ -2623,10 +3708,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2623
3708
|
identifier: e.getIdentifier(),
|
|
2624
3709
|
state,
|
|
2625
3710
|
actions: e.actions,
|
|
2626
|
-
customActions:
|
|
3711
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
2627
3712
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2628
3713
|
registrationRoute: e.registrationRoute,
|
|
2629
3714
|
visibility,
|
|
3715
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
2630
3716
|
...bbox ? { bbox } : {},
|
|
2631
3717
|
...vision
|
|
2632
3718
|
};
|
|
@@ -2648,7 +3734,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2648
3734
|
identifier: element.getIdentifier(),
|
|
2649
3735
|
state: resolveElementState(element),
|
|
2650
3736
|
actions: element.actions,
|
|
2651
|
-
customActions:
|
|
3737
|
+
customActions: serializeElementCustomActions(element.customActions),
|
|
2652
3738
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2653
3739
|
registrationRoute: element.registrationRoute
|
|
2654
3740
|
}
|
|
@@ -2669,13 +3755,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2669
3755
|
if (!envelope?.action) {
|
|
2670
3756
|
return error2("Action is required", "INVALID_REQUEST");
|
|
2671
3757
|
}
|
|
2672
|
-
const response = await executor.executeAction(id,
|
|
2673
|
-
action: envelope.action,
|
|
2674
|
-
params: envelope.params,
|
|
2675
|
-
waitOptions: envelope.waitOptions
|
|
2676
|
-
});
|
|
3758
|
+
const response = await executor.executeAction(id, envelope);
|
|
2677
3759
|
if (!response.success) {
|
|
2678
|
-
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
3760
|
+
return error2(response.error || "Action failed", response.code ?? "ACTION_FAILED");
|
|
2679
3761
|
}
|
|
2680
3762
|
return success2(response);
|
|
2681
3763
|
},
|
|
@@ -2685,7 +3767,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2685
3767
|
id: c.id,
|
|
2686
3768
|
name: c.name,
|
|
2687
3769
|
description: c.description,
|
|
2688
|
-
|
|
3770
|
+
// Phase 4 (plan 2026-08-20-ui-bridge-action-declaration-shape):
|
|
3771
|
+
// carry the safety annotation onto the native wire too — a walker
|
|
3772
|
+
// driving a React Native app needs the same `destructive` exclusion
|
|
3773
|
+
// the web channel gets.
|
|
3774
|
+
actions: c.actions.map((a) => ({ id: a.id, label: a.label, effect: a.effect })),
|
|
2689
3775
|
elementIds: c.elementIds
|
|
2690
3776
|
}));
|
|
2691
3777
|
return success2({ components });
|
|
@@ -2704,7 +3790,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2704
3790
|
actions: component.actions.map((a) => ({
|
|
2705
3791
|
id: a.id,
|
|
2706
3792
|
label: a.label,
|
|
2707
|
-
description: a.description
|
|
3793
|
+
description: a.description,
|
|
3794
|
+
// Phase 4 — see the listing twin above.
|
|
3795
|
+
effect: a.effect
|
|
2708
3796
|
})),
|
|
2709
3797
|
elementIds: component.elementIds
|
|
2710
3798
|
}
|
|
@@ -2715,7 +3803,8 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2715
3803
|
const body = ctx.body;
|
|
2716
3804
|
const response = await executor.executeComponentAction(id, {
|
|
2717
3805
|
action: actionId,
|
|
2718
|
-
params: body?.params
|
|
3806
|
+
params: body?.params,
|
|
3807
|
+
timeoutMs: body?.timeoutMs
|
|
2719
3808
|
});
|
|
2720
3809
|
if (!response.success) {
|
|
2721
3810
|
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
@@ -2789,6 +3878,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2789
3878
|
});
|
|
2790
3879
|
},
|
|
2791
3880
|
getSnapshot: async (ctx) => {
|
|
3881
|
+
await registry.refreshMeasurements();
|
|
2792
3882
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
2793
3883
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2794
3884
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
@@ -2906,9 +3996,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2906
3996
|
//
|
|
2907
3997
|
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
2908
3998
|
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
2909
|
-
// get a structured envelope explaining the gap rather than a
|
|
2910
|
-
//
|
|
2911
|
-
//
|
|
3999
|
+
// get a structured envelope explaining the gap rather than a bare
|
|
4000
|
+
// "no such route". These answer **HTTP 501** — `httpStatusForResponse` in
|
|
4001
|
+
// server/http-server.ts maps NOT_SUPPORTED → 501, so the status line and
|
|
4002
|
+
// the envelope agree; an unmounted path answers 404 instead. Each error
|
|
4003
|
+
// message names an in-tree mobile replacement so operators aren't left
|
|
4004
|
+
// guessing what to use instead.
|
|
2912
4005
|
aiForms: async () => {
|
|
2913
4006
|
return error2(
|
|
2914
4007
|
'ai/forms (form discovery) is runner-only; mobile React Native has no DOM to walk. Use GET /ui-bridge/control/snapshot and filter for type === "input" instead.',
|
|
@@ -3104,6 +4197,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3104
4197
|
// that don't measure (the fallback only matches single-rooted screens but
|
|
3105
4198
|
// is good enough for the unit suite).
|
|
3106
4199
|
tapAt: async (ctx) => {
|
|
4200
|
+
await registry.refreshMeasurements();
|
|
3107
4201
|
const body = ctx.body;
|
|
3108
4202
|
const x = body?.x;
|
|
3109
4203
|
const y = body?.y;
|
|
@@ -3234,18 +4328,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3234
4328
|
pushModal: async (ctx) => {
|
|
3235
4329
|
const detector = registry.getEnrichers().modalDetector;
|
|
3236
4330
|
if (!detector) {
|
|
3237
|
-
return error2(
|
|
3238
|
-
"modal detector not registered",
|
|
3239
|
-
"ENRICHER_UNAVAILABLE"
|
|
3240
|
-
);
|
|
4331
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
3241
4332
|
}
|
|
3242
4333
|
const body = ctx.body;
|
|
3243
4334
|
const id = body?.id;
|
|
3244
4335
|
if (typeof id !== "string" || id.length === 0) {
|
|
3245
|
-
return error2(
|
|
3246
|
-
'pushModal requires a non-empty "id"',
|
|
3247
|
-
"INVALID_REQUEST"
|
|
3248
|
-
);
|
|
4336
|
+
return error2('pushModal requires a non-empty "id"', "INVALID_REQUEST");
|
|
3249
4337
|
}
|
|
3250
4338
|
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
3251
4339
|
const requestedType = body?.type;
|
|
@@ -3278,10 +4366,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3278
4366
|
dismissModal: async (ctx) => {
|
|
3279
4367
|
const detector = registry.getEnrichers().modalDetector;
|
|
3280
4368
|
if (!detector) {
|
|
3281
|
-
return error2(
|
|
3282
|
-
"modal detector not registered",
|
|
3283
|
-
"ENRICHER_UNAVAILABLE"
|
|
3284
|
-
);
|
|
4369
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
3285
4370
|
}
|
|
3286
4371
|
const { id } = ctx.params;
|
|
3287
4372
|
if (typeof id !== "string" || id.length === 0) {
|
|
@@ -3311,7 +4396,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3311
4396
|
};
|
|
3312
4397
|
if (config?.appInfo) {
|
|
3313
4398
|
response.uiBridge = {
|
|
3314
|
-
version: "0.6.
|
|
4399
|
+
version: "0.6.13",
|
|
3315
4400
|
...config.appInfo,
|
|
3316
4401
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3317
4402
|
elementCount: stats.elements,
|
|
@@ -3459,8 +4544,48 @@ function extractMethod(input, init) {
|
|
|
3459
4544
|
var NetworkRequestBuffer = class {
|
|
3460
4545
|
constructor(capacity = 100) {
|
|
3461
4546
|
this.installed = false;
|
|
4547
|
+
/**
|
|
4548
|
+
* Synchronous re-entrancy depth: non-zero while the ORIGINAL `fetch` is
|
|
4549
|
+
* executing, which is exactly the window in which React Native's whatwg-fetch
|
|
4550
|
+
* polyfill constructs and opens its internal `XMLHttpRequest`.
|
|
4551
|
+
*
|
|
4552
|
+
* A counter rather than a boolean so a `fetch` called from inside a `fetch`
|
|
4553
|
+
* (an interceptor, a retry wrapper) still unwinds to zero. Safe as a plain
|
|
4554
|
+
* number: JS is single-threaded and the only writes are the paired
|
|
4555
|
+
* increment/decrement around one synchronous call.
|
|
4556
|
+
*/
|
|
4557
|
+
this.fetchDepth = { value: 0 };
|
|
3462
4558
|
this.buffer = new RingBuffer(capacity);
|
|
3463
4559
|
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Patch `fetch` and `XMLHttpRequest` so `GET /sdk/network-requests` can
|
|
4562
|
+
* report what the app talked to.
|
|
4563
|
+
*
|
|
4564
|
+
* ONE REQUEST MUST PRODUCE ONE ENTRY. React Native implements `fetch` **on
|
|
4565
|
+
* top of** XHR (whatwg-fetch), so both patches observe the same request and
|
|
4566
|
+
* the buffer used to carry every call twice — same url, timestamps ~1ms
|
|
4567
|
+
* apart, and on failure one `"Network request failed"` beside one
|
|
4568
|
+
* `"XMLHttpRequest error"`. A tester reads that as a double-fetch bug in the
|
|
4569
|
+
* app and chases a defect that does not exist.
|
|
4570
|
+
*
|
|
4571
|
+
* The de-dup: `fetchDepth` is raised across the synchronous call into the
|
|
4572
|
+
* original `fetch`, which is when the polyfill builds its XHR; `open` stamps
|
|
4573
|
+
* any XHR created in that window with `__uiBridgeFromFetch`, and `send`
|
|
4574
|
+
* skips recording those. The `fetch` patch keeps the entry, because it is
|
|
4575
|
+
* the layer the app actually called and the only one that sees `Response.ok`.
|
|
4576
|
+
*
|
|
4577
|
+
* Uninstalling both patches together keeps this sound: an XHR can only be
|
|
4578
|
+
* marked while the fetch patch is live.
|
|
4579
|
+
*
|
|
4580
|
+
* KNOWN LIMIT — it is coupled to the polyfill building its XHR SYNCHRONOUSLY,
|
|
4581
|
+
* which whatwg-fetch does (its work happens in the Promise executor). If the
|
|
4582
|
+
* host's `fetch` is wrapped by an interceptor that defers the real call to a
|
|
4583
|
+
* microtask or `setTimeout`, the depth is back to 0 by the time `open` runs
|
|
4584
|
+
* and every request duplicates again — the original symptom, with no signal
|
|
4585
|
+
* that the mechanism disengaged. Duplicates in `/sdk/network-requests` are
|
|
4586
|
+
* the thing to look for; they mean this assumption stopped holding, not that
|
|
4587
|
+
* the app double-fetched.
|
|
4588
|
+
*/
|
|
3464
4589
|
install() {
|
|
3465
4590
|
if (this.installed) return;
|
|
3466
4591
|
const g = globalThis;
|
|
@@ -3468,12 +4593,20 @@ var NetworkRequestBuffer = class {
|
|
|
3468
4593
|
this.originalFetch = g.fetch;
|
|
3469
4594
|
const buffer = this.buffer;
|
|
3470
4595
|
const original = this.originalFetch;
|
|
4596
|
+
const fetchDepth = this.fetchDepth;
|
|
3471
4597
|
const patched = async (input, init) => {
|
|
3472
4598
|
const start = Date.now();
|
|
3473
4599
|
const url = extractUrl(input);
|
|
3474
4600
|
const method = extractMethod(input, init);
|
|
3475
4601
|
try {
|
|
3476
|
-
|
|
4602
|
+
let pending;
|
|
4603
|
+
fetchDepth.value += 1;
|
|
4604
|
+
try {
|
|
4605
|
+
pending = original.call(globalThis, input, init);
|
|
4606
|
+
} finally {
|
|
4607
|
+
fetchDepth.value -= 1;
|
|
4608
|
+
}
|
|
4609
|
+
const res = await pending;
|
|
3477
4610
|
buffer.push({
|
|
3478
4611
|
timestamp: start,
|
|
3479
4612
|
method,
|
|
@@ -3503,10 +4636,14 @@ var NetworkRequestBuffer = class {
|
|
|
3503
4636
|
this.originalXhrOpen = Xhr.prototype.open;
|
|
3504
4637
|
this.originalXhrSend = Xhr.prototype.send;
|
|
3505
4638
|
const buffer = this.buffer;
|
|
4639
|
+
const fetchDepth = this.fetchDepth;
|
|
3506
4640
|
const origOpen = this.originalXhrOpen;
|
|
3507
4641
|
Xhr.prototype.open = function(method, url, ...rest) {
|
|
3508
4642
|
this.__uiBridgeMethod = method.toUpperCase();
|
|
3509
4643
|
this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
|
|
4644
|
+
if (fetchDepth.value > 0) {
|
|
4645
|
+
this.__uiBridgeFromFetch = true;
|
|
4646
|
+
}
|
|
3510
4647
|
return origOpen.call(
|
|
3511
4648
|
this,
|
|
3512
4649
|
method,
|
|
@@ -3516,6 +4653,9 @@ var NetworkRequestBuffer = class {
|
|
|
3516
4653
|
};
|
|
3517
4654
|
const origSend = this.originalXhrSend;
|
|
3518
4655
|
Xhr.prototype.send = function(body) {
|
|
4656
|
+
if (this.__uiBridgeFromFetch === true) {
|
|
4657
|
+
return origSend.call(this, body);
|
|
4658
|
+
}
|
|
3519
4659
|
const start = Date.now();
|
|
3520
4660
|
const method = this.__uiBridgeMethod ?? "GET";
|
|
3521
4661
|
const url = this.__uiBridgeUrl ?? "";
|
|
@@ -3595,6 +4735,16 @@ function isUnsubscribe(msg) {
|
|
|
3595
4735
|
}
|
|
3596
4736
|
|
|
3597
4737
|
// src/server/http-server.ts
|
|
4738
|
+
var ERROR_CODE_HTTP_STATUS = /* @__PURE__ */ new Map([
|
|
4739
|
+
["NOT_SUPPORTED", 501],
|
|
4740
|
+
["NOT_FOUND", 404],
|
|
4741
|
+
["METHOD_NOT_ALLOWED", 405]
|
|
4742
|
+
]);
|
|
4743
|
+
function httpStatusForResponse(response) {
|
|
4744
|
+
if (response.success) return 200;
|
|
4745
|
+
const mapped = response.code ? ERROR_CODE_HTTP_STATUS.get(response.code) : void 0;
|
|
4746
|
+
return mapped ?? 400;
|
|
4747
|
+
}
|
|
3598
4748
|
function parsePath(pattern, actual) {
|
|
3599
4749
|
const patternParts = pattern.split("/");
|
|
3600
4750
|
const actualParts = actual.split("/");
|
|
@@ -3757,6 +4907,23 @@ var WS_ROUTES = [
|
|
|
3757
4907
|
httpMethods: ["GET"]
|
|
3758
4908
|
}
|
|
3759
4909
|
];
|
|
4910
|
+
var DISCOVERY_PATHS = /* @__PURE__ */ new Set(["_help", "_routes"]);
|
|
4911
|
+
function allowedMethodsForPath(path, testHooks) {
|
|
4912
|
+
const PREFIX = "/ui-bridge/";
|
|
4913
|
+
if (!path.startsWith(PREFIX)) return [];
|
|
4914
|
+
const stripped = path.slice(PREFIX.length);
|
|
4915
|
+
const methods = [];
|
|
4916
|
+
if (DISCOVERY_PATHS.has(stripped)) methods.push("GET");
|
|
4917
|
+
for (const route of WS_ROUTES) {
|
|
4918
|
+
if (!route.httpMethods) continue;
|
|
4919
|
+
if (route.requiresTestHooks && !testHooks) continue;
|
|
4920
|
+
if (parsePath(route.pattern, stripped) === null) continue;
|
|
4921
|
+
for (const verb of route.httpMethods) {
|
|
4922
|
+
if (!methods.includes(verb)) methods.push(verb);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
return methods;
|
|
4926
|
+
}
|
|
3760
4927
|
var HANDLER_DESCRIPTIONS = {
|
|
3761
4928
|
health: "Liveness probe; returns app metadata + element count",
|
|
3762
4929
|
getElements: "Flat list of registered UI elements",
|
|
@@ -3819,6 +4986,10 @@ var ACTION_REFERENCE = [
|
|
|
3819
4986
|
{ name: "focus", params: "none" },
|
|
3820
4987
|
{ name: "blur", params: "none" },
|
|
3821
4988
|
{ name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
|
|
4989
|
+
{
|
|
4990
|
+
name: "scrollIntoView",
|
|
4991
|
+
params: "ScrollIntoViewActionParams { padding? (dp, default 16) } \u2014 scrolls the element's DECLARED scrollAncestorId ScrollView; returns { alreadyVisible, scrolled }; NOT_SUPPORTED (501) without a usable declared ancestor"
|
|
4992
|
+
},
|
|
3822
4993
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
3823
4994
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
3824
4995
|
];
|
|
@@ -4016,7 +5187,9 @@ var NativeUIBridgeServer = class {
|
|
|
4016
5187
|
}
|
|
4017
5188
|
/**
|
|
4018
5189
|
* Set a route provider for reporting the current navigation route.
|
|
4019
|
-
* When set, `control/snapshot` responses include `currentRoute` and
|
|
5190
|
+
* When set, `control/snapshot` responses include `currentRoute` and
|
|
5191
|
+
* `segments`, plus the canonical `route` / `activeTab` aliases the web SDK's
|
|
5192
|
+
* `BridgeSnapshot` spells the same way.
|
|
4020
5193
|
*
|
|
4021
5194
|
* Also forwards the provider into the registry so the *default*
|
|
4022
5195
|
* `getSnapshot` handler (and any other call site of
|
|
@@ -4031,7 +5204,12 @@ var NativeUIBridgeServer = class {
|
|
|
4031
5204
|
this.handlers.getElements = async (ctx) => {
|
|
4032
5205
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
4033
5206
|
if (currentRouteOnly && !ctx.query?.route && !ctx.body?.route) {
|
|
4034
|
-
|
|
5207
|
+
let currentRoute;
|
|
5208
|
+
try {
|
|
5209
|
+
currentRoute = provider.getCurrentRoute();
|
|
5210
|
+
} catch {
|
|
5211
|
+
currentRoute = null;
|
|
5212
|
+
}
|
|
4035
5213
|
if (currentRoute) {
|
|
4036
5214
|
ctx.query = { ...ctx.query, route: currentRoute };
|
|
4037
5215
|
}
|
|
@@ -4041,11 +5219,26 @@ var NativeUIBridgeServer = class {
|
|
|
4041
5219
|
this.handlers.getSnapshot = async (ctx) => {
|
|
4042
5220
|
const visibleOnly = ctx?.query?.visibleOnly === "true" || ctx?.body?.visibleOnly === true;
|
|
4043
5221
|
const currentRouteOnly = ctx?.query?.currentRouteOnly === "true" || ctx?.body?.currentRouteOnly === true;
|
|
5222
|
+
let currentRoute;
|
|
5223
|
+
try {
|
|
5224
|
+
currentRoute = provider.getCurrentRoute() ?? null;
|
|
5225
|
+
} catch {
|
|
5226
|
+
currentRoute = null;
|
|
5227
|
+
}
|
|
5228
|
+
let segments;
|
|
5229
|
+
try {
|
|
5230
|
+
segments = provider.getSegments?.();
|
|
5231
|
+
} catch {
|
|
5232
|
+
segments = void 0;
|
|
5233
|
+
}
|
|
5234
|
+
let activeTab;
|
|
5235
|
+
try {
|
|
5236
|
+
activeTab = provider.getActiveTab?.();
|
|
5237
|
+
} catch {
|
|
5238
|
+
activeTab = void 0;
|
|
5239
|
+
}
|
|
4044
5240
|
const snapshot = this.registry.createSnapshot(
|
|
4045
|
-
{
|
|
4046
|
-
currentRoute: provider.getCurrentRoute(),
|
|
4047
|
-
segments: provider.getSegments?.()
|
|
4048
|
-
},
|
|
5241
|
+
{ currentRoute, segments, activeTab },
|
|
4049
5242
|
{ visibleOnly, currentRouteOnly }
|
|
4050
5243
|
);
|
|
4051
5244
|
return { success: true, data: snapshot, timestamp: Date.now() };
|
|
@@ -4711,8 +5904,15 @@ var NativeUIBridgeServer = class {
|
|
|
4711
5904
|
}
|
|
4712
5905
|
try {
|
|
4713
5906
|
const response = await this.routeRequest(request);
|
|
5907
|
+
if (response.code === "METHOD_NOT_ALLOWED") {
|
|
5908
|
+
const allow = allowedMethodsForPath(request.path, this.config.testHooks === true);
|
|
5909
|
+
if (allow.length > 0) {
|
|
5910
|
+
headers["Allow"] = allow.join(", ");
|
|
5911
|
+
if (this.config.cors) headers["Access-Control-Expose-Headers"] = "Allow";
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
4714
5914
|
return {
|
|
4715
|
-
status: response
|
|
5915
|
+
status: httpStatusForResponse(response),
|
|
4716
5916
|
headers,
|
|
4717
5917
|
body: JSON.stringify(response)
|
|
4718
5918
|
};
|
|
@@ -4769,7 +5969,7 @@ var NativeUIBridgeServer = class {
|
|
|
4769
5969
|
timestamp: Date.now()
|
|
4770
5970
|
};
|
|
4771
5971
|
}
|
|
4772
|
-
let patternMatchedButWrongVerb =
|
|
5972
|
+
let patternMatchedButWrongVerb = DISCOVERY_PATHS.has(stripped);
|
|
4773
5973
|
for (const route of WS_ROUTES) {
|
|
4774
5974
|
const pathParams = parsePath(route.pattern, stripped);
|
|
4775
5975
|
if (!pathParams) continue;
|
|
@@ -4786,9 +5986,10 @@ var NativeUIBridgeServer = class {
|
|
|
4786
5986
|
const ctx = { params: pathParams, query, body };
|
|
4787
5987
|
return this.handlers[route.handler](ctx);
|
|
4788
5988
|
}
|
|
5989
|
+
const allowed = patternMatchedButWrongVerb ? allowedMethodsForPath(path, testHooks) : [];
|
|
4789
5990
|
return {
|
|
4790
5991
|
success: false,
|
|
4791
|
-
error: patternMatchedButWrongVerb ? `Method not allowed: ${method} ${path}` : `Route not found: ${method} ${path}`,
|
|
5992
|
+
error: patternMatchedButWrongVerb ? `Method not allowed: ${method} ${path}${allowed.length > 0 ? ` (allowed: ${allowed.join(", ")})` : ""}` : `Route not found: ${method} ${path}`,
|
|
4792
5993
|
code: patternMatchedButWrongVerb ? "METHOD_NOT_ALLOWED" : "NOT_FOUND",
|
|
4793
5994
|
timestamp: Date.now()
|
|
4794
5995
|
};
|
|
@@ -5092,6 +6293,31 @@ function createTransportLogger() {
|
|
|
5092
6293
|
}
|
|
5093
6294
|
var transportLogger = createTransportLogger();
|
|
5094
6295
|
|
|
6296
|
+
// src/transport/relay-logging.ts
|
|
6297
|
+
var CREDENTIAL_PARAMS = ["token", "access_token", "auth", "authorization", "key", "sig"];
|
|
6298
|
+
var CREDENTIAL_PARAM_RE = new RegExp(`([?&](?:${CREDENTIAL_PARAMS.join("|")})=)[^&#]*`, "gi");
|
|
6299
|
+
var USERINFO_RE = /^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i;
|
|
6300
|
+
function redactRelayUrl(url) {
|
|
6301
|
+
return url.replace(USERINFO_RE, "$1<redacted>@").replace(CREDENTIAL_PARAM_RE, "$1<redacted>");
|
|
6302
|
+
}
|
|
6303
|
+
function describeCloseEvent(event) {
|
|
6304
|
+
const e = event ?? {};
|
|
6305
|
+
const code = typeof e.code === "number" ? String(e.code) : "unknown";
|
|
6306
|
+
const reason = typeof e.reason === "string" && e.reason.length > 0 ? e.reason : "(none)";
|
|
6307
|
+
const wasClean = typeof e.wasClean === "boolean" ? String(e.wasClean) : "unknown";
|
|
6308
|
+
return `code=${code} reason=${reason} wasClean=${wasClean}`;
|
|
6309
|
+
}
|
|
6310
|
+
function describeErrorEvent(event) {
|
|
6311
|
+
const e = event ?? {};
|
|
6312
|
+
const parts = [];
|
|
6313
|
+
if (typeof e.message === "string" && e.message.length > 0) parts.push(`message=${e.message}`);
|
|
6314
|
+
if (e.error instanceof Error) parts.push(`error=${e.error.message}`);
|
|
6315
|
+
else if (typeof e.error === "string" && e.error.length > 0) parts.push(`error=${e.error}`);
|
|
6316
|
+
if (typeof e.code === "number") parts.push(`code=${e.code}`);
|
|
6317
|
+
if (parts.length === 0) parts.push("message=(none reported by the transport)");
|
|
6318
|
+
return parts.join(" ");
|
|
6319
|
+
}
|
|
6320
|
+
|
|
5095
6321
|
// src/transport/DeviceAnnouncer.ts
|
|
5096
6322
|
var DEFAULT_PORT = 8087;
|
|
5097
6323
|
var RECONNECT_INITIAL_DELAY_MS = 2e3;
|
|
@@ -5161,7 +6387,8 @@ var DeviceAnnouncer = class {
|
|
|
5161
6387
|
}
|
|
5162
6388
|
if (this.stopped) return;
|
|
5163
6389
|
const url = `${this.config.cloudRelayUrl}?token=${encodeURIComponent(this.config.cloudToken)}`;
|
|
5164
|
-
|
|
6390
|
+
const safeUrl = redactRelayUrl(url);
|
|
6391
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connecting to", safeUrl);
|
|
5165
6392
|
const ws = new WebSocket(url);
|
|
5166
6393
|
this.cloudWs = ws;
|
|
5167
6394
|
ws.onopen = () => {
|
|
@@ -5190,14 +6417,22 @@ var DeviceAnnouncer = class {
|
|
|
5190
6417
|
} catch {
|
|
5191
6418
|
}
|
|
5192
6419
|
};
|
|
5193
|
-
ws.onclose = () => {
|
|
6420
|
+
ws.onclose = (event) => {
|
|
5194
6421
|
this.state = { ...this.state, cloudConnected: false };
|
|
5195
|
-
|
|
6422
|
+
const detail = describeCloseEvent(event);
|
|
6423
|
+
if (this.stopped) {
|
|
6424
|
+
transportLogger.log(`[DeviceAnnouncer] Cloud relay closed ${detail} url=${safeUrl}`);
|
|
6425
|
+
} else {
|
|
6426
|
+
transportLogger.warn(
|
|
6427
|
+
`[DeviceAnnouncer] Cloud relay closed unexpectedly ${detail} url=${safeUrl}`
|
|
6428
|
+
);
|
|
5196
6429
|
this.scheduleReconnect();
|
|
5197
6430
|
}
|
|
5198
6431
|
};
|
|
5199
6432
|
ws.onerror = (err) => {
|
|
5200
|
-
transportLogger.warn(
|
|
6433
|
+
transportLogger.warn(
|
|
6434
|
+
`[DeviceAnnouncer] Cloud relay error: ${describeErrorEvent(err)} url=${safeUrl}`
|
|
6435
|
+
);
|
|
5201
6436
|
};
|
|
5202
6437
|
}
|
|
5203
6438
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
@@ -5294,7 +6529,8 @@ var CloudRelayClient = class {
|
|
|
5294
6529
|
connect() {
|
|
5295
6530
|
if (this.stopped) return;
|
|
5296
6531
|
const url = `${this.config.relayUrl}?token=${encodeURIComponent(this.config.authToken)}`;
|
|
5297
|
-
|
|
6532
|
+
const safeUrl = redactRelayUrl(url);
|
|
6533
|
+
transportLogger.log("[CloudRelayClient] Connecting to", safeUrl);
|
|
5298
6534
|
const ws = new WebSocket(url);
|
|
5299
6535
|
this.ws = ws;
|
|
5300
6536
|
ws.onopen = () => {
|
|
@@ -5315,14 +6551,22 @@ var CloudRelayClient = class {
|
|
|
5315
6551
|
ws.onmessage = (event) => {
|
|
5316
6552
|
void this.onMessage(event.data);
|
|
5317
6553
|
};
|
|
5318
|
-
ws.onclose = () => {
|
|
6554
|
+
ws.onclose = (event) => {
|
|
5319
6555
|
this._isConnected = false;
|
|
5320
|
-
|
|
6556
|
+
const detail = describeCloseEvent(event);
|
|
6557
|
+
if (this.stopped) {
|
|
6558
|
+
transportLogger.log(`[CloudRelayClient] WebSocket closed ${detail} url=${safeUrl}`);
|
|
6559
|
+
} else {
|
|
6560
|
+
transportLogger.warn(
|
|
6561
|
+
`[CloudRelayClient] WebSocket closed unexpectedly ${detail} url=${safeUrl}`
|
|
6562
|
+
);
|
|
5321
6563
|
this.scheduleReconnect();
|
|
5322
6564
|
}
|
|
5323
6565
|
};
|
|
5324
6566
|
ws.onerror = (err) => {
|
|
5325
|
-
transportLogger.warn(
|
|
6567
|
+
transportLogger.warn(
|
|
6568
|
+
`[CloudRelayClient] WebSocket error: ${describeErrorEvent(err)} url=${safeUrl}`
|
|
6569
|
+
);
|
|
5326
6570
|
};
|
|
5327
6571
|
}
|
|
5328
6572
|
async onMessage(raw) {
|
|
@@ -5601,10 +6845,20 @@ var UndoTracker = class {
|
|
|
5601
6845
|
}
|
|
5602
6846
|
};
|
|
5603
6847
|
var UIBridgeNativeContext = react.createContext(null);
|
|
6848
|
+
var EMPTY_FEATURES = {};
|
|
6849
|
+
var EMPTY_CONFIG = {};
|
|
6850
|
+
function useValueStable(value) {
|
|
6851
|
+
const signature = JSON.stringify(value);
|
|
6852
|
+
return react.useMemo(() => value, [signature]);
|
|
6853
|
+
}
|
|
6854
|
+
function readWindowViewport() {
|
|
6855
|
+
const win = reactNative.Dimensions.get("window");
|
|
6856
|
+
return { width: win.width, height: win.height };
|
|
6857
|
+
}
|
|
5604
6858
|
function UIBridgeNativeProvider({
|
|
5605
6859
|
children,
|
|
5606
|
-
features =
|
|
5607
|
-
config =
|
|
6860
|
+
features: featuresProp = EMPTY_FEATURES,
|
|
6861
|
+
config: configProp = EMPTY_CONFIG,
|
|
5608
6862
|
onEvent,
|
|
5609
6863
|
serverAdapter,
|
|
5610
6864
|
navigationProvider,
|
|
@@ -5615,6 +6869,8 @@ function UIBridgeNativeProvider({
|
|
|
5615
6869
|
enableMdnsAnnounce,
|
|
5616
6870
|
deviceId
|
|
5617
6871
|
}) {
|
|
6872
|
+
const features = useValueStable(featuresProp);
|
|
6873
|
+
const config = useValueStable(configProp);
|
|
5618
6874
|
const registryRef = react.useRef(null);
|
|
5619
6875
|
const executorRef = react.useRef(null);
|
|
5620
6876
|
const modalDetectorRef = react.useRef(null);
|
|
@@ -5627,6 +6883,7 @@ function UIBridgeNativeProvider({
|
|
|
5627
6883
|
onEvent
|
|
5628
6884
|
});
|
|
5629
6885
|
registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
|
|
6886
|
+
registryRef.current.setViewportProvider(readWindowViewport);
|
|
5630
6887
|
setGlobalRegistry(registryRef.current);
|
|
5631
6888
|
}
|
|
5632
6889
|
const registry = registryRef.current;
|
|
@@ -5661,16 +6918,10 @@ function UIBridgeNativeProvider({
|
|
|
5661
6918
|
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5662
6919
|
// keeps existing consumers unchanged.
|
|
5663
6920
|
observability: features.observability,
|
|
5664
|
-
// Injected device-viewport getter for POST /control/page-health
|
|
5665
|
-
//
|
|
5666
|
-
//
|
|
5667
|
-
|
|
5668
|
-
// already has a live react-native import, so it's the safe injection
|
|
5669
|
-
// point. See feedback_metro_require_gotcha for the full incident.
|
|
5670
|
-
viewportProvider: () => {
|
|
5671
|
-
const win = reactNative.Dimensions.get("window");
|
|
5672
|
-
return { width: win.width, height: win.height };
|
|
5673
|
-
}
|
|
6921
|
+
// Injected device-viewport getter for POST /control/page-health and the
|
|
6922
|
+
// design handlers. Same source as the registry's clipping bounds — see
|
|
6923
|
+
// `readWindowViewport`.
|
|
6924
|
+
viewportProvider: readWindowViewport
|
|
5674
6925
|
});
|
|
5675
6926
|
server.setAdapter(serverAdapter);
|
|
5676
6927
|
if (navigationProvider) {
|
|
@@ -5768,10 +7019,7 @@ function UIBridgeNativeProvider({
|
|
|
5768
7019
|
appInfo: config.appInfo,
|
|
5769
7020
|
testHooks: features.testHooks === true,
|
|
5770
7021
|
observability: features.observability,
|
|
5771
|
-
viewportProvider:
|
|
5772
|
-
const win = reactNative.Dimensions.get("window");
|
|
5773
|
-
return { width: win.width, height: win.height };
|
|
5774
|
-
}
|
|
7022
|
+
viewportProvider: readWindowViewport
|
|
5775
7023
|
});
|
|
5776
7024
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
5777
7025
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -5976,6 +7224,7 @@ function useUIElement(options) {
|
|
|
5976
7224
|
const propsRef = react.useRef({});
|
|
5977
7225
|
const registeredRef = react.useRef(false);
|
|
5978
7226
|
const registeredIdRef = react.useRef(null);
|
|
7227
|
+
const autoPublishedLabelRef = react.useRef(void 0);
|
|
5979
7228
|
const bridgeRef = react.useRef(bridge);
|
|
5980
7229
|
bridgeRef.current = bridge;
|
|
5981
7230
|
const nodeRef = react.useRef(null);
|
|
@@ -6015,6 +7264,7 @@ function useUIElement(options) {
|
|
|
6015
7264
|
autoRegister = true,
|
|
6016
7265
|
onStateChange,
|
|
6017
7266
|
parentPath,
|
|
7267
|
+
scrollAncestorId,
|
|
6018
7268
|
style,
|
|
6019
7269
|
stateStyles: stateStylesProp,
|
|
6020
7270
|
handlers: handlersProp,
|
|
@@ -6039,22 +7289,38 @@ function useUIElement(options) {
|
|
|
6039
7289
|
testId: id,
|
|
6040
7290
|
accessibilityLabel: label,
|
|
6041
7291
|
registrationRoute: bridge.getCurrentRoute(),
|
|
7292
|
+
scrollAncestorId,
|
|
6042
7293
|
flatStyle: flattenStyle(style),
|
|
6043
7294
|
stateStyles: flattenStateStyles(stateStylesProp)
|
|
6044
7295
|
});
|
|
6045
7296
|
registeredRef.current = true;
|
|
6046
7297
|
registeredIdRef.current = id;
|
|
7298
|
+
autoPublishedLabelRef.current = typeof label === "string" ? label : void 0;
|
|
6047
7299
|
if (typeof valueProp === "string") {
|
|
6048
7300
|
bridge.registry.updateElementState(id, { value: valueProp });
|
|
6049
7301
|
}
|
|
6050
7302
|
setRegistered(true);
|
|
6051
7303
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
6052
|
-
}, [
|
|
7304
|
+
}, [
|
|
7305
|
+
bridge,
|
|
7306
|
+
id,
|
|
7307
|
+
type,
|
|
7308
|
+
label,
|
|
7309
|
+
actions,
|
|
7310
|
+
customActions,
|
|
7311
|
+
treePath,
|
|
7312
|
+
scrollAncestorId,
|
|
7313
|
+
style,
|
|
7314
|
+
stateStylesProp,
|
|
7315
|
+
valueProp,
|
|
7316
|
+
ref
|
|
7317
|
+
]);
|
|
6053
7318
|
const unregister = react.useCallback(() => {
|
|
6054
7319
|
if (!bridge || !registeredRef.current) return;
|
|
6055
7320
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
6056
7321
|
registeredRef.current = false;
|
|
6057
7322
|
registeredIdRef.current = null;
|
|
7323
|
+
autoPublishedLabelRef.current = void 0;
|
|
6058
7324
|
setRegistered(false);
|
|
6059
7325
|
}, [bridge, id]);
|
|
6060
7326
|
const onStateChangeRef = react.useRef(onStateChange);
|
|
@@ -6082,7 +7348,7 @@ function useUIElement(options) {
|
|
|
6082
7348
|
writeLayout({ x, y, width, height, pageX, pageY });
|
|
6083
7349
|
});
|
|
6084
7350
|
} else {
|
|
6085
|
-
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
7351
|
+
writeLayout({ x, y, width, height, pageX: x, pageY: y, pageOriginUnmeasured: true });
|
|
6086
7352
|
}
|
|
6087
7353
|
},
|
|
6088
7354
|
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
@@ -6103,7 +7369,7 @@ function useUIElement(options) {
|
|
|
6103
7369
|
unregisterRef.current();
|
|
6104
7370
|
}
|
|
6105
7371
|
};
|
|
6106
|
-
}, [autoRegister, bridge]);
|
|
7372
|
+
}, [autoRegister, bridge, id]);
|
|
6107
7373
|
react.useEffect(() => {
|
|
6108
7374
|
if (!bridge || !registered) return;
|
|
6109
7375
|
const node = ref.current;
|
|
@@ -6194,6 +7460,16 @@ function useUIElement(options) {
|
|
|
6194
7460
|
if (current === valueProp) return;
|
|
6195
7461
|
bridge.registry.updateElementState(id, { value: valueProp });
|
|
6196
7462
|
}, [bridge, registered, id, valueProp]);
|
|
7463
|
+
react.useEffect(() => {
|
|
7464
|
+
if (!bridge || !registered) return;
|
|
7465
|
+
if (typeof label !== "string") return;
|
|
7466
|
+
autoPublishedLabelRef.current = label;
|
|
7467
|
+
bridge.registry.updateElementMeta(id, {
|
|
7468
|
+
label,
|
|
7469
|
+
accessibilityLabel: label,
|
|
7470
|
+
testId: id
|
|
7471
|
+
});
|
|
7472
|
+
}, [bridge, registered, id, label]);
|
|
6197
7473
|
react.useCallback(
|
|
6198
7474
|
(props) => {
|
|
6199
7475
|
propsRef.current = { ...propsRef.current, ...props };
|
|
@@ -6236,7 +7512,8 @@ function useUIElement(options) {
|
|
|
6236
7512
|
accessibilityLabel: newLabel,
|
|
6237
7513
|
testId: id
|
|
6238
7514
|
});
|
|
6239
|
-
|
|
7515
|
+
const alreadyPublishedBySdk = newLabel === autoPublishedLabelRef.current;
|
|
7516
|
+
if (!changed && !alreadyPublishedBySdk && !warnedRedundantUpdateLabelIds.has(id)) {
|
|
6240
7517
|
warnedRedundantUpdateLabelIds.add(id);
|
|
6241
7518
|
console.warn(
|
|
6242
7519
|
`[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.`
|
|
@@ -6292,36 +7569,56 @@ function useUIElementWithProps(options) {
|
|
|
6292
7569
|
}
|
|
6293
7570
|
function useUIComponent(options) {
|
|
6294
7571
|
const bridge = useUIBridgeNativeOptional();
|
|
7572
|
+
const [registered, setRegistered] = react.useState(false);
|
|
7573
|
+
const [publishVersion, setPublishVersion] = react.useState(0);
|
|
7574
|
+
const publishedIdRef = react.useRef(null);
|
|
6295
7575
|
const registeredRef = react.useRef(false);
|
|
6296
7576
|
const registeredIdRef = react.useRef(null);
|
|
6297
7577
|
const actionsRef = react.useRef(options.actions || []);
|
|
6298
7578
|
const elementIdsRef = react.useRef(options.elementIds || []);
|
|
6299
7579
|
const { id, name, description, autoRegister = true } = options;
|
|
7580
|
+
const actionsSignature = JSON.stringify(
|
|
7581
|
+
(options.actions ?? []).map((a) => [a.id, a.label, a.description, a.paramSchema, a.effect])
|
|
7582
|
+
);
|
|
7583
|
+
const elementIdsSignature = JSON.stringify(options.elementIds ?? []);
|
|
7584
|
+
react.useEffect(() => {
|
|
7585
|
+
if (options.actions !== void 0) actionsRef.current = options.actions;
|
|
7586
|
+
}, [options.actions]);
|
|
6300
7587
|
react.useEffect(() => {
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
7588
|
+
if (options.elementIds !== void 0) elementIdsRef.current = options.elementIds;
|
|
7589
|
+
}, [elementIdsSignature]);
|
|
7590
|
+
const toRegistryActions = react.useCallback(
|
|
7591
|
+
() => actionsRef.current.map(({ handler: declaredHandler, ...rest }) => ({
|
|
7592
|
+
...rest,
|
|
7593
|
+
handler: (params, handlerOptions) => {
|
|
7594
|
+
const latest = actionsRef.current.find((a) => a.id === rest.id);
|
|
7595
|
+
return (latest?.handler ?? declaredHandler)(params, handlerOptions);
|
|
7596
|
+
}
|
|
7597
|
+
})),
|
|
7598
|
+
[]
|
|
7599
|
+
);
|
|
6304
7600
|
const register = react.useCallback(() => {
|
|
6305
7601
|
if (!bridge || registeredRef.current) return;
|
|
6306
7602
|
bridge.registry.registerComponent(id, {
|
|
6307
7603
|
name,
|
|
6308
7604
|
description,
|
|
6309
|
-
actions:
|
|
6310
|
-
id: a.id,
|
|
6311
|
-
label: a.label,
|
|
6312
|
-
description: a.description,
|
|
6313
|
-
handler: a.handler
|
|
6314
|
-
})),
|
|
7605
|
+
actions: toRegistryActions(),
|
|
6315
7606
|
elementIds: elementIdsRef.current
|
|
6316
7607
|
});
|
|
6317
7608
|
registeredRef.current = true;
|
|
6318
7609
|
registeredIdRef.current = id;
|
|
6319
|
-
|
|
7610
|
+
setRegistered(true);
|
|
7611
|
+
if (publishedIdRef.current !== id) {
|
|
7612
|
+
publishedIdRef.current = id;
|
|
7613
|
+
setPublishVersion((v) => v + 1);
|
|
7614
|
+
}
|
|
7615
|
+
}, [bridge, id, name, description, toRegistryActions]);
|
|
6320
7616
|
const unregister = react.useCallback(() => {
|
|
6321
7617
|
if (!bridge || !registeredRef.current) return;
|
|
6322
7618
|
bridge.registry.unregisterComponent(registeredIdRef.current ?? id);
|
|
6323
7619
|
registeredRef.current = false;
|
|
6324
7620
|
registeredIdRef.current = null;
|
|
7621
|
+
setRegistered(false);
|
|
6325
7622
|
}, [bridge, id]);
|
|
6326
7623
|
const executeAction = react.useCallback(
|
|
6327
7624
|
async (actionId, params) => {
|
|
@@ -6339,25 +7636,47 @@ function useUIComponent(options) {
|
|
|
6339
7636
|
},
|
|
6340
7637
|
[bridge, id]
|
|
6341
7638
|
);
|
|
7639
|
+
const publishElementIds = react.useCallback(() => {
|
|
7640
|
+
if (!registeredRef.current || !bridge) return;
|
|
7641
|
+
const changed = bridge.registry.updateComponentMeta(registeredIdRef.current ?? id, {
|
|
7642
|
+
elementIds: elementIdsRef.current
|
|
7643
|
+
});
|
|
7644
|
+
if (changed) {
|
|
7645
|
+
setPublishVersion((v) => v + 1);
|
|
7646
|
+
}
|
|
7647
|
+
}, [bridge, id]);
|
|
6342
7648
|
const updateActions = react.useCallback(
|
|
6343
7649
|
(actions) => {
|
|
6344
7650
|
actionsRef.current = actions;
|
|
6345
7651
|
if (registeredRef.current && bridge) {
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
7652
|
+
const targetId = registeredIdRef.current ?? id;
|
|
7653
|
+
const changed = bridge.registry.updateComponentMeta(targetId, {
|
|
7654
|
+
actions: toRegistryActions()
|
|
7655
|
+
});
|
|
7656
|
+
if (changed) {
|
|
7657
|
+
setPublishVersion((v) => v + 1);
|
|
7658
|
+
}
|
|
6349
7659
|
}
|
|
6350
7660
|
},
|
|
6351
|
-
[bridge, id,
|
|
7661
|
+
[bridge, id, toRegistryActions]
|
|
7662
|
+
);
|
|
7663
|
+
const addElement = react.useCallback(
|
|
7664
|
+
(elementId) => {
|
|
7665
|
+
if (!elementIdsRef.current.includes(elementId)) {
|
|
7666
|
+
elementIdsRef.current = [...elementIdsRef.current, elementId];
|
|
7667
|
+
publishElementIds();
|
|
7668
|
+
}
|
|
7669
|
+
},
|
|
7670
|
+
[publishElementIds]
|
|
7671
|
+
);
|
|
7672
|
+
const removeElement = react.useCallback(
|
|
7673
|
+
(elementId) => {
|
|
7674
|
+
if (!elementIdsRef.current.includes(elementId)) return;
|
|
7675
|
+
elementIdsRef.current = elementIdsRef.current.filter((eid) => eid !== elementId);
|
|
7676
|
+
publishElementIds();
|
|
7677
|
+
},
|
|
7678
|
+
[publishElementIds]
|
|
6352
7679
|
);
|
|
6353
|
-
const addElement = react.useCallback((elementId) => {
|
|
6354
|
-
if (!elementIdsRef.current.includes(elementId)) {
|
|
6355
|
-
elementIdsRef.current = [...elementIdsRef.current, elementId];
|
|
6356
|
-
}
|
|
6357
|
-
}, []);
|
|
6358
|
-
const removeElement = react.useCallback((elementId) => {
|
|
6359
|
-
elementIdsRef.current = elementIdsRef.current.filter((eid) => eid !== elementId);
|
|
6360
|
-
}, []);
|
|
6361
7680
|
const registerRef = react.useRef(register);
|
|
6362
7681
|
const unregisterRef = react.useRef(unregister);
|
|
6363
7682
|
react.useEffect(() => {
|
|
@@ -6373,13 +7692,32 @@ function useUIComponent(options) {
|
|
|
6373
7692
|
unregisterRef.current();
|
|
6374
7693
|
}
|
|
6375
7694
|
};
|
|
6376
|
-
}, [autoRegister, bridge]);
|
|
6377
|
-
|
|
6378
|
-
if (!bridge) return
|
|
6379
|
-
|
|
6380
|
-
|
|
7695
|
+
}, [autoRegister, bridge, id]);
|
|
7696
|
+
react.useEffect(() => {
|
|
7697
|
+
if (!bridge || !registered) return;
|
|
7698
|
+
const targetId = registeredIdRef.current ?? id;
|
|
7699
|
+
const changed = bridge.registry.updateComponentMeta(targetId, {
|
|
7700
|
+
name,
|
|
7701
|
+
description,
|
|
7702
|
+
...options.actions !== void 0 ? { actions: toRegistryActions() } : {},
|
|
7703
|
+
...options.elementIds !== void 0 ? { elementIds: elementIdsRef.current } : {}
|
|
7704
|
+
});
|
|
7705
|
+
if (changed) {
|
|
7706
|
+
setPublishVersion((v) => v + 1);
|
|
7707
|
+
}
|
|
7708
|
+
}, [
|
|
7709
|
+
bridge,
|
|
7710
|
+
registered,
|
|
7711
|
+
id,
|
|
7712
|
+
name,
|
|
7713
|
+
description,
|
|
7714
|
+
actionsSignature,
|
|
7715
|
+
elementIdsSignature,
|
|
7716
|
+
toRegistryActions
|
|
7717
|
+
]);
|
|
7718
|
+
const registeredComponent = registered && bridge ? bridge.registry.getComponent(id) ?? null : null;
|
|
6381
7719
|
return {
|
|
6382
|
-
registered
|
|
7720
|
+
registered,
|
|
6383
7721
|
executeAction,
|
|
6384
7722
|
register,
|
|
6385
7723
|
unregister,
|
|
@@ -6537,12 +7875,31 @@ function TransitionTo(props) {
|
|
|
6537
7875
|
|
|
6538
7876
|
// src/server/types.ts
|
|
6539
7877
|
var UI_BRIDGE_NATIVE_ROUTES = {
|
|
7878
|
+
// Discovery / introspection — the two endpoints an agent hits first to learn
|
|
7879
|
+
// the rest of this table without reading docs. Special-cased in
|
|
7880
|
+
// `routeRequest` rather than living in `WS_ROUTES`, and appended by
|
|
7881
|
+
// `buildRoutesPayload` so `_routes` describes itself.
|
|
7882
|
+
HELP: {
|
|
7883
|
+
method: "GET",
|
|
7884
|
+
path: "/ui-bridge/_help",
|
|
7885
|
+
description: "Route index + action reference + tips (rich discovery payload)"
|
|
7886
|
+
},
|
|
7887
|
+
ROUTES: {
|
|
7888
|
+
method: "GET",
|
|
7889
|
+
path: "/ui-bridge/_routes",
|
|
7890
|
+
description: "Minimal {method, path} route list, matching the runner /_routes shape"
|
|
7891
|
+
},
|
|
6540
7892
|
// Control - Elements
|
|
6541
7893
|
GET_ELEMENTS: {
|
|
6542
7894
|
method: "GET",
|
|
6543
7895
|
path: "/ui-bridge/control/elements",
|
|
6544
7896
|
description: "List all registered elements"
|
|
6545
7897
|
},
|
|
7898
|
+
GET_ELEMENTS_FLAT: {
|
|
7899
|
+
method: "GET",
|
|
7900
|
+
path: "/ui-bridge/elements",
|
|
7901
|
+
description: "List all registered elements (flat alias matching the runner path)"
|
|
7902
|
+
},
|
|
6546
7903
|
GET_ELEMENT: {
|
|
6547
7904
|
method: "GET",
|
|
6548
7905
|
path: "/ui-bridge/control/element/:id",
|
|
@@ -6580,6 +7937,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6580
7937
|
path: "/ui-bridge/control/find",
|
|
6581
7938
|
description: "Find elements matching criteria"
|
|
6582
7939
|
},
|
|
7940
|
+
AI_FIND: {
|
|
7941
|
+
method: "POST",
|
|
7942
|
+
path: "/ui-bridge/ai/find",
|
|
7943
|
+
description: "Find elements matching criteria (runner-parity alias for /control/find)"
|
|
7944
|
+
},
|
|
6583
7945
|
GET_SNAPSHOT: {
|
|
6584
7946
|
method: "GET",
|
|
6585
7947
|
path: "/ui-bridge/control/snapshot",
|
|
@@ -6590,11 +7952,78 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6590
7952
|
path: "/ui-bridge/control/discover",
|
|
6591
7953
|
description: "Discover elements (alias for snapshot; web/runner parity)"
|
|
6592
7954
|
},
|
|
7955
|
+
DISCOVER_POST: {
|
|
7956
|
+
method: "POST",
|
|
7957
|
+
path: "/ui-bridge/control/discover",
|
|
7958
|
+
description: "Discover elements over POST (same handler as the GET form; web/runner parity)"
|
|
7959
|
+
},
|
|
6593
7960
|
PAGE_HEALTH: {
|
|
6594
7961
|
method: "POST",
|
|
6595
7962
|
path: "/ui-bridge/control/page-health",
|
|
6596
7963
|
description: "Holistic page health diagnostic: spatial coverage, layout regions, text signals, interactive readiness, anomalies + ASCII heatmap"
|
|
6597
7964
|
},
|
|
7965
|
+
PAGE_HEALTH_GET: {
|
|
7966
|
+
method: "GET",
|
|
7967
|
+
path: "/ui-bridge/control/page-health",
|
|
7968
|
+
description: "Page health diagnostic over GET, for ad-hoc curls with no body"
|
|
7969
|
+
},
|
|
7970
|
+
// Screenshots — GET and POST both accepted so a curl works either way.
|
|
7971
|
+
//
|
|
7972
|
+
// Both keys carry an explicit verb suffix. The pre-existing multi-verb pairs
|
|
7973
|
+
// do not agree on what an UNSUFFIXED key means — `DISCOVER` is the GET and
|
|
7974
|
+
// `PAGE_HEALTH` is the POST — so a consumer reading `ROUTES.<NAME>.method`
|
|
7975
|
+
// off a bare name gets opposite verbs from equally unmarked keys. Those two
|
|
7976
|
+
// names are load-bearing for existing consumers and are left alone; new
|
|
7977
|
+
// multi-verb entries name their verb rather than entrenching the ambiguity.
|
|
7978
|
+
SCREENSHOT_GET: {
|
|
7979
|
+
method: "GET",
|
|
7980
|
+
path: "/ui-bridge/control/screenshot",
|
|
7981
|
+
description: "PNG screenshot of the current screen"
|
|
7982
|
+
},
|
|
7983
|
+
SCREENSHOT_POST: {
|
|
7984
|
+
method: "POST",
|
|
7985
|
+
path: "/ui-bridge/control/screenshot",
|
|
7986
|
+
description: "PNG screenshot of the current screen (POST form)"
|
|
7987
|
+
},
|
|
7988
|
+
// Coordinate-based interaction
|
|
7989
|
+
CONTROL_TAP: {
|
|
7990
|
+
method: "POST",
|
|
7991
|
+
path: "/ui-bridge/control/tap",
|
|
7992
|
+
description: "Synthesize a press at screen coords by matching registered element layout rects: body { x, y, action? }"
|
|
7993
|
+
},
|
|
7994
|
+
// App-agnostic interaction parity with the web/runner surface. The
|
|
7995
|
+
// selector-based pair has no RN analog and answers NOT_SUPPORTED (501).
|
|
7996
|
+
PAGE_CLICK_BY_TEXT: {
|
|
7997
|
+
method: "POST",
|
|
7998
|
+
path: "/ui-bridge/control/page/click-by-text",
|
|
7999
|
+
description: "Press the first element matching free text: body { text, visibleOnly? }"
|
|
8000
|
+
},
|
|
8001
|
+
PAGE_CLICK_BY_SELECTOR: {
|
|
8002
|
+
method: "POST",
|
|
8003
|
+
path: "/ui-bridge/control/page/click-by-selector",
|
|
8004
|
+
description: "Selector-based press (web-only; mobile returns NOT_SUPPORTED)"
|
|
8005
|
+
},
|
|
8006
|
+
PAGE_TYPE_INTO: {
|
|
8007
|
+
method: "POST",
|
|
8008
|
+
path: "/ui-bridge/control/page/type-into",
|
|
8009
|
+
description: "Type into the input matching a label: body { label, text, clear?, visibleOnly? }"
|
|
8010
|
+
},
|
|
8011
|
+
PAGE_READ_VALUE: {
|
|
8012
|
+
method: "POST",
|
|
8013
|
+
path: "/ui-bridge/control/page/read-value",
|
|
8014
|
+
description: "Selector-based value read (web-only; mobile returns NOT_SUPPORTED)"
|
|
8015
|
+
},
|
|
8016
|
+
PAGE_FIND_BY_TEXT: {
|
|
8017
|
+
method: "POST",
|
|
8018
|
+
path: "/ui-bridge/control/page/find-by-text",
|
|
8019
|
+
description: "List elements matching free text without pressing: body { text, visibleOnly? }"
|
|
8020
|
+
},
|
|
8021
|
+
// Meta
|
|
8022
|
+
META_METHODS: {
|
|
8023
|
+
method: "GET",
|
|
8024
|
+
path: "/ui-bridge/meta/methods",
|
|
8025
|
+
description: "List every handler name (legacy introspection; prefer /_routes)"
|
|
8026
|
+
},
|
|
6598
8027
|
// Workflows
|
|
6599
8028
|
GET_WORKFLOWS: {
|
|
6600
8029
|
method: "GET",
|
|
@@ -6673,6 +8102,27 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6673
8102
|
path: "/ui-bridge/design/style-guide",
|
|
6674
8103
|
description: "Clear loaded style guide"
|
|
6675
8104
|
},
|
|
8105
|
+
// Quality Evaluation
|
|
8106
|
+
DESIGN_EVALUATE: {
|
|
8107
|
+
method: "POST",
|
|
8108
|
+
path: "/ui-bridge/design/evaluate",
|
|
8109
|
+
description: "Score the current UI against a quality context"
|
|
8110
|
+
},
|
|
8111
|
+
DESIGN_EVALUATE_CONTEXTS: {
|
|
8112
|
+
method: "GET",
|
|
8113
|
+
path: "/ui-bridge/design/evaluate/contexts",
|
|
8114
|
+
description: "List available quality evaluation contexts"
|
|
8115
|
+
},
|
|
8116
|
+
DESIGN_EVALUATE_BASELINE: {
|
|
8117
|
+
method: "POST",
|
|
8118
|
+
path: "/ui-bridge/design/evaluate/baseline",
|
|
8119
|
+
description: "Save a quality baseline snapshot"
|
|
8120
|
+
},
|
|
8121
|
+
DESIGN_EVALUATE_DIFF: {
|
|
8122
|
+
method: "POST",
|
|
8123
|
+
path: "/ui-bridge/design/evaluate/diff",
|
|
8124
|
+
description: "Diff the current UI against a saved baseline"
|
|
8125
|
+
},
|
|
6676
8126
|
// AI helpers
|
|
6677
8127
|
AI_FILL_FORM: {
|
|
6678
8128
|
method: "POST",
|
|
@@ -6704,18 +8154,34 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
6704
8154
|
path: "/ui-bridge/ai/wait-for-element",
|
|
6705
8155
|
description: "Wait for element predicate via HTTP (runner-only; mobile returns NOT_SUPPORTED \u2014 use WS waitForElement instead)"
|
|
6706
8156
|
},
|
|
6707
|
-
// Test hooks — drive ModalDetector state
|
|
6708
|
-
// (gated by `features.testHooks`). Mirrors `pushModal` /
|
|
6709
|
-
// calls that components normally make in-process.
|
|
8157
|
+
// Test hooks — drive ModalDetector state and the screen lock from outside
|
|
8158
|
+
// the React tree (gated by `features.testHooks`). Mirrors `pushModal` /
|
|
8159
|
+
// `dismissModal` calls that components normally make in-process.
|
|
8160
|
+
//
|
|
8161
|
+
// These are LISTED unconditionally — this table is a static constant with no
|
|
8162
|
+
// config to consult — but each carries `requiresTestHooks: true`, which is
|
|
8163
|
+
// the machine-readable form of the `(testHooks)` in its description. With the
|
|
8164
|
+
// flag off `routeRequest` and `buildRoutesPayload` both drop them and the
|
|
8165
|
+
// route answers a bare 404. `route-table-parity.test.ts` compares the whole
|
|
8166
|
+
// table against `_routes` at `testHooks: true`, and the flagged subset
|
|
8167
|
+
// against `_routes` at `testHooks: false`, so both surfaces are pinned.
|
|
6710
8168
|
PUSH_MODAL: {
|
|
6711
8169
|
method: "POST",
|
|
6712
8170
|
path: "/ui-bridge/control/modal/push",
|
|
6713
|
-
description: "Push a modal onto the modal stack (testHooks)"
|
|
8171
|
+
description: "Push a modal onto the modal stack (testHooks)",
|
|
8172
|
+
requiresTestHooks: true
|
|
6714
8173
|
},
|
|
6715
8174
|
DISMISS_MODAL: {
|
|
6716
8175
|
method: "POST",
|
|
6717
8176
|
path: "/ui-bridge/control/modal/dismiss/:id",
|
|
6718
|
-
description: "Dismiss a modal by id (testHooks)"
|
|
8177
|
+
description: "Dismiss a modal by id (testHooks)",
|
|
8178
|
+
requiresTestHooks: true
|
|
8179
|
+
},
|
|
8180
|
+
KEEP_AWAKE: {
|
|
8181
|
+
method: "POST",
|
|
8182
|
+
path: "/ui-bridge/control/keep-awake",
|
|
8183
|
+
description: "Keep the device screen awake via the injected keepAwakeProvider: body { enabled, durationMs? } (testHooks)",
|
|
8184
|
+
requiresTestHooks: true
|
|
6719
8185
|
},
|
|
6720
8186
|
// Observability — last-N ring buffers (always mounted; `installed:false`
|
|
6721
8187
|
// when the console/network patches are off, i.e. no testHooks).
|
|
@@ -7542,9 +9008,13 @@ var styles = reactNative.StyleSheet.create({
|
|
|
7542
9008
|
});
|
|
7543
9009
|
|
|
7544
9010
|
exports.CloudRelayClient = CloudRelayClient;
|
|
9011
|
+
exports.DEFAULT_PARAM_VALIDATION_MODE = DEFAULT_PARAM_VALIDATION_MODE;
|
|
7545
9012
|
exports.DefaultNativeActionExecutor = DefaultNativeActionExecutor;
|
|
7546
9013
|
exports.DeviceAnnouncer = DeviceAnnouncer;
|
|
9014
|
+
exports.MAX_ACTION_TIMEOUT_MS = MAX_ACTION_TIMEOUT_MS;
|
|
7547
9015
|
exports.ModalDetector = ModalDetector;
|
|
9016
|
+
exports.NATIVE_STANDARD_ACTION_EFFECTS = NATIVE_STANDARD_ACTION_EFFECTS;
|
|
9017
|
+
exports.NativeActionError = NativeActionError;
|
|
7548
9018
|
exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
|
|
7549
9019
|
exports.NativeUIBridgeServer = NativeUIBridgeServer;
|
|
7550
9020
|
exports.State = State;
|
|
@@ -7562,6 +9032,7 @@ exports.buildUpgradeResponse = buildUpgradeResponse;
|
|
|
7562
9032
|
exports.captureNativeResponsiveSnapshot = captureNativeResponsiveSnapshot;
|
|
7563
9033
|
exports.captureNativeStateVariations = captureNativeStateVariations;
|
|
7564
9034
|
exports.computeAcceptKey = computeAcceptKey;
|
|
9035
|
+
exports.computeVisibility = computeVisibility;
|
|
7565
9036
|
exports.createDesignHandlers = createDesignHandlers;
|
|
7566
9037
|
exports.createNativeActionExecutor = createNativeActionExecutor;
|
|
7567
9038
|
exports.createNativeElementIdentifier = createNativeElementIdentifier;
|
|
@@ -7572,17 +9043,30 @@ exports.deriveText = deriveText;
|
|
|
7572
9043
|
exports.extractHandlerNames = extractHandlerNames;
|
|
7573
9044
|
exports.findAllByPattern = findAllByPattern;
|
|
7574
9045
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
9046
|
+
exports.formatParamValidationFailure = formatParamValidationFailure;
|
|
9047
|
+
exports.getDefaultParamValidationMode = getDefaultParamValidationMode;
|
|
7575
9048
|
exports.getGlobalRegistry = getGlobalRegistry;
|
|
7576
9049
|
exports.getNativeElementDesignData = getNativeElementDesignData;
|
|
9050
|
+
exports.httpStatusForResponse = httpStatusForResponse;
|
|
9051
|
+
exports.inertAbortSignal = inertAbortSignal;
|
|
9052
|
+
exports.intersectRects = intersectRects;
|
|
7577
9053
|
exports.isAddrInUseError = isAddrInUseError;
|
|
9054
|
+
exports.isEmptyRect = isEmptyRect;
|
|
7578
9055
|
exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
|
|
7579
9056
|
exports.matchesIdentifier = matchesIdentifier;
|
|
9057
|
+
exports.nativeStandardActionEffect = nativeStandardActionEffect;
|
|
9058
|
+
exports.normalizeActionTimeoutMs = normalizeActionTimeoutMs;
|
|
9059
|
+
exports.pageRectOf = pageRectOf;
|
|
7580
9060
|
exports.parseColorToRgb = parseColorToRgb;
|
|
7581
9061
|
exports.parseFontSizePx = parseFontSizePx;
|
|
7582
9062
|
exports.parseTreePath = parseTreePath;
|
|
7583
9063
|
exports.projectVisionFields = projectVisionFields;
|
|
9064
|
+
exports.resetDefaultParamValidationMode = resetDefaultParamValidationMode;
|
|
7584
9065
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
9066
|
+
exports.resolveActionEffect = resolveActionEffect;
|
|
7585
9067
|
exports.roleForType = roleForType;
|
|
9068
|
+
exports.runAbortable = runAbortable;
|
|
9069
|
+
exports.setDefaultParamValidationMode = setDefaultParamValidationMode;
|
|
7586
9070
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
7587
9071
|
exports.useToastRecorder = useToastRecorder;
|
|
7588
9072
|
exports.useUIBridge = useUIBridge;
|
|
@@ -7596,5 +9080,6 @@ exports.useUIComponent = useUIComponent;
|
|
|
7596
9080
|
exports.useUIComponentAction = useUIComponentAction;
|
|
7597
9081
|
exports.useUIElement = useUIElement;
|
|
7598
9082
|
exports.useUIElementWithProps = useUIElementWithProps;
|
|
9083
|
+
exports.validateActionParams = validateActionParams;
|
|
7599
9084
|
//# sourceMappingURL=index.js.map
|
|
7600
9085
|
//# sourceMappingURL=index.js.map
|