@qontinui/ui-bridge 0.2.0 → 0.3.1
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/ai/index.d.mts +312 -155
- package/dist/ai/index.d.ts +312 -155
- package/dist/ai/index.js +2363 -67
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/index.mjs +2328 -68
- package/dist/ai/index.mjs.map +1 -1
- package/dist/annotations/index.d.mts +218 -0
- package/dist/annotations/index.d.ts +218 -0
- package/dist/annotations/index.js +246 -0
- package/dist/annotations/index.js.map +1 -0
- package/dist/annotations/index.mjs +241 -0
- package/dist/annotations/index.mjs.map +1 -0
- package/dist/assertions-BSR3afVr.d.ts +161 -0
- package/dist/assertions-CTw1hfOx.d.mts +161 -0
- package/dist/babel-plugin/index.js +504 -0
- package/dist/babel-plugin/index.js.map +1 -0
- package/dist/babel-plugin/index.mjs +488 -0
- package/dist/babel-plugin/index.mjs.map +1 -0
- package/dist/browser-capture-Bms60T6f.d.mts +47 -0
- package/dist/browser-capture-CsTU29mb.d.ts +47 -0
- package/dist/control/index.d.mts +26 -7
- package/dist/control/index.d.ts +26 -7
- package/dist/control/index.js +276 -48
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +276 -48
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +115 -44
- package/dist/core/index.d.ts +115 -44
- package/dist/core/index.js +0 -1560
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +1 -1549
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.d.mts +5 -3
- package/dist/debug/index.d.ts +5 -3
- package/dist/debug/index.js +925 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +924 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.mts +13 -9
- package/dist/index.d.ts +13 -9
- package/dist/index.js +8310 -3777
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8246 -3766
- package/dist/index.mjs.map +1 -1
- package/dist/{metrics-NC3csD0R.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
- package/dist/{metrics-C9XRi_mL.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
- package/dist/native/control/index.js +448 -0
- package/dist/native/control/index.js.map +1 -0
- package/dist/native/control/index.mjs +445 -0
- package/dist/native/control/index.mjs.map +1 -0
- package/dist/native/core/index.js +486 -0
- package/dist/native/core/index.js.map +1 -0
- package/dist/native/core/index.mjs +475 -0
- package/dist/native/core/index.mjs.map +1 -0
- package/dist/native/debug/index.js +408 -0
- package/dist/native/debug/index.js.map +1 -0
- package/dist/native/debug/index.mjs +406 -0
- package/dist/native/debug/index.mjs.map +1 -0
- package/dist/native/index.js +2232 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/index.mjs +2204 -0
- package/dist/native/index.mjs.map +1 -0
- package/dist/native/react/index.js +1377 -0
- package/dist/native/react/index.js.map +1 -0
- package/dist/native/react/index.mjs +1365 -0
- package/dist/native/react/index.mjs.map +1 -0
- package/dist/native/server/index.js +440 -0
- package/dist/native/server/index.js.map +1 -0
- package/dist/native/server/index.mjs +435 -0
- package/dist/native/server/index.mjs.map +1 -0
- package/dist/react/index.d.mts +121 -9
- package/dist/react/index.d.ts +121 -9
- package/dist/react/index.js +2239 -91
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +2239 -92
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-CIEDjbQ9.d.ts → registry-C6dDtn1v.d.ts} +34 -15
- package/dist/{registry-SsSDq46X.d.mts → registry-POtcxnal.d.mts} +34 -15
- package/dist/render-log/index.d.mts +1 -1
- package/dist/render-log/index.d.ts +1 -1
- package/dist/server/express.d.mts +37 -0
- package/dist/server/express.d.ts +37 -0
- package/dist/server/express.js +298 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/express.mjs +294 -0
- package/dist/server/express.mjs.map +1 -0
- package/dist/server/handlers.d.mts +124 -0
- package/dist/server/handlers.d.ts +124 -0
- package/dist/server/handlers.js +7183 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/handlers.mjs +7180 -0
- package/dist/server/handlers.mjs.map +1 -0
- package/dist/server/index.d.mts +12 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +8384 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +8369 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/server/nextjs.d.mts +128 -0
- package/dist/server/nextjs.d.ts +128 -0
- package/dist/server/nextjs.js +390 -0
- package/dist/server/nextjs.js.map +1 -0
- package/dist/server/nextjs.mjs +385 -0
- package/dist/server/nextjs.mjs.map +1 -0
- package/dist/server/standalone.d.mts +7 -0
- package/dist/server/standalone.d.ts +7 -0
- package/dist/server/standalone.js +845 -0
- package/dist/server/standalone.js.map +1 -0
- package/dist/server/standalone.mjs +841 -0
- package/dist/server/standalone.mjs.map +1 -0
- package/dist/specs/index.d.mts +365 -0
- package/dist/specs/index.d.ts +365 -0
- package/dist/specs/index.js +2809 -0
- package/dist/specs/index.js.map +1 -0
- package/dist/specs/index.mjs +2786 -0
- package/dist/specs/index.mjs.map +1 -0
- package/dist/standalone-B6GLIEmR.d.ts +216 -0
- package/dist/standalone-CjdYqj3P.d.mts +216 -0
- package/dist/swc-plugin/index.d.mts +79 -0
- package/dist/swc-plugin/index.d.ts +79 -0
- package/dist/swc-plugin/index.js +15 -0
- package/dist/swc-plugin/index.js.map +1 -0
- package/dist/swc-plugin/index.mjs +9 -0
- package/dist/swc-plugin/index.mjs.map +1 -0
- package/dist/types-B2EfvEaq.d.ts +236 -0
- package/dist/{types-Dr6tH-bm.d.mts → types-C7gVYRnF.d.ts} +72 -2
- package/dist/{types-oCTrRxSw.d.ts → types-CJGrBEhC.d.mts} +72 -2
- package/dist/types-CebMQj76.d.ts +1275 -0
- package/dist/types-D_ypYl3T.d.mts +1275 -0
- package/dist/types-UBtp7R0u.d.mts +132 -0
- package/dist/types-UBtp7R0u.d.ts +132 -0
- package/dist/types-gO696T_t.d.mts +236 -0
- package/dist/{types-CPMbN_Iw.d.mts → types-suaYwWWg.d.mts} +519 -152
- package/dist/{types-CPMbN_Iw.d.ts → types-suaYwWWg.d.ts} +519 -152
- package/package.json +123 -4
- package/swc-plugin-wasm/ui_bridge_swc_plugin.wasm +0 -0
- package/dist/types-BvCfFuEV.d.ts +0 -534
- package/dist/types-CFT3Dnx4.d.mts +0 -534
- package/dist/websocket-client-CX4QJesI.d.ts +0 -124
- package/dist/websocket-client-C_Na0OSp.d.mts +0 -124
package/dist/react/index.js
CHANGED
|
@@ -552,7 +552,9 @@ function generateDescription(input) {
|
|
|
552
552
|
}
|
|
553
553
|
parts.push(`"${name}"`);
|
|
554
554
|
}
|
|
555
|
-
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
555
|
+
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
556
|
+
input.elementType || "element"
|
|
557
|
+
];
|
|
556
558
|
parts.push(typeWords[0]);
|
|
557
559
|
if (input.inputType && input.inputType !== "text") {
|
|
558
560
|
parts.push(`(${input.inputType})`);
|
|
@@ -773,12 +775,38 @@ var UIBridgeRegistry = class {
|
|
|
773
775
|
getState: () => getElementState(element),
|
|
774
776
|
getIdentifier: () => createElementIdentifier(element),
|
|
775
777
|
registeredAt: Date.now(),
|
|
776
|
-
mounted: true
|
|
778
|
+
mounted: true,
|
|
779
|
+
category: options.category ?? "interactive",
|
|
780
|
+
contentMetadata: options.contentMetadata
|
|
777
781
|
};
|
|
778
782
|
this.elements.set(actualId, registered);
|
|
779
783
|
this.emit("element:registered", { id: actualId, type, label: options.label });
|
|
780
784
|
return registered;
|
|
781
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Register a content (non-interactive) element
|
|
788
|
+
*/
|
|
789
|
+
registerContentElement(id, element, options) {
|
|
790
|
+
return this.registerElement(id, element, {
|
|
791
|
+
type: options.contentType,
|
|
792
|
+
label: options.label,
|
|
793
|
+
actions: [],
|
|
794
|
+
category: "content",
|
|
795
|
+
contentMetadata: options.contentMetadata
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Get all content (non-interactive) elements
|
|
800
|
+
*/
|
|
801
|
+
getAllContentElements() {
|
|
802
|
+
return Array.from(this.elements.values()).filter((el) => el.category === "content");
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get all interactive elements
|
|
806
|
+
*/
|
|
807
|
+
getAllInteractiveElements() {
|
|
808
|
+
return Array.from(this.elements.values()).filter((el) => el.category !== "content");
|
|
809
|
+
}
|
|
782
810
|
/**
|
|
783
811
|
* Unregister an element
|
|
784
812
|
*/
|
|
@@ -875,7 +903,9 @@ var UIBridgeRegistry = class {
|
|
|
875
903
|
scores.accessibility = result.similarity;
|
|
876
904
|
if (result.similarity > maxScore) {
|
|
877
905
|
maxScore = result.similarity;
|
|
878
|
-
matchReasons.push(
|
|
906
|
+
matchReasons.push(
|
|
907
|
+
`accessible name similarity: ${(result.similarity * 100).toFixed(0)}%`
|
|
908
|
+
);
|
|
879
909
|
}
|
|
880
910
|
}
|
|
881
911
|
}
|
|
@@ -1035,7 +1065,9 @@ var UIBridgeRegistry = class {
|
|
|
1035
1065
|
})) ?? [],
|
|
1036
1066
|
elementIds: options.elementIds,
|
|
1037
1067
|
registeredAt: Date.now(),
|
|
1038
|
-
mounted: true
|
|
1068
|
+
mounted: true,
|
|
1069
|
+
getState: options.getState,
|
|
1070
|
+
getComputed: options.getComputed
|
|
1039
1071
|
};
|
|
1040
1072
|
this.components.set(id, registered);
|
|
1041
1073
|
this.emit("component:registered", { id, name: options.name });
|
|
@@ -1066,6 +1098,20 @@ var UIBridgeRegistry = class {
|
|
|
1066
1098
|
getAllComponents() {
|
|
1067
1099
|
return Array.from(this.components.values());
|
|
1068
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Get the current state and computed properties of a component
|
|
1103
|
+
*/
|
|
1104
|
+
getComponentState(id) {
|
|
1105
|
+
const component = this.components.get(id);
|
|
1106
|
+
if (!component || !component.mounted) {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
state: component.getState?.() ?? {},
|
|
1111
|
+
computed: component.getComputed?.() ?? {},
|
|
1112
|
+
timestamp: Date.now()
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1069
1115
|
/**
|
|
1070
1116
|
* Register a workflow
|
|
1071
1117
|
*/
|
|
@@ -1455,8 +1501,56 @@ var UIBridgeRegistry = class {
|
|
|
1455
1501
|
identifier: el.getIdentifier(),
|
|
1456
1502
|
state: el.getState(),
|
|
1457
1503
|
actions: el.actions,
|
|
1458
|
-
customActions: el.customActions ? Object.keys(el.customActions) : void 0
|
|
1504
|
+
customActions: el.customActions ? Object.keys(el.customActions) : void 0,
|
|
1505
|
+
category: el.category,
|
|
1506
|
+
contentMetadata: el.contentMetadata
|
|
1507
|
+
})),
|
|
1508
|
+
components: this.getAllComponents().map((comp) => ({
|
|
1509
|
+
id: comp.id,
|
|
1510
|
+
name: comp.name,
|
|
1511
|
+
description: comp.description,
|
|
1512
|
+
actions: comp.actions.map((a) => a.id),
|
|
1513
|
+
elementIds: comp.elementIds
|
|
1459
1514
|
})),
|
|
1515
|
+
workflows: this.getAllWorkflows().map((wf) => ({
|
|
1516
|
+
id: wf.id,
|
|
1517
|
+
name: wf.name,
|
|
1518
|
+
description: wf.description,
|
|
1519
|
+
stepCount: wf.steps.length
|
|
1520
|
+
}))
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Create a snapshot asynchronously, processing elements in batches to avoid
|
|
1525
|
+
* blocking the main thread. This prevents "Page Unresponsive" dialogs when
|
|
1526
|
+
* there are many registered elements (200-500+), since getState() and
|
|
1527
|
+
* getIdentifier() force layout/style recalculation for each element.
|
|
1528
|
+
*/
|
|
1529
|
+
async createSnapshotAsync(batchSize = 50) {
|
|
1530
|
+
const allElements = this.getAllElements();
|
|
1531
|
+
const elementSnapshots = [];
|
|
1532
|
+
for (let i = 0; i < allElements.length; i += batchSize) {
|
|
1533
|
+
const batch = allElements.slice(i, i + batchSize);
|
|
1534
|
+
for (const el of batch) {
|
|
1535
|
+
elementSnapshots.push({
|
|
1536
|
+
id: el.id,
|
|
1537
|
+
type: el.type,
|
|
1538
|
+
label: el.label,
|
|
1539
|
+
identifier: el.getIdentifier(),
|
|
1540
|
+
state: el.getState(),
|
|
1541
|
+
actions: el.actions,
|
|
1542
|
+
customActions: el.customActions ? Object.keys(el.customActions) : void 0,
|
|
1543
|
+
category: el.category,
|
|
1544
|
+
contentMetadata: el.contentMetadata
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (i + batchSize < allElements.length) {
|
|
1548
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
timestamp: Date.now(),
|
|
1553
|
+
elements: elementSnapshots,
|
|
1460
1554
|
components: this.getAllComponents().map((comp) => ({
|
|
1461
1555
|
id: comp.id,
|
|
1462
1556
|
name: comp.name,
|
|
@@ -1981,6 +2075,10 @@ function getElementState2(element) {
|
|
|
1981
2075
|
pointerEvents: style.pointerEvents
|
|
1982
2076
|
}
|
|
1983
2077
|
};
|
|
2078
|
+
const rawText = element.textContent?.trim();
|
|
2079
|
+
if (rawText) {
|
|
2080
|
+
state.textContent = rawText.replace(/\s+/g, " ").slice(0, 500);
|
|
2081
|
+
}
|
|
1984
2082
|
if (element instanceof HTMLInputElement) {
|
|
1985
2083
|
state.value = element.value;
|
|
1986
2084
|
if (element.type === "checkbox" || element.type === "radio") {
|
|
@@ -2022,9 +2120,25 @@ function createMouseEvent(type, element, options) {
|
|
|
2022
2120
|
clientY: rect.top + y
|
|
2023
2121
|
});
|
|
2024
2122
|
}
|
|
2123
|
+
function elementFromPointSafe(x, y) {
|
|
2124
|
+
if (typeof document.elementFromPoint === "function") {
|
|
2125
|
+
return document.elementFromPoint(x, y);
|
|
2126
|
+
}
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
function createMouseEventAt(type, clientX, clientY) {
|
|
2130
|
+
return new MouseEvent(type, {
|
|
2131
|
+
bubbles: true,
|
|
2132
|
+
cancelable: true,
|
|
2133
|
+
button: 0,
|
|
2134
|
+
clientX,
|
|
2135
|
+
clientY
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2025
2138
|
var DefaultActionExecutor = class {
|
|
2026
|
-
constructor(registry) {
|
|
2139
|
+
constructor(registry, consoleCapture) {
|
|
2027
2140
|
this.registry = registry;
|
|
2141
|
+
this.consoleCapture = consoleCapture;
|
|
2028
2142
|
}
|
|
2029
2143
|
/**
|
|
2030
2144
|
* Execute an action on an element
|
|
@@ -2061,11 +2175,19 @@ var DefaultActionExecutor = class {
|
|
|
2061
2175
|
};
|
|
2062
2176
|
}
|
|
2063
2177
|
}
|
|
2178
|
+
const actionStartTime = Date.now();
|
|
2064
2179
|
const result = await this.performAction(element, request.action, request.params);
|
|
2180
|
+
let consoleErrors;
|
|
2181
|
+
if (this.consoleCapture) {
|
|
2182
|
+
await sleep(50);
|
|
2183
|
+
const errors = this.consoleCapture.getConsoleSince(actionStartTime);
|
|
2184
|
+
if (errors.length > 0) consoleErrors = errors;
|
|
2185
|
+
}
|
|
2065
2186
|
return {
|
|
2066
2187
|
success: true,
|
|
2067
2188
|
elementState: getElementState2(element),
|
|
2068
2189
|
result,
|
|
2190
|
+
consoleErrors,
|
|
2069
2191
|
durationMs: performance.now() - startTime,
|
|
2070
2192
|
timestamp: Date.now(),
|
|
2071
2193
|
requestId: request.requestId,
|
|
@@ -2157,47 +2279,76 @@ var DefaultActionExecutor = class {
|
|
|
2157
2279
|
const rootEl = document.querySelector(options.root);
|
|
2158
2280
|
if (rootEl) root = rootEl;
|
|
2159
2281
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
if (
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2282
|
+
if (!options?.contentOnly) {
|
|
2283
|
+
const interactiveSelectors = [
|
|
2284
|
+
"a[href]",
|
|
2285
|
+
"button",
|
|
2286
|
+
"input",
|
|
2287
|
+
"select",
|
|
2288
|
+
"textarea",
|
|
2289
|
+
"[onclick]",
|
|
2290
|
+
'[role="button"]',
|
|
2291
|
+
'[role="link"]',
|
|
2292
|
+
'[role="checkbox"]',
|
|
2293
|
+
'[role="radio"]',
|
|
2294
|
+
'[role="menuitem"]',
|
|
2295
|
+
'[role="tab"]',
|
|
2296
|
+
'[role="switch"]',
|
|
2297
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
2298
|
+
'[contenteditable="true"]',
|
|
2299
|
+
"[data-ui-element]",
|
|
2300
|
+
"[data-ui-id]",
|
|
2301
|
+
"[data-testid]"
|
|
2302
|
+
];
|
|
2303
|
+
const selector = options?.selector || interactiveSelectors.join(", ");
|
|
2304
|
+
const foundElements = root.querySelectorAll(selector);
|
|
2305
|
+
for (const el of foundElements) {
|
|
2306
|
+
if (options?.limit && elements.length >= options.limit) break;
|
|
2307
|
+
const state = getElementState2(el);
|
|
2308
|
+
if (!options?.includeHidden && !state.visible) continue;
|
|
2309
|
+
if (options?.types) {
|
|
2310
|
+
const type = this.inferElementType(el);
|
|
2311
|
+
if (!options.types.includes(type)) continue;
|
|
2312
|
+
}
|
|
2313
|
+
const registered = this.registry.findByDOMElement(el);
|
|
2314
|
+
elements.push({
|
|
2315
|
+
id: registered?.id || this.getElementId(el),
|
|
2316
|
+
type: registered?.type || this.inferElementType(el),
|
|
2317
|
+
label: registered?.label || this.getElementLabel(el),
|
|
2318
|
+
tagName: el.tagName.toLowerCase(),
|
|
2319
|
+
role: el.getAttribute("role") || void 0,
|
|
2320
|
+
accessibleName: this.getAccessibleName(el),
|
|
2321
|
+
actions: registered?.actions || this.inferActions(el),
|
|
2322
|
+
state,
|
|
2323
|
+
registered: !!registered,
|
|
2324
|
+
category: registered?.category || "interactive",
|
|
2325
|
+
contentMetadata: registered?.contentMetadata
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (options?.includeContent || options?.contentOnly) {
|
|
2330
|
+
const contentElements = this.registry.getAllContentElements();
|
|
2331
|
+
for (const el of contentElements) {
|
|
2332
|
+
if (options?.limit && elements.length >= options.limit) break;
|
|
2333
|
+
const state = el.getState();
|
|
2334
|
+
if (!options?.includeHidden && !state.visible) continue;
|
|
2335
|
+
if (options?.contentRole && el.contentMetadata?.contentRole !== options.contentRole) {
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
elements.push({
|
|
2339
|
+
id: el.id,
|
|
2340
|
+
type: el.type,
|
|
2341
|
+
label: el.label,
|
|
2342
|
+
tagName: el.element.tagName.toLowerCase(),
|
|
2343
|
+
role: el.element.getAttribute("role") || void 0,
|
|
2344
|
+
accessibleName: el.label || state.textContent?.trim(),
|
|
2345
|
+
actions: [],
|
|
2346
|
+
state,
|
|
2347
|
+
registered: true,
|
|
2348
|
+
category: "content",
|
|
2349
|
+
contentMetadata: el.contentMetadata
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2201
2352
|
}
|
|
2202
2353
|
return {
|
|
2203
2354
|
elements,
|
|
@@ -2227,7 +2378,9 @@ var DefaultActionExecutor = class {
|
|
|
2227
2378
|
type: el.type,
|
|
2228
2379
|
label: el.label,
|
|
2229
2380
|
actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
|
|
2230
|
-
state: el.getState()
|
|
2381
|
+
state: el.getState(),
|
|
2382
|
+
category: el.category,
|
|
2383
|
+
contentMetadata: el.contentMetadata
|
|
2231
2384
|
})),
|
|
2232
2385
|
components: components.map((comp) => ({
|
|
2233
2386
|
id: comp.id,
|
|
@@ -2311,6 +2464,14 @@ var DefaultActionExecutor = class {
|
|
|
2311
2464
|
return this.performCheck(element, false);
|
|
2312
2465
|
case "toggle":
|
|
2313
2466
|
return this.performToggle(element);
|
|
2467
|
+
case "drag":
|
|
2468
|
+
return this.performDrag(element, params);
|
|
2469
|
+
case "setValue":
|
|
2470
|
+
return this.performSetValue(element, params);
|
|
2471
|
+
case "submit":
|
|
2472
|
+
return this.performSubmit(element);
|
|
2473
|
+
case "reset":
|
|
2474
|
+
return this.performReset(element);
|
|
2314
2475
|
default: {
|
|
2315
2476
|
const registered = this.registry.findByDOMElement(element);
|
|
2316
2477
|
if (registered?.customActions?.[action]) {
|
|
@@ -2340,15 +2501,26 @@ var DefaultActionExecutor = class {
|
|
|
2340
2501
|
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
2341
2502
|
throw new Error("Type action requires an input or textarea element");
|
|
2342
2503
|
}
|
|
2504
|
+
const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
2505
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
2343
2506
|
element.focus();
|
|
2344
2507
|
if (options?.clear) {
|
|
2345
|
-
|
|
2508
|
+
if (nativeSetter) {
|
|
2509
|
+
nativeSetter.call(element, "");
|
|
2510
|
+
} else {
|
|
2511
|
+
element.value = "";
|
|
2512
|
+
}
|
|
2346
2513
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2347
2514
|
}
|
|
2348
2515
|
const text = options?.text || "";
|
|
2349
2516
|
const delay = options?.delay || 0;
|
|
2350
2517
|
for (const char of text) {
|
|
2351
|
-
element.value
|
|
2518
|
+
const current = element.value;
|
|
2519
|
+
if (nativeSetter) {
|
|
2520
|
+
nativeSetter.call(element, current + char);
|
|
2521
|
+
} else {
|
|
2522
|
+
element.value = current + char;
|
|
2523
|
+
}
|
|
2352
2524
|
if (options?.triggerEvents !== false) {
|
|
2353
2525
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2354
2526
|
}
|
|
@@ -2362,7 +2534,13 @@ var DefaultActionExecutor = class {
|
|
|
2362
2534
|
}
|
|
2363
2535
|
performClear(element) {
|
|
2364
2536
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
2365
|
-
element.
|
|
2537
|
+
const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
2538
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
2539
|
+
if (nativeSetter) {
|
|
2540
|
+
nativeSetter.call(element, "");
|
|
2541
|
+
} else {
|
|
2542
|
+
element.value = "";
|
|
2543
|
+
}
|
|
2366
2544
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2367
2545
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2368
2546
|
}
|
|
@@ -2444,6 +2622,150 @@ var DefaultActionExecutor = class {
|
|
|
2444
2622
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2445
2623
|
}
|
|
2446
2624
|
}
|
|
2625
|
+
performSetValue(element, params) {
|
|
2626
|
+
const value = params?.value;
|
|
2627
|
+
if (value === void 0) {
|
|
2628
|
+
throw new Error('setValue requires a "value" parameter');
|
|
2629
|
+
}
|
|
2630
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
2631
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
2632
|
+
element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
|
|
2633
|
+
"value"
|
|
2634
|
+
)?.set;
|
|
2635
|
+
if (nativeSetter) {
|
|
2636
|
+
nativeSetter.call(element, value);
|
|
2637
|
+
} else {
|
|
2638
|
+
element.value = value;
|
|
2639
|
+
}
|
|
2640
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2641
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2642
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
2643
|
+
element.value = value;
|
|
2644
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
performSubmit(element) {
|
|
2648
|
+
const form = element instanceof HTMLFormElement ? element : element.closest("form");
|
|
2649
|
+
if (form) {
|
|
2650
|
+
const submitEvent = new Event("submit", { bubbles: true, cancelable: true });
|
|
2651
|
+
if (form.dispatchEvent(submitEvent)) {
|
|
2652
|
+
form.requestSubmit();
|
|
2653
|
+
}
|
|
2654
|
+
} else {
|
|
2655
|
+
throw new Error("No form found for submit action");
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
performReset(element) {
|
|
2659
|
+
const form = element instanceof HTMLFormElement ? element : element.closest("form");
|
|
2660
|
+
if (form) {
|
|
2661
|
+
form.reset();
|
|
2662
|
+
form.dispatchEvent(new Event("reset", { bubbles: true }));
|
|
2663
|
+
} else {
|
|
2664
|
+
throw new Error("No form found for reset action");
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Perform a drag operation by dispatching a sequence of mouse events.
|
|
2669
|
+
*
|
|
2670
|
+
* Follows the same composite pattern as the qontinui core library:
|
|
2671
|
+
* mousedown on source → wait → mousemove × N along path → mouseup on target.
|
|
2672
|
+
*
|
|
2673
|
+
* Optionally dispatches HTML5 drag events (dragstart/dragover/drop/dragend)
|
|
2674
|
+
* for apps that use the HTML5 Drag and Drop API instead of mouse events.
|
|
2675
|
+
*/
|
|
2676
|
+
async performDrag(sourceElement, options) {
|
|
2677
|
+
const sourceRect = sourceElement.getBoundingClientRect();
|
|
2678
|
+
const sourceX = sourceRect.left + (options?.sourceOffset?.x ?? sourceRect.width / 2);
|
|
2679
|
+
const sourceY = sourceRect.top + (options?.sourceOffset?.y ?? sourceRect.height / 2);
|
|
2680
|
+
let targetX;
|
|
2681
|
+
let targetY;
|
|
2682
|
+
if (options?.targetPosition) {
|
|
2683
|
+
targetX = options.targetPosition.x;
|
|
2684
|
+
targetY = options.targetPosition.y;
|
|
2685
|
+
} else if (options?.target) {
|
|
2686
|
+
const targetElement = this.resolveTargetElement(options.target);
|
|
2687
|
+
if (!targetElement) {
|
|
2688
|
+
throw new Error(`Drag target element not found: ${JSON.stringify(options.target)}`);
|
|
2689
|
+
}
|
|
2690
|
+
const targetRect = targetElement.getBoundingClientRect();
|
|
2691
|
+
targetX = targetRect.left + (options?.targetOffset?.x ?? targetRect.width / 2);
|
|
2692
|
+
targetY = targetRect.top + (options?.targetOffset?.y ?? targetRect.height / 2);
|
|
2693
|
+
} else {
|
|
2694
|
+
throw new Error("Drag requires either target or targetPosition");
|
|
2695
|
+
}
|
|
2696
|
+
const steps = options?.steps ?? 10;
|
|
2697
|
+
const holdDelay = options?.holdDelay ?? 100;
|
|
2698
|
+
const releaseDelay = options?.releaseDelay ?? 50;
|
|
2699
|
+
sourceElement.dispatchEvent(createMouseEventAt("mousedown", sourceX, sourceY));
|
|
2700
|
+
const canHTML5 = options?.html5 && typeof DragEvent !== "undefined";
|
|
2701
|
+
if (canHTML5) {
|
|
2702
|
+
sourceElement.dispatchEvent(
|
|
2703
|
+
new DragEvent("dragstart", {
|
|
2704
|
+
bubbles: true,
|
|
2705
|
+
cancelable: true,
|
|
2706
|
+
clientX: sourceX,
|
|
2707
|
+
clientY: sourceY
|
|
2708
|
+
})
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
if (holdDelay > 0) {
|
|
2712
|
+
await sleep(holdDelay);
|
|
2713
|
+
}
|
|
2714
|
+
for (let i = 1; i <= steps; i++) {
|
|
2715
|
+
const progress = i / steps;
|
|
2716
|
+
const currentX = sourceX + (targetX - sourceX) * progress;
|
|
2717
|
+
const currentY = sourceY + (targetY - sourceY) * progress;
|
|
2718
|
+
const dispatchTarget = elementFromPointSafe(currentX, currentY) || sourceElement;
|
|
2719
|
+
dispatchTarget.dispatchEvent(createMouseEventAt("mousemove", currentX, currentY));
|
|
2720
|
+
if (canHTML5) {
|
|
2721
|
+
dispatchTarget.dispatchEvent(
|
|
2722
|
+
new DragEvent("dragover", {
|
|
2723
|
+
bubbles: true,
|
|
2724
|
+
cancelable: true,
|
|
2725
|
+
clientX: currentX,
|
|
2726
|
+
clientY: currentY
|
|
2727
|
+
})
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
const dropTarget = elementFromPointSafe(targetX, targetY) || sourceElement;
|
|
2732
|
+
dropTarget.dispatchEvent(createMouseEventAt("mouseup", targetX, targetY));
|
|
2733
|
+
if (canHTML5) {
|
|
2734
|
+
dropTarget.dispatchEvent(
|
|
2735
|
+
new DragEvent("drop", {
|
|
2736
|
+
bubbles: true,
|
|
2737
|
+
cancelable: true,
|
|
2738
|
+
clientX: targetX,
|
|
2739
|
+
clientY: targetY
|
|
2740
|
+
})
|
|
2741
|
+
);
|
|
2742
|
+
sourceElement.dispatchEvent(
|
|
2743
|
+
new DragEvent("dragend", {
|
|
2744
|
+
bubbles: true,
|
|
2745
|
+
cancelable: true,
|
|
2746
|
+
clientX: targetX,
|
|
2747
|
+
clientY: targetY
|
|
2748
|
+
})
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (releaseDelay > 0) {
|
|
2752
|
+
await sleep(releaseDelay);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Resolve a drag target element from a target descriptor.
|
|
2757
|
+
*/
|
|
2758
|
+
resolveTargetElement(target) {
|
|
2759
|
+
if (target.elementId) {
|
|
2760
|
+
const registered = this.registry.getElement(target.elementId);
|
|
2761
|
+
if (registered?.element) return registered.element;
|
|
2762
|
+
return findElementByIdentifier(target.elementId);
|
|
2763
|
+
}
|
|
2764
|
+
if (target.selector) {
|
|
2765
|
+
return document.querySelector(target.selector);
|
|
2766
|
+
}
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2447
2769
|
getElementId(element) {
|
|
2448
2770
|
return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
2449
2771
|
}
|
|
@@ -2539,8 +2861,442 @@ var DefaultActionExecutor = class {
|
|
|
2539
2861
|
}
|
|
2540
2862
|
}
|
|
2541
2863
|
};
|
|
2542
|
-
function createActionExecutor(registry) {
|
|
2543
|
-
return new DefaultActionExecutor(registry);
|
|
2864
|
+
function createActionExecutor(registry, consoleCapture) {
|
|
2865
|
+
return new DefaultActionExecutor(registry, consoleCapture);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/specs/types.ts
|
|
2869
|
+
var SPEC_CONFIG_VERSION = "1.0.0";
|
|
2870
|
+
var VALID_ASSERTION_TYPES = [
|
|
2871
|
+
"visible",
|
|
2872
|
+
"hidden",
|
|
2873
|
+
"enabled",
|
|
2874
|
+
"disabled",
|
|
2875
|
+
"focused",
|
|
2876
|
+
"checked",
|
|
2877
|
+
"unchecked",
|
|
2878
|
+
"hasText",
|
|
2879
|
+
"containsText",
|
|
2880
|
+
"hasValue",
|
|
2881
|
+
"hasClass",
|
|
2882
|
+
"exists",
|
|
2883
|
+
"notExists",
|
|
2884
|
+
"count",
|
|
2885
|
+
"attribute",
|
|
2886
|
+
"cssProperty"
|
|
2887
|
+
];
|
|
2888
|
+
var VALID_SPEC_CATEGORIES = [
|
|
2889
|
+
"element-presence",
|
|
2890
|
+
"accessibility",
|
|
2891
|
+
"form-validation",
|
|
2892
|
+
"state-consistency",
|
|
2893
|
+
"modal-dialog",
|
|
2894
|
+
"navigation",
|
|
2895
|
+
"cross-page-consistency",
|
|
2896
|
+
"custom"
|
|
2897
|
+
];
|
|
2898
|
+
var VALID_SPEC_SEVERITIES = [
|
|
2899
|
+
"critical",
|
|
2900
|
+
"warning",
|
|
2901
|
+
"info"
|
|
2902
|
+
];
|
|
2903
|
+
var VALID_SPEC_SOURCES = [
|
|
2904
|
+
"auto",
|
|
2905
|
+
"manual",
|
|
2906
|
+
"ai-generated"
|
|
2907
|
+
];
|
|
2908
|
+
|
|
2909
|
+
// src/specs/validator.ts
|
|
2910
|
+
function isValidAssertionType(value) {
|
|
2911
|
+
return typeof value === "string" && VALID_ASSERTION_TYPES.includes(value);
|
|
2912
|
+
}
|
|
2913
|
+
function isValidSpecCategory(value) {
|
|
2914
|
+
return typeof value === "string" && VALID_SPEC_CATEGORIES.includes(value);
|
|
2915
|
+
}
|
|
2916
|
+
function isValidSpecSeverity(value) {
|
|
2917
|
+
return typeof value === "string" && VALID_SPEC_SEVERITIES.includes(value);
|
|
2918
|
+
}
|
|
2919
|
+
function isValidSpecSource(value) {
|
|
2920
|
+
return typeof value === "string" && VALID_SPEC_SOURCES.includes(value);
|
|
2921
|
+
}
|
|
2922
|
+
function validateSpecAssertion(data, path = "assertion") {
|
|
2923
|
+
const errors = [];
|
|
2924
|
+
if (!data || typeof data !== "object") {
|
|
2925
|
+
errors.push({ path, message: "must be an object" });
|
|
2926
|
+
return errors;
|
|
2927
|
+
}
|
|
2928
|
+
const obj = data;
|
|
2929
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
|
2930
|
+
errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
|
|
2931
|
+
}
|
|
2932
|
+
if (typeof obj.description !== "string") {
|
|
2933
|
+
errors.push({ path: `${path}.description`, message: "must be a string" });
|
|
2934
|
+
}
|
|
2935
|
+
if (!isValidSpecCategory(obj.category)) {
|
|
2936
|
+
errors.push({
|
|
2937
|
+
path: `${path}.category`,
|
|
2938
|
+
message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
if (!isValidSpecSeverity(obj.severity)) {
|
|
2942
|
+
errors.push({
|
|
2943
|
+
path: `${path}.severity`,
|
|
2944
|
+
message: `must be one of: ${VALID_SPEC_SEVERITIES.join(", ")}`
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
if (!obj.target || typeof obj.target !== "object") {
|
|
2948
|
+
errors.push({ path: `${path}.target`, message: "must be an object" });
|
|
2949
|
+
} else {
|
|
2950
|
+
const target = obj.target;
|
|
2951
|
+
if (target.type === "elementId") {
|
|
2952
|
+
if (typeof target.elementId !== "string" || target.elementId.length === 0) {
|
|
2953
|
+
errors.push({ path: `${path}.target.elementId`, message: "must be a non-empty string" });
|
|
2954
|
+
}
|
|
2955
|
+
} else if (target.type === "search") {
|
|
2956
|
+
if (!target.criteria || typeof target.criteria !== "object") {
|
|
2957
|
+
errors.push({ path: `${path}.target.criteria`, message: "must be an object" });
|
|
2958
|
+
}
|
|
2959
|
+
} else {
|
|
2960
|
+
errors.push({ path: `${path}.target.type`, message: 'must be "elementId" or "search"' });
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
if (!isValidAssertionType(obj.assertionType)) {
|
|
2964
|
+
errors.push({
|
|
2965
|
+
path: `${path}.assertionType`,
|
|
2966
|
+
message: `must be one of: ${VALID_ASSERTION_TYPES.join(", ")}`
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
if (!isValidSpecSource(obj.source)) {
|
|
2970
|
+
errors.push({
|
|
2971
|
+
path: `${path}.source`,
|
|
2972
|
+
message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
if (typeof obj.reviewed !== "boolean") {
|
|
2976
|
+
errors.push({ path: `${path}.reviewed`, message: "must be a boolean" });
|
|
2977
|
+
}
|
|
2978
|
+
if (typeof obj.enabled !== "boolean") {
|
|
2979
|
+
errors.push({ path: `${path}.enabled`, message: "must be a boolean" });
|
|
2980
|
+
}
|
|
2981
|
+
if (obj.timeout !== void 0 && (typeof obj.timeout !== "number" || obj.timeout < 0)) {
|
|
2982
|
+
errors.push({ path: `${path}.timeout`, message: "must be a non-negative number" });
|
|
2983
|
+
}
|
|
2984
|
+
return errors;
|
|
2985
|
+
}
|
|
2986
|
+
function validateSpecGroup(data, path = "group") {
|
|
2987
|
+
const errors = [];
|
|
2988
|
+
if (!data || typeof data !== "object") {
|
|
2989
|
+
errors.push({ path, message: "must be an object" });
|
|
2990
|
+
return errors;
|
|
2991
|
+
}
|
|
2992
|
+
const obj = data;
|
|
2993
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
|
2994
|
+
errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
|
|
2995
|
+
}
|
|
2996
|
+
if (typeof obj.name !== "string") {
|
|
2997
|
+
errors.push({ path: `${path}.name`, message: "must be a string" });
|
|
2998
|
+
}
|
|
2999
|
+
if (typeof obj.description !== "string") {
|
|
3000
|
+
errors.push({ path: `${path}.description`, message: "must be a string" });
|
|
3001
|
+
}
|
|
3002
|
+
if (!isValidSpecCategory(obj.category)) {
|
|
3003
|
+
errors.push({
|
|
3004
|
+
path: `${path}.category`,
|
|
3005
|
+
message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
if (!isValidSpecSource(obj.source)) {
|
|
3009
|
+
errors.push({
|
|
3010
|
+
path: `${path}.source`,
|
|
3011
|
+
message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
if (!Array.isArray(obj.assertions)) {
|
|
3015
|
+
errors.push({ path: `${path}.assertions`, message: "must be an array" });
|
|
3016
|
+
} else {
|
|
3017
|
+
for (let i = 0; i < obj.assertions.length; i++) {
|
|
3018
|
+
errors.push(...validateSpecAssertion(obj.assertions[i], `${path}.assertions[${i}]`));
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return errors;
|
|
3022
|
+
}
|
|
3023
|
+
function validateSpecConfig(data) {
|
|
3024
|
+
const errors = [];
|
|
3025
|
+
if (!data || typeof data !== "object") {
|
|
3026
|
+
return { valid: false, errors: [{ path: "", message: "must be an object" }] };
|
|
3027
|
+
}
|
|
3028
|
+
const obj = data;
|
|
3029
|
+
if (obj.version !== SPEC_CONFIG_VERSION) {
|
|
3030
|
+
errors.push({ path: "version", message: `must be "${SPEC_CONFIG_VERSION}"` });
|
|
3031
|
+
}
|
|
3032
|
+
if (obj.description !== void 0 && typeof obj.description !== "string") {
|
|
3033
|
+
errors.push({ path: "description", message: "must be a string if provided" });
|
|
3034
|
+
}
|
|
3035
|
+
if (!Array.isArray(obj.groups)) {
|
|
3036
|
+
errors.push({ path: "groups", message: "must be an array" });
|
|
3037
|
+
} else {
|
|
3038
|
+
for (let i = 0; i < obj.groups.length; i++) {
|
|
3039
|
+
errors.push(...validateSpecGroup(obj.groups[i], `groups[${i}]`));
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (obj.assertions !== void 0) {
|
|
3043
|
+
if (!Array.isArray(obj.assertions)) {
|
|
3044
|
+
errors.push({ path: "assertions", message: "must be an array if provided" });
|
|
3045
|
+
} else {
|
|
3046
|
+
for (let i = 0; i < obj.assertions.length; i++) {
|
|
3047
|
+
errors.push(...validateSpecAssertion(obj.assertions[i], `assertions[${i}]`));
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
if (obj.metadata !== void 0 && (typeof obj.metadata !== "object" || obj.metadata === null)) {
|
|
3052
|
+
errors.push({ path: "metadata", message: "must be an object if provided" });
|
|
3053
|
+
}
|
|
3054
|
+
return { valid: errors.length === 0, errors };
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/specs/store.ts
|
|
3058
|
+
var SpecStore = class {
|
|
3059
|
+
constructor() {
|
|
3060
|
+
this.configs = /* @__PURE__ */ new Map();
|
|
3061
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
3062
|
+
}
|
|
3063
|
+
// ---------------------------------------------------------------------------
|
|
3064
|
+
// CRUD — Config Level
|
|
3065
|
+
// ---------------------------------------------------------------------------
|
|
3066
|
+
load(specId, config) {
|
|
3067
|
+
this.configs.set(specId, config);
|
|
3068
|
+
this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
|
|
3069
|
+
}
|
|
3070
|
+
unload(specId) {
|
|
3071
|
+
const existed = this.configs.delete(specId);
|
|
3072
|
+
if (existed) {
|
|
3073
|
+
this.emit({ type: "spec:unloaded", specId, timestamp: Date.now() });
|
|
3074
|
+
}
|
|
3075
|
+
return existed;
|
|
3076
|
+
}
|
|
3077
|
+
get(specId) {
|
|
3078
|
+
return this.configs.get(specId);
|
|
3079
|
+
}
|
|
3080
|
+
has(specId) {
|
|
3081
|
+
return this.configs.has(specId);
|
|
3082
|
+
}
|
|
3083
|
+
getIds() {
|
|
3084
|
+
return Array.from(this.configs.keys());
|
|
3085
|
+
}
|
|
3086
|
+
getAll() {
|
|
3087
|
+
return new Map(this.configs);
|
|
3088
|
+
}
|
|
3089
|
+
get count() {
|
|
3090
|
+
return this.configs.size;
|
|
3091
|
+
}
|
|
3092
|
+
clear() {
|
|
3093
|
+
this.configs.clear();
|
|
3094
|
+
this.emit({ type: "spec:cleared", timestamp: Date.now() });
|
|
3095
|
+
}
|
|
3096
|
+
// ---------------------------------------------------------------------------
|
|
3097
|
+
// CRUD — Group Level
|
|
3098
|
+
// ---------------------------------------------------------------------------
|
|
3099
|
+
addGroup(specId, group) {
|
|
3100
|
+
const config = this.configs.get(specId);
|
|
3101
|
+
if (!config) return false;
|
|
3102
|
+
config.groups.push(group);
|
|
3103
|
+
this.emit({ type: "spec:group-added", specId, groupId: group.id, timestamp: Date.now() });
|
|
3104
|
+
return true;
|
|
3105
|
+
}
|
|
3106
|
+
removeGroup(specId, groupId) {
|
|
3107
|
+
const config = this.configs.get(specId);
|
|
3108
|
+
if (!config) return false;
|
|
3109
|
+
const idx = config.groups.findIndex((g) => g.id === groupId);
|
|
3110
|
+
if (idx === -1) return false;
|
|
3111
|
+
config.groups.splice(idx, 1);
|
|
3112
|
+
this.emit({ type: "spec:group-removed", specId, groupId, timestamp: Date.now() });
|
|
3113
|
+
return true;
|
|
3114
|
+
}
|
|
3115
|
+
getGroup(specId, groupId) {
|
|
3116
|
+
const config = this.configs.get(specId);
|
|
3117
|
+
if (!config) return void 0;
|
|
3118
|
+
return config.groups.find((g) => g.id === groupId);
|
|
3119
|
+
}
|
|
3120
|
+
// ---------------------------------------------------------------------------
|
|
3121
|
+
// CRUD — Assertion Level
|
|
3122
|
+
// ---------------------------------------------------------------------------
|
|
3123
|
+
addAssertion(specId, groupId, assertion) {
|
|
3124
|
+
const config = this.configs.get(specId);
|
|
3125
|
+
if (!config) return false;
|
|
3126
|
+
if (groupId) {
|
|
3127
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
3128
|
+
if (!group) return false;
|
|
3129
|
+
group.assertions.push(assertion);
|
|
3130
|
+
} else {
|
|
3131
|
+
if (!config.assertions) config.assertions = [];
|
|
3132
|
+
config.assertions.push(assertion);
|
|
3133
|
+
}
|
|
3134
|
+
this.emit({
|
|
3135
|
+
type: "spec:assertion-added",
|
|
3136
|
+
specId,
|
|
3137
|
+
groupId: groupId ?? void 0,
|
|
3138
|
+
assertionId: assertion.id,
|
|
3139
|
+
timestamp: Date.now()
|
|
3140
|
+
});
|
|
3141
|
+
return true;
|
|
3142
|
+
}
|
|
3143
|
+
removeAssertion(specId, groupId, assertionId) {
|
|
3144
|
+
const config = this.configs.get(specId);
|
|
3145
|
+
if (!config) return false;
|
|
3146
|
+
let removed = false;
|
|
3147
|
+
if (groupId) {
|
|
3148
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
3149
|
+
if (group) {
|
|
3150
|
+
const idx = group.assertions.findIndex((a) => a.id === assertionId);
|
|
3151
|
+
if (idx !== -1) {
|
|
3152
|
+
group.assertions.splice(idx, 1);
|
|
3153
|
+
removed = true;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
} else if (config.assertions) {
|
|
3157
|
+
const idx = config.assertions.findIndex((a) => a.id === assertionId);
|
|
3158
|
+
if (idx !== -1) {
|
|
3159
|
+
config.assertions.splice(idx, 1);
|
|
3160
|
+
removed = true;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
if (removed) {
|
|
3164
|
+
this.emit({
|
|
3165
|
+
type: "spec:assertion-removed",
|
|
3166
|
+
specId,
|
|
3167
|
+
groupId: groupId ?? void 0,
|
|
3168
|
+
assertionId,
|
|
3169
|
+
timestamp: Date.now()
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
return removed;
|
|
3173
|
+
}
|
|
3174
|
+
toggleAssertion(specId, groupId, assertionId) {
|
|
3175
|
+
const assertion = this.findAssertion(specId, groupId, assertionId);
|
|
3176
|
+
if (!assertion) return false;
|
|
3177
|
+
assertion.enabled = !assertion.enabled;
|
|
3178
|
+
this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
|
|
3179
|
+
return true;
|
|
3180
|
+
}
|
|
3181
|
+
markReviewed(specId, groupId, assertionId) {
|
|
3182
|
+
const assertion = this.findAssertion(specId, groupId, assertionId);
|
|
3183
|
+
if (!assertion) return false;
|
|
3184
|
+
assertion.reviewed = !assertion.reviewed;
|
|
3185
|
+
this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
|
|
3186
|
+
return true;
|
|
3187
|
+
}
|
|
3188
|
+
// ---------------------------------------------------------------------------
|
|
3189
|
+
// Queries
|
|
3190
|
+
// ---------------------------------------------------------------------------
|
|
3191
|
+
getAllAssertions() {
|
|
3192
|
+
const result = [];
|
|
3193
|
+
for (const config of this.configs.values()) {
|
|
3194
|
+
for (const group of config.groups) {
|
|
3195
|
+
result.push(...group.assertions);
|
|
3196
|
+
}
|
|
3197
|
+
if (config.assertions) {
|
|
3198
|
+
result.push(...config.assertions);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
return result;
|
|
3202
|
+
}
|
|
3203
|
+
filterAssertions(opts) {
|
|
3204
|
+
return this.getAllAssertions().filter((a) => {
|
|
3205
|
+
if (opts.categories && !opts.categories.includes(a.category)) return false;
|
|
3206
|
+
if (opts.severities && !opts.severities.includes(a.severity)) return false;
|
|
3207
|
+
if (opts.enabledOnly && !a.enabled) return false;
|
|
3208
|
+
if (opts.reviewedOnly && !a.reviewed) return false;
|
|
3209
|
+
return true;
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
// ---------------------------------------------------------------------------
|
|
3213
|
+
// Coverage
|
|
3214
|
+
// ---------------------------------------------------------------------------
|
|
3215
|
+
getCoverage(allElementIds) {
|
|
3216
|
+
const specifiedIdSet = /* @__PURE__ */ new Set();
|
|
3217
|
+
for (const assertion of this.getAllAssertions()) {
|
|
3218
|
+
if (assertion.target.type === "elementId") {
|
|
3219
|
+
specifiedIdSet.add(assertion.target.elementId);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
const specifiedIds = [];
|
|
3223
|
+
const unspecifiedIds = [];
|
|
3224
|
+
for (const id of allElementIds) {
|
|
3225
|
+
if (specifiedIdSet.has(id)) {
|
|
3226
|
+
specifiedIds.push(id);
|
|
3227
|
+
} else {
|
|
3228
|
+
unspecifiedIds.push(id);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
const total = allElementIds.length;
|
|
3232
|
+
return {
|
|
3233
|
+
totalElements: total,
|
|
3234
|
+
specifiedElements: specifiedIds.length,
|
|
3235
|
+
coveragePercent: total > 0 ? specifiedIds.length / total * 100 : 0,
|
|
3236
|
+
specifiedIds,
|
|
3237
|
+
unspecifiedIds,
|
|
3238
|
+
timestamp: Date.now()
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
// ---------------------------------------------------------------------------
|
|
3242
|
+
// Import / Export
|
|
3243
|
+
// ---------------------------------------------------------------------------
|
|
3244
|
+
importConfig(specId, config) {
|
|
3245
|
+
const result = validateSpecConfig(config);
|
|
3246
|
+
if (!result.valid) return false;
|
|
3247
|
+
this.configs.set(specId, config);
|
|
3248
|
+
this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
|
|
3249
|
+
return true;
|
|
3250
|
+
}
|
|
3251
|
+
exportConfig(specId) {
|
|
3252
|
+
const config = this.configs.get(specId);
|
|
3253
|
+
if (!config) return void 0;
|
|
3254
|
+
return {
|
|
3255
|
+
...config,
|
|
3256
|
+
version: SPEC_CONFIG_VERSION,
|
|
3257
|
+
metadata: {
|
|
3258
|
+
...config.metadata,
|
|
3259
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3260
|
+
}
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
// ---------------------------------------------------------------------------
|
|
3264
|
+
// Events
|
|
3265
|
+
// ---------------------------------------------------------------------------
|
|
3266
|
+
on(listener) {
|
|
3267
|
+
this.listeners.add(listener);
|
|
3268
|
+
return () => {
|
|
3269
|
+
this.listeners.delete(listener);
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
emit(event) {
|
|
3273
|
+
for (const listener of this.listeners) {
|
|
3274
|
+
try {
|
|
3275
|
+
listener(event);
|
|
3276
|
+
} catch {
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
// ---------------------------------------------------------------------------
|
|
3281
|
+
// Private Helpers
|
|
3282
|
+
// ---------------------------------------------------------------------------
|
|
3283
|
+
findAssertion(specId, groupId, assertionId) {
|
|
3284
|
+
const config = this.configs.get(specId);
|
|
3285
|
+
if (!config) return void 0;
|
|
3286
|
+
if (groupId) {
|
|
3287
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
3288
|
+
if (!group) return void 0;
|
|
3289
|
+
return group.assertions.find((a) => a.id === assertionId);
|
|
3290
|
+
}
|
|
3291
|
+
return config.assertions?.find((a) => a.id === assertionId);
|
|
3292
|
+
}
|
|
3293
|
+
};
|
|
3294
|
+
var globalStore = null;
|
|
3295
|
+
function getGlobalSpecStore() {
|
|
3296
|
+
if (!globalStore) {
|
|
3297
|
+
globalStore = new SpecStore();
|
|
3298
|
+
}
|
|
3299
|
+
return globalStore;
|
|
2544
3300
|
}
|
|
2545
3301
|
|
|
2546
3302
|
// src/control/workflow-engine.ts
|
|
@@ -3572,36 +4328,692 @@ var MetricsCollector = class {
|
|
|
3572
4328
|
function createMetricsCollector(options) {
|
|
3573
4329
|
return new MetricsCollector(options);
|
|
3574
4330
|
}
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
4331
|
+
|
|
4332
|
+
// src/debug/browser-capture-types.ts
|
|
4333
|
+
var DEFAULT_CAPTURE_CONFIG = {
|
|
4334
|
+
console: true,
|
|
4335
|
+
network: true,
|
|
4336
|
+
navigation: true,
|
|
4337
|
+
longTasks: true,
|
|
4338
|
+
longAnimationFrames: true,
|
|
4339
|
+
resourceErrors: true,
|
|
4340
|
+
wsDisconnections: true,
|
|
4341
|
+
hmr: true,
|
|
4342
|
+
webVitals: false,
|
|
4343
|
+
memory: false,
|
|
4344
|
+
memoryIntervalMs: 3e4,
|
|
4345
|
+
maxEntries: 200
|
|
4346
|
+
};
|
|
4347
|
+
|
|
4348
|
+
// src/debug/captures/console.ts
|
|
4349
|
+
function argsToMessage(args) {
|
|
4350
|
+
return args.map((a) => {
|
|
4351
|
+
if (a instanceof Error) return a.message;
|
|
4352
|
+
if (typeof a === "string") return a;
|
|
4353
|
+
try {
|
|
4354
|
+
return JSON.stringify(a);
|
|
4355
|
+
} catch {
|
|
4356
|
+
return String(a);
|
|
3600
4357
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
4358
|
+
}).join(" ");
|
|
4359
|
+
}
|
|
4360
|
+
function extractStack(args) {
|
|
4361
|
+
const err = args.find((a) => a instanceof Error);
|
|
4362
|
+
return err?.stack;
|
|
4363
|
+
}
|
|
4364
|
+
function makeEvent(level, message, stack) {
|
|
4365
|
+
return {
|
|
4366
|
+
type: "console",
|
|
4367
|
+
timestamp: Date.now(),
|
|
4368
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4369
|
+
level,
|
|
4370
|
+
message,
|
|
4371
|
+
stack
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
function installConsoleCapture(emit) {
|
|
4375
|
+
const originalError = console.error;
|
|
4376
|
+
const originalWarn = console.warn;
|
|
4377
|
+
console.error = (...args) => {
|
|
4378
|
+
emit(makeEvent("error", argsToMessage(args), extractStack(args)));
|
|
4379
|
+
originalError.apply(console, args);
|
|
4380
|
+
};
|
|
4381
|
+
console.warn = (...args) => {
|
|
4382
|
+
emit(makeEvent("warn", argsToMessage(args), extractStack(args)));
|
|
4383
|
+
originalWarn.apply(console, args);
|
|
4384
|
+
};
|
|
4385
|
+
const rejectionHandler = (event) => {
|
|
4386
|
+
const reason = event.reason;
|
|
4387
|
+
const message = reason instanceof Error ? reason.message : String(reason ?? "Unhandled rejection");
|
|
4388
|
+
const stack = reason instanceof Error ? reason.stack : void 0;
|
|
4389
|
+
emit(makeEvent("unhandledrejection", message, stack));
|
|
4390
|
+
};
|
|
4391
|
+
if (typeof window !== "undefined") {
|
|
4392
|
+
window.addEventListener("unhandledrejection", rejectionHandler);
|
|
4393
|
+
}
|
|
4394
|
+
return () => {
|
|
4395
|
+
console.error = originalError;
|
|
4396
|
+
console.warn = originalWarn;
|
|
4397
|
+
if (typeof window !== "undefined") {
|
|
4398
|
+
window.removeEventListener("unhandledrejection", rejectionHandler);
|
|
4399
|
+
}
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/debug/captures/network.ts
|
|
4404
|
+
var DEFAULT_IGNORE = ["/api/dev-debug/", "/api/ui-bridge/", "localhost:9876"];
|
|
4405
|
+
function installNetworkCapture(emit, options) {
|
|
4406
|
+
if (typeof window === "undefined" || typeof window.fetch !== "function") {
|
|
4407
|
+
return () => {
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
const originalFetch = window.fetch;
|
|
4411
|
+
const ignorePatterns = options?.ignorePatterns ?? DEFAULT_IGNORE;
|
|
4412
|
+
function shouldIgnore(url) {
|
|
4413
|
+
return ignorePatterns.some((p) => url.includes(p));
|
|
4414
|
+
}
|
|
4415
|
+
function getMethod(input, init) {
|
|
4416
|
+
if (init?.method) return init.method.toUpperCase();
|
|
4417
|
+
if (input instanceof Request) return input.method.toUpperCase();
|
|
4418
|
+
return "GET";
|
|
4419
|
+
}
|
|
4420
|
+
function getUrl(input) {
|
|
4421
|
+
if (typeof input === "string") return input;
|
|
4422
|
+
if (input instanceof URL) return input.href;
|
|
4423
|
+
if (input instanceof Request) return input.url;
|
|
4424
|
+
return String(input);
|
|
4425
|
+
}
|
|
4426
|
+
window.fetch = async function patchedFetch(input, init) {
|
|
4427
|
+
const requestUrl = getUrl(input);
|
|
4428
|
+
if (shouldIgnore(requestUrl)) {
|
|
4429
|
+
return originalFetch.call(window, input, init);
|
|
4430
|
+
}
|
|
4431
|
+
const method = getMethod(input, init);
|
|
4432
|
+
const start = performance.now();
|
|
4433
|
+
try {
|
|
4434
|
+
const response = await originalFetch.call(window, input, init);
|
|
4435
|
+
const durationMs = Math.round(performance.now() - start);
|
|
4436
|
+
if (response.status >= 400) {
|
|
4437
|
+
const event = {
|
|
4438
|
+
type: "network",
|
|
4439
|
+
timestamp: Date.now(),
|
|
4440
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4441
|
+
method,
|
|
4442
|
+
requestUrl,
|
|
4443
|
+
status: response.status,
|
|
4444
|
+
statusText: response.statusText,
|
|
4445
|
+
durationMs,
|
|
4446
|
+
kind: "http-error"
|
|
4447
|
+
};
|
|
4448
|
+
emit(event);
|
|
4449
|
+
}
|
|
4450
|
+
return response;
|
|
4451
|
+
} catch (err) {
|
|
4452
|
+
const durationMs = Math.round(performance.now() - start);
|
|
4453
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4454
|
+
let kind = "network-error";
|
|
4455
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
4456
|
+
kind = "abort";
|
|
4457
|
+
} else if (errorMessage.includes("CORS") || errorMessage.includes("cross-origin")) {
|
|
4458
|
+
kind = "cors";
|
|
4459
|
+
} else if (errorMessage.includes("timeout") || errorMessage.includes("timed out")) {
|
|
4460
|
+
kind = "timeout";
|
|
4461
|
+
}
|
|
4462
|
+
const event = {
|
|
4463
|
+
type: "network",
|
|
4464
|
+
timestamp: Date.now(),
|
|
4465
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4466
|
+
method,
|
|
4467
|
+
requestUrl,
|
|
4468
|
+
durationMs,
|
|
4469
|
+
kind,
|
|
4470
|
+
errorMessage
|
|
4471
|
+
};
|
|
4472
|
+
emit(event);
|
|
4473
|
+
throw err;
|
|
4474
|
+
}
|
|
4475
|
+
};
|
|
4476
|
+
return () => {
|
|
4477
|
+
window.fetch = originalFetch;
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
// src/debug/captures/navigation.ts
|
|
4482
|
+
function installNavigationCapture(emit) {
|
|
4483
|
+
if (typeof window === "undefined" || typeof history === "undefined") {
|
|
4484
|
+
return () => {
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
let lastUrl = window.location.href;
|
|
4488
|
+
function emitNav(to, trigger) {
|
|
4489
|
+
const from = lastUrl;
|
|
4490
|
+
lastUrl = to;
|
|
4491
|
+
if (from === to) return;
|
|
4492
|
+
emit({
|
|
4493
|
+
type: "navigation",
|
|
4494
|
+
timestamp: Date.now(),
|
|
4495
|
+
url: to,
|
|
4496
|
+
from,
|
|
4497
|
+
to,
|
|
4498
|
+
trigger
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
const originalPushState = history.pushState;
|
|
4502
|
+
const originalReplaceState = history.replaceState;
|
|
4503
|
+
history.pushState = function(...args) {
|
|
4504
|
+
originalPushState.apply(this, args);
|
|
4505
|
+
emitNav(window.location.href, "pushState");
|
|
4506
|
+
};
|
|
4507
|
+
history.replaceState = function(...args) {
|
|
4508
|
+
originalReplaceState.apply(this, args);
|
|
4509
|
+
emitNav(window.location.href, "replaceState");
|
|
4510
|
+
};
|
|
4511
|
+
const popstateHandler = () => {
|
|
4512
|
+
emitNav(window.location.href, "popstate");
|
|
4513
|
+
};
|
|
4514
|
+
window.addEventListener("popstate", popstateHandler);
|
|
4515
|
+
return () => {
|
|
4516
|
+
history.pushState = originalPushState;
|
|
4517
|
+
history.replaceState = originalReplaceState;
|
|
4518
|
+
window.removeEventListener("popstate", popstateHandler);
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
// src/debug/captures/long-tasks.ts
|
|
4523
|
+
function installLongTaskCapture(emit) {
|
|
4524
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
|
|
4525
|
+
return () => {
|
|
4526
|
+
};
|
|
4527
|
+
}
|
|
4528
|
+
try {
|
|
4529
|
+
const observer = new PerformanceObserver((list) => {
|
|
4530
|
+
for (const entry of list.getEntries()) {
|
|
4531
|
+
emit({
|
|
4532
|
+
type: "long-task",
|
|
4533
|
+
timestamp: Date.now(),
|
|
4534
|
+
url: window.location.href,
|
|
4535
|
+
durationMs: Math.round(entry.duration)
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4539
|
+
observer.observe({ type: "longtask", buffered: true });
|
|
4540
|
+
return () => {
|
|
4541
|
+
observer.disconnect();
|
|
4542
|
+
};
|
|
4543
|
+
} catch {
|
|
4544
|
+
return () => {
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
// src/debug/captures/resource-errors.ts
|
|
4550
|
+
var TRACKED_TAGS = /* @__PURE__ */ new Set(["IMG", "SCRIPT", "LINK"]);
|
|
4551
|
+
function installResourceErrorCapture(emit) {
|
|
4552
|
+
if (typeof window === "undefined") {
|
|
4553
|
+
return () => {
|
|
4554
|
+
};
|
|
4555
|
+
}
|
|
4556
|
+
const handler = (event) => {
|
|
4557
|
+
const target = event.target;
|
|
4558
|
+
if (!target || !target.tagName) return;
|
|
4559
|
+
if (!TRACKED_TAGS.has(target.tagName)) return;
|
|
4560
|
+
const resourceUrl = target.src || target.src || target.href || "";
|
|
4561
|
+
if (!resourceUrl) return;
|
|
4562
|
+
emit({
|
|
4563
|
+
type: "resource-error",
|
|
4564
|
+
timestamp: Date.now(),
|
|
4565
|
+
url: window.location.href,
|
|
4566
|
+
resourceUrl,
|
|
4567
|
+
tagName: target.tagName
|
|
4568
|
+
});
|
|
4569
|
+
};
|
|
4570
|
+
window.addEventListener("error", handler, true);
|
|
4571
|
+
return () => {
|
|
4572
|
+
window.removeEventListener("error", handler, true);
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
// src/debug/captures/web-vitals.ts
|
|
4577
|
+
function installWebVitalsCapture(emit) {
|
|
4578
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
|
|
4579
|
+
return () => {
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
const observers = [];
|
|
4583
|
+
try {
|
|
4584
|
+
const lcpObserver = new PerformanceObserver((list) => {
|
|
4585
|
+
const entries = list.getEntries();
|
|
4586
|
+
const last = entries[entries.length - 1];
|
|
4587
|
+
if (last) {
|
|
4588
|
+
emit({
|
|
4589
|
+
type: "web-vital",
|
|
4590
|
+
timestamp: Date.now(),
|
|
4591
|
+
url: window.location.href,
|
|
4592
|
+
metric: "LCP",
|
|
4593
|
+
value: Math.round(last.startTime)
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
});
|
|
4597
|
+
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
|
|
4598
|
+
observers.push(lcpObserver);
|
|
4599
|
+
} catch {
|
|
4600
|
+
}
|
|
4601
|
+
try {
|
|
4602
|
+
let clsValue = 0;
|
|
4603
|
+
const clsObserver = new PerformanceObserver((list) => {
|
|
4604
|
+
for (const entry of list.getEntries()) {
|
|
4605
|
+
if (!entry.hadRecentInput) {
|
|
4606
|
+
clsValue += entry.value ?? 0;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
emit({
|
|
4610
|
+
type: "web-vital",
|
|
4611
|
+
timestamp: Date.now(),
|
|
4612
|
+
url: window.location.href,
|
|
4613
|
+
metric: "CLS",
|
|
4614
|
+
value: Math.round(clsValue * 1e3) / 1e3
|
|
4615
|
+
});
|
|
4616
|
+
});
|
|
4617
|
+
clsObserver.observe({ type: "layout-shift", buffered: true });
|
|
4618
|
+
observers.push(clsObserver);
|
|
4619
|
+
} catch {
|
|
4620
|
+
}
|
|
4621
|
+
return () => {
|
|
4622
|
+
for (const obs of observers) {
|
|
4623
|
+
obs.disconnect();
|
|
4624
|
+
}
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
// src/debug/captures/memory.ts
|
|
4629
|
+
function installMemoryCapture(emit, intervalMs = 3e4) {
|
|
4630
|
+
if (typeof window === "undefined") {
|
|
4631
|
+
return () => {
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
const perf = performance;
|
|
4635
|
+
if (!perf.memory) {
|
|
4636
|
+
return () => {
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
4639
|
+
const tick = () => {
|
|
4640
|
+
const mem = perf.memory;
|
|
4641
|
+
if (!mem) return;
|
|
4642
|
+
emit({
|
|
4643
|
+
type: "memory",
|
|
4644
|
+
timestamp: Date.now(),
|
|
4645
|
+
url: window.location.href,
|
|
4646
|
+
usedJSHeapSize: mem.usedJSHeapSize,
|
|
4647
|
+
totalJSHeapSize: mem.totalJSHeapSize,
|
|
4648
|
+
jsHeapSizeLimit: mem.jsHeapSizeLimit
|
|
4649
|
+
});
|
|
4650
|
+
};
|
|
4651
|
+
tick();
|
|
4652
|
+
const id = setInterval(tick, intervalMs);
|
|
4653
|
+
return () => {
|
|
4654
|
+
clearInterval(id);
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
// src/debug/captures/hmr.ts
|
|
4659
|
+
var HMR_PATH_PATTERNS = ["/_next/webpack-hmr", "/__turbopack_hmr", "/_next/turbopack-hmr"];
|
|
4660
|
+
function isHmrUrl(url) {
|
|
4661
|
+
return HMR_PATH_PATTERNS.some((p) => url.includes(p));
|
|
4662
|
+
}
|
|
4663
|
+
function makeEvent2(level, message, moduleName, loc) {
|
|
4664
|
+
return {
|
|
4665
|
+
type: "hmr",
|
|
4666
|
+
level,
|
|
4667
|
+
message,
|
|
4668
|
+
moduleName,
|
|
4669
|
+
loc,
|
|
4670
|
+
timestamp: Date.now(),
|
|
4671
|
+
url: typeof window !== "undefined" ? window.location.href : ""
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
function processHmrMessage(data, emit) {
|
|
4675
|
+
try {
|
|
4676
|
+
const msg = JSON.parse(data);
|
|
4677
|
+
if (Array.isArray(msg.errors)) {
|
|
4678
|
+
for (const err of msg.errors) {
|
|
4679
|
+
emit(
|
|
4680
|
+
makeEvent2(
|
|
4681
|
+
"error",
|
|
4682
|
+
typeof err === "string" ? err : err.message ?? String(err),
|
|
4683
|
+
err.moduleName ?? err.moduleIdentifier,
|
|
4684
|
+
err.loc ? String(err.loc) : void 0
|
|
4685
|
+
)
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (Array.isArray(msg.warnings)) {
|
|
4690
|
+
for (const warn of msg.warnings) {
|
|
4691
|
+
emit(
|
|
4692
|
+
makeEvent2(
|
|
4693
|
+
"warning",
|
|
4694
|
+
typeof warn === "string" ? warn : warn.message ?? String(warn),
|
|
4695
|
+
warn.moduleName ?? warn.moduleIdentifier,
|
|
4696
|
+
warn.loc ? String(warn.loc) : void 0
|
|
4697
|
+
)
|
|
4698
|
+
);
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
if (msg.action === "serverError" && msg.errorJSON) {
|
|
4702
|
+
try {
|
|
4703
|
+
const err = JSON.parse(msg.errorJSON);
|
|
4704
|
+
emit(makeEvent2("error", err.message ?? String(err)));
|
|
4705
|
+
} catch {
|
|
4706
|
+
emit(makeEvent2("error", msg.errorJSON));
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
if ((msg.action === "turbopack-message" || msg.type === "turbopack-message") && msg.data?.diagnostics) {
|
|
4710
|
+
for (const diag of msg.data.diagnostics) {
|
|
4711
|
+
emit(
|
|
4712
|
+
makeEvent2(
|
|
4713
|
+
diag.category === "warning" ? "warning" : "error",
|
|
4714
|
+
diag.message ?? String(diag),
|
|
4715
|
+
diag.filePath,
|
|
4716
|
+
diag.line != null ? `${diag.line}:${diag.column ?? 0}` : void 0
|
|
4717
|
+
)
|
|
4718
|
+
);
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
} catch {
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
function installWebSocketCapture(emit, cleanups) {
|
|
4725
|
+
if (!window.WebSocket) return;
|
|
4726
|
+
const OriginalWebSocket = window.WebSocket;
|
|
4727
|
+
const trackedSockets = [];
|
|
4728
|
+
const PatchedWebSocket = function(url, protocols) {
|
|
4729
|
+
const ws = new OriginalWebSocket(url, protocols);
|
|
4730
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
4731
|
+
if (isHmrUrl(urlStr)) {
|
|
4732
|
+
ws.addEventListener("message", (event) => {
|
|
4733
|
+
if (typeof event.data === "string") {
|
|
4734
|
+
processHmrMessage(event.data, emit);
|
|
4735
|
+
}
|
|
4736
|
+
});
|
|
4737
|
+
trackedSockets.push(ws);
|
|
4738
|
+
}
|
|
4739
|
+
return ws;
|
|
4740
|
+
};
|
|
4741
|
+
PatchedWebSocket.prototype = OriginalWebSocket.prototype;
|
|
4742
|
+
Object.defineProperty(PatchedWebSocket, "CONNECTING", { value: OriginalWebSocket.CONNECTING });
|
|
4743
|
+
Object.defineProperty(PatchedWebSocket, "OPEN", { value: OriginalWebSocket.OPEN });
|
|
4744
|
+
Object.defineProperty(PatchedWebSocket, "CLOSING", { value: OriginalWebSocket.CLOSING });
|
|
4745
|
+
Object.defineProperty(PatchedWebSocket, "CLOSED", { value: OriginalWebSocket.CLOSED });
|
|
4746
|
+
window.WebSocket = PatchedWebSocket;
|
|
4747
|
+
cleanups.push(() => {
|
|
4748
|
+
window.WebSocket = OriginalWebSocket;
|
|
4749
|
+
for (const ws of trackedSockets) {
|
|
4750
|
+
ws.close();
|
|
4751
|
+
}
|
|
4752
|
+
trackedSockets.length = 0;
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
function installEventSourceCapture(emit, cleanups) {
|
|
4756
|
+
if (!window.EventSource) return;
|
|
4757
|
+
const OriginalEventSource = window.EventSource;
|
|
4758
|
+
const trackedSources = [];
|
|
4759
|
+
const messageHandler = (event) => {
|
|
4760
|
+
if (typeof event.data === "string") {
|
|
4761
|
+
processHmrMessage(event.data, emit);
|
|
4762
|
+
}
|
|
4763
|
+
};
|
|
4764
|
+
const PatchedEventSource = function(url, init) {
|
|
4765
|
+
const es = new OriginalEventSource(url, init);
|
|
4766
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
4767
|
+
if (isHmrUrl(urlStr)) {
|
|
4768
|
+
es.addEventListener("message", messageHandler);
|
|
4769
|
+
trackedSources.push(es);
|
|
4770
|
+
}
|
|
4771
|
+
return es;
|
|
4772
|
+
};
|
|
4773
|
+
PatchedEventSource.prototype = OriginalEventSource.prototype;
|
|
4774
|
+
Object.defineProperty(PatchedEventSource, "CONNECTING", {
|
|
4775
|
+
value: OriginalEventSource.CONNECTING
|
|
4776
|
+
});
|
|
4777
|
+
Object.defineProperty(PatchedEventSource, "OPEN", { value: OriginalEventSource.OPEN });
|
|
4778
|
+
Object.defineProperty(PatchedEventSource, "CLOSED", { value: OriginalEventSource.CLOSED });
|
|
4779
|
+
window.EventSource = PatchedEventSource;
|
|
4780
|
+
cleanups.push(() => {
|
|
4781
|
+
window.EventSource = OriginalEventSource;
|
|
4782
|
+
for (const es of trackedSources) {
|
|
4783
|
+
es.close();
|
|
4784
|
+
}
|
|
4785
|
+
trackedSources.length = 0;
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
function installHmrCapture(emit) {
|
|
4789
|
+
if (typeof window === "undefined") return () => {
|
|
4790
|
+
};
|
|
4791
|
+
const cleanups = [];
|
|
4792
|
+
installWebSocketCapture(emit, cleanups);
|
|
4793
|
+
installEventSourceCapture(emit, cleanups);
|
|
4794
|
+
return () => {
|
|
4795
|
+
for (const cleanup of cleanups) {
|
|
4796
|
+
cleanup();
|
|
4797
|
+
}
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
// src/debug/captures/long-animation-frames.ts
|
|
4802
|
+
function installLoafCapture(emit) {
|
|
4803
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
|
|
4804
|
+
return () => {
|
|
4805
|
+
};
|
|
4806
|
+
}
|
|
4807
|
+
try {
|
|
4808
|
+
const observer = new PerformanceObserver((list) => {
|
|
4809
|
+
for (const entry of list.getEntries()) {
|
|
4810
|
+
const scripts = (entry.scripts ?? []).map((s) => {
|
|
4811
|
+
const script = s;
|
|
4812
|
+
return {
|
|
4813
|
+
invoker: script.invoker ?? "",
|
|
4814
|
+
sourceURL: script.sourceURL ?? "",
|
|
4815
|
+
sourceFunctionName: script.sourceFunctionName ?? "",
|
|
4816
|
+
sourceCharPosition: script.sourceCharPosition ?? 0,
|
|
4817
|
+
duration: Math.round(script.duration ?? 0)
|
|
4818
|
+
};
|
|
4819
|
+
});
|
|
4820
|
+
emit({
|
|
4821
|
+
type: "long-animation-frame",
|
|
4822
|
+
timestamp: Date.now(),
|
|
4823
|
+
url: window.location.href,
|
|
4824
|
+
durationMs: Math.round(entry.duration),
|
|
4825
|
+
blockingDurationMs: Math.round(
|
|
4826
|
+
entry.blockingDuration ?? 0
|
|
4827
|
+
),
|
|
4828
|
+
scripts
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
observer.observe({ type: "long-animation-frame", buffered: true });
|
|
4833
|
+
return () => {
|
|
4834
|
+
observer.disconnect();
|
|
4835
|
+
};
|
|
4836
|
+
} catch {
|
|
4837
|
+
return () => {
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
// src/debug/browser-capture.ts
|
|
4843
|
+
var BrowserEventCapture = class {
|
|
4844
|
+
constructor(config) {
|
|
4845
|
+
this.buffer = [];
|
|
4846
|
+
this.installed = false;
|
|
4847
|
+
this.cleanups = [];
|
|
4848
|
+
this.onEvent = null;
|
|
4849
|
+
this.config = config ?? {};
|
|
4850
|
+
this.maxEntries = config?.maxEntries ?? DEFAULT_CAPTURE_CONFIG.maxEntries;
|
|
4851
|
+
}
|
|
4852
|
+
setOnEvent(cb) {
|
|
4853
|
+
this.onEvent = cb;
|
|
4854
|
+
}
|
|
4855
|
+
/**
|
|
4856
|
+
* Install all enabled capture sub-modules.
|
|
4857
|
+
* Safe to call multiple times (no-ops if already installed).
|
|
4858
|
+
*/
|
|
4859
|
+
install() {
|
|
4860
|
+
if (this.installed) return;
|
|
4861
|
+
const cfg = { ...DEFAULT_CAPTURE_CONFIG, ...this.config };
|
|
4862
|
+
const emit = (event) => {
|
|
4863
|
+
this.buffer.push(event);
|
|
4864
|
+
this.trim();
|
|
4865
|
+
this.onEvent?.(event);
|
|
4866
|
+
};
|
|
4867
|
+
if (cfg.console) {
|
|
4868
|
+
this.cleanups.push(installConsoleCapture(emit));
|
|
4869
|
+
}
|
|
4870
|
+
if (cfg.network) {
|
|
4871
|
+
this.cleanups.push(installNetworkCapture(emit, cfg.networkOptions));
|
|
4872
|
+
}
|
|
4873
|
+
if (cfg.navigation) {
|
|
4874
|
+
this.cleanups.push(installNavigationCapture(emit));
|
|
4875
|
+
}
|
|
4876
|
+
if (cfg.longTasks) {
|
|
4877
|
+
this.cleanups.push(installLongTaskCapture(emit));
|
|
4878
|
+
}
|
|
4879
|
+
if (cfg.longAnimationFrames) {
|
|
4880
|
+
this.cleanups.push(installLoafCapture(emit));
|
|
4881
|
+
}
|
|
4882
|
+
if (cfg.resourceErrors) {
|
|
4883
|
+
this.cleanups.push(installResourceErrorCapture(emit));
|
|
4884
|
+
}
|
|
4885
|
+
if (cfg.webVitals) {
|
|
4886
|
+
this.cleanups.push(installWebVitalsCapture(emit));
|
|
4887
|
+
}
|
|
4888
|
+
if (cfg.memory) {
|
|
4889
|
+
this.cleanups.push(installMemoryCapture(emit, cfg.memoryIntervalMs));
|
|
4890
|
+
}
|
|
4891
|
+
if (cfg.hmr) {
|
|
4892
|
+
this.cleanups.push(installHmrCapture(emit));
|
|
4893
|
+
}
|
|
4894
|
+
this.installed = true;
|
|
4895
|
+
}
|
|
4896
|
+
/**
|
|
4897
|
+
* Uninstall all capture sub-modules.
|
|
4898
|
+
*/
|
|
4899
|
+
uninstall() {
|
|
4900
|
+
if (!this.installed) return;
|
|
4901
|
+
for (const cleanup of this.cleanups) {
|
|
4902
|
+
cleanup();
|
|
4903
|
+
}
|
|
4904
|
+
this.cleanups = [];
|
|
4905
|
+
this.installed = false;
|
|
4906
|
+
}
|
|
4907
|
+
// -------------------------------------------------------------------------
|
|
4908
|
+
// Manual event reporting (for events that can't be auto-captured)
|
|
4909
|
+
// -------------------------------------------------------------------------
|
|
4910
|
+
reportReactError(error, errorInfo) {
|
|
4911
|
+
const event = {
|
|
4912
|
+
type: "react-error",
|
|
4913
|
+
timestamp: Date.now(),
|
|
4914
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4915
|
+
message: error.message,
|
|
4916
|
+
stack: error.stack,
|
|
4917
|
+
componentStack: errorInfo.componentStack
|
|
4918
|
+
};
|
|
4919
|
+
this.buffer.push(event);
|
|
4920
|
+
this.trim();
|
|
4921
|
+
this.onEvent?.(event);
|
|
4922
|
+
}
|
|
4923
|
+
reportWsStateChange(prev, next, reconnectAttempt) {
|
|
4924
|
+
if (next === "disconnected" || next === "error") {
|
|
4925
|
+
const event = {
|
|
4926
|
+
type: "ws-disconnection",
|
|
4927
|
+
timestamp: Date.now(),
|
|
4928
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4929
|
+
previousState: prev,
|
|
4930
|
+
newState: next,
|
|
4931
|
+
reconnectAttempt
|
|
4932
|
+
};
|
|
4933
|
+
this.buffer.push(event);
|
|
4934
|
+
this.trim();
|
|
4935
|
+
this.onEvent?.(event);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
// -------------------------------------------------------------------------
|
|
4939
|
+
// Query methods
|
|
4940
|
+
// -------------------------------------------------------------------------
|
|
4941
|
+
getSince(ts) {
|
|
4942
|
+
return this.buffer.filter((e) => e.timestamp >= ts);
|
|
4943
|
+
}
|
|
4944
|
+
getRecent(n = 50) {
|
|
4945
|
+
return this.buffer.slice(-n);
|
|
4946
|
+
}
|
|
4947
|
+
getByType(type) {
|
|
4948
|
+
return this.buffer.filter((e) => e.type === type);
|
|
4949
|
+
}
|
|
4950
|
+
/**
|
|
4951
|
+
* Get console errors since a timestamp (backward-compat for ActionExecutor).
|
|
4952
|
+
*/
|
|
4953
|
+
getConsoleSince(ts) {
|
|
4954
|
+
return this.buffer.filter((e) => (e.type === "console" || e.type === "hmr") && e.timestamp >= ts).map((e) => ({
|
|
4955
|
+
timestamp: e.timestamp,
|
|
4956
|
+
level: e.type === "hmr" ? e.level === "warning" ? "warn" : e.level : e.level,
|
|
4957
|
+
message: e.message,
|
|
4958
|
+
stack: e.stack
|
|
4959
|
+
}));
|
|
4960
|
+
}
|
|
4961
|
+
/**
|
|
4962
|
+
* Get recent console errors (backward-compat for ActionExecutor).
|
|
4963
|
+
*/
|
|
4964
|
+
getConsoleRecent(n = 50) {
|
|
4965
|
+
return this.buffer.filter((e) => e.type === "console" || e.type === "hmr").slice(-n).map((e) => ({
|
|
4966
|
+
timestamp: e.timestamp,
|
|
4967
|
+
level: e.type === "hmr" ? e.level === "warning" ? "warn" : e.level : e.level,
|
|
4968
|
+
message: e.message,
|
|
4969
|
+
stack: e.stack
|
|
4970
|
+
}));
|
|
4971
|
+
}
|
|
4972
|
+
clear() {
|
|
4973
|
+
this.buffer = [];
|
|
4974
|
+
}
|
|
4975
|
+
trim() {
|
|
4976
|
+
if (this.buffer.length > this.maxEntries) {
|
|
4977
|
+
this.buffer = this.buffer.slice(-this.maxEntries);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
};
|
|
4981
|
+
var UIBridgeContext = react.createContext(null);
|
|
4982
|
+
function UIBridgeProvider({
|
|
4983
|
+
children,
|
|
4984
|
+
features = {},
|
|
4985
|
+
config = {},
|
|
4986
|
+
onEvent,
|
|
4987
|
+
onBrowserEvent,
|
|
4988
|
+
browserCaptureConfig
|
|
4989
|
+
}) {
|
|
4990
|
+
const registryRef = react.useRef(null);
|
|
4991
|
+
const renderLogRef = react.useRef(null);
|
|
4992
|
+
const metricsRef = react.useRef(null);
|
|
4993
|
+
const browserCaptureRef = react.useRef(null);
|
|
4994
|
+
const wsClientRef = react.useRef(null);
|
|
4995
|
+
const [wsConnectionState, setWsConnectionState] = react.useState("disconnected");
|
|
4996
|
+
const prevWsStateRef = react.useRef("disconnected");
|
|
4997
|
+
if (!registryRef.current) {
|
|
4998
|
+
registryRef.current = new UIBridgeRegistry({
|
|
4999
|
+
verbose: config.verbose,
|
|
5000
|
+
onEvent
|
|
5001
|
+
});
|
|
5002
|
+
setGlobalRegistry(registryRef.current);
|
|
5003
|
+
if (features.renderLog) {
|
|
5004
|
+
renderLogRef.current = createRenderLogManager({
|
|
5005
|
+
maxEntries: config.maxLogEntries
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
if (features.debug) {
|
|
5009
|
+
metricsRef.current = createMetricsCollector();
|
|
5010
|
+
}
|
|
5011
|
+
browserCaptureRef.current = new BrowserEventCapture(browserCaptureConfig);
|
|
5012
|
+
browserCaptureRef.current.install();
|
|
5013
|
+
if (config.websocket) {
|
|
5014
|
+
const wsPort = config.websocketPort || config.serverPort || 9876;
|
|
5015
|
+
const wsUrl = `ws://localhost:${wsPort}`;
|
|
5016
|
+
wsClientRef.current = createWSClient({
|
|
3605
5017
|
url: wsUrl,
|
|
3606
5018
|
autoReconnect: true,
|
|
3607
5019
|
reconnectDelay: 1e3,
|
|
@@ -3609,12 +5021,27 @@ function UIBridgeProvider({
|
|
|
3609
5021
|
pingInterval: 3e4
|
|
3610
5022
|
});
|
|
3611
5023
|
}
|
|
5024
|
+
if (typeof window !== "undefined") {
|
|
5025
|
+
const w = window;
|
|
5026
|
+
if (!w.__UI_BRIDGE__) {
|
|
5027
|
+
w.__UI_BRIDGE__ = {};
|
|
5028
|
+
}
|
|
5029
|
+
w.__UI_BRIDGE__.specs = {
|
|
5030
|
+
getGlobalSpecStore
|
|
5031
|
+
};
|
|
5032
|
+
w.__UI_BRIDGE__.browserCapture = browserCaptureRef.current;
|
|
5033
|
+
w.__UI_BRIDGE__.consoleCapture = browserCaptureRef.current;
|
|
5034
|
+
}
|
|
3612
5035
|
}
|
|
3613
5036
|
const registry = registryRef.current;
|
|
3614
5037
|
const renderLog = renderLogRef.current || void 0;
|
|
3615
5038
|
const metrics = metricsRef.current || void 0;
|
|
3616
5039
|
const wsClient = wsClientRef.current || void 0;
|
|
3617
|
-
const
|
|
5040
|
+
const browserCapture = browserCaptureRef.current || void 0;
|
|
5041
|
+
const executor = react.useMemo(
|
|
5042
|
+
() => createActionExecutor(registry, browserCapture),
|
|
5043
|
+
[registry, browserCapture]
|
|
5044
|
+
);
|
|
3618
5045
|
const workflowEngine = react.useMemo(
|
|
3619
5046
|
() => createWorkflowEngine(registry, executor),
|
|
3620
5047
|
[registry, executor]
|
|
@@ -3641,13 +5068,21 @@ function UIBridgeProvider({
|
|
|
3641
5068
|
react.useEffect(() => {
|
|
3642
5069
|
if (!wsClient) return;
|
|
3643
5070
|
const unsubscribe = wsClient.onConnectionChange((state) => {
|
|
5071
|
+
const prev = prevWsStateRef.current;
|
|
5072
|
+
prevWsStateRef.current = state;
|
|
3644
5073
|
setWsConnectionState(state);
|
|
5074
|
+
browserCaptureRef.current?.reportWsStateChange(prev, state);
|
|
3645
5075
|
});
|
|
3646
5076
|
return unsubscribe;
|
|
3647
5077
|
}, [wsClient]);
|
|
5078
|
+
react.useEffect(() => {
|
|
5079
|
+
browserCaptureRef.current?.setOnEvent(onBrowserEvent ?? null);
|
|
5080
|
+
}, [onBrowserEvent]);
|
|
3648
5081
|
react.useEffect(() => {
|
|
3649
5082
|
return () => {
|
|
3650
5083
|
renderLog?.stop();
|
|
5084
|
+
browserCaptureRef.current?.setOnEvent(null);
|
|
5085
|
+
browserCaptureRef.current?.uninstall();
|
|
3651
5086
|
wsClient?.disconnect();
|
|
3652
5087
|
resetGlobalRegistry();
|
|
3653
5088
|
};
|
|
@@ -3682,6 +5117,10 @@ function UIBridgeProvider({
|
|
|
3682
5117
|
const getElements = react.useCallback(() => registry.getAllElements(), [registry]);
|
|
3683
5118
|
const getComponents = react.useCallback(() => registry.getAllComponents(), [registry]);
|
|
3684
5119
|
const createSnapshot = react.useCallback(() => registry.createSnapshot(), [registry]);
|
|
5120
|
+
const createSnapshotAsync = react.useCallback(
|
|
5121
|
+
(batchSize) => registry.createSnapshotAsync(batchSize),
|
|
5122
|
+
[registry]
|
|
5123
|
+
);
|
|
3685
5124
|
const on = react.useCallback(
|
|
3686
5125
|
(type, listener) => registry.on(type, listener),
|
|
3687
5126
|
[registry]
|
|
@@ -3704,6 +5143,7 @@ function UIBridgeProvider({
|
|
|
3704
5143
|
getElements,
|
|
3705
5144
|
getComponents,
|
|
3706
5145
|
createSnapshot,
|
|
5146
|
+
createSnapshotAsync,
|
|
3707
5147
|
on,
|
|
3708
5148
|
off,
|
|
3709
5149
|
initialized: true,
|
|
@@ -3725,6 +5165,7 @@ function UIBridgeProvider({
|
|
|
3725
5165
|
getElements,
|
|
3726
5166
|
getComponents,
|
|
3727
5167
|
createSnapshot,
|
|
5168
|
+
createSnapshotAsync,
|
|
3728
5169
|
on,
|
|
3729
5170
|
off,
|
|
3730
5171
|
wsConnect,
|
|
@@ -3838,11 +5279,31 @@ function useUIComponent(options) {
|
|
|
3838
5279
|
const registeredRef = react.useRef(false);
|
|
3839
5280
|
const actionsRef = react.useRef(options.actions || []);
|
|
3840
5281
|
const elementIdsRef = react.useRef(options.elementIds || []);
|
|
5282
|
+
const stateRef = react.useRef(options.state);
|
|
5283
|
+
const computedRef = react.useRef(options.computed);
|
|
3841
5284
|
const { id, name, description, autoRegister = true } = options;
|
|
3842
5285
|
react.useEffect(() => {
|
|
3843
5286
|
actionsRef.current = options.actions || [];
|
|
3844
5287
|
elementIdsRef.current = options.elementIds || [];
|
|
3845
|
-
|
|
5288
|
+
stateRef.current = options.state;
|
|
5289
|
+
computedRef.current = options.computed;
|
|
5290
|
+
}, [options.actions, options.elementIds, options.state, options.computed]);
|
|
5291
|
+
const createGetComputed = react.useCallback(() => {
|
|
5292
|
+
return () => {
|
|
5293
|
+
const computed = computedRef.current;
|
|
5294
|
+
if (!computed) return {};
|
|
5295
|
+
const result = {};
|
|
5296
|
+
for (const [key, def] of Object.entries(computed)) {
|
|
5297
|
+
try {
|
|
5298
|
+
const getter = typeof def === "function" ? def : def.getter;
|
|
5299
|
+
result[key] = getter();
|
|
5300
|
+
} catch {
|
|
5301
|
+
result[key] = void 0;
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
return result;
|
|
5305
|
+
};
|
|
5306
|
+
}, []);
|
|
3846
5307
|
const register = react.useCallback(() => {
|
|
3847
5308
|
if (!bridge || registeredRef.current) return;
|
|
3848
5309
|
bridge.registry.registerComponent(id, {
|
|
@@ -3854,10 +5315,12 @@ function useUIComponent(options) {
|
|
|
3854
5315
|
description: a.description,
|
|
3855
5316
|
handler: a.handler
|
|
3856
5317
|
})),
|
|
3857
|
-
elementIds: elementIdsRef.current
|
|
5318
|
+
elementIds: elementIdsRef.current,
|
|
5319
|
+
getState: stateRef.current,
|
|
5320
|
+
getComputed: createGetComputed()
|
|
3858
5321
|
});
|
|
3859
5322
|
registeredRef.current = true;
|
|
3860
|
-
}, [bridge, id, name, description]);
|
|
5323
|
+
}, [bridge, id, name, description, createGetComputed]);
|
|
3861
5324
|
const unregister = react.useCallback(() => {
|
|
3862
5325
|
if (!bridge || !registeredRef.current) return;
|
|
3863
5326
|
bridge.registry.unregisterComponent(id);
|
|
@@ -3942,6 +5405,20 @@ function useUIBridge() {
|
|
|
3942
5405
|
}
|
|
3943
5406
|
return context.createSnapshot();
|
|
3944
5407
|
}, [context]);
|
|
5408
|
+
const createSnapshotAsync = react.useCallback(
|
|
5409
|
+
async (batchSize) => {
|
|
5410
|
+
if (!context) {
|
|
5411
|
+
return {
|
|
5412
|
+
timestamp: Date.now(),
|
|
5413
|
+
elements: [],
|
|
5414
|
+
components: [],
|
|
5415
|
+
workflows: []
|
|
5416
|
+
};
|
|
5417
|
+
}
|
|
5418
|
+
return context.createSnapshotAsync(batchSize);
|
|
5419
|
+
},
|
|
5420
|
+
[context]
|
|
5421
|
+
);
|
|
3945
5422
|
const executeAction = react.useCallback(
|
|
3946
5423
|
async (elementId, request) => {
|
|
3947
5424
|
if (!context) {
|
|
@@ -4063,6 +5540,7 @@ function useUIBridge() {
|
|
|
4063
5540
|
components,
|
|
4064
5541
|
workflows,
|
|
4065
5542
|
createSnapshot,
|
|
5543
|
+
createSnapshotAsync,
|
|
4066
5544
|
executeAction,
|
|
4067
5545
|
executeComponentAction,
|
|
4068
5546
|
find,
|
|
@@ -4478,6 +5956,305 @@ function useNavigationPath(targetStates) {
|
|
|
4478
5956
|
return bridge.registry.findPath(targetStates);
|
|
4479
5957
|
}, [bridge, targetStates]);
|
|
4480
5958
|
}
|
|
5959
|
+
|
|
5960
|
+
// src/react/content-discovery.ts
|
|
5961
|
+
var CONTENT_SELECTORS = [
|
|
5962
|
+
"h1",
|
|
5963
|
+
"h2",
|
|
5964
|
+
"h3",
|
|
5965
|
+
"h4",
|
|
5966
|
+
"h5",
|
|
5967
|
+
"h6",
|
|
5968
|
+
"p",
|
|
5969
|
+
"li",
|
|
5970
|
+
"td",
|
|
5971
|
+
"th",
|
|
5972
|
+
"label:not([for])",
|
|
5973
|
+
"figcaption",
|
|
5974
|
+
"caption",
|
|
5975
|
+
"blockquote",
|
|
5976
|
+
"pre",
|
|
5977
|
+
"code",
|
|
5978
|
+
"dd",
|
|
5979
|
+
"dt",
|
|
5980
|
+
'[role="heading"]',
|
|
5981
|
+
'[role="status"]',
|
|
5982
|
+
'[role="alert"]',
|
|
5983
|
+
"[aria-live]",
|
|
5984
|
+
"legend",
|
|
5985
|
+
"summary",
|
|
5986
|
+
"[data-content-role]"
|
|
5987
|
+
];
|
|
5988
|
+
var CONTENT_EXCLUDE_SELECTORS = [
|
|
5989
|
+
"script",
|
|
5990
|
+
"style",
|
|
5991
|
+
"noscript",
|
|
5992
|
+
"template",
|
|
5993
|
+
'[aria-hidden="true"]',
|
|
5994
|
+
"[data-no-register]",
|
|
5995
|
+
".sr-only",
|
|
5996
|
+
".visually-hidden"
|
|
5997
|
+
];
|
|
5998
|
+
function getDirectTextContent(element) {
|
|
5999
|
+
let text = "";
|
|
6000
|
+
for (const node of element.childNodes) {
|
|
6001
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
6002
|
+
text += node.textContent || "";
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
return text.trim();
|
|
6006
|
+
}
|
|
6007
|
+
var SEMANTIC_CONTENT_TAGS = /* @__PURE__ */ new Set([
|
|
6008
|
+
"h1",
|
|
6009
|
+
"h2",
|
|
6010
|
+
"h3",
|
|
6011
|
+
"h4",
|
|
6012
|
+
"h5",
|
|
6013
|
+
"h6",
|
|
6014
|
+
"p",
|
|
6015
|
+
"li",
|
|
6016
|
+
"td",
|
|
6017
|
+
"th",
|
|
6018
|
+
"label",
|
|
6019
|
+
"figcaption",
|
|
6020
|
+
"caption",
|
|
6021
|
+
"blockquote",
|
|
6022
|
+
"pre",
|
|
6023
|
+
"code",
|
|
6024
|
+
"dd",
|
|
6025
|
+
"dt",
|
|
6026
|
+
"legend",
|
|
6027
|
+
"summary"
|
|
6028
|
+
]);
|
|
6029
|
+
function isNoise(element) {
|
|
6030
|
+
const text = getDirectTextContent(element);
|
|
6031
|
+
const tag = element.tagName.toLowerCase();
|
|
6032
|
+
if (!text && element.children.length > 0 && !SEMANTIC_CONTENT_TAGS.has(tag) && !element.hasAttribute("data-content-role")) {
|
|
6033
|
+
return true;
|
|
6034
|
+
}
|
|
6035
|
+
const fullText = element.textContent?.trim() || "";
|
|
6036
|
+
if (fullText.length === 1 && !/\w/.test(fullText)) {
|
|
6037
|
+
return true;
|
|
6038
|
+
}
|
|
6039
|
+
return false;
|
|
6040
|
+
}
|
|
6041
|
+
function isContentVisible(element) {
|
|
6042
|
+
const style = window.getComputedStyle(element);
|
|
6043
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
6044
|
+
return false;
|
|
6045
|
+
}
|
|
6046
|
+
if (parseFloat(style.opacity) === 0) {
|
|
6047
|
+
return false;
|
|
6048
|
+
}
|
|
6049
|
+
const rect = element.getBoundingClientRect();
|
|
6050
|
+
return rect.width > 0 && rect.height > 0;
|
|
6051
|
+
}
|
|
6052
|
+
function shouldRegisterContent(element, options = {}, registeredIds) {
|
|
6053
|
+
const minTextLength = options.minTextLength ?? 1;
|
|
6054
|
+
const excludeSelectors = [
|
|
6055
|
+
...CONTENT_EXCLUDE_SELECTORS,
|
|
6056
|
+
...options.excludeContentSelectors || []
|
|
6057
|
+
];
|
|
6058
|
+
for (const sel of excludeSelectors) {
|
|
6059
|
+
if (element.matches(sel)) {
|
|
6060
|
+
return false;
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
if (!isContentVisible(element)) {
|
|
6064
|
+
return false;
|
|
6065
|
+
}
|
|
6066
|
+
const text = element.textContent?.trim() || "";
|
|
6067
|
+
if (text.length < minTextLength) {
|
|
6068
|
+
return false;
|
|
6069
|
+
}
|
|
6070
|
+
if (isNoise(element)) {
|
|
6071
|
+
return false;
|
|
6072
|
+
}
|
|
6073
|
+
if (isInteractiveElement(element)) {
|
|
6074
|
+
return false;
|
|
6075
|
+
}
|
|
6076
|
+
const id = generateContentId(element);
|
|
6077
|
+
if (registeredIds.has(id)) {
|
|
6078
|
+
return false;
|
|
6079
|
+
}
|
|
6080
|
+
if (options.contentRoles && options.contentRoles.length > 0) {
|
|
6081
|
+
const metadata = inferContentMetadata(element);
|
|
6082
|
+
if (!options.contentRoles.includes(metadata.contentRole)) {
|
|
6083
|
+
return false;
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
return true;
|
|
6087
|
+
}
|
|
6088
|
+
function isInteractiveElement(element) {
|
|
6089
|
+
const tag = element.tagName.toLowerCase();
|
|
6090
|
+
const interactiveTags = ["button", "input", "select", "textarea", "a"];
|
|
6091
|
+
if (interactiveTags.includes(tag)) return true;
|
|
6092
|
+
const role = element.getAttribute("role");
|
|
6093
|
+
const interactiveRoles = [
|
|
6094
|
+
"button",
|
|
6095
|
+
"link",
|
|
6096
|
+
"checkbox",
|
|
6097
|
+
"radio",
|
|
6098
|
+
"menuitem",
|
|
6099
|
+
"tab",
|
|
6100
|
+
"switch",
|
|
6101
|
+
"slider",
|
|
6102
|
+
"spinbutton",
|
|
6103
|
+
"combobox",
|
|
6104
|
+
"listbox",
|
|
6105
|
+
"option",
|
|
6106
|
+
"textbox"
|
|
6107
|
+
];
|
|
6108
|
+
if (role && interactiveRoles.includes(role)) return true;
|
|
6109
|
+
if (element.getAttribute("contenteditable") === "true") return true;
|
|
6110
|
+
if (element.hasAttribute("data-ui-element")) return true;
|
|
6111
|
+
return false;
|
|
6112
|
+
}
|
|
6113
|
+
function inferContentType(element) {
|
|
6114
|
+
const explicitRole = element.getAttribute("data-content-role");
|
|
6115
|
+
if (explicitRole) return roleToContentType(explicitRole);
|
|
6116
|
+
const tag = element.tagName.toLowerCase();
|
|
6117
|
+
const role = element.getAttribute("role");
|
|
6118
|
+
if (role === "heading") return "heading";
|
|
6119
|
+
if (role === "status") return "status-message";
|
|
6120
|
+
if (role === "alert") return "status-message";
|
|
6121
|
+
if (/^h[1-6]$/.test(tag)) return "heading";
|
|
6122
|
+
if (tag === "p") return "paragraph";
|
|
6123
|
+
if (tag === "li") return "list-item";
|
|
6124
|
+
if (tag === "td") return "table-cell";
|
|
6125
|
+
if (tag === "th") return "table-header";
|
|
6126
|
+
if (tag === "label") return "label";
|
|
6127
|
+
if (tag === "figcaption" || tag === "caption") return "caption";
|
|
6128
|
+
if (tag === "blockquote") return "blockquote";
|
|
6129
|
+
if (tag === "pre" || tag === "code") return "code-block";
|
|
6130
|
+
if (tag === "dd") return "description-text";
|
|
6131
|
+
if (tag === "dt") return "label";
|
|
6132
|
+
if (tag === "legend") return "label";
|
|
6133
|
+
if (tag === "summary") return "label";
|
|
6134
|
+
if (element.hasAttribute("aria-live")) return "status-message";
|
|
6135
|
+
const classList = element.className?.toLowerCase() || "";
|
|
6136
|
+
if (classList.includes("badge")) return "badge";
|
|
6137
|
+
if (classList.includes("status")) return "status-message";
|
|
6138
|
+
if (classList.includes("metric") || classList.includes("stat")) return "metric-value";
|
|
6139
|
+
return "content-generic";
|
|
6140
|
+
}
|
|
6141
|
+
function roleToContentType(role) {
|
|
6142
|
+
const map = {
|
|
6143
|
+
heading: "heading",
|
|
6144
|
+
"body-text": "paragraph",
|
|
6145
|
+
"list-item": "list-item",
|
|
6146
|
+
"table-cell": "table-cell",
|
|
6147
|
+
"table-header": "table-header",
|
|
6148
|
+
label: "label",
|
|
6149
|
+
caption: "caption",
|
|
6150
|
+
quote: "blockquote",
|
|
6151
|
+
code: "code-block",
|
|
6152
|
+
badge: "badge",
|
|
6153
|
+
status: "status-message",
|
|
6154
|
+
metric: "metric-value",
|
|
6155
|
+
description: "description-text",
|
|
6156
|
+
navigation: "nav-text",
|
|
6157
|
+
generic: "content-generic"
|
|
6158
|
+
};
|
|
6159
|
+
return map[role] ?? "content-generic";
|
|
6160
|
+
}
|
|
6161
|
+
function contentTypeToRole(contentType) {
|
|
6162
|
+
const map = {
|
|
6163
|
+
heading: "heading",
|
|
6164
|
+
paragraph: "body-text",
|
|
6165
|
+
"list-item": "list-item",
|
|
6166
|
+
"table-cell": "table-cell",
|
|
6167
|
+
"table-header": "table-header",
|
|
6168
|
+
label: "label",
|
|
6169
|
+
caption: "caption",
|
|
6170
|
+
blockquote: "quote",
|
|
6171
|
+
"code-block": "code",
|
|
6172
|
+
badge: "badge",
|
|
6173
|
+
"status-message": "status",
|
|
6174
|
+
"metric-value": "metric",
|
|
6175
|
+
"description-text": "description",
|
|
6176
|
+
"nav-text": "navigation",
|
|
6177
|
+
"content-generic": "generic"
|
|
6178
|
+
};
|
|
6179
|
+
return map[contentType];
|
|
6180
|
+
}
|
|
6181
|
+
function inferContentMetadata(element) {
|
|
6182
|
+
const contentType = inferContentType(element);
|
|
6183
|
+
const explicitRole = element.getAttribute("data-content-role");
|
|
6184
|
+
const contentRole = explicitRole ? explicitRole : contentTypeToRole(contentType);
|
|
6185
|
+
const metadata = {
|
|
6186
|
+
contentRole
|
|
6187
|
+
};
|
|
6188
|
+
const explicitLevel = element.getAttribute("data-content-level");
|
|
6189
|
+
const tag = element.tagName.toLowerCase();
|
|
6190
|
+
const role = element.getAttribute("role");
|
|
6191
|
+
if (explicitLevel) {
|
|
6192
|
+
metadata.headingLevel = parseInt(explicitLevel, 10);
|
|
6193
|
+
} else if (/^h([1-6])$/.test(tag)) {
|
|
6194
|
+
metadata.headingLevel = parseInt(tag[1], 10);
|
|
6195
|
+
} else if (role === "heading") {
|
|
6196
|
+
const ariaLevel = element.getAttribute("aria-level");
|
|
6197
|
+
metadata.headingLevel = ariaLevel ? parseInt(ariaLevel, 10) : 2;
|
|
6198
|
+
}
|
|
6199
|
+
if (element.hasAttribute("aria-live") || role === "status" || role === "alert") {
|
|
6200
|
+
metadata.dynamic = true;
|
|
6201
|
+
}
|
|
6202
|
+
if (tag === "td" || tag === "th") {
|
|
6203
|
+
const row = element.closest("tr");
|
|
6204
|
+
const table = element.closest("table");
|
|
6205
|
+
if (row && table) {
|
|
6206
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
6207
|
+
const rowIndex = rows.indexOf(row);
|
|
6208
|
+
const cells = Array.from(row.children);
|
|
6209
|
+
const colIndex = cells.indexOf(element);
|
|
6210
|
+
metadata.structuralContext = `table > row ${rowIndex} > col ${colIndex}`;
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
if (metadata.dynamic) {
|
|
6214
|
+
const text = element.textContent?.trim() || "";
|
|
6215
|
+
if (text.length > 10) {
|
|
6216
|
+
metadata.stableTextPrefix = text.substring(0, 10);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
return metadata;
|
|
6220
|
+
}
|
|
6221
|
+
function slugify(text, maxLength = 30) {
|
|
6222
|
+
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, maxLength).replace(/-+$/, "");
|
|
6223
|
+
}
|
|
6224
|
+
function generateContentId(element) {
|
|
6225
|
+
const explicitId = element.getAttribute("data-content-id");
|
|
6226
|
+
if (explicitId) return explicitId;
|
|
6227
|
+
const contentType = inferContentType(element);
|
|
6228
|
+
const tag = element.tagName.toLowerCase();
|
|
6229
|
+
const contentLabel = element.getAttribute("data-content-label");
|
|
6230
|
+
if (contentType === "heading") {
|
|
6231
|
+
const level = element.getAttribute("data-content-level") || (/^h([1-6])$/.test(tag) ? tag[1] : element.getAttribute("aria-level") || "2");
|
|
6232
|
+
const text = contentLabel || element.textContent?.trim() || "";
|
|
6233
|
+
const slug = slugify(text);
|
|
6234
|
+
return `heading-${level}-${slug || "untitled"}`;
|
|
6235
|
+
}
|
|
6236
|
+
if (contentType === "table-cell" || contentType === "table-header") {
|
|
6237
|
+
const row = element.closest("tr");
|
|
6238
|
+
const table = element.closest("table");
|
|
6239
|
+
if (row && table) {
|
|
6240
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
6241
|
+
const rowIndex = rows.indexOf(row);
|
|
6242
|
+
const cells = Array.from(row.children);
|
|
6243
|
+
const colIndex = cells.indexOf(element);
|
|
6244
|
+
const tableAnchor = table.getAttribute("data-ui-id") || table.getAttribute("data-testid") || table.id || "table";
|
|
6245
|
+
return `cell-r${rowIndex}-c${colIndex}-${tableAnchor}`;
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
const parent = element.parentElement;
|
|
6249
|
+
const anchorId = parent?.getAttribute("data-ui-id") || parent?.getAttribute("data-testid") || parent?.id || "";
|
|
6250
|
+
const siblings = parent ? Array.from(parent.querySelectorAll(`:scope > ${tag}`)) : [];
|
|
6251
|
+
const siblingIndex = siblings.indexOf(element);
|
|
6252
|
+
const textSlug = slugify(contentLabel || element.textContent?.trim() || "", 20);
|
|
6253
|
+
const anchor = anchorId ? slugify(anchorId, 15) : textSlug;
|
|
6254
|
+
return `content-${contentType}-${anchor || "unknown"}-${siblingIndex >= 0 ? siblingIndex : 0}`;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
// src/react/useAutoRegister.ts
|
|
4481
6258
|
var INTERACTIVE_SELECTORS2 = [
|
|
4482
6259
|
"a[href]",
|
|
4483
6260
|
"button",
|
|
@@ -4672,12 +6449,17 @@ function useAutoRegister(options = {}) {
|
|
|
4672
6449
|
excludeSelectors = [],
|
|
4673
6450
|
generateId: customGenerateId,
|
|
4674
6451
|
onRegister,
|
|
4675
|
-
onUnregister
|
|
6452
|
+
onUnregister,
|
|
6453
|
+
contentDiscovery
|
|
4676
6454
|
} = options;
|
|
6455
|
+
const contentEnabled = contentDiscovery?.enabled !== false;
|
|
4677
6456
|
const bridge = useUIBridgeOptional();
|
|
4678
6457
|
const registeredElementsRef = react.useRef(/* @__PURE__ */ new Map());
|
|
6458
|
+
const registeredContentElementsRef = react.useRef(/* @__PURE__ */ new Map());
|
|
4679
6459
|
const pendingRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
6460
|
+
const pendingContentRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
4680
6461
|
const debounceTimeoutRef = react.useRef(null);
|
|
6462
|
+
const contentDebounceTimeoutRef = react.useRef(null);
|
|
4681
6463
|
const shouldRegister = react.useCallback(
|
|
4682
6464
|
(element) => {
|
|
4683
6465
|
if (!includeHidden && !isElementVisible2(element)) {
|
|
@@ -4745,6 +6527,41 @@ function useAutoRegister(options = {}) {
|
|
|
4745
6527
|
},
|
|
4746
6528
|
[bridge, onUnregister]
|
|
4747
6529
|
);
|
|
6530
|
+
const registerContentElement = react.useCallback(
|
|
6531
|
+
(element) => {
|
|
6532
|
+
if (!bridge?.registry || registeredContentElementsRef.current.has(element)) {
|
|
6533
|
+
return;
|
|
6534
|
+
}
|
|
6535
|
+
const maxElements = contentDiscovery?.maxContentElements ?? 500;
|
|
6536
|
+
if (registeredContentElementsRef.current.size >= maxElements) {
|
|
6537
|
+
return;
|
|
6538
|
+
}
|
|
6539
|
+
const id = generateContentId(element);
|
|
6540
|
+
const existing = bridge.registry.getElement(id);
|
|
6541
|
+
if (existing) {
|
|
6542
|
+
return;
|
|
6543
|
+
}
|
|
6544
|
+
const contentType = inferContentType(element);
|
|
6545
|
+
const metadata = inferContentMetadata(element);
|
|
6546
|
+
const label = element.getAttribute("data-content-label") || element.textContent?.trim().substring(0, 50) || void 0;
|
|
6547
|
+
bridge.registry.registerContentElement(id, element, {
|
|
6548
|
+
contentType,
|
|
6549
|
+
contentMetadata: metadata,
|
|
6550
|
+
label
|
|
6551
|
+
});
|
|
6552
|
+
registeredContentElementsRef.current.set(element, id);
|
|
6553
|
+
},
|
|
6554
|
+
[bridge, contentDiscovery?.maxContentElements]
|
|
6555
|
+
);
|
|
6556
|
+
const unregisterContentElement = react.useCallback(
|
|
6557
|
+
(element) => {
|
|
6558
|
+
const id = registeredContentElementsRef.current.get(element);
|
|
6559
|
+
if (!id || !bridge?.registry) return;
|
|
6560
|
+
bridge.registry.unregisterElement(id);
|
|
6561
|
+
registeredContentElementsRef.current.delete(element);
|
|
6562
|
+
},
|
|
6563
|
+
[bridge]
|
|
6564
|
+
);
|
|
4748
6565
|
const processPendingRegistrations = react.useCallback(() => {
|
|
4749
6566
|
pendingRegistrationsRef.current.forEach((element) => {
|
|
4750
6567
|
if (shouldRegister(element)) {
|
|
@@ -4753,6 +6570,15 @@ function useAutoRegister(options = {}) {
|
|
|
4753
6570
|
});
|
|
4754
6571
|
pendingRegistrationsRef.current.clear();
|
|
4755
6572
|
}, [shouldRegister, registerElement]);
|
|
6573
|
+
const processPendingContentRegistrations = react.useCallback(() => {
|
|
6574
|
+
const registeredIds = new Set(registeredContentElementsRef.current.values());
|
|
6575
|
+
pendingContentRegistrationsRef.current.forEach((element) => {
|
|
6576
|
+
if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
|
|
6577
|
+
registerContentElement(element);
|
|
6578
|
+
}
|
|
6579
|
+
});
|
|
6580
|
+
pendingContentRegistrationsRef.current.clear();
|
|
6581
|
+
}, [contentDiscovery, registerContentElement]);
|
|
4756
6582
|
const queueRegistration = react.useCallback(
|
|
4757
6583
|
(element) => {
|
|
4758
6584
|
pendingRegistrationsRef.current.add(element);
|
|
@@ -4763,6 +6589,20 @@ function useAutoRegister(options = {}) {
|
|
|
4763
6589
|
},
|
|
4764
6590
|
[debounceMs, processPendingRegistrations]
|
|
4765
6591
|
);
|
|
6592
|
+
const queueContentRegistration = react.useCallback(
|
|
6593
|
+
(element) => {
|
|
6594
|
+
pendingContentRegistrationsRef.current.add(element);
|
|
6595
|
+
if (contentDebounceTimeoutRef.current) {
|
|
6596
|
+
clearTimeout(contentDebounceTimeoutRef.current);
|
|
6597
|
+
}
|
|
6598
|
+
const contentDebounceMs = contentDiscovery?.contentDebounceMs ?? 250;
|
|
6599
|
+
contentDebounceTimeoutRef.current = setTimeout(
|
|
6600
|
+
processPendingContentRegistrations,
|
|
6601
|
+
contentDebounceMs
|
|
6602
|
+
);
|
|
6603
|
+
},
|
|
6604
|
+
[contentDiscovery?.contentDebounceMs, processPendingContentRegistrations]
|
|
6605
|
+
);
|
|
4766
6606
|
const scanAndRegister = react.useCallback(
|
|
4767
6607
|
(rootElement) => {
|
|
4768
6608
|
const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
|
|
@@ -4772,8 +6612,28 @@ function useAutoRegister(options = {}) {
|
|
|
4772
6612
|
queueRegistration(element);
|
|
4773
6613
|
}
|
|
4774
6614
|
});
|
|
6615
|
+
if (contentEnabled) {
|
|
6616
|
+
const contentSelectors = [
|
|
6617
|
+
...CONTENT_SELECTORS,
|
|
6618
|
+
...contentDiscovery?.includeContentSelectors || []
|
|
6619
|
+
].join(", ");
|
|
6620
|
+
const contentElements = rootElement.querySelectorAll(contentSelectors);
|
|
6621
|
+
const registeredIds = new Set(registeredContentElementsRef.current.values());
|
|
6622
|
+
contentElements.forEach((element) => {
|
|
6623
|
+
if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
|
|
6624
|
+
queueContentRegistration(element);
|
|
6625
|
+
}
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
4775
6628
|
},
|
|
4776
|
-
[
|
|
6629
|
+
[
|
|
6630
|
+
includeSelectors,
|
|
6631
|
+
shouldRegister,
|
|
6632
|
+
queueRegistration,
|
|
6633
|
+
contentEnabled,
|
|
6634
|
+
contentDiscovery,
|
|
6635
|
+
queueContentRegistration
|
|
6636
|
+
]
|
|
4777
6637
|
);
|
|
4778
6638
|
const handleMutations = react.useCallback(
|
|
4779
6639
|
(mutations) => {
|
|
@@ -4791,6 +6651,22 @@ function useAutoRegister(options = {}) {
|
|
|
4791
6651
|
queueRegistration(descendant);
|
|
4792
6652
|
}
|
|
4793
6653
|
});
|
|
6654
|
+
if (contentEnabled) {
|
|
6655
|
+
const contentSelectors = [
|
|
6656
|
+
...CONTENT_SELECTORS,
|
|
6657
|
+
...contentDiscovery?.includeContentSelectors || []
|
|
6658
|
+
].join(", ");
|
|
6659
|
+
const registeredIds = new Set(registeredContentElementsRef.current.values());
|
|
6660
|
+
if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
|
|
6661
|
+
queueContentRegistration(element);
|
|
6662
|
+
}
|
|
6663
|
+
const contentDescendants = element.querySelectorAll(contentSelectors);
|
|
6664
|
+
contentDescendants.forEach((descendant) => {
|
|
6665
|
+
if (shouldRegisterContent(descendant, contentDiscovery, registeredIds)) {
|
|
6666
|
+
queueContentRegistration(descendant);
|
|
6667
|
+
}
|
|
6668
|
+
});
|
|
6669
|
+
}
|
|
4794
6670
|
}
|
|
4795
6671
|
});
|
|
4796
6672
|
mutation.removedNodes.forEach((node) => {
|
|
@@ -4799,17 +6675,32 @@ function useAutoRegister(options = {}) {
|
|
|
4799
6675
|
if (registeredElementsRef.current.has(element)) {
|
|
4800
6676
|
unregisterElement(element);
|
|
4801
6677
|
}
|
|
6678
|
+
if (registeredContentElementsRef.current.has(element)) {
|
|
6679
|
+
unregisterContentElement(element);
|
|
6680
|
+
}
|
|
4802
6681
|
const descendants = element.querySelectorAll("*");
|
|
4803
6682
|
descendants.forEach((descendant) => {
|
|
4804
6683
|
if (registeredElementsRef.current.has(descendant)) {
|
|
4805
6684
|
unregisterElement(descendant);
|
|
4806
6685
|
}
|
|
6686
|
+
if (registeredContentElementsRef.current.has(descendant)) {
|
|
6687
|
+
unregisterContentElement(descendant);
|
|
6688
|
+
}
|
|
4807
6689
|
});
|
|
4808
6690
|
}
|
|
4809
6691
|
});
|
|
4810
6692
|
});
|
|
4811
6693
|
},
|
|
4812
|
-
[
|
|
6694
|
+
[
|
|
6695
|
+
shouldRegister,
|
|
6696
|
+
queueRegistration,
|
|
6697
|
+
unregisterElement,
|
|
6698
|
+
includeSelectors,
|
|
6699
|
+
contentEnabled,
|
|
6700
|
+
contentDiscovery,
|
|
6701
|
+
queueContentRegistration,
|
|
6702
|
+
unregisterContentElement
|
|
6703
|
+
]
|
|
4813
6704
|
);
|
|
4814
6705
|
react.useEffect(() => {
|
|
4815
6706
|
if (!enabled || !bridge?.registry) return;
|
|
@@ -4825,10 +6716,17 @@ function useAutoRegister(options = {}) {
|
|
|
4825
6716
|
if (debounceTimeoutRef.current) {
|
|
4826
6717
|
clearTimeout(debounceTimeoutRef.current);
|
|
4827
6718
|
}
|
|
6719
|
+
if (contentDebounceTimeoutRef.current) {
|
|
6720
|
+
clearTimeout(contentDebounceTimeoutRef.current);
|
|
6721
|
+
}
|
|
4828
6722
|
registeredElementsRef.current.forEach((id, _element) => {
|
|
4829
6723
|
bridge.registry.unregisterElement(id);
|
|
4830
6724
|
});
|
|
4831
6725
|
registeredElementsRef.current.clear();
|
|
6726
|
+
registeredContentElementsRef.current.forEach((id, _element) => {
|
|
6727
|
+
bridge.registry.unregisterElement(id);
|
|
6728
|
+
});
|
|
6729
|
+
registeredContentElementsRef.current.clear();
|
|
4832
6730
|
};
|
|
4833
6731
|
}, [enabled, bridge, root, scanAndRegister, handleMutations]);
|
|
4834
6732
|
}
|
|
@@ -4843,7 +6741,8 @@ function AutoRegisterProvider({
|
|
|
4843
6741
|
excludeSelectors = [],
|
|
4844
6742
|
generateId: generateId4,
|
|
4845
6743
|
onRegister,
|
|
4846
|
-
onUnregister
|
|
6744
|
+
onUnregister,
|
|
6745
|
+
contentDiscovery
|
|
4847
6746
|
}) {
|
|
4848
6747
|
const containerRef = react.useRef(null);
|
|
4849
6748
|
useAutoRegister({
|
|
@@ -4856,7 +6755,8 @@ function AutoRegisterProvider({
|
|
|
4856
6755
|
excludeSelectors,
|
|
4857
6756
|
generateId: generateId4,
|
|
4858
6757
|
onRegister,
|
|
4859
|
-
onUnregister
|
|
6758
|
+
onUnregister,
|
|
6759
|
+
contentDiscovery
|
|
4860
6760
|
});
|
|
4861
6761
|
if (scopeToChildren) {
|
|
4862
6762
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: { display: "contents" }, children });
|
|
@@ -4864,6 +6764,253 @@ function AutoRegisterProvider({
|
|
|
4864
6764
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
4865
6765
|
}
|
|
4866
6766
|
|
|
6767
|
+
// src/annotations/types.ts
|
|
6768
|
+
var ANNOTATION_CONFIG_VERSION = "1.0.0";
|
|
6769
|
+
|
|
6770
|
+
// src/annotations/store.ts
|
|
6771
|
+
var AnnotationStore = class {
|
|
6772
|
+
constructor() {
|
|
6773
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6774
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
6775
|
+
}
|
|
6776
|
+
/**
|
|
6777
|
+
* Get an annotation by element ID.
|
|
6778
|
+
*/
|
|
6779
|
+
get(elementId) {
|
|
6780
|
+
return this.store.get(elementId);
|
|
6781
|
+
}
|
|
6782
|
+
/**
|
|
6783
|
+
* Get all annotations as a record.
|
|
6784
|
+
*/
|
|
6785
|
+
getAll() {
|
|
6786
|
+
const result = {};
|
|
6787
|
+
for (const [id, annotation] of this.store) {
|
|
6788
|
+
result[id] = annotation;
|
|
6789
|
+
}
|
|
6790
|
+
return result;
|
|
6791
|
+
}
|
|
6792
|
+
/**
|
|
6793
|
+
* Set an annotation for an element. Auto-sets `updatedAt`.
|
|
6794
|
+
*/
|
|
6795
|
+
set(elementId, annotation) {
|
|
6796
|
+
const updated = {
|
|
6797
|
+
...annotation,
|
|
6798
|
+
updatedAt: Date.now()
|
|
6799
|
+
};
|
|
6800
|
+
this.store.set(elementId, updated);
|
|
6801
|
+
this.emit({
|
|
6802
|
+
type: "annotation:set",
|
|
6803
|
+
elementId,
|
|
6804
|
+
annotation: updated,
|
|
6805
|
+
timestamp: Date.now()
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
/**
|
|
6809
|
+
* Delete an annotation by element ID.
|
|
6810
|
+
*
|
|
6811
|
+
* @returns true if the annotation existed and was deleted
|
|
6812
|
+
*/
|
|
6813
|
+
delete(elementId) {
|
|
6814
|
+
const existed = this.store.delete(elementId);
|
|
6815
|
+
if (existed) {
|
|
6816
|
+
this.emit({
|
|
6817
|
+
type: "annotation:deleted",
|
|
6818
|
+
elementId,
|
|
6819
|
+
timestamp: Date.now()
|
|
6820
|
+
});
|
|
6821
|
+
}
|
|
6822
|
+
return existed;
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* Check if an annotation exists for an element.
|
|
6826
|
+
*/
|
|
6827
|
+
has(elementId) {
|
|
6828
|
+
return this.store.has(elementId);
|
|
6829
|
+
}
|
|
6830
|
+
/**
|
|
6831
|
+
* Get the number of stored annotations.
|
|
6832
|
+
*/
|
|
6833
|
+
get count() {
|
|
6834
|
+
return this.store.size;
|
|
6835
|
+
}
|
|
6836
|
+
/**
|
|
6837
|
+
* Clear all annotations.
|
|
6838
|
+
*/
|
|
6839
|
+
clear() {
|
|
6840
|
+
this.store.clear();
|
|
6841
|
+
this.emit({
|
|
6842
|
+
type: "annotation:cleared",
|
|
6843
|
+
timestamp: Date.now()
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
/**
|
|
6847
|
+
* Import annotations from a config object.
|
|
6848
|
+
*
|
|
6849
|
+
* Merges with existing annotations (new values overwrite per element ID).
|
|
6850
|
+
*
|
|
6851
|
+
* @returns Number of annotations imported
|
|
6852
|
+
*
|
|
6853
|
+
* @example
|
|
6854
|
+
* ```ts
|
|
6855
|
+
* const config: AnnotationConfig = {
|
|
6856
|
+
* version: '1.0.0',
|
|
6857
|
+
* annotations: {
|
|
6858
|
+
* 'btn-1': { description: 'Submit button', tags: ['form'] },
|
|
6859
|
+
* 'input-1': { description: 'Name field' },
|
|
6860
|
+
* },
|
|
6861
|
+
* };
|
|
6862
|
+
* const count = store.importConfig(config); // 2
|
|
6863
|
+
* ```
|
|
6864
|
+
*/
|
|
6865
|
+
importConfig(config) {
|
|
6866
|
+
let count = 0;
|
|
6867
|
+
for (const [id, annotation] of Object.entries(config.annotations)) {
|
|
6868
|
+
this.store.set(id, {
|
|
6869
|
+
...annotation,
|
|
6870
|
+
updatedAt: annotation.updatedAt ?? Date.now()
|
|
6871
|
+
});
|
|
6872
|
+
count++;
|
|
6873
|
+
}
|
|
6874
|
+
this.emit({
|
|
6875
|
+
type: "annotation:imported",
|
|
6876
|
+
count,
|
|
6877
|
+
timestamp: Date.now()
|
|
6878
|
+
});
|
|
6879
|
+
return count;
|
|
6880
|
+
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Export all annotations as a config object.
|
|
6883
|
+
*
|
|
6884
|
+
* The returned object can be serialized to JSON and saved to a file,
|
|
6885
|
+
* then later re-imported with {@link importConfig}.
|
|
6886
|
+
*
|
|
6887
|
+
* @param metadata - Optional metadata to include (appName, description, etc.)
|
|
6888
|
+
* @returns AnnotationConfig with all current annotations
|
|
6889
|
+
*
|
|
6890
|
+
* @example
|
|
6891
|
+
* ```ts
|
|
6892
|
+
* const config = store.exportConfig({ appName: 'MyApp' });
|
|
6893
|
+
* // config.version === '1.0.0'
|
|
6894
|
+
* // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
|
|
6895
|
+
* // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
|
|
6896
|
+
*
|
|
6897
|
+
* // Save to file
|
|
6898
|
+
* fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
|
|
6899
|
+
* ```
|
|
6900
|
+
*/
|
|
6901
|
+
exportConfig(metadata) {
|
|
6902
|
+
return {
|
|
6903
|
+
version: ANNOTATION_CONFIG_VERSION,
|
|
6904
|
+
annotations: this.getAll(),
|
|
6905
|
+
metadata: {
|
|
6906
|
+
...metadata,
|
|
6907
|
+
exportedAt: Date.now()
|
|
6908
|
+
}
|
|
6909
|
+
};
|
|
6910
|
+
}
|
|
6911
|
+
/**
|
|
6912
|
+
* Compute annotation coverage against a set of known element IDs.
|
|
6913
|
+
*
|
|
6914
|
+
* Compares the store's annotations against the provided list of element IDs
|
|
6915
|
+
* to determine what percentage of elements have been annotated.
|
|
6916
|
+
*
|
|
6917
|
+
* @param allElementIds - Array of all known element IDs in the UI
|
|
6918
|
+
* @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
|
|
6919
|
+
*
|
|
6920
|
+
* @example
|
|
6921
|
+
* ```ts
|
|
6922
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
6923
|
+
* store.set('input-1', { description: 'Name' });
|
|
6924
|
+
*
|
|
6925
|
+
* const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
|
|
6926
|
+
* // coverage.totalElements === 4
|
|
6927
|
+
* // coverage.annotatedElements === 2
|
|
6928
|
+
* // coverage.coveragePercent === 50
|
|
6929
|
+
* // coverage.annotatedIds === ['btn-1', 'input-1']
|
|
6930
|
+
* // coverage.unannotatedIds === ['input-2', 'link-1']
|
|
6931
|
+
* ```
|
|
6932
|
+
*/
|
|
6933
|
+
getCoverage(allElementIds) {
|
|
6934
|
+
const annotatedIds = [];
|
|
6935
|
+
const unannotatedIds = [];
|
|
6936
|
+
for (const id of allElementIds) {
|
|
6937
|
+
if (this.store.has(id)) {
|
|
6938
|
+
annotatedIds.push(id);
|
|
6939
|
+
} else {
|
|
6940
|
+
unannotatedIds.push(id);
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
const total = allElementIds.length;
|
|
6944
|
+
return {
|
|
6945
|
+
totalElements: total,
|
|
6946
|
+
annotatedElements: annotatedIds.length,
|
|
6947
|
+
coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
|
|
6948
|
+
annotatedIds,
|
|
6949
|
+
unannotatedIds,
|
|
6950
|
+
timestamp: Date.now()
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6953
|
+
/**
|
|
6954
|
+
* Subscribe to annotation events.
|
|
6955
|
+
*
|
|
6956
|
+
* The listener is called whenever annotations are set, deleted, imported,
|
|
6957
|
+
* or cleared. Returns an unsubscribe function to stop listening.
|
|
6958
|
+
*
|
|
6959
|
+
* @param listener - Callback function receiving {@link AnnotationEvent} objects
|
|
6960
|
+
* @returns Unsubscribe function - call it to remove the listener
|
|
6961
|
+
*
|
|
6962
|
+
* @example
|
|
6963
|
+
* ```ts
|
|
6964
|
+
* const unsubscribe = store.on((event) => {
|
|
6965
|
+
* if (event.type === 'annotation:set') {
|
|
6966
|
+
* console.log(`Element ${event.elementId} annotated:`, event.annotation);
|
|
6967
|
+
* }
|
|
6968
|
+
* });
|
|
6969
|
+
*
|
|
6970
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
6971
|
+
* // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
|
|
6972
|
+
*
|
|
6973
|
+
* unsubscribe(); // Stop listening
|
|
6974
|
+
* ```
|
|
6975
|
+
*/
|
|
6976
|
+
on(listener) {
|
|
6977
|
+
this.listeners.add(listener);
|
|
6978
|
+
return () => {
|
|
6979
|
+
this.listeners.delete(listener);
|
|
6980
|
+
};
|
|
6981
|
+
}
|
|
6982
|
+
/**
|
|
6983
|
+
* Emit an event to all listeners.
|
|
6984
|
+
*/
|
|
6985
|
+
emit(event) {
|
|
6986
|
+
for (const listener of this.listeners) {
|
|
6987
|
+
try {
|
|
6988
|
+
listener(event);
|
|
6989
|
+
} catch {
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
};
|
|
6994
|
+
var globalStore2 = null;
|
|
6995
|
+
function getGlobalAnnotationStore() {
|
|
6996
|
+
if (!globalStore2) {
|
|
6997
|
+
globalStore2 = new AnnotationStore();
|
|
6998
|
+
}
|
|
6999
|
+
return globalStore2;
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
// src/react/useUIAnnotation.ts
|
|
7003
|
+
function useUIAnnotation(elementId, annotation) {
|
|
7004
|
+
const serializedRef = react.useRef("");
|
|
7005
|
+
react.useEffect(() => {
|
|
7006
|
+
const serialized = JSON.stringify(annotation);
|
|
7007
|
+
if (serialized !== serializedRef.current) {
|
|
7008
|
+
serializedRef.current = serialized;
|
|
7009
|
+
getGlobalAnnotationStore().set(elementId, annotation);
|
|
7010
|
+
}
|
|
7011
|
+
}, [elementId, annotation]);
|
|
7012
|
+
}
|
|
7013
|
+
|
|
4867
7014
|
exports.AutoRegisterProvider = AutoRegisterProvider;
|
|
4868
7015
|
exports.UIBridgeProvider = UIBridgeProvider;
|
|
4869
7016
|
exports.useActiveStates = useActiveStates;
|
|
@@ -4873,6 +7020,7 @@ exports.useCanNavigateTo = useCanNavigateTo;
|
|
|
4873
7020
|
exports.useNavigationPath = useNavigationPath;
|
|
4874
7021
|
exports.useStateSnapshot = useStateSnapshot;
|
|
4875
7022
|
exports.useTransitions = useTransitions;
|
|
7023
|
+
exports.useUIAnnotation = useUIAnnotation;
|
|
4876
7024
|
exports.useUIBridge = useUIBridge;
|
|
4877
7025
|
exports.useUIBridgeContext = useUIBridgeContext;
|
|
4878
7026
|
exports.useUIBridgeOptional = useUIBridgeOptional;
|