@qontinui/ui-bridge-native 0.1.5 → 0.2.0
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/dist/control/index.d.mts +9 -5
- package/dist/control/index.d.ts +9 -5
- package/dist/control/index.js +75 -12
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +75 -12
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +69 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +69 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/design/index.d.mts +1 -1
- package/dist/design/index.d.ts +1 -1
- package/dist/{http-server-D7uVANBU.d.mts → http-server-DCdR8WlL.d.mts} +3 -3
- package/dist/{http-server-B7KudcYJ.d.ts → http-server-Dj3grQW4.d.ts} +3 -3
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +498 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +493 -17
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +504 -16
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +502 -17
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-DhbGbl9c.d.mts → registry-Cahd8Gn2.d.mts} +16 -1
- package/dist/{registry-B8A_EoxS.d.ts → registry-z-3WJsxp.d.ts} +16 -1
- package/dist/server/index.d.mts +5 -5
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +102 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +102 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-Bcj7ji97.d.mts → types-DAfCeWXc.d.mts} +22 -2
- package/dist/{types-BPNuYxDx.d.mts → types-Oz13YLZq.d.mts} +95 -1
- package/dist/{types-BPNuYxDx.d.ts → types-Oz13YLZq.d.ts} +95 -1
- package/dist/{types-BCYWwyVy.d.ts → types-ajakyp-l.d.ts} +22 -2
- package/dist/{useUIBridge-CDZbWxII.d.ts → useUIBridgeToast-D32KXyL1.d.ts} +154 -5
- package/dist/{useUIBridge-DfIEq9EQ.d.mts → useUIBridgeToast-GmT1TzUe.d.mts} +154 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -55,9 +55,34 @@ var NativeUIBridgeRegistry = class {
|
|
|
55
55
|
this.components = /* @__PURE__ */ new Map();
|
|
56
56
|
this.workflows = /* @__PURE__ */ new Map();
|
|
57
57
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
58
|
+
this.enrichers = {};
|
|
59
|
+
this.snapshotExtras = /* @__PURE__ */ new Map();
|
|
58
60
|
this.config = config;
|
|
59
61
|
}
|
|
60
62
|
// ============================================================================
|
|
63
|
+
// Snapshot Enricher Slots
|
|
64
|
+
// ============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
|
|
67
|
+
* with a partial set merges into existing slots instead of clobbering them.
|
|
68
|
+
*/
|
|
69
|
+
setEnrichers(e) {
|
|
70
|
+
this.enrichers = { ...this.enrichers, ...e };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register a custom snapshot enricher. The returned object will be
|
|
74
|
+
* `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
|
|
75
|
+
* clobbering canonical fields. Returns a disposer.
|
|
76
|
+
*/
|
|
77
|
+
registerSnapshotEnricher(name, fn) {
|
|
78
|
+
this.snapshotExtras.set(name, fn);
|
|
79
|
+
return () => this.unregisterSnapshotEnricher(name);
|
|
80
|
+
}
|
|
81
|
+
/** Remove a custom snapshot enricher by name */
|
|
82
|
+
unregisterSnapshotEnricher(name) {
|
|
83
|
+
this.snapshotExtras.delete(name);
|
|
84
|
+
}
|
|
85
|
+
// ============================================================================
|
|
61
86
|
// Element Management
|
|
62
87
|
// ============================================================================
|
|
63
88
|
/**
|
|
@@ -420,7 +445,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
420
445
|
const currentRoute = routeInfo.currentRoute;
|
|
421
446
|
elements = elements.filter((e) => e.registrationRoute === currentRoute);
|
|
422
447
|
}
|
|
423
|
-
|
|
448
|
+
const snapshot = {
|
|
424
449
|
timestamp: Date.now(),
|
|
425
450
|
elements: elements.map((e) => {
|
|
426
451
|
const handlers = extractHandlerNames(e.props);
|
|
@@ -452,6 +477,49 @@ var NativeUIBridgeRegistry = class {
|
|
|
452
477
|
currentRoute: routeInfo?.currentRoute ?? null,
|
|
453
478
|
segments: routeInfo?.segments
|
|
454
479
|
};
|
|
480
|
+
if (this.enrichers.modalDetector) {
|
|
481
|
+
try {
|
|
482
|
+
snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
|
|
483
|
+
} catch (error3) {
|
|
484
|
+
if (this.config.verbose) {
|
|
485
|
+
console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error3);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (this.enrichers.toastCapture) {
|
|
490
|
+
try {
|
|
491
|
+
snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
|
|
492
|
+
} catch (error3) {
|
|
493
|
+
if (this.config.verbose) {
|
|
494
|
+
console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error3);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (this.enrichers.undoTracker) {
|
|
499
|
+
try {
|
|
500
|
+
snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
|
|
501
|
+
} catch (error3) {
|
|
502
|
+
if (this.config.verbose) {
|
|
503
|
+
console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error3);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (this.snapshotExtras.size > 0) {
|
|
508
|
+
const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
|
|
509
|
+
for (const [name, fn] of this.snapshotExtras) {
|
|
510
|
+
try {
|
|
511
|
+
const extra = fn(ctx);
|
|
512
|
+
if (extra && typeof extra === "object") {
|
|
513
|
+
Object.assign(snapshot, extra);
|
|
514
|
+
}
|
|
515
|
+
} catch (error3) {
|
|
516
|
+
if (this.config.verbose) {
|
|
517
|
+
console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error3);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return snapshot;
|
|
455
523
|
}
|
|
456
524
|
/**
|
|
457
525
|
* Get registry statistics
|
|
@@ -597,6 +665,23 @@ function sleep(ms) {
|
|
|
597
665
|
var DefaultNativeActionExecutor = class {
|
|
598
666
|
constructor(registry) {
|
|
599
667
|
this.registry = registry;
|
|
668
|
+
this.actionListeners = /* @__PURE__ */ new Set();
|
|
669
|
+
}
|
|
670
|
+
onActionExecuted(listener) {
|
|
671
|
+
this.actionListeners.add(listener);
|
|
672
|
+
return () => {
|
|
673
|
+
this.actionListeners.delete(listener);
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
|
|
677
|
+
emitActionEvent(event) {
|
|
678
|
+
for (const listener of this.actionListeners) {
|
|
679
|
+
try {
|
|
680
|
+
listener(event);
|
|
681
|
+
} catch (error3) {
|
|
682
|
+
console.warn(`[ui-bridge-native] Action listener error:`, error3);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
600
685
|
}
|
|
601
686
|
/**
|
|
602
687
|
* Execute an action on an element
|
|
@@ -610,11 +695,23 @@ var DefaultNativeActionExecutor = class {
|
|
|
610
695
|
registered = findElementByIdentifier(elementId) ?? void 0;
|
|
611
696
|
}
|
|
612
697
|
if (!registered) {
|
|
698
|
+
const failTime = Date.now();
|
|
699
|
+
const errorMsg = `Element not found: ${elementId}`;
|
|
700
|
+
this.emitActionEvent({
|
|
701
|
+
elementId,
|
|
702
|
+
action: request.action,
|
|
703
|
+
params: request.params,
|
|
704
|
+
success: false,
|
|
705
|
+
error: errorMsg,
|
|
706
|
+
timestamp: failTime,
|
|
707
|
+
requestId: request.requestId,
|
|
708
|
+
durationMs: failTime - startTime
|
|
709
|
+
});
|
|
613
710
|
return {
|
|
614
711
|
success: false,
|
|
615
|
-
error:
|
|
616
|
-
durationMs:
|
|
617
|
-
timestamp:
|
|
712
|
+
error: errorMsg,
|
|
713
|
+
durationMs: failTime - startTime,
|
|
714
|
+
timestamp: failTime,
|
|
618
715
|
requestId: request.requestId
|
|
619
716
|
};
|
|
620
717
|
}
|
|
@@ -622,33 +719,67 @@ var DefaultNativeActionExecutor = class {
|
|
|
622
719
|
const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
|
|
623
720
|
waitDurationMs = waitResult.waitedMs;
|
|
624
721
|
if (!waitResult.met) {
|
|
722
|
+
const failTime = Date.now();
|
|
723
|
+
const errorMsg = waitResult.error || "Wait condition not met";
|
|
724
|
+
this.emitActionEvent({
|
|
725
|
+
elementId,
|
|
726
|
+
action: request.action,
|
|
727
|
+
params: request.params,
|
|
728
|
+
success: false,
|
|
729
|
+
error: errorMsg,
|
|
730
|
+
timestamp: failTime,
|
|
731
|
+
requestId: request.requestId,
|
|
732
|
+
durationMs: failTime - startTime
|
|
733
|
+
});
|
|
625
734
|
return {
|
|
626
735
|
success: false,
|
|
627
|
-
error:
|
|
628
|
-
durationMs:
|
|
629
|
-
timestamp:
|
|
736
|
+
error: errorMsg,
|
|
737
|
+
durationMs: failTime - startTime,
|
|
738
|
+
timestamp: failTime,
|
|
630
739
|
requestId: request.requestId,
|
|
631
740
|
waitDurationMs
|
|
632
741
|
};
|
|
633
742
|
}
|
|
634
743
|
}
|
|
635
744
|
const result = await this.performAction(registered, request.action, request.params);
|
|
745
|
+
const successTime = Date.now();
|
|
746
|
+
this.emitActionEvent({
|
|
747
|
+
elementId,
|
|
748
|
+
action: request.action,
|
|
749
|
+
params: request.params,
|
|
750
|
+
success: true,
|
|
751
|
+
timestamp: successTime,
|
|
752
|
+
requestId: request.requestId,
|
|
753
|
+
durationMs: successTime - startTime
|
|
754
|
+
});
|
|
636
755
|
return {
|
|
637
756
|
success: true,
|
|
638
757
|
elementState: registered.getState(),
|
|
639
758
|
result,
|
|
640
|
-
durationMs:
|
|
641
|
-
timestamp:
|
|
759
|
+
durationMs: successTime - startTime,
|
|
760
|
+
timestamp: successTime,
|
|
642
761
|
requestId: request.requestId,
|
|
643
762
|
waitDurationMs
|
|
644
763
|
};
|
|
645
764
|
} catch (error3) {
|
|
765
|
+
const failTime = Date.now();
|
|
766
|
+
const errorMsg = error3 instanceof Error ? error3.message : String(error3);
|
|
767
|
+
this.emitActionEvent({
|
|
768
|
+
elementId,
|
|
769
|
+
action: request.action,
|
|
770
|
+
params: request.params,
|
|
771
|
+
success: false,
|
|
772
|
+
error: errorMsg,
|
|
773
|
+
timestamp: failTime,
|
|
774
|
+
requestId: request.requestId,
|
|
775
|
+
durationMs: failTime - startTime
|
|
776
|
+
});
|
|
646
777
|
return {
|
|
647
778
|
success: false,
|
|
648
|
-
error:
|
|
779
|
+
error: errorMsg,
|
|
649
780
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
650
|
-
durationMs:
|
|
651
|
-
timestamp:
|
|
781
|
+
durationMs: failTime - startTime,
|
|
782
|
+
timestamp: failTime,
|
|
652
783
|
requestId: request.requestId,
|
|
653
784
|
waitDurationMs
|
|
654
785
|
};
|
|
@@ -741,7 +872,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
741
872
|
if (!params?.text) {
|
|
742
873
|
throw new Error("Type action requires text parameter");
|
|
743
874
|
}
|
|
744
|
-
if (params.clearFirst) {
|
|
875
|
+
if (params.clearFirst || params.clear) {
|
|
745
876
|
await this.performClear(element, props);
|
|
746
877
|
}
|
|
747
878
|
if (params.delay && params.delay > 0) {
|
|
@@ -1974,6 +2105,10 @@ var WS_ROUTES = [
|
|
|
1974
2105
|
{ pattern: "health", handler: "health", httpMethods: ["GET"] },
|
|
1975
2106
|
// Elements
|
|
1976
2107
|
{ pattern: "control/elements", handler: "getElements", httpMethods: ["GET"] },
|
|
2108
|
+
// Flat alias — matches the runner UI Bridge's `/ui-bridge/elements` path so
|
|
2109
|
+
// agents can use the same URL across runner + mobile without remembering
|
|
2110
|
+
// the `control/` prefix. Delegates to the same handler.
|
|
2111
|
+
{ pattern: "elements", handler: "getElements", httpMethods: ["GET"] },
|
|
1977
2112
|
{ pattern: "control/element/:id", handler: "getElement", httpMethods: ["GET"] },
|
|
1978
2113
|
{ pattern: "control/element/:id/state", handler: "getElementState", httpMethods: ["GET"] },
|
|
1979
2114
|
{ pattern: "control/element/:id/action", handler: "executeAction", httpMethods: ["POST"] },
|
|
@@ -2024,6 +2159,87 @@ var WS_ROUTES = [
|
|
|
2024
2159
|
// Meta / Introspection
|
|
2025
2160
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
|
|
2026
2161
|
];
|
|
2162
|
+
var HANDLER_DESCRIPTIONS = {
|
|
2163
|
+
health: "Liveness probe; returns app metadata + element count",
|
|
2164
|
+
getElements: "Flat list of registered UI elements",
|
|
2165
|
+
getElement: "Single element by id",
|
|
2166
|
+
getElementState: "Dynamic state (value, focused, checked) for one element",
|
|
2167
|
+
executeAction: 'Run an action on an element (see "actions" below for names)',
|
|
2168
|
+
getComponents: "Higher-level components with action schemas",
|
|
2169
|
+
getComponent: "Component detail + per-action paramSchema",
|
|
2170
|
+
executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
|
|
2171
|
+
find: "AI-powered element search by natural-language query",
|
|
2172
|
+
getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
|
|
2173
|
+
getWorkflows: "List registered workflows",
|
|
2174
|
+
runWorkflow: "Trigger a workflow by id",
|
|
2175
|
+
pageRefresh: "Reload the current route",
|
|
2176
|
+
pageNavigate: "Imperative router.push(url) \u2014 preferred over tapping tab buttons",
|
|
2177
|
+
pageReplace: "router.replace(url)",
|
|
2178
|
+
pageGoBack: "router.back()",
|
|
2179
|
+
pageGoForward: "router.forward()",
|
|
2180
|
+
getScreenshot: "PNG screenshot of the current screen",
|
|
2181
|
+
getElementStyles: "Computed styles for an element (design review)",
|
|
2182
|
+
getElementStateStyles: "Computed styles per interaction state (hover/focus/disabled)",
|
|
2183
|
+
getDesignSnapshot: "Visual + style snapshot for design review",
|
|
2184
|
+
getResponsiveSnapshots: "Screenshots at multiple viewport widths",
|
|
2185
|
+
runDesignAudit: "Accessibility + visual audit",
|
|
2186
|
+
loadStyleGuide: "Load a design system style guide",
|
|
2187
|
+
getStyleGuide: "Fetch the currently loaded style guide",
|
|
2188
|
+
clearStyleGuide: "Unload the style guide",
|
|
2189
|
+
evaluateQuality: "Score the current UI against a quality context",
|
|
2190
|
+
getQualityContexts: "List available quality evaluation contexts",
|
|
2191
|
+
saveBaseline: "Save a quality baseline snapshot",
|
|
2192
|
+
diffBaseline: "Diff current UI against a saved baseline",
|
|
2193
|
+
getMethods: "List every handler name (legacy introspection)"
|
|
2194
|
+
};
|
|
2195
|
+
var ACTION_REFERENCE = [
|
|
2196
|
+
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
2197
|
+
{ name: "longPress", params: "PressActionParams (optional position)" },
|
|
2198
|
+
{ name: "doubleTap", params: "none" },
|
|
2199
|
+
{
|
|
2200
|
+
name: "type",
|
|
2201
|
+
params: "TypeActionParams { text, delay?, clear? | clearFirst? } \u2014 `clear:true` replaces content atomically"
|
|
2202
|
+
},
|
|
2203
|
+
{ name: "clear", params: "none \u2014 empties the input" },
|
|
2204
|
+
{ name: "focus", params: "none" },
|
|
2205
|
+
{ name: "blur", params: "none" },
|
|
2206
|
+
{ name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
|
|
2207
|
+
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2208
|
+
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2209
|
+
];
|
|
2210
|
+
function buildRoutesPayload() {
|
|
2211
|
+
const entries = [];
|
|
2212
|
+
for (const r of WS_ROUTES) {
|
|
2213
|
+
if (!r.httpMethods) continue;
|
|
2214
|
+
for (const m of r.httpMethods) {
|
|
2215
|
+
entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
entries.push({ method: "GET", path: "/ui-bridge/_help" });
|
|
2219
|
+
entries.push({ method: "GET", path: "/ui-bridge/_routes" });
|
|
2220
|
+
entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
2221
|
+
return { routes: entries, count: entries.length };
|
|
2222
|
+
}
|
|
2223
|
+
function buildHelpPayload() {
|
|
2224
|
+
return {
|
|
2225
|
+
version: 1,
|
|
2226
|
+
description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
|
|
2227
|
+
routes: WS_ROUTES.map((r) => ({
|
|
2228
|
+
pattern: r.pattern,
|
|
2229
|
+
httpMethods: r.httpMethods ?? [],
|
|
2230
|
+
wsOnly: !r.httpMethods,
|
|
2231
|
+
handler: r.handler,
|
|
2232
|
+
description: HANDLER_DESCRIPTIONS[r.handler] ?? ""
|
|
2233
|
+
})),
|
|
2234
|
+
actions: ACTION_REFERENCE,
|
|
2235
|
+
tips: [
|
|
2236
|
+
"Start with `GET /ui-bridge/control/snapshot?visibleOnly=true` to see what the user actually sees.",
|
|
2237
|
+
"Prefer `control/component/:id/action/:actionId` over button presses \u2014 typed paramSchema, no button-id guessing.",
|
|
2238
|
+
"Use `pageNavigate` for tab switches instead of pressing tab buttons; gesture state often swallows clicks.",
|
|
2239
|
+
"The `type` action supports `clear:true` to replace existing text atomically in one call."
|
|
2240
|
+
]
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2027
2243
|
var NativeUIBridgeServer = class {
|
|
2028
2244
|
constructor(registry, executor, config = {}) {
|
|
2029
2245
|
this.registry = registry;
|
|
@@ -2040,7 +2256,9 @@ var NativeUIBridgeServer = class {
|
|
|
2040
2256
|
cors: true,
|
|
2041
2257
|
...config
|
|
2042
2258
|
};
|
|
2043
|
-
this.handlers = createServerHandlers(registry, executor
|
|
2259
|
+
this.handlers = createServerHandlers(registry, executor, {
|
|
2260
|
+
appInfo: this.config.appInfo
|
|
2261
|
+
});
|
|
2044
2262
|
this.handlers.getMethods = async () => {
|
|
2045
2263
|
const methods = WS_ROUTES.map((r) => ({
|
|
2046
2264
|
method: r.pattern,
|
|
@@ -2844,6 +3062,20 @@ var NativeUIBridgeServer = class {
|
|
|
2844
3062
|
};
|
|
2845
3063
|
}
|
|
2846
3064
|
const stripped = path.slice(PREFIX.length);
|
|
3065
|
+
if (method === "GET" && stripped === "_help") {
|
|
3066
|
+
return {
|
|
3067
|
+
success: true,
|
|
3068
|
+
data: buildHelpPayload(),
|
|
3069
|
+
timestamp: Date.now()
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
if (method === "GET" && stripped === "_routes") {
|
|
3073
|
+
return {
|
|
3074
|
+
success: true,
|
|
3075
|
+
data: buildRoutesPayload(),
|
|
3076
|
+
timestamp: Date.now()
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
2847
3079
|
let patternMatchedButWrongVerb = false;
|
|
2848
3080
|
for (const route of WS_ROUTES) {
|
|
2849
3081
|
const pathParams = parsePath(route.pattern, stripped);
|
|
@@ -3465,6 +3697,186 @@ function parseQueryParams(path) {
|
|
|
3465
3697
|
}
|
|
3466
3698
|
return result;
|
|
3467
3699
|
}
|
|
3700
|
+
|
|
3701
|
+
// src/modal/modal-detector.ts
|
|
3702
|
+
var ModalDetector = class {
|
|
3703
|
+
constructor() {
|
|
3704
|
+
/** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
|
|
3705
|
+
this.modals = /* @__PURE__ */ new Map();
|
|
3706
|
+
}
|
|
3707
|
+
pushModal(modal) {
|
|
3708
|
+
const existing = this.modals.get(modal.id);
|
|
3709
|
+
const detectedAt = existing ? existing.detectedAt : Date.now();
|
|
3710
|
+
const next = {
|
|
3711
|
+
id: modal.id,
|
|
3712
|
+
title: modal.title,
|
|
3713
|
+
type: modal.type ?? "modal",
|
|
3714
|
+
blocking: modal.blocking ?? true,
|
|
3715
|
+
dismissible: modal.dismissible ?? true,
|
|
3716
|
+
detectedAt
|
|
3717
|
+
};
|
|
3718
|
+
this.modals.set(modal.id, next);
|
|
3719
|
+
}
|
|
3720
|
+
dismissModal(id) {
|
|
3721
|
+
this.modals.delete(id);
|
|
3722
|
+
}
|
|
3723
|
+
getSnapshotModalContext() {
|
|
3724
|
+
const active = this.getActive();
|
|
3725
|
+
const ctx = {
|
|
3726
|
+
modals: active,
|
|
3727
|
+
hasBlockingModal: active.some((m) => m.blocking),
|
|
3728
|
+
count: active.length
|
|
3729
|
+
};
|
|
3730
|
+
if (active.length > 0) {
|
|
3731
|
+
ctx.topModal = active[active.length - 1];
|
|
3732
|
+
}
|
|
3733
|
+
return ctx;
|
|
3734
|
+
}
|
|
3735
|
+
getActive() {
|
|
3736
|
+
return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
|
|
3737
|
+
}
|
|
3738
|
+
clear() {
|
|
3739
|
+
this.modals.clear();
|
|
3740
|
+
}
|
|
3741
|
+
};
|
|
3742
|
+
|
|
3743
|
+
// src/toast/toast-capture.ts
|
|
3744
|
+
var DEFAULT_MAX_RECENT = 50;
|
|
3745
|
+
var ToastCapture = class {
|
|
3746
|
+
constructor(config = {}) {
|
|
3747
|
+
this.active = /* @__PURE__ */ new Map();
|
|
3748
|
+
this.recent = [];
|
|
3749
|
+
this.totalCaptured = 0;
|
|
3750
|
+
this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
|
|
3751
|
+
}
|
|
3752
|
+
recordToast(toast) {
|
|
3753
|
+
if (this.active.has(toast.id)) {
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
const appearedAt = toast.appearedAt ?? Date.now();
|
|
3757
|
+
const entry = {
|
|
3758
|
+
id: toast.id,
|
|
3759
|
+
message: toast.message,
|
|
3760
|
+
level: toast.level ?? "info",
|
|
3761
|
+
appearedAt,
|
|
3762
|
+
visible: true,
|
|
3763
|
+
durationMs: 0
|
|
3764
|
+
};
|
|
3765
|
+
this.active.set(toast.id, entry);
|
|
3766
|
+
this.totalCaptured += 1;
|
|
3767
|
+
}
|
|
3768
|
+
dismissToast(id) {
|
|
3769
|
+
const entry = this.active.get(id);
|
|
3770
|
+
if (!entry) {
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
this.active.delete(id);
|
|
3774
|
+
const dismissedAt = Date.now();
|
|
3775
|
+
const dismissed = {
|
|
3776
|
+
...entry,
|
|
3777
|
+
dismissedAt,
|
|
3778
|
+
visible: false,
|
|
3779
|
+
durationMs: dismissedAt - entry.appearedAt
|
|
3780
|
+
};
|
|
3781
|
+
this.recent.push(dismissed);
|
|
3782
|
+
if (this.recent.length > this.maxRecent) {
|
|
3783
|
+
this.recent.shift();
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
getSnapshotToastContext() {
|
|
3787
|
+
const now = Date.now();
|
|
3788
|
+
const active = Array.from(this.active.values()).map((entry) => ({
|
|
3789
|
+
...entry,
|
|
3790
|
+
durationMs: now - entry.appearedAt
|
|
3791
|
+
}));
|
|
3792
|
+
return {
|
|
3793
|
+
active,
|
|
3794
|
+
recent: [...this.recent],
|
|
3795
|
+
totalCaptured: this.totalCaptured
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
clear() {
|
|
3799
|
+
this.active.clear();
|
|
3800
|
+
this.recent = [];
|
|
3801
|
+
this.totalCaptured = 0;
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
|
|
3805
|
+
// src/undo/undo-tracker.ts
|
|
3806
|
+
var DEFAULT_MAX_HISTORY = 50;
|
|
3807
|
+
function buildSummary(ctx) {
|
|
3808
|
+
if (!ctx.canUndo && !ctx.canRedo) {
|
|
3809
|
+
return "no undo available";
|
|
3810
|
+
}
|
|
3811
|
+
const parts = [];
|
|
3812
|
+
if (ctx.canUndo) {
|
|
3813
|
+
parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
|
|
3814
|
+
}
|
|
3815
|
+
if (ctx.canRedo) {
|
|
3816
|
+
parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
|
|
3817
|
+
}
|
|
3818
|
+
return parts.join(" / ");
|
|
3819
|
+
}
|
|
3820
|
+
var UndoTracker = class {
|
|
3821
|
+
constructor(config = {}) {
|
|
3822
|
+
this.actions = [];
|
|
3823
|
+
this.declaredState = null;
|
|
3824
|
+
this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
|
|
3825
|
+
}
|
|
3826
|
+
recordAction(record) {
|
|
3827
|
+
this.actions.push(record);
|
|
3828
|
+
if (this.actions.length > this.maxHistory) {
|
|
3829
|
+
this.actions = this.actions.slice(-this.maxHistory);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
setDeclaredState(state) {
|
|
3833
|
+
this.declaredState = state;
|
|
3834
|
+
}
|
|
3835
|
+
getActionHistory() {
|
|
3836
|
+
return this.actions.slice();
|
|
3837
|
+
}
|
|
3838
|
+
getSnapshotUndoContext() {
|
|
3839
|
+
const declared = this.declaredState;
|
|
3840
|
+
if (declared) {
|
|
3841
|
+
const summary2 = buildSummary({
|
|
3842
|
+
canUndo: declared.canUndo,
|
|
3843
|
+
canRedo: declared.canRedo,
|
|
3844
|
+
undoDescription: declared.undoDescription,
|
|
3845
|
+
redoDescription: declared.redoDescription
|
|
3846
|
+
});
|
|
3847
|
+
return {
|
|
3848
|
+
canUndo: declared.canUndo,
|
|
3849
|
+
canRedo: declared.canRedo,
|
|
3850
|
+
undoDescription: declared.undoDescription,
|
|
3851
|
+
redoDescription: declared.redoDescription,
|
|
3852
|
+
undoDepth: declared.undoDepth,
|
|
3853
|
+
redoDepth: declared.redoDepth,
|
|
3854
|
+
summary: summary2
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
const canUndo = this.actions.some((a) => a.reversible !== false);
|
|
3858
|
+
const canRedo = false;
|
|
3859
|
+
const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
|
|
3860
|
+
const undoDescription = lastReversible?.description;
|
|
3861
|
+
const summary = buildSummary({ canUndo, canRedo, undoDescription });
|
|
3862
|
+
return {
|
|
3863
|
+
canUndo,
|
|
3864
|
+
canRedo,
|
|
3865
|
+
undoDescription,
|
|
3866
|
+
summary
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
executeUndo() {
|
|
3870
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
3871
|
+
}
|
|
3872
|
+
executeRedo() {
|
|
3873
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
3874
|
+
}
|
|
3875
|
+
clear() {
|
|
3876
|
+
this.actions = [];
|
|
3877
|
+
this.declaredState = null;
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3468
3880
|
var UIBridgeNativeContext = react.createContext(null);
|
|
3469
3881
|
function UIBridgeNativeProvider({
|
|
3470
3882
|
children,
|
|
@@ -3481,6 +3893,9 @@ function UIBridgeNativeProvider({
|
|
|
3481
3893
|
}) {
|
|
3482
3894
|
const registryRef = react.useRef(null);
|
|
3483
3895
|
const executorRef = react.useRef(null);
|
|
3896
|
+
const modalDetectorRef = react.useRef(null);
|
|
3897
|
+
const toastCaptureRef = react.useRef(null);
|
|
3898
|
+
const undoTrackerRef = react.useRef(null);
|
|
3484
3899
|
const [serverRunning, setServerRunning] = react.useState(false);
|
|
3485
3900
|
if (!registryRef.current) {
|
|
3486
3901
|
registryRef.current = new NativeUIBridgeRegistry({
|
|
@@ -3494,6 +3909,12 @@ function UIBridgeNativeProvider({
|
|
|
3494
3909
|
executorRef.current = createNativeActionExecutor(registry);
|
|
3495
3910
|
}
|
|
3496
3911
|
const executor = executorRef.current;
|
|
3912
|
+
if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
|
|
3913
|
+
if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
|
|
3914
|
+
if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
|
|
3915
|
+
const modalDetector = modalDetectorRef.current;
|
|
3916
|
+
const toastCapture = toastCaptureRef.current;
|
|
3917
|
+
const undoTracker = undoTrackerRef.current;
|
|
3497
3918
|
const serverRef = react.useRef(null);
|
|
3498
3919
|
const eventBridgeRef = react.useRef(null);
|
|
3499
3920
|
const announcerRef = react.useRef(null);
|
|
@@ -3508,7 +3929,8 @@ function UIBridgeNativeProvider({
|
|
|
3508
3929
|
}
|
|
3509
3930
|
const server = createNativeServer(registry, executor, {
|
|
3510
3931
|
serverPort: config.serverPort || 8087,
|
|
3511
|
-
cors: true
|
|
3932
|
+
cors: true,
|
|
3933
|
+
appInfo: config.appInfo
|
|
3512
3934
|
});
|
|
3513
3935
|
server.setAdapter(serverAdapter);
|
|
3514
3936
|
if (navigationProvider) {
|
|
@@ -3595,7 +4017,8 @@ function UIBridgeNativeProvider({
|
|
|
3595
4017
|
try {
|
|
3596
4018
|
const bareServer = createNativeServer(registry, executor, {
|
|
3597
4019
|
serverPort: config.serverPort || 8087,
|
|
3598
|
-
cors: true
|
|
4020
|
+
cors: true,
|
|
4021
|
+
appInfo: config.appInfo
|
|
3599
4022
|
});
|
|
3600
4023
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
3601
4024
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -3680,6 +4103,22 @@ function UIBridgeNativeProvider({
|
|
|
3680
4103
|
lastRoute = current;
|
|
3681
4104
|
});
|
|
3682
4105
|
}, [routeProvider, registry]);
|
|
4106
|
+
react.useEffect(() => {
|
|
4107
|
+
registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
|
|
4108
|
+
}, [registry, modalDetector, toastCapture, undoTracker]);
|
|
4109
|
+
react.useEffect(() => {
|
|
4110
|
+
const unsubscribe = executor.onActionExecuted((event) => {
|
|
4111
|
+
if (!event.success) return;
|
|
4112
|
+
undoTracker.recordAction({
|
|
4113
|
+
id: event.requestId ?? `action-${event.timestamp}`,
|
|
4114
|
+
type: event.action,
|
|
4115
|
+
targetId: event.elementId,
|
|
4116
|
+
timestamp: event.timestamp,
|
|
4117
|
+
reversible: true
|
|
4118
|
+
});
|
|
4119
|
+
});
|
|
4120
|
+
return unsubscribe;
|
|
4121
|
+
}, [executor, undoTracker]);
|
|
3683
4122
|
const on = react.useCallback(
|
|
3684
4123
|
(type, listener) => registry.on(type, listener),
|
|
3685
4124
|
[registry]
|
|
@@ -3694,6 +4133,9 @@ function UIBridgeNativeProvider({
|
|
|
3694
4133
|
config,
|
|
3695
4134
|
registry,
|
|
3696
4135
|
executor,
|
|
4136
|
+
modalDetector,
|
|
4137
|
+
toastCapture,
|
|
4138
|
+
undoTracker,
|
|
3697
4139
|
getElements,
|
|
3698
4140
|
getComponents,
|
|
3699
4141
|
createSnapshot,
|
|
@@ -3710,6 +4152,9 @@ function UIBridgeNativeProvider({
|
|
|
3710
4152
|
config,
|
|
3711
4153
|
registry,
|
|
3712
4154
|
executor,
|
|
4155
|
+
modalDetector,
|
|
4156
|
+
toastCapture,
|
|
4157
|
+
undoTracker,
|
|
3713
4158
|
getElements,
|
|
3714
4159
|
getComponents,
|
|
3715
4160
|
createSnapshot,
|
|
@@ -4190,6 +4635,37 @@ function useUIBridgeRequired() {
|
|
|
4190
4635
|
}
|
|
4191
4636
|
return result;
|
|
4192
4637
|
}
|
|
4638
|
+
function useUIBridgeModal(opts) {
|
|
4639
|
+
const { modalDetector } = useUIBridgeNative();
|
|
4640
|
+
const { id, title, type, blocking, dismissible } = opts;
|
|
4641
|
+
react.useEffect(() => {
|
|
4642
|
+
modalDetector.pushModal({ id, title, type, blocking, dismissible });
|
|
4643
|
+
return () => {
|
|
4644
|
+
modalDetector.dismissModal(id);
|
|
4645
|
+
};
|
|
4646
|
+
}, [modalDetector, id, title, type, blocking, dismissible]);
|
|
4647
|
+
}
|
|
4648
|
+
function useUIBridgeToast(opts) {
|
|
4649
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4650
|
+
const { id, message, level } = opts;
|
|
4651
|
+
react.useEffect(() => {
|
|
4652
|
+
toastCapture.recordToast({ id, message, level });
|
|
4653
|
+
return () => {
|
|
4654
|
+
toastCapture.dismissToast(id);
|
|
4655
|
+
};
|
|
4656
|
+
}, [toastCapture, id, message, level]);
|
|
4657
|
+
}
|
|
4658
|
+
function useToastRecorder() {
|
|
4659
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4660
|
+
const recordToast = react.useCallback(
|
|
4661
|
+
(toast) => {
|
|
4662
|
+
toastCapture.recordToast(toast);
|
|
4663
|
+
},
|
|
4664
|
+
[toastCapture]
|
|
4665
|
+
);
|
|
4666
|
+
const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4667
|
+
return { recordToast, dismissToast };
|
|
4668
|
+
}
|
|
4193
4669
|
|
|
4194
4670
|
// src/server/types.ts
|
|
4195
4671
|
var UI_BRIDGE_NATIVE_ROUTES = {
|
|
@@ -5094,11 +5570,14 @@ var styles = reactNative.StyleSheet.create({
|
|
|
5094
5570
|
exports.CloudRelayClient = CloudRelayClient;
|
|
5095
5571
|
exports.DefaultNativeActionExecutor = DefaultNativeActionExecutor;
|
|
5096
5572
|
exports.DeviceAnnouncer = DeviceAnnouncer;
|
|
5573
|
+
exports.ModalDetector = ModalDetector;
|
|
5097
5574
|
exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
|
|
5098
5575
|
exports.NativeUIBridgeServer = NativeUIBridgeServer;
|
|
5576
|
+
exports.ToastCapture = ToastCapture;
|
|
5099
5577
|
exports.UIBridgeInspector = UIBridgeInspector;
|
|
5100
5578
|
exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
|
|
5101
5579
|
exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
|
|
5580
|
+
exports.UndoTracker = UndoTracker;
|
|
5102
5581
|
exports.WebSocketConnection = WebSocketConnection;
|
|
5103
5582
|
exports.WebSocketEventBridge = WebSocketEventBridge;
|
|
5104
5583
|
exports.buildTreePath = buildTreePath;
|
|
@@ -5121,11 +5600,14 @@ exports.matchesIdentifier = matchesIdentifier;
|
|
|
5121
5600
|
exports.parseTreePath = parseTreePath;
|
|
5122
5601
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
5123
5602
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
5603
|
+
exports.useToastRecorder = useToastRecorder;
|
|
5124
5604
|
exports.useUIBridge = useUIBridge;
|
|
5605
|
+
exports.useUIBridgeModal = useUIBridgeModal;
|
|
5125
5606
|
exports.useUIBridgeNative = useUIBridgeNative;
|
|
5126
5607
|
exports.useUIBridgeNativeOptional = useUIBridgeNativeOptional;
|
|
5127
5608
|
exports.useUIBridgeNativeRequired = useUIBridgeNativeRequired;
|
|
5128
5609
|
exports.useUIBridgeRequired = useUIBridgeRequired;
|
|
5610
|
+
exports.useUIBridgeToast = useUIBridgeToast;
|
|
5129
5611
|
exports.useUIComponent = useUIComponent;
|
|
5130
5612
|
exports.useUIComponentAction = useUIComponentAction;
|
|
5131
5613
|
exports.useUIElement = useUIElement;
|