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