@qontinui/ui-bridge-native 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/control/{action-executor.d.ts → index.d.mts} +14 -6
  2. package/dist/control/index.d.ts +95 -5
  3. package/dist/control/index.js +74 -11
  4. package/dist/control/index.js.map +1 -1
  5. package/dist/control/index.mjs +74 -11
  6. package/dist/control/index.mjs.map +1 -1
  7. package/dist/core/index.d.mts +46 -0
  8. package/dist/core/index.d.ts +43 -6
  9. package/dist/core/index.js +69 -1
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/index.mjs +69 -1
  12. package/dist/core/index.mjs.map +1 -1
  13. package/dist/debug/{inspector.d.ts → index.d.mts} +6 -3
  14. package/dist/debug/index.d.ts +44 -4
  15. package/dist/debug/index.js.map +1 -1
  16. package/dist/debug/index.mjs.map +1 -1
  17. package/dist/design/{design-inspector-native.d.ts → index.d.mts} +11 -7
  18. package/dist/design/index.d.ts +60 -5
  19. package/dist/{design/design-types.d.ts → design-types-BHr6Q1FX.d.mts} +11 -10
  20. package/dist/design-types-BHr6Q1FX.d.ts +205 -0
  21. package/dist/http-server-DCdR8WlL.d.mts +622 -0
  22. package/dist/http-server-Dj3grQW4.d.ts +622 -0
  23. package/dist/index.d.mts +89 -0
  24. package/dist/index.d.ts +86 -28
  25. package/dist/index.js +391 -12
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +386 -13
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/react/{useAutoRegister.d.ts → index.d.mts} +15 -7
  30. package/dist/react/index.d.ts +69 -8
  31. package/dist/react/index.js +388 -12
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/react/index.mjs +386 -13
  34. package/dist/react/index.mjs.map +1 -1
  35. package/dist/registry-Cahd8Gn2.d.mts +230 -0
  36. package/dist/{core/registry.d.ts → registry-z-3WJsxp.d.ts} +28 -10
  37. package/dist/server/index.d.mts +119 -0
  38. package/dist/server/index.d.ts +117 -13
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/index.mjs.map +1 -1
  41. package/dist/types-DAfCeWXc.d.mts +197 -0
  42. package/dist/{core/types.d.ts → types-Oz13YLZq.d.mts} +124 -30
  43. package/dist/types-Oz13YLZq.d.ts +477 -0
  44. package/dist/{control/types.d.ts → types-ajakyp-l.d.ts} +36 -15
  45. package/dist/useUIBridgeToast-D32KXyL1.d.ts +720 -0
  46. package/dist/useUIBridgeToast-GmT1TzUe.d.mts +720 -0
  47. package/package.json +1 -1
  48. package/dist/control/action-executor.d.ts.map +0 -1
  49. package/dist/control/index.d.ts.map +0 -1
  50. package/dist/control/types.d.ts.map +0 -1
  51. package/dist/core/element-identifier.d.ts +0 -41
  52. package/dist/core/element-identifier.d.ts.map +0 -1
  53. package/dist/core/index.d.ts.map +0 -1
  54. package/dist/core/registry.d.ts.map +0 -1
  55. package/dist/core/types.d.ts.map +0 -1
  56. package/dist/debug/index.d.ts.map +0 -1
  57. package/dist/debug/inspector.d.ts.map +0 -1
  58. package/dist/design/design-inspector-native.d.ts.map +0 -1
  59. package/dist/design/design-types.d.ts.map +0 -1
  60. package/dist/design/index.d.ts.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/react/UIBridgeNativeProvider.d.ts +0 -147
  63. package/dist/react/UIBridgeNativeProvider.d.ts.map +0 -1
  64. package/dist/react/index.d.ts.map +0 -1
  65. package/dist/react/useAutoRegister.d.ts.map +0 -1
  66. package/dist/react/useUIBridge.d.ts +0 -103
  67. package/dist/react/useUIBridge.d.ts.map +0 -1
  68. package/dist/react/useUIComponent.d.ts +0 -112
  69. package/dist/react/useUIComponent.d.ts.map +0 -1
  70. package/dist/react/useUIElement.d.ts +0 -162
  71. package/dist/react/useUIElement.d.ts.map +0 -1
  72. package/dist/server/design-handlers.d.ts +0 -47
  73. package/dist/server/design-handlers.d.ts.map +0 -1
  74. package/dist/server/handlers.d.ts +0 -20
  75. package/dist/server/handlers.d.ts.map +0 -1
  76. package/dist/server/http-server.d.ts +0 -247
  77. package/dist/server/http-server.d.ts.map +0 -1
  78. package/dist/server/index.d.ts.map +0 -1
  79. package/dist/server/types.d.ts +0 -200
  80. package/dist/server/types.d.ts.map +0 -1
  81. package/dist/server/ws-connection.d.ts +0 -48
  82. package/dist/server/ws-connection.d.ts.map +0 -1
  83. package/dist/server/ws-event-bridge.d.ts +0 -113
  84. package/dist/server/ws-event-bridge.d.ts.map +0 -1
  85. package/dist/server/ws-protocol.d.ts +0 -48
  86. package/dist/server/ws-protocol.d.ts.map +0 -1
  87. package/dist/server/ws-types.d.ts +0 -65
  88. package/dist/server/ws-types.d.ts.map +0 -1
  89. package/dist/transport/CloudRelayClient.d.ts +0 -51
  90. package/dist/transport/CloudRelayClient.d.ts.map +0 -1
  91. package/dist/transport/DeviceAnnouncer.d.ts +0 -75
  92. package/dist/transport/DeviceAnnouncer.d.ts.map +0 -1
  93. package/dist/transport/index.d.ts +0 -3
  94. package/dist/transport/index.d.ts.map +0 -1
@@ -1,3 +1,12 @@
1
+ export { b as ComponentActionDef, c as ComponentActionRequest, d as ComponentActionResponse, h as ToastRecorderInput, U as UIBridgeNativeContextValue, i as UIBridgeNativeProvider, j as UIBridgeNativeProviderProps, k as UIBridgeProps, n as UseUIBridgeModalOptions, o as UseUIBridgeReturn, p as UseUIBridgeToastOptions, q as UseUIComponentOptions, r as UseUIComponentReturn, s as UseUIElementOptions, t as UseUIElementReturn, u as UseUIElementWithPropsReturn, v as useToastRecorder, w as useUIBridge, x as useUIBridgeModal, y as useUIBridgeNative, z as useUIBridgeNativeOptional, B as useUIBridgeNativeRequired, E as useUIBridgeRequired, F as useUIBridgeToast, G as useUIComponent, H as useUIComponentAction, I as useUIElement, J as useUIElementWithProps } from '../useUIBridgeToast-GmT1TzUe.mjs';
2
+ import { j as NativeElementRef } from '../types-Oz13YLZq.mjs';
3
+ import { b as RegisterElementOptions } from '../registry-Cahd8Gn2.mjs';
4
+ import 'react/jsx-runtime';
5
+ import 'react';
6
+ import '../types-DAfCeWXc.mjs';
7
+ import '../http-server-DCdR8WlL.mjs';
8
+ import '../design-types-BHr6Q1FX.mjs';
9
+
1
10
  /**
2
11
  * useAutoRegister Hook for React Native
3
12
  *
@@ -9,12 +18,11 @@
9
18
  * 1. Use this hook at the app root to enable the registry
10
19
  * 2. Use useUIElement on interactive components (or wrap with Pressable/TouchableOpacity)
11
20
  */
12
- import type { NativeElementRef } from '../core/types';
13
- import type { RegisterElementOptions } from '../core/registry';
21
+
14
22
  /**
15
23
  * Options for auto-registration in React Native
16
24
  */
17
- export interface NativeAutoRegisterOptions {
25
+ interface NativeAutoRegisterOptions {
18
26
  /** Enable auto-registration (default: true in dev mode) */
19
27
  enabled?: boolean;
20
28
  /** Callback when element is registered */
@@ -25,7 +33,7 @@ export interface NativeAutoRegisterOptions {
25
33
  /**
26
34
  * Handle returned by useAutoRegister for programmatic registration
27
35
  */
28
- export interface NativeAutoRegisterHandle {
36
+ interface NativeAutoRegisterHandle {
29
37
  /** Register an element programmatically */
30
38
  register: (id: string, ref: React.RefObject<NativeElementRef>, options?: RegisterElementOptions) => void;
31
39
  /** Unregister an element programmatically */
@@ -59,6 +67,6 @@ export interface NativeAutoRegisterHandle {
59
67
  * }
60
68
  * ```
61
69
  */
62
- export declare function useAutoRegister(options?: NativeAutoRegisterOptions): NativeAutoRegisterHandle;
63
- export default useAutoRegister;
64
- //# sourceMappingURL=useAutoRegister.d.ts.map
70
+ declare function useAutoRegister(options?: NativeAutoRegisterOptions): NativeAutoRegisterHandle;
71
+
72
+ export { type NativeAutoRegisterHandle, type NativeAutoRegisterOptions, useAutoRegister };
@@ -1,11 +1,72 @@
1
+ export { b as ComponentActionDef, c as ComponentActionRequest, d as ComponentActionResponse, h as ToastRecorderInput, U as UIBridgeNativeContextValue, i as UIBridgeNativeProvider, j as UIBridgeNativeProviderProps, k as UIBridgeProps, n as UseUIBridgeModalOptions, o as UseUIBridgeReturn, p as UseUIBridgeToastOptions, q as UseUIComponentOptions, r as UseUIComponentReturn, s as UseUIElementOptions, t as UseUIElementReturn, u as UseUIElementWithPropsReturn, v as useToastRecorder, w as useUIBridge, x as useUIBridgeModal, y as useUIBridgeNative, z as useUIBridgeNativeOptional, B as useUIBridgeNativeRequired, E as useUIBridgeRequired, F as useUIBridgeToast, G as useUIComponent, H as useUIComponentAction, I as useUIElement, J as useUIElementWithProps } from '../useUIBridgeToast-D32KXyL1.js';
2
+ import { j as NativeElementRef } from '../types-Oz13YLZq.js';
3
+ import { b as RegisterElementOptions } from '../registry-z-3WJsxp.js';
4
+ import 'react/jsx-runtime';
5
+ import 'react';
6
+ import '../types-ajakyp-l.js';
7
+ import '../http-server-Dj3grQW4.js';
8
+ import '../design-types-BHr6Q1FX.js';
9
+
1
10
  /**
2
- * UI Bridge Native - React
11
+ * useAutoRegister Hook for React Native
3
12
  *
4
- * React hooks and provider for React Native.
13
+ * Enables automatic registration of interactive React Native components.
14
+ * Unlike the web version, this doesn't use MutationObserver (no DOM).
15
+ * Instead, it provides a simple API for components to register themselves.
16
+ *
17
+ * For comprehensive coverage in React Native, components should:
18
+ * 1. Use this hook at the app root to enable the registry
19
+ * 2. Use useUIElement on interactive components (or wrap with Pressable/TouchableOpacity)
20
+ */
21
+
22
+ /**
23
+ * Options for auto-registration in React Native
24
+ */
25
+ interface NativeAutoRegisterOptions {
26
+ /** Enable auto-registration (default: true in dev mode) */
27
+ enabled?: boolean;
28
+ /** Callback when element is registered */
29
+ onRegister?: (id: string) => void;
30
+ /** Callback when element is unregistered */
31
+ onUnregister?: (id: string) => void;
32
+ }
33
+ /**
34
+ * Handle returned by useAutoRegister for programmatic registration
35
+ */
36
+ interface NativeAutoRegisterHandle {
37
+ /** Register an element programmatically */
38
+ register: (id: string, ref: React.RefObject<NativeElementRef>, options?: RegisterElementOptions) => void;
39
+ /** Unregister an element programmatically */
40
+ unregister: (id: string) => void;
41
+ /** Check if auto-registration is enabled */
42
+ isEnabled: boolean;
43
+ }
44
+ /**
45
+ * Hook for enabling automatic element registration in React Native.
46
+ *
47
+ * Since React Native doesn't have a DOM to observe, this hook provides
48
+ * a handle for programmatic registration. For most use cases, you should
49
+ * use useUIElement directly on interactive components.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * function App() {
54
+ * const autoRegister = useAutoRegister({ enabled: __DEV__ });
55
+ *
56
+ * // Programmatic registration (rare use case)
57
+ * useEffect(() => {
58
+ * autoRegister.register('custom-element', {
59
+ * type: 'button',
60
+ * label: 'Custom Button',
61
+ * });
62
+ *
63
+ * return () => autoRegister.unregister('custom-element');
64
+ * }, []);
65
+ *
66
+ * return <YourApp />;
67
+ * }
68
+ * ```
5
69
  */
6
- export * from './UIBridgeNativeProvider';
7
- export * from './useUIElement';
8
- export * from './useUIComponent';
9
- export * from './useUIBridge';
10
- export * from './useAutoRegister';
11
- //# sourceMappingURL=index.d.ts.map
70
+ declare function useAutoRegister(options?: NativeAutoRegisterOptions): NativeAutoRegisterHandle;
71
+
72
+ export { type NativeAutoRegisterHandle, type NativeAutoRegisterOptions, useAutoRegister };
@@ -55,9 +55,34 @@ var NativeUIBridgeRegistry = class {
55
55
  this.components = /* @__PURE__ */ new Map();
56
56
  this.workflows = /* @__PURE__ */ new Map();
57
57
  this.eventListeners = /* @__PURE__ */ new Map();
58
+ this.enrichers = {};
59
+ this.snapshotExtras = /* @__PURE__ */ new Map();
58
60
  this.config = config;
59
61
  }
60
62
  // ============================================================================
63
+ // Snapshot Enricher Slots
64
+ // ============================================================================
65
+ /**
66
+ * Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
67
+ * with a partial set merges into existing slots instead of clobbering them.
68
+ */
69
+ setEnrichers(e) {
70
+ this.enrichers = { ...this.enrichers, ...e };
71
+ }
72
+ /**
73
+ * Register a custom snapshot enricher. The returned object will be
74
+ * `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
75
+ * clobbering canonical fields. Returns a disposer.
76
+ */
77
+ registerSnapshotEnricher(name, fn) {
78
+ this.snapshotExtras.set(name, fn);
79
+ return () => this.unregisterSnapshotEnricher(name);
80
+ }
81
+ /** Remove a custom snapshot enricher by name */
82
+ unregisterSnapshotEnricher(name) {
83
+ this.snapshotExtras.delete(name);
84
+ }
85
+ // ============================================================================
61
86
  // Element Management
62
87
  // ============================================================================
63
88
  /**
@@ -420,7 +445,7 @@ var NativeUIBridgeRegistry = class {
420
445
  const currentRoute = routeInfo.currentRoute;
421
446
  elements = elements.filter((e) => e.registrationRoute === currentRoute);
422
447
  }
423
- return {
448
+ const snapshot = {
424
449
  timestamp: Date.now(),
425
450
  elements: elements.map((e) => {
426
451
  const handlers = extractHandlerNames(e.props);
@@ -452,6 +477,49 @@ var NativeUIBridgeRegistry = class {
452
477
  currentRoute: routeInfo?.currentRoute ?? null,
453
478
  segments: routeInfo?.segments
454
479
  };
480
+ if (this.enrichers.modalDetector) {
481
+ try {
482
+ snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
483
+ } catch (error3) {
484
+ if (this.config.verbose) {
485
+ console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error3);
486
+ }
487
+ }
488
+ }
489
+ if (this.enrichers.toastCapture) {
490
+ try {
491
+ snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
492
+ } catch (error3) {
493
+ if (this.config.verbose) {
494
+ console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error3);
495
+ }
496
+ }
497
+ }
498
+ if (this.enrichers.undoTracker) {
499
+ try {
500
+ snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
501
+ } catch (error3) {
502
+ if (this.config.verbose) {
503
+ console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error3);
504
+ }
505
+ }
506
+ }
507
+ if (this.snapshotExtras.size > 0) {
508
+ const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
509
+ for (const [name, fn] of this.snapshotExtras) {
510
+ try {
511
+ const extra = fn(ctx);
512
+ if (extra && typeof extra === "object") {
513
+ Object.assign(snapshot, extra);
514
+ }
515
+ } catch (error3) {
516
+ if (this.config.verbose) {
517
+ console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error3);
518
+ }
519
+ }
520
+ }
521
+ }
522
+ return snapshot;
455
523
  }
456
524
  /**
457
525
  * Get registry statistics
@@ -545,6 +613,23 @@ function sleep(ms) {
545
613
  var DefaultNativeActionExecutor = class {
546
614
  constructor(registry) {
547
615
  this.registry = registry;
616
+ this.actionListeners = /* @__PURE__ */ new Set();
617
+ }
618
+ onActionExecuted(listener) {
619
+ this.actionListeners.add(listener);
620
+ return () => {
621
+ this.actionListeners.delete(listener);
622
+ };
623
+ }
624
+ /** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
625
+ emitActionEvent(event) {
626
+ for (const listener of this.actionListeners) {
627
+ try {
628
+ listener(event);
629
+ } catch (error3) {
630
+ console.warn(`[ui-bridge-native] Action listener error:`, error3);
631
+ }
632
+ }
548
633
  }
549
634
  /**
550
635
  * Execute an action on an element
@@ -558,11 +643,23 @@ var DefaultNativeActionExecutor = class {
558
643
  registered = findElementByIdentifier(elementId) ?? void 0;
559
644
  }
560
645
  if (!registered) {
646
+ const failTime = Date.now();
647
+ const errorMsg = `Element not found: ${elementId}`;
648
+ this.emitActionEvent({
649
+ elementId,
650
+ action: request.action,
651
+ params: request.params,
652
+ success: false,
653
+ error: errorMsg,
654
+ timestamp: failTime,
655
+ requestId: request.requestId,
656
+ durationMs: failTime - startTime
657
+ });
561
658
  return {
562
659
  success: false,
563
- error: `Element not found: ${elementId}`,
564
- durationMs: Date.now() - startTime,
565
- timestamp: Date.now(),
660
+ error: errorMsg,
661
+ durationMs: failTime - startTime,
662
+ timestamp: failTime,
566
663
  requestId: request.requestId
567
664
  };
568
665
  }
@@ -570,33 +667,67 @@ var DefaultNativeActionExecutor = class {
570
667
  const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
571
668
  waitDurationMs = waitResult.waitedMs;
572
669
  if (!waitResult.met) {
670
+ const failTime = Date.now();
671
+ const errorMsg = waitResult.error || "Wait condition not met";
672
+ this.emitActionEvent({
673
+ elementId,
674
+ action: request.action,
675
+ params: request.params,
676
+ success: false,
677
+ error: errorMsg,
678
+ timestamp: failTime,
679
+ requestId: request.requestId,
680
+ durationMs: failTime - startTime
681
+ });
573
682
  return {
574
683
  success: false,
575
- error: waitResult.error || "Wait condition not met",
576
- durationMs: Date.now() - startTime,
577
- timestamp: Date.now(),
684
+ error: errorMsg,
685
+ durationMs: failTime - startTime,
686
+ timestamp: failTime,
578
687
  requestId: request.requestId,
579
688
  waitDurationMs
580
689
  };
581
690
  }
582
691
  }
583
692
  const result = await this.performAction(registered, request.action, request.params);
693
+ const successTime = Date.now();
694
+ this.emitActionEvent({
695
+ elementId,
696
+ action: request.action,
697
+ params: request.params,
698
+ success: true,
699
+ timestamp: successTime,
700
+ requestId: request.requestId,
701
+ durationMs: successTime - startTime
702
+ });
584
703
  return {
585
704
  success: true,
586
705
  elementState: registered.getState(),
587
706
  result,
588
- durationMs: Date.now() - startTime,
589
- timestamp: Date.now(),
707
+ durationMs: successTime - startTime,
708
+ timestamp: successTime,
590
709
  requestId: request.requestId,
591
710
  waitDurationMs
592
711
  };
593
712
  } catch (error3) {
713
+ const failTime = Date.now();
714
+ const errorMsg = error3 instanceof Error ? error3.message : String(error3);
715
+ this.emitActionEvent({
716
+ elementId,
717
+ action: request.action,
718
+ params: request.params,
719
+ success: false,
720
+ error: errorMsg,
721
+ timestamp: failTime,
722
+ requestId: request.requestId,
723
+ durationMs: failTime - startTime
724
+ });
594
725
  return {
595
726
  success: false,
596
- error: error3 instanceof Error ? error3.message : String(error3),
727
+ error: errorMsg,
597
728
  stack: error3 instanceof Error ? error3.stack : void 0,
598
- durationMs: Date.now() - startTime,
599
- timestamp: Date.now(),
729
+ durationMs: failTime - startTime,
730
+ timestamp: failTime,
600
731
  requestId: request.requestId,
601
732
  waitDurationMs
602
733
  };
@@ -3514,6 +3645,186 @@ function parseQueryParams(path) {
3514
3645
  }
3515
3646
  return result;
3516
3647
  }
3648
+
3649
+ // src/modal/modal-detector.ts
3650
+ var ModalDetector = class {
3651
+ constructor() {
3652
+ /** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
3653
+ this.modals = /* @__PURE__ */ new Map();
3654
+ }
3655
+ pushModal(modal) {
3656
+ const existing = this.modals.get(modal.id);
3657
+ const detectedAt = existing ? existing.detectedAt : Date.now();
3658
+ const next = {
3659
+ id: modal.id,
3660
+ title: modal.title,
3661
+ type: modal.type ?? "modal",
3662
+ blocking: modal.blocking ?? true,
3663
+ dismissible: modal.dismissible ?? true,
3664
+ detectedAt
3665
+ };
3666
+ this.modals.set(modal.id, next);
3667
+ }
3668
+ dismissModal(id) {
3669
+ this.modals.delete(id);
3670
+ }
3671
+ getSnapshotModalContext() {
3672
+ const active = this.getActive();
3673
+ const ctx = {
3674
+ modals: active,
3675
+ hasBlockingModal: active.some((m) => m.blocking),
3676
+ count: active.length
3677
+ };
3678
+ if (active.length > 0) {
3679
+ ctx.topModal = active[active.length - 1];
3680
+ }
3681
+ return ctx;
3682
+ }
3683
+ getActive() {
3684
+ return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
3685
+ }
3686
+ clear() {
3687
+ this.modals.clear();
3688
+ }
3689
+ };
3690
+
3691
+ // src/toast/toast-capture.ts
3692
+ var DEFAULT_MAX_RECENT = 50;
3693
+ var ToastCapture = class {
3694
+ constructor(config = {}) {
3695
+ this.active = /* @__PURE__ */ new Map();
3696
+ this.recent = [];
3697
+ this.totalCaptured = 0;
3698
+ this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
3699
+ }
3700
+ recordToast(toast) {
3701
+ if (this.active.has(toast.id)) {
3702
+ return;
3703
+ }
3704
+ const appearedAt = toast.appearedAt ?? Date.now();
3705
+ const entry = {
3706
+ id: toast.id,
3707
+ message: toast.message,
3708
+ level: toast.level ?? "info",
3709
+ appearedAt,
3710
+ visible: true,
3711
+ durationMs: 0
3712
+ };
3713
+ this.active.set(toast.id, entry);
3714
+ this.totalCaptured += 1;
3715
+ }
3716
+ dismissToast(id) {
3717
+ const entry = this.active.get(id);
3718
+ if (!entry) {
3719
+ return;
3720
+ }
3721
+ this.active.delete(id);
3722
+ const dismissedAt = Date.now();
3723
+ const dismissed = {
3724
+ ...entry,
3725
+ dismissedAt,
3726
+ visible: false,
3727
+ durationMs: dismissedAt - entry.appearedAt
3728
+ };
3729
+ this.recent.push(dismissed);
3730
+ if (this.recent.length > this.maxRecent) {
3731
+ this.recent.shift();
3732
+ }
3733
+ }
3734
+ getSnapshotToastContext() {
3735
+ const now = Date.now();
3736
+ const active = Array.from(this.active.values()).map((entry) => ({
3737
+ ...entry,
3738
+ durationMs: now - entry.appearedAt
3739
+ }));
3740
+ return {
3741
+ active,
3742
+ recent: [...this.recent],
3743
+ totalCaptured: this.totalCaptured
3744
+ };
3745
+ }
3746
+ clear() {
3747
+ this.active.clear();
3748
+ this.recent = [];
3749
+ this.totalCaptured = 0;
3750
+ }
3751
+ };
3752
+
3753
+ // src/undo/undo-tracker.ts
3754
+ var DEFAULT_MAX_HISTORY = 50;
3755
+ function buildSummary(ctx) {
3756
+ if (!ctx.canUndo && !ctx.canRedo) {
3757
+ return "no undo available";
3758
+ }
3759
+ const parts = [];
3760
+ if (ctx.canUndo) {
3761
+ parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
3762
+ }
3763
+ if (ctx.canRedo) {
3764
+ parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
3765
+ }
3766
+ return parts.join(" / ");
3767
+ }
3768
+ var UndoTracker = class {
3769
+ constructor(config = {}) {
3770
+ this.actions = [];
3771
+ this.declaredState = null;
3772
+ this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
3773
+ }
3774
+ recordAction(record) {
3775
+ this.actions.push(record);
3776
+ if (this.actions.length > this.maxHistory) {
3777
+ this.actions = this.actions.slice(-this.maxHistory);
3778
+ }
3779
+ }
3780
+ setDeclaredState(state) {
3781
+ this.declaredState = state;
3782
+ }
3783
+ getActionHistory() {
3784
+ return this.actions.slice();
3785
+ }
3786
+ getSnapshotUndoContext() {
3787
+ const declared = this.declaredState;
3788
+ if (declared) {
3789
+ const summary2 = buildSummary({
3790
+ canUndo: declared.canUndo,
3791
+ canRedo: declared.canRedo,
3792
+ undoDescription: declared.undoDescription,
3793
+ redoDescription: declared.redoDescription
3794
+ });
3795
+ return {
3796
+ canUndo: declared.canUndo,
3797
+ canRedo: declared.canRedo,
3798
+ undoDescription: declared.undoDescription,
3799
+ redoDescription: declared.redoDescription,
3800
+ undoDepth: declared.undoDepth,
3801
+ redoDepth: declared.redoDepth,
3802
+ summary: summary2
3803
+ };
3804
+ }
3805
+ const canUndo = this.actions.some((a) => a.reversible !== false);
3806
+ const canRedo = false;
3807
+ const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
3808
+ const undoDescription = lastReversible?.description;
3809
+ const summary = buildSummary({ canUndo, canRedo, undoDescription });
3810
+ return {
3811
+ canUndo,
3812
+ canRedo,
3813
+ undoDescription,
3814
+ summary
3815
+ };
3816
+ }
3817
+ executeUndo() {
3818
+ return { ok: false, reason: "Not implemented in native" };
3819
+ }
3820
+ executeRedo() {
3821
+ return { ok: false, reason: "Not implemented in native" };
3822
+ }
3823
+ clear() {
3824
+ this.actions = [];
3825
+ this.declaredState = null;
3826
+ }
3827
+ };
3517
3828
  var UIBridgeNativeContext = react.createContext(null);
3518
3829
  function UIBridgeNativeProvider({
3519
3830
  children,
@@ -3530,6 +3841,9 @@ function UIBridgeNativeProvider({
3530
3841
  }) {
3531
3842
  const registryRef = react.useRef(null);
3532
3843
  const executorRef = react.useRef(null);
3844
+ const modalDetectorRef = react.useRef(null);
3845
+ const toastCaptureRef = react.useRef(null);
3846
+ const undoTrackerRef = react.useRef(null);
3533
3847
  const [serverRunning, setServerRunning] = react.useState(false);
3534
3848
  if (!registryRef.current) {
3535
3849
  registryRef.current = new NativeUIBridgeRegistry({
@@ -3543,6 +3857,12 @@ function UIBridgeNativeProvider({
3543
3857
  executorRef.current = createNativeActionExecutor(registry);
3544
3858
  }
3545
3859
  const executor = executorRef.current;
3860
+ if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
3861
+ if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
3862
+ if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
3863
+ const modalDetector = modalDetectorRef.current;
3864
+ const toastCapture = toastCaptureRef.current;
3865
+ const undoTracker = undoTrackerRef.current;
3546
3866
  const serverRef = react.useRef(null);
3547
3867
  const eventBridgeRef = react.useRef(null);
3548
3868
  const announcerRef = react.useRef(null);
@@ -3731,6 +4051,22 @@ function UIBridgeNativeProvider({
3731
4051
  lastRoute = current;
3732
4052
  });
3733
4053
  }, [routeProvider, registry]);
4054
+ react.useEffect(() => {
4055
+ registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
4056
+ }, [registry, modalDetector, toastCapture, undoTracker]);
4057
+ react.useEffect(() => {
4058
+ const unsubscribe = executor.onActionExecuted((event) => {
4059
+ if (!event.success) return;
4060
+ undoTracker.recordAction({
4061
+ id: event.requestId ?? `action-${event.timestamp}`,
4062
+ type: event.action,
4063
+ targetId: event.elementId,
4064
+ timestamp: event.timestamp,
4065
+ reversible: true
4066
+ });
4067
+ });
4068
+ return unsubscribe;
4069
+ }, [executor, undoTracker]);
3734
4070
  const on = react.useCallback(
3735
4071
  (type, listener) => registry.on(type, listener),
3736
4072
  [registry]
@@ -3745,6 +4081,9 @@ function UIBridgeNativeProvider({
3745
4081
  config,
3746
4082
  registry,
3747
4083
  executor,
4084
+ modalDetector,
4085
+ toastCapture,
4086
+ undoTracker,
3748
4087
  getElements,
3749
4088
  getComponents,
3750
4089
  createSnapshot,
@@ -3761,6 +4100,9 @@ function UIBridgeNativeProvider({
3761
4100
  config,
3762
4101
  registry,
3763
4102
  executor,
4103
+ modalDetector,
4104
+ toastCapture,
4105
+ undoTracker,
3764
4106
  getElements,
3765
4107
  getComponents,
3766
4108
  createSnapshot,
@@ -4278,14 +4620,48 @@ function useAutoRegister(options = {}) {
4278
4620
  isEnabled: enabled && !!bridge?.registry
4279
4621
  };
4280
4622
  }
4623
+ function useUIBridgeModal(opts) {
4624
+ const { modalDetector } = useUIBridgeNative();
4625
+ const { id, title, type, blocking, dismissible } = opts;
4626
+ react.useEffect(() => {
4627
+ modalDetector.pushModal({ id, title, type, blocking, dismissible });
4628
+ return () => {
4629
+ modalDetector.dismissModal(id);
4630
+ };
4631
+ }, [modalDetector, id, title, type, blocking, dismissible]);
4632
+ }
4633
+ function useUIBridgeToast(opts) {
4634
+ const { toastCapture } = useUIBridgeNative();
4635
+ const { id, message, level } = opts;
4636
+ react.useEffect(() => {
4637
+ toastCapture.recordToast({ id, message, level });
4638
+ return () => {
4639
+ toastCapture.dismissToast(id);
4640
+ };
4641
+ }, [toastCapture, id, message, level]);
4642
+ }
4643
+ function useToastRecorder() {
4644
+ const { toastCapture } = useUIBridgeNative();
4645
+ const recordToast = react.useCallback(
4646
+ (toast) => {
4647
+ toastCapture.recordToast(toast);
4648
+ },
4649
+ [toastCapture]
4650
+ );
4651
+ const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
4652
+ return { recordToast, dismissToast };
4653
+ }
4281
4654
 
4282
4655
  exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
4283
4656
  exports.useAutoRegister = useAutoRegister;
4657
+ exports.useToastRecorder = useToastRecorder;
4284
4658
  exports.useUIBridge = useUIBridge;
4659
+ exports.useUIBridgeModal = useUIBridgeModal;
4285
4660
  exports.useUIBridgeNative = useUIBridgeNative;
4286
4661
  exports.useUIBridgeNativeOptional = useUIBridgeNativeOptional;
4287
4662
  exports.useUIBridgeNativeRequired = useUIBridgeNativeRequired;
4288
4663
  exports.useUIBridgeRequired = useUIBridgeRequired;
4664
+ exports.useUIBridgeToast = useUIBridgeToast;
4289
4665
  exports.useUIComponent = useUIComponent;
4290
4666
  exports.useUIComponentAction = useUIComponentAction;
4291
4667
  exports.useUIElement = useUIElement;