@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/react/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
|
|
@@ -545,6 +613,23 @@ function sleep(ms) {
|
|
|
545
613
|
var DefaultNativeActionExecutor = class {
|
|
546
614
|
constructor(registry) {
|
|
547
615
|
this.registry = registry;
|
|
616
|
+
this.actionListeners = /* @__PURE__ */ new Set();
|
|
617
|
+
}
|
|
618
|
+
onActionExecuted(listener) {
|
|
619
|
+
this.actionListeners.add(listener);
|
|
620
|
+
return () => {
|
|
621
|
+
this.actionListeners.delete(listener);
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
|
|
625
|
+
emitActionEvent(event) {
|
|
626
|
+
for (const listener of this.actionListeners) {
|
|
627
|
+
try {
|
|
628
|
+
listener(event);
|
|
629
|
+
} catch (error3) {
|
|
630
|
+
console.warn(`[ui-bridge-native] Action listener error:`, error3);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
548
633
|
}
|
|
549
634
|
/**
|
|
550
635
|
* Execute an action on an element
|
|
@@ -558,11 +643,23 @@ var DefaultNativeActionExecutor = class {
|
|
|
558
643
|
registered = findElementByIdentifier(elementId) ?? void 0;
|
|
559
644
|
}
|
|
560
645
|
if (!registered) {
|
|
646
|
+
const failTime = Date.now();
|
|
647
|
+
const errorMsg = `Element not found: ${elementId}`;
|
|
648
|
+
this.emitActionEvent({
|
|
649
|
+
elementId,
|
|
650
|
+
action: request.action,
|
|
651
|
+
params: request.params,
|
|
652
|
+
success: false,
|
|
653
|
+
error: errorMsg,
|
|
654
|
+
timestamp: failTime,
|
|
655
|
+
requestId: request.requestId,
|
|
656
|
+
durationMs: failTime - startTime
|
|
657
|
+
});
|
|
561
658
|
return {
|
|
562
659
|
success: false,
|
|
563
|
-
error:
|
|
564
|
-
durationMs:
|
|
565
|
-
timestamp:
|
|
660
|
+
error: errorMsg,
|
|
661
|
+
durationMs: failTime - startTime,
|
|
662
|
+
timestamp: failTime,
|
|
566
663
|
requestId: request.requestId
|
|
567
664
|
};
|
|
568
665
|
}
|
|
@@ -570,33 +667,67 @@ var DefaultNativeActionExecutor = class {
|
|
|
570
667
|
const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
|
|
571
668
|
waitDurationMs = waitResult.waitedMs;
|
|
572
669
|
if (!waitResult.met) {
|
|
670
|
+
const failTime = Date.now();
|
|
671
|
+
const errorMsg = waitResult.error || "Wait condition not met";
|
|
672
|
+
this.emitActionEvent({
|
|
673
|
+
elementId,
|
|
674
|
+
action: request.action,
|
|
675
|
+
params: request.params,
|
|
676
|
+
success: false,
|
|
677
|
+
error: errorMsg,
|
|
678
|
+
timestamp: failTime,
|
|
679
|
+
requestId: request.requestId,
|
|
680
|
+
durationMs: failTime - startTime
|
|
681
|
+
});
|
|
573
682
|
return {
|
|
574
683
|
success: false,
|
|
575
|
-
error:
|
|
576
|
-
durationMs:
|
|
577
|
-
timestamp:
|
|
684
|
+
error: errorMsg,
|
|
685
|
+
durationMs: failTime - startTime,
|
|
686
|
+
timestamp: failTime,
|
|
578
687
|
requestId: request.requestId,
|
|
579
688
|
waitDurationMs
|
|
580
689
|
};
|
|
581
690
|
}
|
|
582
691
|
}
|
|
583
692
|
const result = await this.performAction(registered, request.action, request.params);
|
|
693
|
+
const successTime = Date.now();
|
|
694
|
+
this.emitActionEvent({
|
|
695
|
+
elementId,
|
|
696
|
+
action: request.action,
|
|
697
|
+
params: request.params,
|
|
698
|
+
success: true,
|
|
699
|
+
timestamp: successTime,
|
|
700
|
+
requestId: request.requestId,
|
|
701
|
+
durationMs: successTime - startTime
|
|
702
|
+
});
|
|
584
703
|
return {
|
|
585
704
|
success: true,
|
|
586
705
|
elementState: registered.getState(),
|
|
587
706
|
result,
|
|
588
|
-
durationMs:
|
|
589
|
-
timestamp:
|
|
707
|
+
durationMs: successTime - startTime,
|
|
708
|
+
timestamp: successTime,
|
|
590
709
|
requestId: request.requestId,
|
|
591
710
|
waitDurationMs
|
|
592
711
|
};
|
|
593
712
|
} catch (error3) {
|
|
713
|
+
const failTime = Date.now();
|
|
714
|
+
const errorMsg = error3 instanceof Error ? error3.message : String(error3);
|
|
715
|
+
this.emitActionEvent({
|
|
716
|
+
elementId,
|
|
717
|
+
action: request.action,
|
|
718
|
+
params: request.params,
|
|
719
|
+
success: false,
|
|
720
|
+
error: errorMsg,
|
|
721
|
+
timestamp: failTime,
|
|
722
|
+
requestId: request.requestId,
|
|
723
|
+
durationMs: failTime - startTime
|
|
724
|
+
});
|
|
594
725
|
return {
|
|
595
726
|
success: false,
|
|
596
|
-
error:
|
|
727
|
+
error: errorMsg,
|
|
597
728
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
598
|
-
durationMs:
|
|
599
|
-
timestamp:
|
|
729
|
+
durationMs: failTime - startTime,
|
|
730
|
+
timestamp: failTime,
|
|
600
731
|
requestId: request.requestId,
|
|
601
732
|
waitDurationMs
|
|
602
733
|
};
|
|
@@ -689,7 +820,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
689
820
|
if (!params?.text) {
|
|
690
821
|
throw new Error("Type action requires text parameter");
|
|
691
822
|
}
|
|
692
|
-
if (params.clearFirst) {
|
|
823
|
+
if (params.clearFirst || params.clear) {
|
|
693
824
|
await this.performClear(element, props);
|
|
694
825
|
}
|
|
695
826
|
if (params.delay && params.delay > 0) {
|
|
@@ -1875,6 +2006,15 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1875
2006
|
timestamp: Date.now(),
|
|
1876
2007
|
...stats
|
|
1877
2008
|
};
|
|
2009
|
+
if (config?.appInfo) {
|
|
2010
|
+
response.uiBridge = {
|
|
2011
|
+
version: "0.3.0",
|
|
2012
|
+
...config.appInfo,
|
|
2013
|
+
capabilities: ["elements", "components", "actions", "design"],
|
|
2014
|
+
elementCount: stats.elements,
|
|
2015
|
+
componentCount: stats.components
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
1878
2018
|
return success2(response);
|
|
1879
2019
|
}
|
|
1880
2020
|
};
|
|
@@ -1913,6 +2053,10 @@ var WS_ROUTES = [
|
|
|
1913
2053
|
{ pattern: "health", handler: "health", httpMethods: ["GET"] },
|
|
1914
2054
|
// Elements
|
|
1915
2055
|
{ pattern: "control/elements", handler: "getElements", httpMethods: ["GET"] },
|
|
2056
|
+
// Flat alias — matches the runner UI Bridge's `/ui-bridge/elements` path so
|
|
2057
|
+
// agents can use the same URL across runner + mobile without remembering
|
|
2058
|
+
// the `control/` prefix. Delegates to the same handler.
|
|
2059
|
+
{ pattern: "elements", handler: "getElements", httpMethods: ["GET"] },
|
|
1916
2060
|
{ pattern: "control/element/:id", handler: "getElement", httpMethods: ["GET"] },
|
|
1917
2061
|
{ pattern: "control/element/:id/state", handler: "getElementState", httpMethods: ["GET"] },
|
|
1918
2062
|
{ pattern: "control/element/:id/action", handler: "executeAction", httpMethods: ["POST"] },
|
|
@@ -1963,6 +2107,87 @@ var WS_ROUTES = [
|
|
|
1963
2107
|
// Meta / Introspection
|
|
1964
2108
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
|
|
1965
2109
|
];
|
|
2110
|
+
var HANDLER_DESCRIPTIONS = {
|
|
2111
|
+
health: "Liveness probe; returns app metadata + element count",
|
|
2112
|
+
getElements: "Flat list of registered UI elements",
|
|
2113
|
+
getElement: "Single element by id",
|
|
2114
|
+
getElementState: "Dynamic state (value, focused, checked) for one element",
|
|
2115
|
+
executeAction: 'Run an action on an element (see "actions" below for names)',
|
|
2116
|
+
getComponents: "Higher-level components with action schemas",
|
|
2117
|
+
getComponent: "Component detail + per-action paramSchema",
|
|
2118
|
+
executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
|
|
2119
|
+
find: "AI-powered element search by natural-language query",
|
|
2120
|
+
getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
|
|
2121
|
+
getWorkflows: "List registered workflows",
|
|
2122
|
+
runWorkflow: "Trigger a workflow by id",
|
|
2123
|
+
pageRefresh: "Reload the current route",
|
|
2124
|
+
pageNavigate: "Imperative router.push(url) \u2014 preferred over tapping tab buttons",
|
|
2125
|
+
pageReplace: "router.replace(url)",
|
|
2126
|
+
pageGoBack: "router.back()",
|
|
2127
|
+
pageGoForward: "router.forward()",
|
|
2128
|
+
getScreenshot: "PNG screenshot of the current screen",
|
|
2129
|
+
getElementStyles: "Computed styles for an element (design review)",
|
|
2130
|
+
getElementStateStyles: "Computed styles per interaction state (hover/focus/disabled)",
|
|
2131
|
+
getDesignSnapshot: "Visual + style snapshot for design review",
|
|
2132
|
+
getResponsiveSnapshots: "Screenshots at multiple viewport widths",
|
|
2133
|
+
runDesignAudit: "Accessibility + visual audit",
|
|
2134
|
+
loadStyleGuide: "Load a design system style guide",
|
|
2135
|
+
getStyleGuide: "Fetch the currently loaded style guide",
|
|
2136
|
+
clearStyleGuide: "Unload the style guide",
|
|
2137
|
+
evaluateQuality: "Score the current UI against a quality context",
|
|
2138
|
+
getQualityContexts: "List available quality evaluation contexts",
|
|
2139
|
+
saveBaseline: "Save a quality baseline snapshot",
|
|
2140
|
+
diffBaseline: "Diff current UI against a saved baseline",
|
|
2141
|
+
getMethods: "List every handler name (legacy introspection)"
|
|
2142
|
+
};
|
|
2143
|
+
var ACTION_REFERENCE = [
|
|
2144
|
+
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
2145
|
+
{ name: "longPress", params: "PressActionParams (optional position)" },
|
|
2146
|
+
{ name: "doubleTap", params: "none" },
|
|
2147
|
+
{
|
|
2148
|
+
name: "type",
|
|
2149
|
+
params: "TypeActionParams { text, delay?, clear? | clearFirst? } \u2014 `clear:true` replaces content atomically"
|
|
2150
|
+
},
|
|
2151
|
+
{ name: "clear", params: "none \u2014 empties the input" },
|
|
2152
|
+
{ name: "focus", params: "none" },
|
|
2153
|
+
{ name: "blur", params: "none" },
|
|
2154
|
+
{ name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
|
|
2155
|
+
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2156
|
+
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2157
|
+
];
|
|
2158
|
+
function buildRoutesPayload() {
|
|
2159
|
+
const entries = [];
|
|
2160
|
+
for (const r of WS_ROUTES) {
|
|
2161
|
+
if (!r.httpMethods) continue;
|
|
2162
|
+
for (const m of r.httpMethods) {
|
|
2163
|
+
entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
entries.push({ method: "GET", path: "/ui-bridge/_help" });
|
|
2167
|
+
entries.push({ method: "GET", path: "/ui-bridge/_routes" });
|
|
2168
|
+
entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
2169
|
+
return { routes: entries, count: entries.length };
|
|
2170
|
+
}
|
|
2171
|
+
function buildHelpPayload() {
|
|
2172
|
+
return {
|
|
2173
|
+
version: 1,
|
|
2174
|
+
description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
|
|
2175
|
+
routes: WS_ROUTES.map((r) => ({
|
|
2176
|
+
pattern: r.pattern,
|
|
2177
|
+
httpMethods: r.httpMethods ?? [],
|
|
2178
|
+
wsOnly: !r.httpMethods,
|
|
2179
|
+
handler: r.handler,
|
|
2180
|
+
description: HANDLER_DESCRIPTIONS[r.handler] ?? ""
|
|
2181
|
+
})),
|
|
2182
|
+
actions: ACTION_REFERENCE,
|
|
2183
|
+
tips: [
|
|
2184
|
+
"Start with `GET /ui-bridge/control/snapshot?visibleOnly=true` to see what the user actually sees.",
|
|
2185
|
+
"Prefer `control/component/:id/action/:actionId` over button presses \u2014 typed paramSchema, no button-id guessing.",
|
|
2186
|
+
"Use `pageNavigate` for tab switches instead of pressing tab buttons; gesture state often swallows clicks.",
|
|
2187
|
+
"The `type` action supports `clear:true` to replace existing text atomically in one call."
|
|
2188
|
+
]
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
1966
2191
|
var NativeUIBridgeServer = class {
|
|
1967
2192
|
constructor(registry, executor, config = {}) {
|
|
1968
2193
|
this.registry = registry;
|
|
@@ -1979,7 +2204,9 @@ var NativeUIBridgeServer = class {
|
|
|
1979
2204
|
cors: true,
|
|
1980
2205
|
...config
|
|
1981
2206
|
};
|
|
1982
|
-
this.handlers = createServerHandlers(registry, executor
|
|
2207
|
+
this.handlers = createServerHandlers(registry, executor, {
|
|
2208
|
+
appInfo: this.config.appInfo
|
|
2209
|
+
});
|
|
1983
2210
|
this.handlers.getMethods = async () => {
|
|
1984
2211
|
const methods = WS_ROUTES.map((r) => ({
|
|
1985
2212
|
method: r.pattern,
|
|
@@ -2783,6 +3010,20 @@ var NativeUIBridgeServer = class {
|
|
|
2783
3010
|
};
|
|
2784
3011
|
}
|
|
2785
3012
|
const stripped = path.slice(PREFIX.length);
|
|
3013
|
+
if (method === "GET" && stripped === "_help") {
|
|
3014
|
+
return {
|
|
3015
|
+
success: true,
|
|
3016
|
+
data: buildHelpPayload(),
|
|
3017
|
+
timestamp: Date.now()
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
if (method === "GET" && stripped === "_routes") {
|
|
3021
|
+
return {
|
|
3022
|
+
success: true,
|
|
3023
|
+
data: buildRoutesPayload(),
|
|
3024
|
+
timestamp: Date.now()
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
2786
3027
|
let patternMatchedButWrongVerb = false;
|
|
2787
3028
|
for (const route of WS_ROUTES) {
|
|
2788
3029
|
const pathParams = parsePath(route.pattern, stripped);
|
|
@@ -3404,6 +3645,186 @@ function parseQueryParams(path) {
|
|
|
3404
3645
|
}
|
|
3405
3646
|
return result;
|
|
3406
3647
|
}
|
|
3648
|
+
|
|
3649
|
+
// src/modal/modal-detector.ts
|
|
3650
|
+
var ModalDetector = class {
|
|
3651
|
+
constructor() {
|
|
3652
|
+
/** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
|
|
3653
|
+
this.modals = /* @__PURE__ */ new Map();
|
|
3654
|
+
}
|
|
3655
|
+
pushModal(modal) {
|
|
3656
|
+
const existing = this.modals.get(modal.id);
|
|
3657
|
+
const detectedAt = existing ? existing.detectedAt : Date.now();
|
|
3658
|
+
const next = {
|
|
3659
|
+
id: modal.id,
|
|
3660
|
+
title: modal.title,
|
|
3661
|
+
type: modal.type ?? "modal",
|
|
3662
|
+
blocking: modal.blocking ?? true,
|
|
3663
|
+
dismissible: modal.dismissible ?? true,
|
|
3664
|
+
detectedAt
|
|
3665
|
+
};
|
|
3666
|
+
this.modals.set(modal.id, next);
|
|
3667
|
+
}
|
|
3668
|
+
dismissModal(id) {
|
|
3669
|
+
this.modals.delete(id);
|
|
3670
|
+
}
|
|
3671
|
+
getSnapshotModalContext() {
|
|
3672
|
+
const active = this.getActive();
|
|
3673
|
+
const ctx = {
|
|
3674
|
+
modals: active,
|
|
3675
|
+
hasBlockingModal: active.some((m) => m.blocking),
|
|
3676
|
+
count: active.length
|
|
3677
|
+
};
|
|
3678
|
+
if (active.length > 0) {
|
|
3679
|
+
ctx.topModal = active[active.length - 1];
|
|
3680
|
+
}
|
|
3681
|
+
return ctx;
|
|
3682
|
+
}
|
|
3683
|
+
getActive() {
|
|
3684
|
+
return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
|
|
3685
|
+
}
|
|
3686
|
+
clear() {
|
|
3687
|
+
this.modals.clear();
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
|
|
3691
|
+
// src/toast/toast-capture.ts
|
|
3692
|
+
var DEFAULT_MAX_RECENT = 50;
|
|
3693
|
+
var ToastCapture = class {
|
|
3694
|
+
constructor(config = {}) {
|
|
3695
|
+
this.active = /* @__PURE__ */ new Map();
|
|
3696
|
+
this.recent = [];
|
|
3697
|
+
this.totalCaptured = 0;
|
|
3698
|
+
this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
|
|
3699
|
+
}
|
|
3700
|
+
recordToast(toast) {
|
|
3701
|
+
if (this.active.has(toast.id)) {
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
const appearedAt = toast.appearedAt ?? Date.now();
|
|
3705
|
+
const entry = {
|
|
3706
|
+
id: toast.id,
|
|
3707
|
+
message: toast.message,
|
|
3708
|
+
level: toast.level ?? "info",
|
|
3709
|
+
appearedAt,
|
|
3710
|
+
visible: true,
|
|
3711
|
+
durationMs: 0
|
|
3712
|
+
};
|
|
3713
|
+
this.active.set(toast.id, entry);
|
|
3714
|
+
this.totalCaptured += 1;
|
|
3715
|
+
}
|
|
3716
|
+
dismissToast(id) {
|
|
3717
|
+
const entry = this.active.get(id);
|
|
3718
|
+
if (!entry) {
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
this.active.delete(id);
|
|
3722
|
+
const dismissedAt = Date.now();
|
|
3723
|
+
const dismissed = {
|
|
3724
|
+
...entry,
|
|
3725
|
+
dismissedAt,
|
|
3726
|
+
visible: false,
|
|
3727
|
+
durationMs: dismissedAt - entry.appearedAt
|
|
3728
|
+
};
|
|
3729
|
+
this.recent.push(dismissed);
|
|
3730
|
+
if (this.recent.length > this.maxRecent) {
|
|
3731
|
+
this.recent.shift();
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
getSnapshotToastContext() {
|
|
3735
|
+
const now = Date.now();
|
|
3736
|
+
const active = Array.from(this.active.values()).map((entry) => ({
|
|
3737
|
+
...entry,
|
|
3738
|
+
durationMs: now - entry.appearedAt
|
|
3739
|
+
}));
|
|
3740
|
+
return {
|
|
3741
|
+
active,
|
|
3742
|
+
recent: [...this.recent],
|
|
3743
|
+
totalCaptured: this.totalCaptured
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
clear() {
|
|
3747
|
+
this.active.clear();
|
|
3748
|
+
this.recent = [];
|
|
3749
|
+
this.totalCaptured = 0;
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3753
|
+
// src/undo/undo-tracker.ts
|
|
3754
|
+
var DEFAULT_MAX_HISTORY = 50;
|
|
3755
|
+
function buildSummary(ctx) {
|
|
3756
|
+
if (!ctx.canUndo && !ctx.canRedo) {
|
|
3757
|
+
return "no undo available";
|
|
3758
|
+
}
|
|
3759
|
+
const parts = [];
|
|
3760
|
+
if (ctx.canUndo) {
|
|
3761
|
+
parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
|
|
3762
|
+
}
|
|
3763
|
+
if (ctx.canRedo) {
|
|
3764
|
+
parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
|
|
3765
|
+
}
|
|
3766
|
+
return parts.join(" / ");
|
|
3767
|
+
}
|
|
3768
|
+
var UndoTracker = class {
|
|
3769
|
+
constructor(config = {}) {
|
|
3770
|
+
this.actions = [];
|
|
3771
|
+
this.declaredState = null;
|
|
3772
|
+
this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
|
|
3773
|
+
}
|
|
3774
|
+
recordAction(record) {
|
|
3775
|
+
this.actions.push(record);
|
|
3776
|
+
if (this.actions.length > this.maxHistory) {
|
|
3777
|
+
this.actions = this.actions.slice(-this.maxHistory);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
setDeclaredState(state) {
|
|
3781
|
+
this.declaredState = state;
|
|
3782
|
+
}
|
|
3783
|
+
getActionHistory() {
|
|
3784
|
+
return this.actions.slice();
|
|
3785
|
+
}
|
|
3786
|
+
getSnapshotUndoContext() {
|
|
3787
|
+
const declared = this.declaredState;
|
|
3788
|
+
if (declared) {
|
|
3789
|
+
const summary2 = buildSummary({
|
|
3790
|
+
canUndo: declared.canUndo,
|
|
3791
|
+
canRedo: declared.canRedo,
|
|
3792
|
+
undoDescription: declared.undoDescription,
|
|
3793
|
+
redoDescription: declared.redoDescription
|
|
3794
|
+
});
|
|
3795
|
+
return {
|
|
3796
|
+
canUndo: declared.canUndo,
|
|
3797
|
+
canRedo: declared.canRedo,
|
|
3798
|
+
undoDescription: declared.undoDescription,
|
|
3799
|
+
redoDescription: declared.redoDescription,
|
|
3800
|
+
undoDepth: declared.undoDepth,
|
|
3801
|
+
redoDepth: declared.redoDepth,
|
|
3802
|
+
summary: summary2
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
const canUndo = this.actions.some((a) => a.reversible !== false);
|
|
3806
|
+
const canRedo = false;
|
|
3807
|
+
const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
|
|
3808
|
+
const undoDescription = lastReversible?.description;
|
|
3809
|
+
const summary = buildSummary({ canUndo, canRedo, undoDescription });
|
|
3810
|
+
return {
|
|
3811
|
+
canUndo,
|
|
3812
|
+
canRedo,
|
|
3813
|
+
undoDescription,
|
|
3814
|
+
summary
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
executeUndo() {
|
|
3818
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
3819
|
+
}
|
|
3820
|
+
executeRedo() {
|
|
3821
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
3822
|
+
}
|
|
3823
|
+
clear() {
|
|
3824
|
+
this.actions = [];
|
|
3825
|
+
this.declaredState = null;
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3407
3828
|
var UIBridgeNativeContext = react.createContext(null);
|
|
3408
3829
|
function UIBridgeNativeProvider({
|
|
3409
3830
|
children,
|
|
@@ -3420,6 +3841,9 @@ function UIBridgeNativeProvider({
|
|
|
3420
3841
|
}) {
|
|
3421
3842
|
const registryRef = react.useRef(null);
|
|
3422
3843
|
const executorRef = react.useRef(null);
|
|
3844
|
+
const modalDetectorRef = react.useRef(null);
|
|
3845
|
+
const toastCaptureRef = react.useRef(null);
|
|
3846
|
+
const undoTrackerRef = react.useRef(null);
|
|
3423
3847
|
const [serverRunning, setServerRunning] = react.useState(false);
|
|
3424
3848
|
if (!registryRef.current) {
|
|
3425
3849
|
registryRef.current = new NativeUIBridgeRegistry({
|
|
@@ -3433,6 +3857,12 @@ function UIBridgeNativeProvider({
|
|
|
3433
3857
|
executorRef.current = createNativeActionExecutor(registry);
|
|
3434
3858
|
}
|
|
3435
3859
|
const executor = executorRef.current;
|
|
3860
|
+
if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
|
|
3861
|
+
if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
|
|
3862
|
+
if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
|
|
3863
|
+
const modalDetector = modalDetectorRef.current;
|
|
3864
|
+
const toastCapture = toastCaptureRef.current;
|
|
3865
|
+
const undoTracker = undoTrackerRef.current;
|
|
3436
3866
|
const serverRef = react.useRef(null);
|
|
3437
3867
|
const eventBridgeRef = react.useRef(null);
|
|
3438
3868
|
const announcerRef = react.useRef(null);
|
|
@@ -3447,7 +3877,8 @@ function UIBridgeNativeProvider({
|
|
|
3447
3877
|
}
|
|
3448
3878
|
const server = createNativeServer(registry, executor, {
|
|
3449
3879
|
serverPort: config.serverPort || 8087,
|
|
3450
|
-
cors: true
|
|
3880
|
+
cors: true,
|
|
3881
|
+
appInfo: config.appInfo
|
|
3451
3882
|
});
|
|
3452
3883
|
server.setAdapter(serverAdapter);
|
|
3453
3884
|
if (navigationProvider) {
|
|
@@ -3534,7 +3965,8 @@ function UIBridgeNativeProvider({
|
|
|
3534
3965
|
try {
|
|
3535
3966
|
const bareServer = createNativeServer(registry, executor, {
|
|
3536
3967
|
serverPort: config.serverPort || 8087,
|
|
3537
|
-
cors: true
|
|
3968
|
+
cors: true,
|
|
3969
|
+
appInfo: config.appInfo
|
|
3538
3970
|
});
|
|
3539
3971
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
3540
3972
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -3619,6 +4051,22 @@ function UIBridgeNativeProvider({
|
|
|
3619
4051
|
lastRoute = current;
|
|
3620
4052
|
});
|
|
3621
4053
|
}, [routeProvider, registry]);
|
|
4054
|
+
react.useEffect(() => {
|
|
4055
|
+
registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
|
|
4056
|
+
}, [registry, modalDetector, toastCapture, undoTracker]);
|
|
4057
|
+
react.useEffect(() => {
|
|
4058
|
+
const unsubscribe = executor.onActionExecuted((event) => {
|
|
4059
|
+
if (!event.success) return;
|
|
4060
|
+
undoTracker.recordAction({
|
|
4061
|
+
id: event.requestId ?? `action-${event.timestamp}`,
|
|
4062
|
+
type: event.action,
|
|
4063
|
+
targetId: event.elementId,
|
|
4064
|
+
timestamp: event.timestamp,
|
|
4065
|
+
reversible: true
|
|
4066
|
+
});
|
|
4067
|
+
});
|
|
4068
|
+
return unsubscribe;
|
|
4069
|
+
}, [executor, undoTracker]);
|
|
3622
4070
|
const on = react.useCallback(
|
|
3623
4071
|
(type, listener) => registry.on(type, listener),
|
|
3624
4072
|
[registry]
|
|
@@ -3633,6 +4081,9 @@ function UIBridgeNativeProvider({
|
|
|
3633
4081
|
config,
|
|
3634
4082
|
registry,
|
|
3635
4083
|
executor,
|
|
4084
|
+
modalDetector,
|
|
4085
|
+
toastCapture,
|
|
4086
|
+
undoTracker,
|
|
3636
4087
|
getElements,
|
|
3637
4088
|
getComponents,
|
|
3638
4089
|
createSnapshot,
|
|
@@ -3649,6 +4100,9 @@ function UIBridgeNativeProvider({
|
|
|
3649
4100
|
config,
|
|
3650
4101
|
registry,
|
|
3651
4102
|
executor,
|
|
4103
|
+
modalDetector,
|
|
4104
|
+
toastCapture,
|
|
4105
|
+
undoTracker,
|
|
3652
4106
|
getElements,
|
|
3653
4107
|
getComponents,
|
|
3654
4108
|
createSnapshot,
|
|
@@ -4166,14 +4620,48 @@ function useAutoRegister(options = {}) {
|
|
|
4166
4620
|
isEnabled: enabled && !!bridge?.registry
|
|
4167
4621
|
};
|
|
4168
4622
|
}
|
|
4623
|
+
function useUIBridgeModal(opts) {
|
|
4624
|
+
const { modalDetector } = useUIBridgeNative();
|
|
4625
|
+
const { id, title, type, blocking, dismissible } = opts;
|
|
4626
|
+
react.useEffect(() => {
|
|
4627
|
+
modalDetector.pushModal({ id, title, type, blocking, dismissible });
|
|
4628
|
+
return () => {
|
|
4629
|
+
modalDetector.dismissModal(id);
|
|
4630
|
+
};
|
|
4631
|
+
}, [modalDetector, id, title, type, blocking, dismissible]);
|
|
4632
|
+
}
|
|
4633
|
+
function useUIBridgeToast(opts) {
|
|
4634
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4635
|
+
const { id, message, level } = opts;
|
|
4636
|
+
react.useEffect(() => {
|
|
4637
|
+
toastCapture.recordToast({ id, message, level });
|
|
4638
|
+
return () => {
|
|
4639
|
+
toastCapture.dismissToast(id);
|
|
4640
|
+
};
|
|
4641
|
+
}, [toastCapture, id, message, level]);
|
|
4642
|
+
}
|
|
4643
|
+
function useToastRecorder() {
|
|
4644
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4645
|
+
const recordToast = react.useCallback(
|
|
4646
|
+
(toast) => {
|
|
4647
|
+
toastCapture.recordToast(toast);
|
|
4648
|
+
},
|
|
4649
|
+
[toastCapture]
|
|
4650
|
+
);
|
|
4651
|
+
const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4652
|
+
return { recordToast, dismissToast };
|
|
4653
|
+
}
|
|
4169
4654
|
|
|
4170
4655
|
exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
|
|
4171
4656
|
exports.useAutoRegister = useAutoRegister;
|
|
4657
|
+
exports.useToastRecorder = useToastRecorder;
|
|
4172
4658
|
exports.useUIBridge = useUIBridge;
|
|
4659
|
+
exports.useUIBridgeModal = useUIBridgeModal;
|
|
4173
4660
|
exports.useUIBridgeNative = useUIBridgeNative;
|
|
4174
4661
|
exports.useUIBridgeNativeOptional = useUIBridgeNativeOptional;
|
|
4175
4662
|
exports.useUIBridgeNativeRequired = useUIBridgeNativeRequired;
|
|
4176
4663
|
exports.useUIBridgeRequired = useUIBridgeRequired;
|
|
4664
|
+
exports.useUIBridgeToast = useUIBridgeToast;
|
|
4177
4665
|
exports.useUIComponent = useUIComponent;
|
|
4178
4666
|
exports.useUIComponentAction = useUIComponentAction;
|
|
4179
4667
|
exports.useUIElement = useUIElement;
|