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