@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.
Files changed (140) hide show
  1. package/dist/ai/index.d.mts +312 -155
  2. package/dist/ai/index.d.ts +312 -155
  3. package/dist/ai/index.js +2363 -67
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/index.mjs +2328 -68
  6. package/dist/ai/index.mjs.map +1 -1
  7. package/dist/annotations/index.d.mts +218 -0
  8. package/dist/annotations/index.d.ts +218 -0
  9. package/dist/annotations/index.js +246 -0
  10. package/dist/annotations/index.js.map +1 -0
  11. package/dist/annotations/index.mjs +241 -0
  12. package/dist/annotations/index.mjs.map +1 -0
  13. package/dist/assertions-BSR3afVr.d.ts +161 -0
  14. package/dist/assertions-CTw1hfOx.d.mts +161 -0
  15. package/dist/babel-plugin/index.js +504 -0
  16. package/dist/babel-plugin/index.js.map +1 -0
  17. package/dist/babel-plugin/index.mjs +488 -0
  18. package/dist/babel-plugin/index.mjs.map +1 -0
  19. package/dist/browser-capture-Bms60T6f.d.mts +47 -0
  20. package/dist/browser-capture-CsTU29mb.d.ts +47 -0
  21. package/dist/control/index.d.mts +26 -7
  22. package/dist/control/index.d.ts +26 -7
  23. package/dist/control/index.js +276 -48
  24. package/dist/control/index.js.map +1 -1
  25. package/dist/control/index.mjs +276 -48
  26. package/dist/control/index.mjs.map +1 -1
  27. package/dist/core/index.d.mts +115 -44
  28. package/dist/core/index.d.ts +115 -44
  29. package/dist/core/index.js +0 -1560
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/index.mjs +1 -1549
  32. package/dist/core/index.mjs.map +1 -1
  33. package/dist/debug/index.d.mts +5 -3
  34. package/dist/debug/index.d.ts +5 -3
  35. package/dist/debug/index.js +925 -1
  36. package/dist/debug/index.js.map +1 -1
  37. package/dist/debug/index.mjs +924 -2
  38. package/dist/debug/index.mjs.map +1 -1
  39. package/dist/index.d.mts +13 -9
  40. package/dist/index.d.ts +13 -9
  41. package/dist/index.js +8310 -3777
  42. package/dist/index.js.map +1 -1
  43. package/dist/index.mjs +8246 -3766
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/{metrics-NC3csD0R.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
  46. package/dist/{metrics-C9XRi_mL.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
  47. package/dist/native/control/index.js +448 -0
  48. package/dist/native/control/index.js.map +1 -0
  49. package/dist/native/control/index.mjs +445 -0
  50. package/dist/native/control/index.mjs.map +1 -0
  51. package/dist/native/core/index.js +486 -0
  52. package/dist/native/core/index.js.map +1 -0
  53. package/dist/native/core/index.mjs +475 -0
  54. package/dist/native/core/index.mjs.map +1 -0
  55. package/dist/native/debug/index.js +408 -0
  56. package/dist/native/debug/index.js.map +1 -0
  57. package/dist/native/debug/index.mjs +406 -0
  58. package/dist/native/debug/index.mjs.map +1 -0
  59. package/dist/native/index.js +2232 -0
  60. package/dist/native/index.js.map +1 -0
  61. package/dist/native/index.mjs +2204 -0
  62. package/dist/native/index.mjs.map +1 -0
  63. package/dist/native/react/index.js +1377 -0
  64. package/dist/native/react/index.js.map +1 -0
  65. package/dist/native/react/index.mjs +1365 -0
  66. package/dist/native/react/index.mjs.map +1 -0
  67. package/dist/native/server/index.js +440 -0
  68. package/dist/native/server/index.js.map +1 -0
  69. package/dist/native/server/index.mjs +435 -0
  70. package/dist/native/server/index.mjs.map +1 -0
  71. package/dist/react/index.d.mts +121 -9
  72. package/dist/react/index.d.ts +121 -9
  73. package/dist/react/index.js +2239 -91
  74. package/dist/react/index.js.map +1 -1
  75. package/dist/react/index.mjs +2239 -92
  76. package/dist/react/index.mjs.map +1 -1
  77. package/dist/{registry-CIEDjbQ9.d.ts → registry-C6dDtn1v.d.ts} +34 -15
  78. package/dist/{registry-SsSDq46X.d.mts → registry-POtcxnal.d.mts} +34 -15
  79. package/dist/render-log/index.d.mts +1 -1
  80. package/dist/render-log/index.d.ts +1 -1
  81. package/dist/server/express.d.mts +37 -0
  82. package/dist/server/express.d.ts +37 -0
  83. package/dist/server/express.js +298 -0
  84. package/dist/server/express.js.map +1 -0
  85. package/dist/server/express.mjs +294 -0
  86. package/dist/server/express.mjs.map +1 -0
  87. package/dist/server/handlers.d.mts +124 -0
  88. package/dist/server/handlers.d.ts +124 -0
  89. package/dist/server/handlers.js +7183 -0
  90. package/dist/server/handlers.js.map +1 -0
  91. package/dist/server/handlers.mjs +7180 -0
  92. package/dist/server/handlers.mjs.map +1 -0
  93. package/dist/server/index.d.mts +12 -0
  94. package/dist/server/index.d.ts +12 -0
  95. package/dist/server/index.js +8384 -0
  96. package/dist/server/index.js.map +1 -0
  97. package/dist/server/index.mjs +8369 -0
  98. package/dist/server/index.mjs.map +1 -0
  99. package/dist/server/nextjs.d.mts +128 -0
  100. package/dist/server/nextjs.d.ts +128 -0
  101. package/dist/server/nextjs.js +390 -0
  102. package/dist/server/nextjs.js.map +1 -0
  103. package/dist/server/nextjs.mjs +385 -0
  104. package/dist/server/nextjs.mjs.map +1 -0
  105. package/dist/server/standalone.d.mts +7 -0
  106. package/dist/server/standalone.d.ts +7 -0
  107. package/dist/server/standalone.js +845 -0
  108. package/dist/server/standalone.js.map +1 -0
  109. package/dist/server/standalone.mjs +841 -0
  110. package/dist/server/standalone.mjs.map +1 -0
  111. package/dist/specs/index.d.mts +365 -0
  112. package/dist/specs/index.d.ts +365 -0
  113. package/dist/specs/index.js +2809 -0
  114. package/dist/specs/index.js.map +1 -0
  115. package/dist/specs/index.mjs +2786 -0
  116. package/dist/specs/index.mjs.map +1 -0
  117. package/dist/standalone-B6GLIEmR.d.ts +216 -0
  118. package/dist/standalone-CjdYqj3P.d.mts +216 -0
  119. package/dist/swc-plugin/index.d.mts +79 -0
  120. package/dist/swc-plugin/index.d.ts +79 -0
  121. package/dist/swc-plugin/index.js +15 -0
  122. package/dist/swc-plugin/index.js.map +1 -0
  123. package/dist/swc-plugin/index.mjs +9 -0
  124. package/dist/swc-plugin/index.mjs.map +1 -0
  125. package/dist/types-B2EfvEaq.d.ts +236 -0
  126. package/dist/{types-Dr6tH-bm.d.mts → types-C7gVYRnF.d.ts} +72 -2
  127. package/dist/{types-oCTrRxSw.d.ts → types-CJGrBEhC.d.mts} +72 -2
  128. package/dist/types-CebMQj76.d.ts +1275 -0
  129. package/dist/types-D_ypYl3T.d.mts +1275 -0
  130. package/dist/types-UBtp7R0u.d.mts +132 -0
  131. package/dist/types-UBtp7R0u.d.ts +132 -0
  132. package/dist/types-gO696T_t.d.mts +236 -0
  133. package/dist/{types-CPMbN_Iw.d.mts → types-suaYwWWg.d.mts} +519 -152
  134. package/dist/{types-CPMbN_Iw.d.ts → types-suaYwWWg.d.ts} +519 -152
  135. package/package.json +123 -4
  136. package/swc-plugin-wasm/ui_bridge_swc_plugin.wasm +0 -0
  137. package/dist/types-BvCfFuEV.d.ts +0 -534
  138. package/dist/types-CFT3Dnx4.d.mts +0 -534
  139. package/dist/websocket-client-CX4QJesI.d.ts +0 -124
  140. package/dist/websocket-client-C_Na0OSp.d.mts +0 -124
@@ -550,7 +550,9 @@ function generateDescription(input) {
550
550
  }
551
551
  parts.push(`"${name}"`);
552
552
  }
553
- const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [input.elementType || "element"];
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(`accessible name similarity: ${(result.similarity * 100).toFixed(0)}%`);
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
- const interactiveSelectors = [
2159
- "a[href]",
2160
- "button",
2161
- "input",
2162
- "select",
2163
- "textarea",
2164
- "[onclick]",
2165
- '[role="button"]',
2166
- '[role="link"]',
2167
- '[role="checkbox"]',
2168
- '[role="radio"]',
2169
- '[role="menuitem"]',
2170
- '[role="tab"]',
2171
- '[role="switch"]',
2172
- '[tabindex]:not([tabindex="-1"])',
2173
- '[contenteditable="true"]',
2174
- "[data-ui-id]",
2175
- "[data-testid]"
2176
- ];
2177
- const selector = options?.selector || interactiveSelectors.join(", ");
2178
- const foundElements = root.querySelectorAll(selector);
2179
- for (const el of foundElements) {
2180
- if (options?.limit && elements.length >= options.limit) break;
2181
- const state = getElementState2(el);
2182
- if (!options?.includeHidden && !state.visible) continue;
2183
- if (options?.types) {
2184
- const type = this.inferElementType(el);
2185
- if (!options.types.includes(type)) continue;
2186
- }
2187
- const registered = this.registry.findByDOMElement(el);
2188
- elements.push({
2189
- id: registered?.id || this.getElementId(el),
2190
- type: registered?.type || this.inferElementType(el),
2191
- label: registered?.label || this.getElementLabel(el),
2192
- tagName: el.tagName.toLowerCase(),
2193
- role: el.getAttribute("role") || void 0,
2194
- accessibleName: this.getAccessibleName(el),
2195
- actions: registered?.actions || this.inferActions(el),
2196
- state,
2197
- registered: !!registered
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
- element.value = "";
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 += char;
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.value = "";
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
- var UIBridgeContext = createContext(null);
3574
- function UIBridgeProvider({
3575
- children,
3576
- features = {},
3577
- config = {},
3578
- onEvent
3579
- }) {
3580
- const registryRef = useRef(null);
3581
- const renderLogRef = useRef(null);
3582
- const metricsRef = useRef(null);
3583
- const wsClientRef = useRef(null);
3584
- const [wsConnectionState, setWsConnectionState] = useState("disconnected");
3585
- if (!registryRef.current) {
3586
- registryRef.current = new UIBridgeRegistry({
3587
- verbose: config.verbose,
3588
- onEvent
3589
- });
3590
- setGlobalRegistry(registryRef.current);
3591
- if (features.renderLog) {
3592
- renderLogRef.current = createRenderLogManager({
3593
- maxEntries: config.maxLogEntries
3594
- });
3595
- }
3596
- if (features.debug) {
3597
- metricsRef.current = createMetricsCollector();
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
- if (config.websocket) {
3600
- const wsPort = config.websocketPort || config.serverPort || 9876;
3601
- const wsUrl = `ws://localhost:${wsPort}`;
3602
- wsClientRef.current = createWSClient({
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 executor = useMemo(() => createActionExecutor(registry), [registry]);
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
- }, [options.actions, options.elementIds]);
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
- [includeSelectors, shouldRegister, queueRegistration]
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
- [shouldRegister, queueRegistration, unregisterElement, includeSelectors]
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
- export { AutoRegisterProvider, UIBridgeProvider, useActiveStates, useAutoRegister, useAvailableTransitions, useCanNavigateTo, useNavigationPath, useStateSnapshot, useTransitions, useUIBridge, useUIBridgeContext, useUIBridgeOptional, useUIBridgeRequired, useUIComponent, useUIComponentAction, useUIElement, useUIElementRef, useUINavigation, useUIState, useUIStateGroup, useUITransition };
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