@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.mjs
CHANGED
|
@@ -53,9 +53,34 @@ var NativeUIBridgeRegistry = class {
|
|
|
53
53
|
this.components = /* @__PURE__ */ new Map();
|
|
54
54
|
this.workflows = /* @__PURE__ */ new Map();
|
|
55
55
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
56
|
+
this.enrichers = {};
|
|
57
|
+
this.snapshotExtras = /* @__PURE__ */ new Map();
|
|
56
58
|
this.config = config;
|
|
57
59
|
}
|
|
58
60
|
// ============================================================================
|
|
61
|
+
// Snapshot Enricher Slots
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
|
|
65
|
+
* with a partial set merges into existing slots instead of clobbering them.
|
|
66
|
+
*/
|
|
67
|
+
setEnrichers(e) {
|
|
68
|
+
this.enrichers = { ...this.enrichers, ...e };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Register a custom snapshot enricher. The returned object will be
|
|
72
|
+
* `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
|
|
73
|
+
* clobbering canonical fields. Returns a disposer.
|
|
74
|
+
*/
|
|
75
|
+
registerSnapshotEnricher(name, fn) {
|
|
76
|
+
this.snapshotExtras.set(name, fn);
|
|
77
|
+
return () => this.unregisterSnapshotEnricher(name);
|
|
78
|
+
}
|
|
79
|
+
/** Remove a custom snapshot enricher by name */
|
|
80
|
+
unregisterSnapshotEnricher(name) {
|
|
81
|
+
this.snapshotExtras.delete(name);
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
59
84
|
// Element Management
|
|
60
85
|
// ============================================================================
|
|
61
86
|
/**
|
|
@@ -418,7 +443,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
418
443
|
const currentRoute = routeInfo.currentRoute;
|
|
419
444
|
elements = elements.filter((e) => e.registrationRoute === currentRoute);
|
|
420
445
|
}
|
|
421
|
-
|
|
446
|
+
const snapshot = {
|
|
422
447
|
timestamp: Date.now(),
|
|
423
448
|
elements: elements.map((e) => {
|
|
424
449
|
const handlers = extractHandlerNames(e.props);
|
|
@@ -450,6 +475,49 @@ var NativeUIBridgeRegistry = class {
|
|
|
450
475
|
currentRoute: routeInfo?.currentRoute ?? null,
|
|
451
476
|
segments: routeInfo?.segments
|
|
452
477
|
};
|
|
478
|
+
if (this.enrichers.modalDetector) {
|
|
479
|
+
try {
|
|
480
|
+
snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
|
|
481
|
+
} catch (error3) {
|
|
482
|
+
if (this.config.verbose) {
|
|
483
|
+
console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error3);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (this.enrichers.toastCapture) {
|
|
488
|
+
try {
|
|
489
|
+
snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
|
|
490
|
+
} catch (error3) {
|
|
491
|
+
if (this.config.verbose) {
|
|
492
|
+
console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error3);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (this.enrichers.undoTracker) {
|
|
497
|
+
try {
|
|
498
|
+
snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
|
|
499
|
+
} catch (error3) {
|
|
500
|
+
if (this.config.verbose) {
|
|
501
|
+
console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error3);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (this.snapshotExtras.size > 0) {
|
|
506
|
+
const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
|
|
507
|
+
for (const [name, fn] of this.snapshotExtras) {
|
|
508
|
+
try {
|
|
509
|
+
const extra = fn(ctx);
|
|
510
|
+
if (extra && typeof extra === "object") {
|
|
511
|
+
Object.assign(snapshot, extra);
|
|
512
|
+
}
|
|
513
|
+
} catch (error3) {
|
|
514
|
+
if (this.config.verbose) {
|
|
515
|
+
console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error3);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return snapshot;
|
|
453
521
|
}
|
|
454
522
|
/**
|
|
455
523
|
* Get registry statistics
|
|
@@ -543,6 +611,23 @@ function sleep(ms) {
|
|
|
543
611
|
var DefaultNativeActionExecutor = class {
|
|
544
612
|
constructor(registry) {
|
|
545
613
|
this.registry = registry;
|
|
614
|
+
this.actionListeners = /* @__PURE__ */ new Set();
|
|
615
|
+
}
|
|
616
|
+
onActionExecuted(listener) {
|
|
617
|
+
this.actionListeners.add(listener);
|
|
618
|
+
return () => {
|
|
619
|
+
this.actionListeners.delete(listener);
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
|
|
623
|
+
emitActionEvent(event) {
|
|
624
|
+
for (const listener of this.actionListeners) {
|
|
625
|
+
try {
|
|
626
|
+
listener(event);
|
|
627
|
+
} catch (error3) {
|
|
628
|
+
console.warn(`[ui-bridge-native] Action listener error:`, error3);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
546
631
|
}
|
|
547
632
|
/**
|
|
548
633
|
* Execute an action on an element
|
|
@@ -556,11 +641,23 @@ var DefaultNativeActionExecutor = class {
|
|
|
556
641
|
registered = findElementByIdentifier(elementId) ?? void 0;
|
|
557
642
|
}
|
|
558
643
|
if (!registered) {
|
|
644
|
+
const failTime = Date.now();
|
|
645
|
+
const errorMsg = `Element not found: ${elementId}`;
|
|
646
|
+
this.emitActionEvent({
|
|
647
|
+
elementId,
|
|
648
|
+
action: request.action,
|
|
649
|
+
params: request.params,
|
|
650
|
+
success: false,
|
|
651
|
+
error: errorMsg,
|
|
652
|
+
timestamp: failTime,
|
|
653
|
+
requestId: request.requestId,
|
|
654
|
+
durationMs: failTime - startTime
|
|
655
|
+
});
|
|
559
656
|
return {
|
|
560
657
|
success: false,
|
|
561
|
-
error:
|
|
562
|
-
durationMs:
|
|
563
|
-
timestamp:
|
|
658
|
+
error: errorMsg,
|
|
659
|
+
durationMs: failTime - startTime,
|
|
660
|
+
timestamp: failTime,
|
|
564
661
|
requestId: request.requestId
|
|
565
662
|
};
|
|
566
663
|
}
|
|
@@ -568,33 +665,67 @@ var DefaultNativeActionExecutor = class {
|
|
|
568
665
|
const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
|
|
569
666
|
waitDurationMs = waitResult.waitedMs;
|
|
570
667
|
if (!waitResult.met) {
|
|
668
|
+
const failTime = Date.now();
|
|
669
|
+
const errorMsg = waitResult.error || "Wait condition not met";
|
|
670
|
+
this.emitActionEvent({
|
|
671
|
+
elementId,
|
|
672
|
+
action: request.action,
|
|
673
|
+
params: request.params,
|
|
674
|
+
success: false,
|
|
675
|
+
error: errorMsg,
|
|
676
|
+
timestamp: failTime,
|
|
677
|
+
requestId: request.requestId,
|
|
678
|
+
durationMs: failTime - startTime
|
|
679
|
+
});
|
|
571
680
|
return {
|
|
572
681
|
success: false,
|
|
573
|
-
error:
|
|
574
|
-
durationMs:
|
|
575
|
-
timestamp:
|
|
682
|
+
error: errorMsg,
|
|
683
|
+
durationMs: failTime - startTime,
|
|
684
|
+
timestamp: failTime,
|
|
576
685
|
requestId: request.requestId,
|
|
577
686
|
waitDurationMs
|
|
578
687
|
};
|
|
579
688
|
}
|
|
580
689
|
}
|
|
581
690
|
const result = await this.performAction(registered, request.action, request.params);
|
|
691
|
+
const successTime = Date.now();
|
|
692
|
+
this.emitActionEvent({
|
|
693
|
+
elementId,
|
|
694
|
+
action: request.action,
|
|
695
|
+
params: request.params,
|
|
696
|
+
success: true,
|
|
697
|
+
timestamp: successTime,
|
|
698
|
+
requestId: request.requestId,
|
|
699
|
+
durationMs: successTime - startTime
|
|
700
|
+
});
|
|
582
701
|
return {
|
|
583
702
|
success: true,
|
|
584
703
|
elementState: registered.getState(),
|
|
585
704
|
result,
|
|
586
|
-
durationMs:
|
|
587
|
-
timestamp:
|
|
705
|
+
durationMs: successTime - startTime,
|
|
706
|
+
timestamp: successTime,
|
|
588
707
|
requestId: request.requestId,
|
|
589
708
|
waitDurationMs
|
|
590
709
|
};
|
|
591
710
|
} catch (error3) {
|
|
711
|
+
const failTime = Date.now();
|
|
712
|
+
const errorMsg = error3 instanceof Error ? error3.message : String(error3);
|
|
713
|
+
this.emitActionEvent({
|
|
714
|
+
elementId,
|
|
715
|
+
action: request.action,
|
|
716
|
+
params: request.params,
|
|
717
|
+
success: false,
|
|
718
|
+
error: errorMsg,
|
|
719
|
+
timestamp: failTime,
|
|
720
|
+
requestId: request.requestId,
|
|
721
|
+
durationMs: failTime - startTime
|
|
722
|
+
});
|
|
592
723
|
return {
|
|
593
724
|
success: false,
|
|
594
|
-
error:
|
|
725
|
+
error: errorMsg,
|
|
595
726
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
596
|
-
durationMs:
|
|
597
|
-
timestamp:
|
|
727
|
+
durationMs: failTime - startTime,
|
|
728
|
+
timestamp: failTime,
|
|
598
729
|
requestId: request.requestId,
|
|
599
730
|
waitDurationMs
|
|
600
731
|
};
|
|
@@ -687,7 +818,7 @@ var DefaultNativeActionExecutor = class {
|
|
|
687
818
|
if (!params?.text) {
|
|
688
819
|
throw new Error("Type action requires text parameter");
|
|
689
820
|
}
|
|
690
|
-
if (params.clearFirst) {
|
|
821
|
+
if (params.clearFirst || params.clear) {
|
|
691
822
|
await this.performClear(element, props);
|
|
692
823
|
}
|
|
693
824
|
if (params.delay && params.delay > 0) {
|
|
@@ -1873,6 +2004,15 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1873
2004
|
timestamp: Date.now(),
|
|
1874
2005
|
...stats
|
|
1875
2006
|
};
|
|
2007
|
+
if (config?.appInfo) {
|
|
2008
|
+
response.uiBridge = {
|
|
2009
|
+
version: "0.3.0",
|
|
2010
|
+
...config.appInfo,
|
|
2011
|
+
capabilities: ["elements", "components", "actions", "design"],
|
|
2012
|
+
elementCount: stats.elements,
|
|
2013
|
+
componentCount: stats.components
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
1876
2016
|
return success2(response);
|
|
1877
2017
|
}
|
|
1878
2018
|
};
|
|
@@ -1911,6 +2051,10 @@ var WS_ROUTES = [
|
|
|
1911
2051
|
{ pattern: "health", handler: "health", httpMethods: ["GET"] },
|
|
1912
2052
|
// Elements
|
|
1913
2053
|
{ pattern: "control/elements", handler: "getElements", httpMethods: ["GET"] },
|
|
2054
|
+
// Flat alias — matches the runner UI Bridge's `/ui-bridge/elements` path so
|
|
2055
|
+
// agents can use the same URL across runner + mobile without remembering
|
|
2056
|
+
// the `control/` prefix. Delegates to the same handler.
|
|
2057
|
+
{ pattern: "elements", handler: "getElements", httpMethods: ["GET"] },
|
|
1914
2058
|
{ pattern: "control/element/:id", handler: "getElement", httpMethods: ["GET"] },
|
|
1915
2059
|
{ pattern: "control/element/:id/state", handler: "getElementState", httpMethods: ["GET"] },
|
|
1916
2060
|
{ pattern: "control/element/:id/action", handler: "executeAction", httpMethods: ["POST"] },
|
|
@@ -1961,6 +2105,87 @@ var WS_ROUTES = [
|
|
|
1961
2105
|
// Meta / Introspection
|
|
1962
2106
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
|
|
1963
2107
|
];
|
|
2108
|
+
var HANDLER_DESCRIPTIONS = {
|
|
2109
|
+
health: "Liveness probe; returns app metadata + element count",
|
|
2110
|
+
getElements: "Flat list of registered UI elements",
|
|
2111
|
+
getElement: "Single element by id",
|
|
2112
|
+
getElementState: "Dynamic state (value, focused, checked) for one element",
|
|
2113
|
+
executeAction: 'Run an action on an element (see "actions" below for names)',
|
|
2114
|
+
getComponents: "Higher-level components with action schemas",
|
|
2115
|
+
getComponent: "Component detail + per-action paramSchema",
|
|
2116
|
+
executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
|
|
2117
|
+
find: "AI-powered element search by natural-language query",
|
|
2118
|
+
getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
|
|
2119
|
+
getWorkflows: "List registered workflows",
|
|
2120
|
+
runWorkflow: "Trigger a workflow by id",
|
|
2121
|
+
pageRefresh: "Reload the current route",
|
|
2122
|
+
pageNavigate: "Imperative router.push(url) \u2014 preferred over tapping tab buttons",
|
|
2123
|
+
pageReplace: "router.replace(url)",
|
|
2124
|
+
pageGoBack: "router.back()",
|
|
2125
|
+
pageGoForward: "router.forward()",
|
|
2126
|
+
getScreenshot: "PNG screenshot of the current screen",
|
|
2127
|
+
getElementStyles: "Computed styles for an element (design review)",
|
|
2128
|
+
getElementStateStyles: "Computed styles per interaction state (hover/focus/disabled)",
|
|
2129
|
+
getDesignSnapshot: "Visual + style snapshot for design review",
|
|
2130
|
+
getResponsiveSnapshots: "Screenshots at multiple viewport widths",
|
|
2131
|
+
runDesignAudit: "Accessibility + visual audit",
|
|
2132
|
+
loadStyleGuide: "Load a design system style guide",
|
|
2133
|
+
getStyleGuide: "Fetch the currently loaded style guide",
|
|
2134
|
+
clearStyleGuide: "Unload the style guide",
|
|
2135
|
+
evaluateQuality: "Score the current UI against a quality context",
|
|
2136
|
+
getQualityContexts: "List available quality evaluation contexts",
|
|
2137
|
+
saveBaseline: "Save a quality baseline snapshot",
|
|
2138
|
+
diffBaseline: "Diff current UI against a saved baseline",
|
|
2139
|
+
getMethods: "List every handler name (legacy introspection)"
|
|
2140
|
+
};
|
|
2141
|
+
var ACTION_REFERENCE = [
|
|
2142
|
+
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
2143
|
+
{ name: "longPress", params: "PressActionParams (optional position)" },
|
|
2144
|
+
{ name: "doubleTap", params: "none" },
|
|
2145
|
+
{
|
|
2146
|
+
name: "type",
|
|
2147
|
+
params: "TypeActionParams { text, delay?, clear? | clearFirst? } \u2014 `clear:true` replaces content atomically"
|
|
2148
|
+
},
|
|
2149
|
+
{ name: "clear", params: "none \u2014 empties the input" },
|
|
2150
|
+
{ name: "focus", params: "none" },
|
|
2151
|
+
{ name: "blur", params: "none" },
|
|
2152
|
+
{ name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
|
|
2153
|
+
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2154
|
+
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2155
|
+
];
|
|
2156
|
+
function buildRoutesPayload() {
|
|
2157
|
+
const entries = [];
|
|
2158
|
+
for (const r of WS_ROUTES) {
|
|
2159
|
+
if (!r.httpMethods) continue;
|
|
2160
|
+
for (const m of r.httpMethods) {
|
|
2161
|
+
entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
entries.push({ method: "GET", path: "/ui-bridge/_help" });
|
|
2165
|
+
entries.push({ method: "GET", path: "/ui-bridge/_routes" });
|
|
2166
|
+
entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
2167
|
+
return { routes: entries, count: entries.length };
|
|
2168
|
+
}
|
|
2169
|
+
function buildHelpPayload() {
|
|
2170
|
+
return {
|
|
2171
|
+
version: 1,
|
|
2172
|
+
description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
|
|
2173
|
+
routes: WS_ROUTES.map((r) => ({
|
|
2174
|
+
pattern: r.pattern,
|
|
2175
|
+
httpMethods: r.httpMethods ?? [],
|
|
2176
|
+
wsOnly: !r.httpMethods,
|
|
2177
|
+
handler: r.handler,
|
|
2178
|
+
description: HANDLER_DESCRIPTIONS[r.handler] ?? ""
|
|
2179
|
+
})),
|
|
2180
|
+
actions: ACTION_REFERENCE,
|
|
2181
|
+
tips: [
|
|
2182
|
+
"Start with `GET /ui-bridge/control/snapshot?visibleOnly=true` to see what the user actually sees.",
|
|
2183
|
+
"Prefer `control/component/:id/action/:actionId` over button presses \u2014 typed paramSchema, no button-id guessing.",
|
|
2184
|
+
"Use `pageNavigate` for tab switches instead of pressing tab buttons; gesture state often swallows clicks.",
|
|
2185
|
+
"The `type` action supports `clear:true` to replace existing text atomically in one call."
|
|
2186
|
+
]
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
1964
2189
|
var NativeUIBridgeServer = class {
|
|
1965
2190
|
constructor(registry, executor, config = {}) {
|
|
1966
2191
|
this.registry = registry;
|
|
@@ -1977,7 +2202,9 @@ var NativeUIBridgeServer = class {
|
|
|
1977
2202
|
cors: true,
|
|
1978
2203
|
...config
|
|
1979
2204
|
};
|
|
1980
|
-
this.handlers = createServerHandlers(registry, executor
|
|
2205
|
+
this.handlers = createServerHandlers(registry, executor, {
|
|
2206
|
+
appInfo: this.config.appInfo
|
|
2207
|
+
});
|
|
1981
2208
|
this.handlers.getMethods = async () => {
|
|
1982
2209
|
const methods = WS_ROUTES.map((r) => ({
|
|
1983
2210
|
method: r.pattern,
|
|
@@ -2781,6 +3008,20 @@ var NativeUIBridgeServer = class {
|
|
|
2781
3008
|
};
|
|
2782
3009
|
}
|
|
2783
3010
|
const stripped = path.slice(PREFIX.length);
|
|
3011
|
+
if (method === "GET" && stripped === "_help") {
|
|
3012
|
+
return {
|
|
3013
|
+
success: true,
|
|
3014
|
+
data: buildHelpPayload(),
|
|
3015
|
+
timestamp: Date.now()
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
if (method === "GET" && stripped === "_routes") {
|
|
3019
|
+
return {
|
|
3020
|
+
success: true,
|
|
3021
|
+
data: buildRoutesPayload(),
|
|
3022
|
+
timestamp: Date.now()
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
2784
3025
|
let patternMatchedButWrongVerb = false;
|
|
2785
3026
|
for (const route of WS_ROUTES) {
|
|
2786
3027
|
const pathParams = parsePath(route.pattern, stripped);
|
|
@@ -3402,6 +3643,186 @@ function parseQueryParams(path) {
|
|
|
3402
3643
|
}
|
|
3403
3644
|
return result;
|
|
3404
3645
|
}
|
|
3646
|
+
|
|
3647
|
+
// src/modal/modal-detector.ts
|
|
3648
|
+
var ModalDetector = class {
|
|
3649
|
+
constructor() {
|
|
3650
|
+
/** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
|
|
3651
|
+
this.modals = /* @__PURE__ */ new Map();
|
|
3652
|
+
}
|
|
3653
|
+
pushModal(modal) {
|
|
3654
|
+
const existing = this.modals.get(modal.id);
|
|
3655
|
+
const detectedAt = existing ? existing.detectedAt : Date.now();
|
|
3656
|
+
const next = {
|
|
3657
|
+
id: modal.id,
|
|
3658
|
+
title: modal.title,
|
|
3659
|
+
type: modal.type ?? "modal",
|
|
3660
|
+
blocking: modal.blocking ?? true,
|
|
3661
|
+
dismissible: modal.dismissible ?? true,
|
|
3662
|
+
detectedAt
|
|
3663
|
+
};
|
|
3664
|
+
this.modals.set(modal.id, next);
|
|
3665
|
+
}
|
|
3666
|
+
dismissModal(id) {
|
|
3667
|
+
this.modals.delete(id);
|
|
3668
|
+
}
|
|
3669
|
+
getSnapshotModalContext() {
|
|
3670
|
+
const active = this.getActive();
|
|
3671
|
+
const ctx = {
|
|
3672
|
+
modals: active,
|
|
3673
|
+
hasBlockingModal: active.some((m) => m.blocking),
|
|
3674
|
+
count: active.length
|
|
3675
|
+
};
|
|
3676
|
+
if (active.length > 0) {
|
|
3677
|
+
ctx.topModal = active[active.length - 1];
|
|
3678
|
+
}
|
|
3679
|
+
return ctx;
|
|
3680
|
+
}
|
|
3681
|
+
getActive() {
|
|
3682
|
+
return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
|
|
3683
|
+
}
|
|
3684
|
+
clear() {
|
|
3685
|
+
this.modals.clear();
|
|
3686
|
+
}
|
|
3687
|
+
};
|
|
3688
|
+
|
|
3689
|
+
// src/toast/toast-capture.ts
|
|
3690
|
+
var DEFAULT_MAX_RECENT = 50;
|
|
3691
|
+
var ToastCapture = class {
|
|
3692
|
+
constructor(config = {}) {
|
|
3693
|
+
this.active = /* @__PURE__ */ new Map();
|
|
3694
|
+
this.recent = [];
|
|
3695
|
+
this.totalCaptured = 0;
|
|
3696
|
+
this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
|
|
3697
|
+
}
|
|
3698
|
+
recordToast(toast) {
|
|
3699
|
+
if (this.active.has(toast.id)) {
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
const appearedAt = toast.appearedAt ?? Date.now();
|
|
3703
|
+
const entry = {
|
|
3704
|
+
id: toast.id,
|
|
3705
|
+
message: toast.message,
|
|
3706
|
+
level: toast.level ?? "info",
|
|
3707
|
+
appearedAt,
|
|
3708
|
+
visible: true,
|
|
3709
|
+
durationMs: 0
|
|
3710
|
+
};
|
|
3711
|
+
this.active.set(toast.id, entry);
|
|
3712
|
+
this.totalCaptured += 1;
|
|
3713
|
+
}
|
|
3714
|
+
dismissToast(id) {
|
|
3715
|
+
const entry = this.active.get(id);
|
|
3716
|
+
if (!entry) {
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
this.active.delete(id);
|
|
3720
|
+
const dismissedAt = Date.now();
|
|
3721
|
+
const dismissed = {
|
|
3722
|
+
...entry,
|
|
3723
|
+
dismissedAt,
|
|
3724
|
+
visible: false,
|
|
3725
|
+
durationMs: dismissedAt - entry.appearedAt
|
|
3726
|
+
};
|
|
3727
|
+
this.recent.push(dismissed);
|
|
3728
|
+
if (this.recent.length > this.maxRecent) {
|
|
3729
|
+
this.recent.shift();
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
getSnapshotToastContext() {
|
|
3733
|
+
const now = Date.now();
|
|
3734
|
+
const active = Array.from(this.active.values()).map((entry) => ({
|
|
3735
|
+
...entry,
|
|
3736
|
+
durationMs: now - entry.appearedAt
|
|
3737
|
+
}));
|
|
3738
|
+
return {
|
|
3739
|
+
active,
|
|
3740
|
+
recent: [...this.recent],
|
|
3741
|
+
totalCaptured: this.totalCaptured
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
clear() {
|
|
3745
|
+
this.active.clear();
|
|
3746
|
+
this.recent = [];
|
|
3747
|
+
this.totalCaptured = 0;
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
|
|
3751
|
+
// src/undo/undo-tracker.ts
|
|
3752
|
+
var DEFAULT_MAX_HISTORY = 50;
|
|
3753
|
+
function buildSummary(ctx) {
|
|
3754
|
+
if (!ctx.canUndo && !ctx.canRedo) {
|
|
3755
|
+
return "no undo available";
|
|
3756
|
+
}
|
|
3757
|
+
const parts = [];
|
|
3758
|
+
if (ctx.canUndo) {
|
|
3759
|
+
parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
|
|
3760
|
+
}
|
|
3761
|
+
if (ctx.canRedo) {
|
|
3762
|
+
parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
|
|
3763
|
+
}
|
|
3764
|
+
return parts.join(" / ");
|
|
3765
|
+
}
|
|
3766
|
+
var UndoTracker = class {
|
|
3767
|
+
constructor(config = {}) {
|
|
3768
|
+
this.actions = [];
|
|
3769
|
+
this.declaredState = null;
|
|
3770
|
+
this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
|
|
3771
|
+
}
|
|
3772
|
+
recordAction(record) {
|
|
3773
|
+
this.actions.push(record);
|
|
3774
|
+
if (this.actions.length > this.maxHistory) {
|
|
3775
|
+
this.actions = this.actions.slice(-this.maxHistory);
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
setDeclaredState(state) {
|
|
3779
|
+
this.declaredState = state;
|
|
3780
|
+
}
|
|
3781
|
+
getActionHistory() {
|
|
3782
|
+
return this.actions.slice();
|
|
3783
|
+
}
|
|
3784
|
+
getSnapshotUndoContext() {
|
|
3785
|
+
const declared = this.declaredState;
|
|
3786
|
+
if (declared) {
|
|
3787
|
+
const summary2 = buildSummary({
|
|
3788
|
+
canUndo: declared.canUndo,
|
|
3789
|
+
canRedo: declared.canRedo,
|
|
3790
|
+
undoDescription: declared.undoDescription,
|
|
3791
|
+
redoDescription: declared.redoDescription
|
|
3792
|
+
});
|
|
3793
|
+
return {
|
|
3794
|
+
canUndo: declared.canUndo,
|
|
3795
|
+
canRedo: declared.canRedo,
|
|
3796
|
+
undoDescription: declared.undoDescription,
|
|
3797
|
+
redoDescription: declared.redoDescription,
|
|
3798
|
+
undoDepth: declared.undoDepth,
|
|
3799
|
+
redoDepth: declared.redoDepth,
|
|
3800
|
+
summary: summary2
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
const canUndo = this.actions.some((a) => a.reversible !== false);
|
|
3804
|
+
const canRedo = false;
|
|
3805
|
+
const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
|
|
3806
|
+
const undoDescription = lastReversible?.description;
|
|
3807
|
+
const summary = buildSummary({ canUndo, canRedo, undoDescription });
|
|
3808
|
+
return {
|
|
3809
|
+
canUndo,
|
|
3810
|
+
canRedo,
|
|
3811
|
+
undoDescription,
|
|
3812
|
+
summary
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
executeUndo() {
|
|
3816
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
3817
|
+
}
|
|
3818
|
+
executeRedo() {
|
|
3819
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
3820
|
+
}
|
|
3821
|
+
clear() {
|
|
3822
|
+
this.actions = [];
|
|
3823
|
+
this.declaredState = null;
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3405
3826
|
var UIBridgeNativeContext = createContext(null);
|
|
3406
3827
|
function UIBridgeNativeProvider({
|
|
3407
3828
|
children,
|
|
@@ -3418,6 +3839,9 @@ function UIBridgeNativeProvider({
|
|
|
3418
3839
|
}) {
|
|
3419
3840
|
const registryRef = useRef(null);
|
|
3420
3841
|
const executorRef = useRef(null);
|
|
3842
|
+
const modalDetectorRef = useRef(null);
|
|
3843
|
+
const toastCaptureRef = useRef(null);
|
|
3844
|
+
const undoTrackerRef = useRef(null);
|
|
3421
3845
|
const [serverRunning, setServerRunning] = useState(false);
|
|
3422
3846
|
if (!registryRef.current) {
|
|
3423
3847
|
registryRef.current = new NativeUIBridgeRegistry({
|
|
@@ -3431,6 +3855,12 @@ function UIBridgeNativeProvider({
|
|
|
3431
3855
|
executorRef.current = createNativeActionExecutor(registry);
|
|
3432
3856
|
}
|
|
3433
3857
|
const executor = executorRef.current;
|
|
3858
|
+
if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
|
|
3859
|
+
if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
|
|
3860
|
+
if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
|
|
3861
|
+
const modalDetector = modalDetectorRef.current;
|
|
3862
|
+
const toastCapture = toastCaptureRef.current;
|
|
3863
|
+
const undoTracker = undoTrackerRef.current;
|
|
3434
3864
|
const serverRef = useRef(null);
|
|
3435
3865
|
const eventBridgeRef = useRef(null);
|
|
3436
3866
|
const announcerRef = useRef(null);
|
|
@@ -3445,7 +3875,8 @@ function UIBridgeNativeProvider({
|
|
|
3445
3875
|
}
|
|
3446
3876
|
const server = createNativeServer(registry, executor, {
|
|
3447
3877
|
serverPort: config.serverPort || 8087,
|
|
3448
|
-
cors: true
|
|
3878
|
+
cors: true,
|
|
3879
|
+
appInfo: config.appInfo
|
|
3449
3880
|
});
|
|
3450
3881
|
server.setAdapter(serverAdapter);
|
|
3451
3882
|
if (navigationProvider) {
|
|
@@ -3532,7 +3963,8 @@ function UIBridgeNativeProvider({
|
|
|
3532
3963
|
try {
|
|
3533
3964
|
const bareServer = createNativeServer(registry, executor, {
|
|
3534
3965
|
serverPort: config.serverPort || 8087,
|
|
3535
|
-
cors: true
|
|
3966
|
+
cors: true,
|
|
3967
|
+
appInfo: config.appInfo
|
|
3536
3968
|
});
|
|
3537
3969
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
3538
3970
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -3617,6 +4049,22 @@ function UIBridgeNativeProvider({
|
|
|
3617
4049
|
lastRoute = current;
|
|
3618
4050
|
});
|
|
3619
4051
|
}, [routeProvider, registry]);
|
|
4052
|
+
useEffect(() => {
|
|
4053
|
+
registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
|
|
4054
|
+
}, [registry, modalDetector, toastCapture, undoTracker]);
|
|
4055
|
+
useEffect(() => {
|
|
4056
|
+
const unsubscribe = executor.onActionExecuted((event) => {
|
|
4057
|
+
if (!event.success) return;
|
|
4058
|
+
undoTracker.recordAction({
|
|
4059
|
+
id: event.requestId ?? `action-${event.timestamp}`,
|
|
4060
|
+
type: event.action,
|
|
4061
|
+
targetId: event.elementId,
|
|
4062
|
+
timestamp: event.timestamp,
|
|
4063
|
+
reversible: true
|
|
4064
|
+
});
|
|
4065
|
+
});
|
|
4066
|
+
return unsubscribe;
|
|
4067
|
+
}, [executor, undoTracker]);
|
|
3620
4068
|
const on = useCallback(
|
|
3621
4069
|
(type, listener) => registry.on(type, listener),
|
|
3622
4070
|
[registry]
|
|
@@ -3631,6 +4079,9 @@ function UIBridgeNativeProvider({
|
|
|
3631
4079
|
config,
|
|
3632
4080
|
registry,
|
|
3633
4081
|
executor,
|
|
4082
|
+
modalDetector,
|
|
4083
|
+
toastCapture,
|
|
4084
|
+
undoTracker,
|
|
3634
4085
|
getElements,
|
|
3635
4086
|
getComponents,
|
|
3636
4087
|
createSnapshot,
|
|
@@ -3647,6 +4098,9 @@ function UIBridgeNativeProvider({
|
|
|
3647
4098
|
config,
|
|
3648
4099
|
registry,
|
|
3649
4100
|
executor,
|
|
4101
|
+
modalDetector,
|
|
4102
|
+
toastCapture,
|
|
4103
|
+
undoTracker,
|
|
3650
4104
|
getElements,
|
|
3651
4105
|
getComponents,
|
|
3652
4106
|
createSnapshot,
|
|
@@ -4164,7 +4618,38 @@ function useAutoRegister(options = {}) {
|
|
|
4164
4618
|
isEnabled: enabled && !!bridge?.registry
|
|
4165
4619
|
};
|
|
4166
4620
|
}
|
|
4621
|
+
function useUIBridgeModal(opts) {
|
|
4622
|
+
const { modalDetector } = useUIBridgeNative();
|
|
4623
|
+
const { id, title, type, blocking, dismissible } = opts;
|
|
4624
|
+
useEffect(() => {
|
|
4625
|
+
modalDetector.pushModal({ id, title, type, blocking, dismissible });
|
|
4626
|
+
return () => {
|
|
4627
|
+
modalDetector.dismissModal(id);
|
|
4628
|
+
};
|
|
4629
|
+
}, [modalDetector, id, title, type, blocking, dismissible]);
|
|
4630
|
+
}
|
|
4631
|
+
function useUIBridgeToast(opts) {
|
|
4632
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4633
|
+
const { id, message, level } = opts;
|
|
4634
|
+
useEffect(() => {
|
|
4635
|
+
toastCapture.recordToast({ id, message, level });
|
|
4636
|
+
return () => {
|
|
4637
|
+
toastCapture.dismissToast(id);
|
|
4638
|
+
};
|
|
4639
|
+
}, [toastCapture, id, message, level]);
|
|
4640
|
+
}
|
|
4641
|
+
function useToastRecorder() {
|
|
4642
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4643
|
+
const recordToast = useCallback(
|
|
4644
|
+
(toast) => {
|
|
4645
|
+
toastCapture.recordToast(toast);
|
|
4646
|
+
},
|
|
4647
|
+
[toastCapture]
|
|
4648
|
+
);
|
|
4649
|
+
const dismissToast = useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4650
|
+
return { recordToast, dismissToast };
|
|
4651
|
+
}
|
|
4167
4652
|
|
|
4168
|
-
export { UIBridgeNativeProvider, useAutoRegister, useUIBridge, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
4653
|
+
export { UIBridgeNativeProvider, useAutoRegister, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
4169
4654
|
//# sourceMappingURL=index.mjs.map
|
|
4170
4655
|
//# sourceMappingURL=index.mjs.map
|