@qontinui/ui-bridge 0.3.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 (127) 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 +23 -34
  16. package/dist/babel-plugin/index.js.map +1 -1
  17. package/dist/babel-plugin/index.mjs +23 -34
  18. package/dist/babel-plugin/index.mjs.map +1 -1
  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 +2 -2
  28. package/dist/core/index.d.ts +2 -2
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.mjs.map +1 -1
  31. package/dist/debug/index.d.mts +5 -3
  32. package/dist/debug/index.d.ts +5 -3
  33. package/dist/debug/index.js +925 -1
  34. package/dist/debug/index.js.map +1 -1
  35. package/dist/debug/index.mjs +924 -2
  36. package/dist/debug/index.mjs.map +1 -1
  37. package/dist/index.d.mts +12 -7
  38. package/dist/index.d.ts +12 -7
  39. package/dist/index.js +4720 -173
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.mjs +4656 -174
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
  44. package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
  45. package/dist/native/control/index.js +2 -7
  46. package/dist/native/control/index.js.map +1 -1
  47. package/dist/native/control/index.mjs +2 -7
  48. package/dist/native/control/index.mjs.map +1 -1
  49. package/dist/native/core/index.js.map +1 -1
  50. package/dist/native/core/index.mjs.map +1 -1
  51. package/dist/native/debug/index.js +23 -66
  52. package/dist/native/debug/index.js.map +1 -1
  53. package/dist/native/debug/index.mjs +23 -66
  54. package/dist/native/debug/index.mjs.map +1 -1
  55. package/dist/native/index.js +89 -131
  56. package/dist/native/index.js.map +1 -1
  57. package/dist/native/index.mjs +89 -131
  58. package/dist/native/index.mjs.map +1 -1
  59. package/dist/native/react/index.js +28 -52
  60. package/dist/native/react/index.js.map +1 -1
  61. package/dist/native/react/index.mjs +28 -52
  62. package/dist/native/react/index.mjs.map +1 -1
  63. package/dist/native/server/index.js +38 -13
  64. package/dist/native/server/index.js.map +1 -1
  65. package/dist/native/server/index.mjs +38 -13
  66. package/dist/native/server/index.mjs.map +1 -1
  67. package/dist/react/index.d.mts +107 -8
  68. package/dist/react/index.d.ts +107 -8
  69. package/dist/react/index.js +2194 -84
  70. package/dist/react/index.js.map +1 -1
  71. package/dist/react/index.mjs +2194 -85
  72. package/dist/react/index.mjs.map +1 -1
  73. package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
  74. package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
  75. package/dist/render-log/index.d.mts +1 -1
  76. package/dist/render-log/index.d.ts +1 -1
  77. package/dist/server/express.d.mts +5 -4
  78. package/dist/server/express.d.ts +5 -4
  79. package/dist/server/express.js +104 -2
  80. package/dist/server/express.js.map +1 -1
  81. package/dist/server/express.mjs +104 -2
  82. package/dist/server/express.mjs.map +1 -1
  83. package/dist/server/handlers.d.mts +36 -5
  84. package/dist/server/handlers.d.ts +36 -5
  85. package/dist/server/handlers.js +3129 -224
  86. package/dist/server/handlers.js.map +1 -1
  87. package/dist/server/handlers.mjs +3129 -224
  88. package/dist/server/handlers.mjs.map +1 -1
  89. package/dist/server/index.d.mts +7 -5
  90. package/dist/server/index.d.ts +7 -5
  91. package/dist/server/index.js +3215 -183
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/index.mjs +3215 -183
  94. package/dist/server/index.mjs.map +1 -1
  95. package/dist/server/nextjs.d.mts +6 -4
  96. package/dist/server/nextjs.d.ts +6 -4
  97. package/dist/server/nextjs.js +106 -3
  98. package/dist/server/nextjs.js.map +1 -1
  99. package/dist/server/nextjs.mjs +106 -3
  100. package/dist/server/nextjs.mjs.map +1 -1
  101. package/dist/server/standalone.d.mts +6 -5
  102. package/dist/server/standalone.d.ts +6 -5
  103. package/dist/server/standalone.js +131 -5
  104. package/dist/server/standalone.js.map +1 -1
  105. package/dist/server/standalone.mjs +131 -5
  106. package/dist/server/standalone.mjs.map +1 -1
  107. package/dist/specs/index.d.mts +365 -0
  108. package/dist/specs/index.d.ts +365 -0
  109. package/dist/specs/index.js +2809 -0
  110. package/dist/specs/index.js.map +1 -0
  111. package/dist/specs/index.mjs +2786 -0
  112. package/dist/specs/index.mjs.map +1 -0
  113. package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
  114. package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
  115. package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
  116. package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
  117. package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
  118. package/dist/types-CebMQj76.d.ts +1275 -0
  119. package/dist/types-D_ypYl3T.d.mts +1275 -0
  120. package/dist/types-UBtp7R0u.d.mts +132 -0
  121. package/dist/types-UBtp7R0u.d.ts +132 -0
  122. package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
  123. package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
  124. package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
  125. package/package.json +18 -2
  126. package/dist/types-B5Q0GVo0.d.mts +0 -646
  127. package/dist/types-DfPqwU-i.d.ts +0 -646
@@ -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
  }
@@ -1469,8 +1499,56 @@ var UIBridgeRegistry = class {
1469
1499
  identifier: el.getIdentifier(),
1470
1500
  state: el.getState(),
1471
1501
  actions: el.actions,
1472
- 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
1473
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,
1474
1552
  components: this.getAllComponents().map((comp) => ({
1475
1553
  id: comp.id,
1476
1554
  name: comp.name,
@@ -1995,6 +2073,10 @@ function getElementState2(element) {
1995
2073
  pointerEvents: style.pointerEvents
1996
2074
  }
1997
2075
  };
2076
+ const rawText = element.textContent?.trim();
2077
+ if (rawText) {
2078
+ state.textContent = rawText.replace(/\s+/g, " ").slice(0, 500);
2079
+ }
1998
2080
  if (element instanceof HTMLInputElement) {
1999
2081
  state.value = element.value;
2000
2082
  if (element.type === "checkbox" || element.type === "radio") {
@@ -2036,9 +2118,25 @@ function createMouseEvent(type, element, options) {
2036
2118
  clientY: rect.top + y
2037
2119
  });
2038
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
+ }
2039
2136
  var DefaultActionExecutor = class {
2040
- constructor(registry) {
2137
+ constructor(registry, consoleCapture) {
2041
2138
  this.registry = registry;
2139
+ this.consoleCapture = consoleCapture;
2042
2140
  }
2043
2141
  /**
2044
2142
  * Execute an action on an element
@@ -2075,11 +2173,19 @@ var DefaultActionExecutor = class {
2075
2173
  };
2076
2174
  }
2077
2175
  }
2176
+ const actionStartTime = Date.now();
2078
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
+ }
2079
2184
  return {
2080
2185
  success: true,
2081
2186
  elementState: getElementState2(element),
2082
2187
  result,
2188
+ consoleErrors,
2083
2189
  durationMs: performance.now() - startTime,
2084
2190
  timestamp: Date.now(),
2085
2191
  requestId: request.requestId,
@@ -2171,47 +2277,76 @@ var DefaultActionExecutor = class {
2171
2277
  const rootEl = document.querySelector(options.root);
2172
2278
  if (rootEl) root = rootEl;
2173
2279
  }
2174
- const interactiveSelectors = [
2175
- "a[href]",
2176
- "button",
2177
- "input",
2178
- "select",
2179
- "textarea",
2180
- "[onclick]",
2181
- '[role="button"]',
2182
- '[role="link"]',
2183
- '[role="checkbox"]',
2184
- '[role="radio"]',
2185
- '[role="menuitem"]',
2186
- '[role="tab"]',
2187
- '[role="switch"]',
2188
- '[tabindex]:not([tabindex="-1"])',
2189
- '[contenteditable="true"]',
2190
- "[data-ui-id]",
2191
- "[data-testid]"
2192
- ];
2193
- const selector = options?.selector || interactiveSelectors.join(", ");
2194
- const foundElements = root.querySelectorAll(selector);
2195
- for (const el of foundElements) {
2196
- if (options?.limit && elements.length >= options.limit) break;
2197
- const state = getElementState2(el);
2198
- if (!options?.includeHidden && !state.visible) continue;
2199
- if (options?.types) {
2200
- const type = this.inferElementType(el);
2201
- if (!options.types.includes(type)) continue;
2202
- }
2203
- const registered = this.registry.findByDOMElement(el);
2204
- elements.push({
2205
- id: registered?.id || this.getElementId(el),
2206
- type: registered?.type || this.inferElementType(el),
2207
- label: registered?.label || this.getElementLabel(el),
2208
- tagName: el.tagName.toLowerCase(),
2209
- role: el.getAttribute("role") || void 0,
2210
- accessibleName: this.getAccessibleName(el),
2211
- actions: registered?.actions || this.inferActions(el),
2212
- state,
2213
- registered: !!registered
2214
- });
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
+ }
2215
2350
  }
2216
2351
  return {
2217
2352
  elements,
@@ -2241,7 +2376,9 @@ var DefaultActionExecutor = class {
2241
2376
  type: el.type,
2242
2377
  label: el.label,
2243
2378
  actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
2244
- state: el.getState()
2379
+ state: el.getState(),
2380
+ category: el.category,
2381
+ contentMetadata: el.contentMetadata
2245
2382
  })),
2246
2383
  components: components.map((comp) => ({
2247
2384
  id: comp.id,
@@ -2325,6 +2462,14 @@ var DefaultActionExecutor = class {
2325
2462
  return this.performCheck(element, false);
2326
2463
  case "toggle":
2327
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);
2328
2473
  default: {
2329
2474
  const registered = this.registry.findByDOMElement(element);
2330
2475
  if (registered?.customActions?.[action]) {
@@ -2354,15 +2499,26 @@ var DefaultActionExecutor = class {
2354
2499
  if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
2355
2500
  throw new Error("Type action requires an input or textarea element");
2356
2501
  }
2502
+ const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
2503
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
2357
2504
  element.focus();
2358
2505
  if (options?.clear) {
2359
- element.value = "";
2506
+ if (nativeSetter) {
2507
+ nativeSetter.call(element, "");
2508
+ } else {
2509
+ element.value = "";
2510
+ }
2360
2511
  element.dispatchEvent(new Event("input", { bubbles: true }));
2361
2512
  }
2362
2513
  const text = options?.text || "";
2363
2514
  const delay = options?.delay || 0;
2364
2515
  for (const char of text) {
2365
- 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
+ }
2366
2522
  if (options?.triggerEvents !== false) {
2367
2523
  element.dispatchEvent(new Event("input", { bubbles: true }));
2368
2524
  }
@@ -2376,7 +2532,13 @@ var DefaultActionExecutor = class {
2376
2532
  }
2377
2533
  performClear(element) {
2378
2534
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
2379
- 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
+ }
2380
2542
  element.dispatchEvent(new Event("input", { bubbles: true }));
2381
2543
  element.dispatchEvent(new Event("change", { bubbles: true }));
2382
2544
  }
@@ -2458,6 +2620,150 @@ var DefaultActionExecutor = class {
2458
2620
  element.dispatchEvent(new Event("change", { bubbles: true }));
2459
2621
  }
2460
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
+ }
2461
2767
  getElementId(element) {
2462
2768
  return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
2463
2769
  }
@@ -2553,8 +2859,442 @@ var DefaultActionExecutor = class {
2553
2859
  }
2554
2860
  }
2555
2861
  };
2556
- function createActionExecutor(registry) {
2557
- 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;
2558
3298
  }
2559
3299
 
2560
3300
  // src/control/workflow-engine.ts
@@ -3586,34 +4326,690 @@ var MetricsCollector = class {
3586
4326
  function createMetricsCollector(options) {
3587
4327
  return new MetricsCollector(options);
3588
4328
  }
3589
- var UIBridgeContext = createContext(null);
3590
- function UIBridgeProvider({
3591
- children,
3592
- features = {},
3593
- config = {},
3594
- onEvent
3595
- }) {
3596
- const registryRef = useRef(null);
3597
- const renderLogRef = useRef(null);
3598
- const metricsRef = useRef(null);
3599
- const wsClientRef = useRef(null);
3600
- const [wsConnectionState, setWsConnectionState] = useState("disconnected");
3601
- if (!registryRef.current) {
3602
- registryRef.current = new UIBridgeRegistry({
3603
- verbose: config.verbose,
3604
- onEvent
3605
- });
3606
- setGlobalRegistry(registryRef.current);
3607
- if (features.renderLog) {
3608
- renderLogRef.current = createRenderLogManager({
3609
- maxEntries: config.maxLogEntries
3610
- });
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);
3611
4355
  }
3612
- if (features.debug) {
3613
- metricsRef.current = createMetricsCollector();
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);
3614
4397
  }
3615
- if (config.websocket) {
3616
- const wsPort = config.websocketPort || config.serverPort || 9876;
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;
3617
5013
  const wsUrl = `ws://localhost:${wsPort}`;
3618
5014
  wsClientRef.current = createWSClient({
3619
5015
  url: wsUrl,
@@ -3623,12 +5019,27 @@ function UIBridgeProvider({
3623
5019
  pingInterval: 3e4
3624
5020
  });
3625
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
+ }
3626
5033
  }
3627
5034
  const registry = registryRef.current;
3628
5035
  const renderLog = renderLogRef.current || void 0;
3629
5036
  const metrics = metricsRef.current || void 0;
3630
5037
  const wsClient = wsClientRef.current || void 0;
3631
- 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
+ );
3632
5043
  const workflowEngine = useMemo(
3633
5044
  () => createWorkflowEngine(registry, executor),
3634
5045
  [registry, executor]
@@ -3655,13 +5066,21 @@ function UIBridgeProvider({
3655
5066
  useEffect(() => {
3656
5067
  if (!wsClient) return;
3657
5068
  const unsubscribe = wsClient.onConnectionChange((state) => {
5069
+ const prev = prevWsStateRef.current;
5070
+ prevWsStateRef.current = state;
3658
5071
  setWsConnectionState(state);
5072
+ browserCaptureRef.current?.reportWsStateChange(prev, state);
3659
5073
  });
3660
5074
  return unsubscribe;
3661
5075
  }, [wsClient]);
5076
+ useEffect(() => {
5077
+ browserCaptureRef.current?.setOnEvent(onBrowserEvent ?? null);
5078
+ }, [onBrowserEvent]);
3662
5079
  useEffect(() => {
3663
5080
  return () => {
3664
5081
  renderLog?.stop();
5082
+ browserCaptureRef.current?.setOnEvent(null);
5083
+ browserCaptureRef.current?.uninstall();
3665
5084
  wsClient?.disconnect();
3666
5085
  resetGlobalRegistry();
3667
5086
  };
@@ -3696,6 +5115,10 @@ function UIBridgeProvider({
3696
5115
  const getElements = useCallback(() => registry.getAllElements(), [registry]);
3697
5116
  const getComponents = useCallback(() => registry.getAllComponents(), [registry]);
3698
5117
  const createSnapshot = useCallback(() => registry.createSnapshot(), [registry]);
5118
+ const createSnapshotAsync = useCallback(
5119
+ (batchSize) => registry.createSnapshotAsync(batchSize),
5120
+ [registry]
5121
+ );
3699
5122
  const on = useCallback(
3700
5123
  (type, listener) => registry.on(type, listener),
3701
5124
  [registry]
@@ -3718,6 +5141,7 @@ function UIBridgeProvider({
3718
5141
  getElements,
3719
5142
  getComponents,
3720
5143
  createSnapshot,
5144
+ createSnapshotAsync,
3721
5145
  on,
3722
5146
  off,
3723
5147
  initialized: true,
@@ -3739,6 +5163,7 @@ function UIBridgeProvider({
3739
5163
  getElements,
3740
5164
  getComponents,
3741
5165
  createSnapshot,
5166
+ createSnapshotAsync,
3742
5167
  on,
3743
5168
  off,
3744
5169
  wsConnect,
@@ -3978,6 +5403,20 @@ function useUIBridge() {
3978
5403
  }
3979
5404
  return context.createSnapshot();
3980
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
+ );
3981
5420
  const executeAction = useCallback(
3982
5421
  async (elementId, request) => {
3983
5422
  if (!context) {
@@ -4099,6 +5538,7 @@ function useUIBridge() {
4099
5538
  components,
4100
5539
  workflows,
4101
5540
  createSnapshot,
5541
+ createSnapshotAsync,
4102
5542
  executeAction,
4103
5543
  executeComponentAction,
4104
5544
  find,
@@ -4514,6 +5954,305 @@ function useNavigationPath(targetStates) {
4514
5954
  return bridge.registry.findPath(targetStates);
4515
5955
  }, [bridge, targetStates]);
4516
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
4517
6256
  var INTERACTIVE_SELECTORS2 = [
4518
6257
  "a[href]",
4519
6258
  "button",
@@ -4708,12 +6447,17 @@ function useAutoRegister(options = {}) {
4708
6447
  excludeSelectors = [],
4709
6448
  generateId: customGenerateId,
4710
6449
  onRegister,
4711
- onUnregister
6450
+ onUnregister,
6451
+ contentDiscovery
4712
6452
  } = options;
6453
+ const contentEnabled = contentDiscovery?.enabled !== false;
4713
6454
  const bridge = useUIBridgeOptional();
4714
6455
  const registeredElementsRef = useRef(/* @__PURE__ */ new Map());
6456
+ const registeredContentElementsRef = useRef(/* @__PURE__ */ new Map());
4715
6457
  const pendingRegistrationsRef = useRef(/* @__PURE__ */ new Set());
6458
+ const pendingContentRegistrationsRef = useRef(/* @__PURE__ */ new Set());
4716
6459
  const debounceTimeoutRef = useRef(null);
6460
+ const contentDebounceTimeoutRef = useRef(null);
4717
6461
  const shouldRegister = useCallback(
4718
6462
  (element) => {
4719
6463
  if (!includeHidden && !isElementVisible2(element)) {
@@ -4781,6 +6525,41 @@ function useAutoRegister(options = {}) {
4781
6525
  },
4782
6526
  [bridge, onUnregister]
4783
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
+ );
4784
6563
  const processPendingRegistrations = useCallback(() => {
4785
6564
  pendingRegistrationsRef.current.forEach((element) => {
4786
6565
  if (shouldRegister(element)) {
@@ -4789,6 +6568,15 @@ function useAutoRegister(options = {}) {
4789
6568
  });
4790
6569
  pendingRegistrationsRef.current.clear();
4791
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]);
4792
6580
  const queueRegistration = useCallback(
4793
6581
  (element) => {
4794
6582
  pendingRegistrationsRef.current.add(element);
@@ -4799,6 +6587,20 @@ function useAutoRegister(options = {}) {
4799
6587
  },
4800
6588
  [debounceMs, processPendingRegistrations]
4801
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
+ );
4802
6604
  const scanAndRegister = useCallback(
4803
6605
  (rootElement) => {
4804
6606
  const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
@@ -4808,8 +6610,28 @@ function useAutoRegister(options = {}) {
4808
6610
  queueRegistration(element);
4809
6611
  }
4810
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
+ }
4811
6626
  },
4812
- [includeSelectors, shouldRegister, queueRegistration]
6627
+ [
6628
+ includeSelectors,
6629
+ shouldRegister,
6630
+ queueRegistration,
6631
+ contentEnabled,
6632
+ contentDiscovery,
6633
+ queueContentRegistration
6634
+ ]
4813
6635
  );
4814
6636
  const handleMutations = useCallback(
4815
6637
  (mutations) => {
@@ -4827,6 +6649,22 @@ function useAutoRegister(options = {}) {
4827
6649
  queueRegistration(descendant);
4828
6650
  }
4829
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
+ }
4830
6668
  }
4831
6669
  });
4832
6670
  mutation.removedNodes.forEach((node) => {
@@ -4835,17 +6673,32 @@ function useAutoRegister(options = {}) {
4835
6673
  if (registeredElementsRef.current.has(element)) {
4836
6674
  unregisterElement(element);
4837
6675
  }
6676
+ if (registeredContentElementsRef.current.has(element)) {
6677
+ unregisterContentElement(element);
6678
+ }
4838
6679
  const descendants = element.querySelectorAll("*");
4839
6680
  descendants.forEach((descendant) => {
4840
6681
  if (registeredElementsRef.current.has(descendant)) {
4841
6682
  unregisterElement(descendant);
4842
6683
  }
6684
+ if (registeredContentElementsRef.current.has(descendant)) {
6685
+ unregisterContentElement(descendant);
6686
+ }
4843
6687
  });
4844
6688
  }
4845
6689
  });
4846
6690
  });
4847
6691
  },
4848
- [shouldRegister, queueRegistration, unregisterElement, includeSelectors]
6692
+ [
6693
+ shouldRegister,
6694
+ queueRegistration,
6695
+ unregisterElement,
6696
+ includeSelectors,
6697
+ contentEnabled,
6698
+ contentDiscovery,
6699
+ queueContentRegistration,
6700
+ unregisterContentElement
6701
+ ]
4849
6702
  );
4850
6703
  useEffect(() => {
4851
6704
  if (!enabled || !bridge?.registry) return;
@@ -4861,10 +6714,17 @@ function useAutoRegister(options = {}) {
4861
6714
  if (debounceTimeoutRef.current) {
4862
6715
  clearTimeout(debounceTimeoutRef.current);
4863
6716
  }
6717
+ if (contentDebounceTimeoutRef.current) {
6718
+ clearTimeout(contentDebounceTimeoutRef.current);
6719
+ }
4864
6720
  registeredElementsRef.current.forEach((id, _element) => {
4865
6721
  bridge.registry.unregisterElement(id);
4866
6722
  });
4867
6723
  registeredElementsRef.current.clear();
6724
+ registeredContentElementsRef.current.forEach((id, _element) => {
6725
+ bridge.registry.unregisterElement(id);
6726
+ });
6727
+ registeredContentElementsRef.current.clear();
4868
6728
  };
4869
6729
  }, [enabled, bridge, root, scanAndRegister, handleMutations]);
4870
6730
  }
@@ -4879,7 +6739,8 @@ function AutoRegisterProvider({
4879
6739
  excludeSelectors = [],
4880
6740
  generateId: generateId4,
4881
6741
  onRegister,
4882
- onUnregister
6742
+ onUnregister,
6743
+ contentDiscovery
4883
6744
  }) {
4884
6745
  const containerRef = useRef(null);
4885
6746
  useAutoRegister({
@@ -4892,7 +6753,8 @@ function AutoRegisterProvider({
4892
6753
  excludeSelectors,
4893
6754
  generateId: generateId4,
4894
6755
  onRegister,
4895
- onUnregister
6756
+ onUnregister,
6757
+ contentDiscovery
4896
6758
  });
4897
6759
  if (scopeToChildren) {
4898
6760
  return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { display: "contents" }, children });
@@ -4900,6 +6762,253 @@ function AutoRegisterProvider({
4900
6762
  return /* @__PURE__ */ jsx(Fragment, { children });
4901
6763
  }
4902
6764
 
4903
- 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 };
4904
7013
  //# sourceMappingURL=index.mjs.map
4905
7014
  //# sourceMappingURL=index.mjs.map