@semiont/react-ui 0.2.33 → 0.2.34-build.89

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 (82) hide show
  1. package/dist/EventBusContext-BmzEcGHZ.d.mts +177 -0
  2. package/dist/{PdfAnnotationCanvas.client-FGV33CWN.mjs → PdfAnnotationCanvas.client-VLNA5O5M.mjs} +7 -7
  3. package/dist/PdfAnnotationCanvas.client-VLNA5O5M.mjs.map +1 -0
  4. package/dist/{chunk-YPYLOBA2.mjs → chunk-C63BARI7.mjs} +3 -2
  5. package/dist/chunk-C63BARI7.mjs.map +1 -0
  6. package/dist/{chunk-FC6SGLLT.mjs → chunk-M7SZRRIE.mjs} +24 -16
  7. package/dist/chunk-M7SZRRIE.mjs.map +1 -0
  8. package/dist/chunk-ULIET3MW.mjs +31 -0
  9. package/dist/chunk-ULIET3MW.mjs.map +1 -0
  10. package/dist/index.d.mts +33 -60
  11. package/dist/index.mjs +171 -363
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/test-utils.d.mts +3 -5
  14. package/dist/test-utils.mjs +2 -2
  15. package/dist/test-utils.mjs.map +1 -1
  16. package/package.json +2 -3
  17. package/src/components/CodeMirrorRenderer.tsx +4 -4
  18. package/src/components/DetectionProgressWidget.tsx +3 -3
  19. package/src/components/LiveRegion.tsx +1 -1
  20. package/src/components/Toolbar.tsx +1 -1
  21. package/src/components/annotation/AnnotateToolbar.tsx +5 -5
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +4 -4
  23. package/src/components/annotation-popups/JsonLdView.tsx +1 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +9 -9
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +5 -5
  26. package/src/components/navigation/CollapsibleResourceNavigation.tsx +4 -4
  27. package/src/components/navigation/ObservableLink.tsx +1 -1
  28. package/src/components/navigation/SimpleNavigation.tsx +1 -1
  29. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -6
  30. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +2 -2
  31. package/src/components/resource/AnnotateView.tsx +5 -4
  32. package/src/components/resource/AnnotationHistory.tsx +1 -1
  33. package/src/components/resource/BrowseView.tsx +5 -4
  34. package/src/components/resource/ResourceViewer.tsx +5 -4
  35. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -22
  36. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +1 -1
  37. package/src/components/resource/event-formatting.ts +1 -1
  38. package/src/components/resource/panels/AssessmentEntry.tsx +2 -2
  39. package/src/components/resource/panels/AssessmentPanel.tsx +4 -4
  40. package/src/components/resource/panels/CommentEntry.tsx +2 -2
  41. package/src/components/resource/panels/CommentsPanel.tsx +4 -4
  42. package/src/components/resource/panels/DetectSection.tsx +3 -3
  43. package/src/components/resource/panels/HighlightEntry.tsx +2 -2
  44. package/src/components/resource/panels/HighlightPanel.tsx +2 -2
  45. package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
  46. package/src/components/resource/panels/ReferenceEntry.tsx +6 -6
  47. package/src/components/resource/panels/ReferencesPanel.tsx +5 -5
  48. package/src/components/resource/panels/ResourceInfoPanel.tsx +3 -3
  49. package/src/components/resource/panels/StatisticsPanel.tsx +1 -1
  50. package/src/components/resource/panels/TagEntry.tsx +2 -2
  51. package/src/components/resource/panels/TaggingPanel.tsx +5 -5
  52. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +1 -1
  53. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +5 -5
  54. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +10 -10
  55. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +5 -5
  56. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +9 -9
  57. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +1 -1
  58. package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +1 -1
  59. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +4 -4
  60. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +4 -4
  61. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +5 -5
  62. package/src/components/settings/SettingsPanel.tsx +5 -5
  63. package/src/components/viewers/ImageViewer.tsx +1 -1
  64. package/src/features/resource-compose/components/ResourceComposePage.tsx +1 -1
  65. package/src/features/resource-discovery/components/ResourceCard.tsx +1 -1
  66. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  67. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +7 -5
  68. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +5 -4
  69. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +5 -5
  70. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +29 -43
  71. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +20 -39
  72. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +38 -46
  73. package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +36 -43
  74. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +8 -8
  75. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +14 -21
  76. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +8 -7
  77. package/dist/EventBusContext-CJjL_cCf.d.mts +0 -462
  78. package/dist/PdfAnnotationCanvas.client-FGV33CWN.mjs.map +0 -1
  79. package/dist/chunk-FC6SGLLT.mjs.map +0 -1
  80. package/dist/chunk-XS27QKGP.mjs +0 -55
  81. package/dist/chunk-XS27QKGP.mjs.map +0 -1
  82. package/dist/chunk-YPYLOBA2.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/react-ui",
3
- "version": "0.2.33",
3
+ "version": "0.2.34-build.89",
4
4
  "description": "React components and hooks for Semiont",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -100,7 +100,6 @@
100
100
  },
101
101
  "dependencies": {
102
102
  "@semiont/api-client": "*",
103
- "@semiont/core": "*",
104
- "mitt": "^3.0.1"
103
+ "@semiont/core": "*"
105
104
  }
106
105
  }
@@ -7,8 +7,8 @@ import { markdown } from '@codemirror/lang-markdown';
7
7
  import { ANNOTATORS } from '../lib/annotation-registry';
8
8
  import { ReferenceResolutionWidget } from '../lib/codemirror-widgets';
9
9
  import { isHighlight, isReference, isResolvedReference, isComment, isAssessment, isTag, getBodySource } from '@semiont/api-client';
10
- import type { components } from '@semiont/api-client';
11
- import type { EventBus } from '../contexts/EventBusContext';
10
+ import type { components } from '@semiont/core';
11
+ import type { EventBus } from "@semiont/core";
12
12
  import { createHoverHandlers } from '../hooks/useAttentionFlow';
13
13
 
14
14
  type Annotation = components['schemas']['Annotation'];
@@ -343,7 +343,7 @@ export function CodeMirrorRenderer({
343
343
  const segment = segmentsRef.current.find(s => s.annotation?.id === annotationId);
344
344
  if (segment?.annotation) {
345
345
  event.preventDefault();
346
- eventBusRef.current.emit('annotation:click', {
346
+ eventBusRef.current.get('annotation:click').next({
347
347
  annotationId,
348
348
  motivation: segment.annotation.motivation
349
349
  });
@@ -411,7 +411,7 @@ export function CodeMirrorRenderer({
411
411
  const container = view.dom;
412
412
 
413
413
  const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
414
- (annotationId) => eventBusRef.current?.emit('annotation:hover', { annotationId })
414
+ (annotationId) => eventBusRef.current?.get('annotation:hover').next({ annotationId })
415
415
  );
416
416
 
417
417
  const handleMouseOver = (e: MouseEvent) => {
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { useTranslations } from '../contexts/TranslationContext';
4
4
  import { useEventBus } from '../contexts/EventBusContext';
5
- import type { DetectionProgress } from '../types/progress';
6
- import type { components } from '@semiont/api-client';
5
+ import type { DetectionProgress } from '@semiont/core';
6
+ import type { components } from '@semiont/core';
7
7
 
8
8
  type Motivation = components['schemas']['Motivation'];
9
9
 
@@ -28,7 +28,7 @@ export function DetectionProgressWidget({ progress, annotationType = 'reference'
28
28
 
29
29
  const handleCancel = () => {
30
30
  // Emit event for job cancellation
31
- eventBus.emit('job:cancel-requested', { jobType: 'detection' });
31
+ eventBus.get('job:cancel-requested').next({ jobType: 'detection' });
32
32
  };
33
33
 
34
34
  if (!progress) return null;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, createContext, useContext, useCallback } from 'react';
4
- import type { components } from '@semiont/api-client';
4
+ import type { components } from '@semiont/core';
5
5
  import type { Annotator } from '../lib/annotation-registry';
6
6
 
7
7
  type Annotation = components['schemas']['Annotation'];
@@ -28,7 +28,7 @@ export function Toolbar<T extends string = string>({
28
28
  const eventBus = useEventBus();
29
29
 
30
30
  const handlePanelToggle = (panel: string) => {
31
- eventBus.emit('panel:toggle', { panel });
31
+ eventBus.get('panel:toggle').next({ panel });
32
32
  };
33
33
 
34
34
  return (
@@ -188,9 +188,9 @@ export function AnnotateToolbar({
188
188
  const handleSelectionClick = (motivation: SelectionMotivation | null) => {
189
189
  // If null is clicked, always deselect. Otherwise toggle.
190
190
  if (motivation === null) {
191
- eventBus.emit('toolbar:selection-changed', { motivation: null });
191
+ eventBus.get('toolbar:selection-changed').next({ motivation: null });
192
192
  } else {
193
- eventBus.emit('toolbar:selection-changed', {
193
+ eventBus.get('toolbar:selection-changed').next({
194
194
  motivation: selectedMotivation === motivation ? null : motivation
195
195
  });
196
196
  }
@@ -200,21 +200,21 @@ export function AnnotateToolbar({
200
200
  };
201
201
 
202
202
  const handleClickClick = (action: ClickAction) => {
203
- eventBus.emit('toolbar:click-changed', { action });
203
+ eventBus.get('toolbar:click-changed').next({ action });
204
204
  // Close dropdown after selection
205
205
  setClickPinned(false);
206
206
  setClickHovered(false);
207
207
  };
208
208
 
209
209
  const handleShapeClick = (shape: ShapeType) => {
210
- eventBus.emit('toolbar:shape-changed', { shape });
210
+ eventBus.get('toolbar:shape-changed').next({ shape });
211
211
  // Close dropdown after selection
212
212
  setShapePinned(false);
213
213
  setShapeHovered(false);
214
214
  };
215
215
 
216
216
  const handleModeToggle = () => {
217
- eventBus.emit('view:mode-toggled', undefined);
217
+ eventBus.get('view:mode-toggled').next(undefined);
218
218
  setModePinned(false);
219
219
  setModeHovered(false);
220
220
  };
@@ -55,16 +55,16 @@ function createEventTracker() {
55
55
  'toolbar:click-changed',
56
56
  'toolbar:selection-changed',
57
57
  'toolbar:shape-changed',
58
- ];
58
+ ] as const;
59
59
 
60
60
  toolbarEvents.forEach(eventName => {
61
61
  const handler = trackEvent(eventName);
62
- eventBus.on(eventName, handler);
63
- handlers.push(() => eventBus.off(eventName, handler));
62
+ const subscription = eventBus.get(eventName).subscribe(handler);
63
+ handlers.push(subscription);
64
64
  });
65
65
 
66
66
  return () => {
67
- handlers.forEach(cleanup => cleanup());
67
+ handlers.forEach(sub => sub.unsubscribe());
68
68
  };
69
69
  }, [eventBus]);
70
70
 
@@ -8,7 +8,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
8
8
  import { syntaxHighlighting } from '@codemirror/language';
9
9
  import { jsonLightTheme, jsonLightHighlightStyle } from '../../lib/codemirror-json-theme';
10
10
  import { useLineNumbers } from '../../hooks/useLineNumbers';
11
- import type { components } from '@semiont/api-client';
11
+ import type { components } from '@semiont/core';
12
12
 
13
13
  type Annotation = components['schemas']['Annotation'];
14
14
 
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { useMemo } from 'react';
4
- import type { components } from '@semiont/api-client';
4
+ import type { components } from '@semiont/core';
5
5
  import { createHoverHandlers } from '../../hooks/useAttentionFlow';
6
6
  import { getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/api-client';
7
7
  import { parseSvgSelector } from '@semiont/api-client';
8
- import type { EventBus } from '../../contexts/EventBusContext';
8
+ import type { EventBus } from "@semiont/core"
9
9
 
10
10
  type Annotation = components['schemas']['Annotation'];
11
11
 
@@ -79,7 +79,7 @@ export function AnnotationOverlay({
79
79
  const scaleY = displayHeight / imageHeight;
80
80
 
81
81
  const { handleMouseEnter, handleMouseLeave } = useMemo(
82
- () => createHoverHandlers((annotationId) => eventBus?.emit('annotation:hover', { annotationId })),
82
+ () => createHoverHandlers((annotationId) => eventBus?.get('annotation:hover').next({ annotationId })),
83
83
  [eventBus]
84
84
  );
85
85
 
@@ -129,7 +129,7 @@ export function AnnotationOverlay({
129
129
  className="semiont-annotation-overlay__shape"
130
130
  data-hovered={isHovered ? 'true' : 'false'}
131
131
  data-selected={isSelected ? 'true' : 'false'}
132
- onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
132
+ onClick={() => eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
133
133
  onMouseEnter={() => handleMouseEnter(annotation.id)}
134
134
  onMouseLeave={handleMouseLeave}
135
135
  />
@@ -142,7 +142,7 @@ export function AnnotationOverlay({
142
142
  style={{ userSelect: 'none' }}
143
143
  onClick={(e) => {
144
144
  e.stopPropagation();
145
- eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
145
+ eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
146
146
  }}
147
147
  onMouseEnter={() => handleMouseEnter(annotation.id)}
148
148
  onMouseLeave={handleMouseLeave}
@@ -176,7 +176,7 @@ export function AnnotationOverlay({
176
176
  className="semiont-annotation-overlay__shape"
177
177
  data-hovered={isHovered ? 'true' : 'false'}
178
178
  data-selected={isSelected ? 'true' : 'false'}
179
- onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
179
+ onClick={() => eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
180
180
  onMouseEnter={() => handleMouseEnter(annotation.id)}
181
181
  onMouseLeave={handleMouseLeave}
182
182
  />
@@ -189,7 +189,7 @@ export function AnnotationOverlay({
189
189
  style={{ userSelect: 'none' }}
190
190
  onClick={(e) => {
191
191
  e.stopPropagation();
192
- eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
192
+ eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
193
193
  }}
194
194
  onMouseEnter={() => handleMouseEnter(annotation.id)}
195
195
  onMouseLeave={handleMouseLeave}
@@ -236,7 +236,7 @@ export function AnnotationOverlay({
236
236
  className="semiont-annotation-overlay__shape"
237
237
  data-hovered={isHovered ? 'true' : 'false'}
238
238
  data-selected={isSelected ? 'true' : 'false'}
239
- onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
239
+ onClick={() => eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
240
240
  onMouseEnter={() => handleMouseEnter(annotation.id)}
241
241
  onMouseLeave={handleMouseLeave}
242
242
  />
@@ -249,7 +249,7 @@ export function AnnotationOverlay({
249
249
  style={{ userSelect: 'none' }}
250
250
  onClick={(e) => {
251
251
  e.stopPropagation();
252
- eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
252
+ eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
253
253
  }}
254
254
  onMouseEnter={() => handleMouseEnter(annotation.id)}
255
255
  onMouseLeave={handleMouseLeave}
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useRef, useState, useEffect, useCallback, useMemo } from 'react';
4
- import type { components, ResourceUri } from '@semiont/api-client';
5
- import { createRectangleSvg, createCircleSvg, createPolygonSvg, scaleSvgToNative, parseSvgSelector, type Point } from '@semiont/api-client';
4
+ import type { components, ResourceUri } from '@semiont/core';
5
+ import { createRectangleSvg, createCircleSvg, createPolygonSvg, scaleSvgToNative, parseSvgSelector, Point } from '@semiont/api-client';
6
6
  import { AnnotationOverlay } from './AnnotationOverlay';
7
7
  import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
8
- import type { EventBus } from '../../contexts/EventBusContext';
8
+ import type { EventBus } from "@semiont/core"
9
9
 
10
10
  type Annotation = components['schemas']['Annotation'];
11
11
 
@@ -213,7 +213,7 @@ export function SvgDrawingCanvas({
213
213
  });
214
214
 
215
215
  if (clickedAnnotation) {
216
- eventBus?.emit('annotation:click', { annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
216
+ eventBus?.get('annotation:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
217
217
  setIsDrawing(false);
218
218
  setStartPoint(null);
219
219
  setCurrentPoint(null);
@@ -276,7 +276,7 @@ export function SvgDrawingCanvas({
276
276
 
277
277
  // Emit annotation:requested event with SvgSelector
278
278
  if (eventBus && selectedMotivation) {
279
- eventBus.emit('annotation:requested', {
279
+ eventBus.get('annotation:requested').next({
280
280
  selector: {
281
281
  type: 'SvgSelector',
282
282
  value: nativeSvg
@@ -110,7 +110,7 @@ export function CollapsibleResourceNavigation({
110
110
  }
111
111
 
112
112
  // Emit event
113
- eventBus.emit('navigation:resource-reorder', { oldIndex: currentIndex, newIndex });
113
+ eventBus.get('navigation:resource-reorder').next({ oldIndex: currentIndex, newIndex });
114
114
 
115
115
  // Announce the change
116
116
  const resource = resources[currentIndex];
@@ -123,7 +123,7 @@ export function CollapsibleResourceNavigation({
123
123
  e.stopPropagation();
124
124
 
125
125
  // Emit event
126
- eventBus.emit('navigation:resource-close', { resourceId });
126
+ eventBus.get('navigation:resource-close').next({ resourceId });
127
127
 
128
128
  // If we're closing the currently viewed resource, navigate to first fixed item or trigger callback
129
129
  const resourceHref = getResourceHref(resourceId);
@@ -151,7 +151,7 @@ export function CollapsibleResourceNavigation({
151
151
  const newIndex = resources.findIndex((resource) => resource.id === over.id);
152
152
  if (oldIndex !== -1 && newIndex !== -1) {
153
153
  // Emit event
154
- eventBus.emit('navigation:resource-reorder', { oldIndex, newIndex });
154
+ eventBus.get('navigation:resource-reorder').next({ oldIndex, newIndex });
155
155
  const resource = resources[oldIndex];
156
156
  announceDrop(resource.name, newIndex + 1, resources.length);
157
157
  }
@@ -189,7 +189,7 @@ export function CollapsibleResourceNavigation({
189
189
  <span className="semiont-nav-section__header-text">{mergedTranslations.title}</span>
190
190
  )}
191
191
  <button
192
- onClick={() => eventBus.emit('navigation:sidebar-toggle', undefined)}
192
+ onClick={() => eventBus.get('navigation:sidebar-toggle').next(undefined)}
193
193
  className="semiont-nav-section__header-icon"
194
194
  title={isCollapsed ? mergedTranslations.expandSidebar : mergedTranslations.collapseSidebar}
195
195
  aria-label={isCollapsed ? mergedTranslations.expandSidebar : mergedTranslations.collapseSidebar}
@@ -70,7 +70,7 @@ export function ObservableLink({
70
70
 
71
71
  const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
72
72
  // Emit event for observability
73
- eventBus.emit('navigation:link-clicked', {
73
+ eventBus.get('navigation:link-clicked').next({
74
74
  href,
75
75
  label
76
76
  });
@@ -86,7 +86,7 @@ export function SimpleNavigation({
86
86
  !isCollapsed && <span className="semiont-nav-section__header-text">{title}</span>
87
87
  )}
88
88
  <button
89
- onClick={() => eventBus.emit('navigation:sidebar-toggle', undefined)}
89
+ onClick={() => eventBus.get('navigation:sidebar-toggle').next(undefined)}
90
90
  className="semiont-nav-section__header-icon"
91
91
  title={isCollapsed ? expandSidebarLabel : collapseSidebarLabel}
92
92
  aria-label={isCollapsed ? expandSidebarLabel : collapseSidebarLabel}
@@ -2,10 +2,10 @@
2
2
 
3
3
  import React, { useRef, useState, useCallback, useEffect, useMemo } from 'react';
4
4
  import { createHoverHandlers } from '../../hooks/useAttentionFlow';
5
- import type { components, ResourceUri } from '@semiont/api-client';
5
+ import type { components, ResourceUri } from '@semiont/core';
6
6
  import { getTargetSelector } from '@semiont/api-client';
7
7
  import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
8
- import type { EventBus } from '../../contexts/EventBusContext';
8
+ import type { EventBus } from "@semiont/core"
9
9
  import {
10
10
  canvasToPdfCoordinates,
11
11
  pdfToCanvasCoordinates,
@@ -285,7 +285,7 @@ export function PdfAnnotationCanvas({
285
285
  });
286
286
 
287
287
  if (clickedAnnotation) {
288
- eventBus?.emit('annotation:click', { annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
288
+ eventBus?.get('annotation:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
289
289
  setIsDrawing(false);
290
290
  setSelection(null);
291
291
  return;
@@ -324,7 +324,7 @@ export function PdfAnnotationCanvas({
324
324
 
325
325
  // Emit annotation:requested event with FragmentSelector
326
326
  if (selectedMotivation) {
327
- eventBus.emit('annotation:requested', {
327
+ eventBus.get('annotation:requested').next({
328
328
  selector: {
329
329
  type: 'FragmentSelector',
330
330
  conformsTo: 'http://tools.ietf.org/rfc/rfc3778',
@@ -362,7 +362,7 @@ export function PdfAnnotationCanvas({
362
362
 
363
363
  // Hover handlers with currentHover guard and dwell delay
364
364
  const { handleMouseEnter, handleMouseLeave } = useMemo(
365
- () => createHoverHandlers((annotationId) => eventBus?.emit('annotation:hover', { annotationId })),
365
+ () => createHoverHandlers((annotationId) => eventBus?.get('annotation:hover').next({ annotationId })),
366
366
  [eventBus]
367
367
  );
368
368
 
@@ -462,7 +462,7 @@ export function PdfAnnotationCanvas({
462
462
  cursor: 'pointer',
463
463
  opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7
464
464
  }}
465
- onClick={() => eventBus?.emit('annotation:click', { annotationId: ann.id, motivation: ann.motivation })}
465
+ onClick={() => eventBus?.get('annotation:click').next({ annotationId: ann.id, motivation: ann.motivation })}
466
466
  onMouseEnter={() => handleMouseEnter(ann.id)}
467
467
  onMouseLeave={handleMouseLeave}
468
468
  />
@@ -11,8 +11,8 @@ import { describe, test, expect, vi, beforeEach } from 'vitest';
11
11
  import { render, screen, waitFor, fireEvent } from '@testing-library/react';
12
12
  import userEvent from '@testing-library/user-event';
13
13
  import { PdfAnnotationCanvas } from '../PdfAnnotationCanvas';
14
- import { resourceUri } from '@semiont/api-client';
15
- import type { components } from '@semiont/api-client';
14
+ import { resourceUri } from '@semiont/core';
15
+ import type { components } from '@semiont/core';
16
16
 
17
17
  type Annotation = components['schemas']['Annotation'];
18
18
 
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect, useCallback, lazy, Suspense } from 'react';
4
- import type { components } from '@semiont/api-client';
5
- import { getTextPositionSelector, getTextQuoteSelector, getTargetSelector, getMimeCategory, isPdfMimeType, resourceUri as toResourceUri, extractContext, findTextWithContext } from '@semiont/api-client';
4
+ import type { components } from '@semiont/core';
5
+ import { resourceUri as toResourceUri } from '@semiont/core';
6
+ import { getTextPositionSelector, getTextQuoteSelector, getTargetSelector, getMimeCategory, isPdfMimeType, extractContext, findTextWithContext } from '@semiont/api-client';
6
7
  import { ANNOTATORS } from '../../lib/annotation-registry';
7
8
  import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
8
9
  import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
@@ -247,7 +248,7 @@ export function AnnotateView({
247
248
 
248
249
  // Unified flow: all text annotations use BOTH TextPositionSelector and TextQuoteSelector
249
250
  if (selectedMotivation) {
250
- eventBus.emit('annotation:requested', {
251
+ eventBus.get('annotation:requested').next({
251
252
  selector: [
252
253
  {
253
254
  type: 'TextPositionSelector',
@@ -281,7 +282,7 @@ export function AnnotateView({
281
282
 
282
283
  // Unified flow: all text annotations use BOTH TextPositionSelector and TextQuoteSelector
283
284
  if (selectedMotivation) {
284
- eventBus.emit('annotation:requested', {
285
+ eventBus.get('annotation:requested').next({
285
286
  selector: [
286
287
  {
287
288
  type: 'TextPositionSelector',
@@ -4,7 +4,7 @@ import React, { useEffect, useRef } from 'react';
4
4
  import { useTranslations } from '../../contexts/TranslationContext';
5
5
  import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
6
6
  import { useResources } from '../../lib/api-hooks';
7
- import type { ResourceUri } from '@semiont/api-client';
7
+ import type { ResourceUri } from '@semiont/core';
8
8
  import type { StoredEvent } from '@semiont/core';
9
9
  import { getAnnotationUriFromEvent } from '@semiont/core';
10
10
  import { HistoryEvent } from './HistoryEvent';
@@ -5,8 +5,9 @@ import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
6
  import { remarkAnnotations, type PreparedAnnotation } from '../../lib/remark-annotations';
7
7
  import { rehypeRenderAnnotations } from '../../lib/rehype-render-annotations';
8
- import type { components } from '@semiont/api-client';
9
- import { getExactText, getTextPositionSelector, getTargetSelector, getBodySource, getMimeCategory, isPdfMimeType, resourceUri as toResourceUri } from '@semiont/api-client';
8
+ import type { components } from '@semiont/core';
9
+ import { resourceUri as toResourceUri } from '@semiont/core';
10
+ import { getExactText, getTextPositionSelector, getTargetSelector, getBodySource, getMimeCategory, isPdfMimeType } from '@semiont/api-client';
10
11
  import { ANNOTATORS } from '../../lib/annotation-registry';
11
12
  import { createHoverHandlers } from '../../hooks/useAttentionFlow';
12
13
  import { ImageViewer } from '../viewers';
@@ -111,13 +112,13 @@ export function BrowseView({
111
112
  if (annotationId && annotationType === 'reference') {
112
113
  const annotation = allAnnotations.find(a => a.id === annotationId);
113
114
  if (annotation) {
114
- eventBus.emit('annotation:click', { annotationId, motivation: annotation.motivation });
115
+ eventBus.get('annotation:click').next({ annotationId, motivation: annotation.motivation });
115
116
  }
116
117
  }
117
118
  };
118
119
 
119
120
  const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
120
- (annotationId) => eventBus.emit('annotation:hover', { annotationId })
121
+ (annotationId) => eventBus.get('annotation:hover').next({ annotationId })
121
122
  );
122
123
 
123
124
  // Single mouseover handler for the container - fires once on enter
@@ -6,8 +6,9 @@ import { AnnotateView, type SelectionMotivation, type ClickAction, type ShapeTyp
6
6
  import { BrowseView } from './BrowseView';
7
7
  import { PopupContainer } from '../annotation-popups/SharedPopupElements';
8
8
  import { JsonLdView } from '../annotation-popups/JsonLdView';
9
- import type { components } from '@semiont/api-client';
10
- import { getExactText, getTargetSelector, resourceUri, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
9
+ import type { components } from '@semiont/core';
10
+ import { resourceUri } from '@semiont/core';
11
+ import { getExactText, getTargetSelector, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
11
12
  import { useEventBus } from '../../contexts/EventBusContext';
12
13
  import { useEventSubscriptions } from '../../contexts/useEventSubscription';
13
14
  import { useCacheManager } from '../../contexts/CacheContext';
@@ -249,7 +250,7 @@ export function ResourceViewer({
249
250
 
250
251
  // Handle deleting annotations - emit event instead of direct call
251
252
  const handleDeleteAnnotation = useCallback((id: string) => {
252
- eventBus.emit('annotation:delete', { annotationId: id });
253
+ eventBus.get('annotation:delete').next({ annotationId: id });
253
254
  }, []); // eventBus is stable
254
255
 
255
256
  // Handle annotation clicks - memoized
@@ -338,7 +339,7 @@ export function ResourceViewer({
338
339
 
339
340
  // All annotations open the unified annotations panel
340
341
  // The panel internally switches tabs based on the motivation → tab mapping in UnifiedAnnotationsPanel
341
- eventBus.emit('panel:open', { panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
342
+ eventBus.get('panel:open').next({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
342
343
  }, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick]);
343
344
 
344
345
  // Event subscriptions - Combined into single useEventSubscriptions call to prevent hook ordering issues
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import React from 'react';
3
3
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import { BrowseView } from '../BrowseView';
5
- import type { components } from '@semiont/api-client';
5
+ import type { components } from '@semiont/core';
6
6
  import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../contexts/EventBusContext';
7
7
 
8
8
  type Annotation = components['schemas']['Annotation'];
@@ -101,20 +101,8 @@ function createEventTracker() {
101
101
  function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
102
102
  const eventBus = useEventBus();
103
103
 
104
- // Track subscriptions by wrapping the on method synchronously before render
105
- const originalOn = React.useRef(eventBus.on.bind(eventBus));
106
-
107
- if (!('__tracked' in eventBus.on)) {
108
- const trackedOn = ((eventName: string, handler: Function) => {
109
- subscriptions.add(eventName);
110
- return originalOn.current(eventName, handler);
111
- }) as typeof eventBus.on & { __tracked: true };
112
- trackedOn.__tracked = true;
113
- eventBus.on = trackedOn;
114
- }
115
-
116
104
  React.useEffect(() => {
117
- const handlers: Array<() => void> = [];
105
+ const handlers: Array<{ unsubscribe: () => void }> = [];
118
106
 
119
107
  // Track all annotation-related events
120
108
  const trackEvent = (eventName: string) => (payload: any) => {
@@ -125,16 +113,17 @@ function createEventTracker() {
125
113
  'annotation:hover',
126
114
  'annotation:click',
127
115
  'annotation:focus',
128
- ];
116
+ ] as const;
129
117
 
130
118
  annotationEvents.forEach(eventName => {
119
+ subscriptions.add(eventName);
131
120
  const handler = trackEvent(eventName);
132
- eventBus.on(eventName, handler);
133
- handlers.push(() => eventBus.off(eventName, handler));
121
+ const subscription = eventBus.get(eventName).subscribe(handler);
122
+ handlers.push(subscription);
134
123
  });
135
124
 
136
125
  return () => {
137
- handlers.forEach(cleanup => cleanup());
126
+ handlers.forEach(sub => sub.unsubscribe());
138
127
  };
139
128
  }, [eventBus]);
140
129
 
@@ -544,12 +533,12 @@ describe('BrowseView Component', () => {
544
533
  eventTracker.push({ event: 'annotation:click', annotationId: payload?.annotationId ?? null });
545
534
  };
546
535
 
547
- eventBus.on('annotation:hover', handleHover);
548
- eventBus.on('annotation:click', handleClick);
536
+ const subscription1 = eventBus.get('annotation:hover').subscribe(handleHover);
537
+ const subscription2 = eventBus.get('annotation:click').subscribe(handleClick);
549
538
 
550
539
  return () => {
551
- eventBus.off('annotation:hover', handleHover);
552
- eventBus.off('annotation:click', handleClick);
540
+ subscription1.unsubscribe();
541
+ subscription2.unsubscribe();
553
542
  };
554
543
  }, [eventBus]);
555
544
 
@@ -20,7 +20,7 @@ import { TranslationProvider } from '../../../contexts/TranslationContext';
20
20
  import { ResourceAnnotationsProvider } from '../../../contexts/ResourceAnnotationsContext';
21
21
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
22
22
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
23
- import type { components } from '@semiont/api-client';
23
+ import type { components } from '@semiont/core';
24
24
 
25
25
  type SemiontResource = components['schemas']['ResourceDescriptor'];
26
26
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { StoredEvent, ResourceEventType } from '@semiont/core';
9
- import type { components } from '@semiont/api-client';
9
+ import type { components } from '@semiont/core';
10
10
  import { getExactText, getTargetSelector } from '@semiont/api-client';
11
11
  import { ANNOTATORS } from '../../lib/annotation-registry';
12
12
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { forwardRef } from 'react';
4
- import type { components } from '@semiont/api-client';
4
+ import type { components } from '@semiont/core';
5
5
  import { getAnnotationExactText } from '@semiont/api-client';
6
6
  import { useEventBus } from '../../../contexts/EventBusContext';
7
7
  import { useHoverEmitter } from '../../../hooks/useAttentionFlow';
@@ -89,7 +89,7 @@ export const AssessmentEntry = forwardRef<HTMLDivElement, AssessmentEntryProps>(
89
89
  data-type="assessment"
90
90
  data-focused={isFocused ? 'true' : 'false'}
91
91
  onClick={() => {
92
- eventBus.emit('annotation:click', { annotationId: assessment.id, motivation: assessment.motivation });
92
+ eventBus.get('annotation:click').next({ annotationId: assessment.id, motivation: assessment.motivation });
93
93
  }}
94
94
  {...hoverProps}
95
95
  >
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
5
  import { useEventBus } from '../../../contexts/EventBusContext';
6
6
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
7
- import type { components, Selector } from '@semiont/api-client';
7
+ import type { components, Selector } from '@semiont/core';
8
8
  import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
9
9
  import { AssessmentEntry } from './AssessmentEntry';
10
10
  import { DetectSection } from './DetectSection';
@@ -141,7 +141,7 @@ export function AssessmentPanel({
141
141
  ? [{ type: 'TextualBody' as const, value: newAssessmentText, purpose: 'assessing' as const }]
142
142
  : [];
143
143
 
144
- eventBus.emit('annotation:create', {
144
+ eventBus.get('annotation:create').next({
145
145
  motivation: 'assessing',
146
146
  selector: pendingAnnotation.selector,
147
147
  body,
@@ -156,7 +156,7 @@ export function AssessmentPanel({
156
156
 
157
157
  const handleEscape = (e: KeyboardEvent) => {
158
158
  if (e.key === 'Escape') {
159
- eventBus.emit('annotation:cancel-pending', undefined);
159
+ eventBus.get('annotation:cancel-pending').next(undefined);
160
160
  setNewAssessmentText('');
161
161
  }
162
162
  };
@@ -209,7 +209,7 @@ export function AssessmentPanel({
209
209
  <div className="semiont-annotation-prompt__actions">
210
210
  <button
211
211
  onClick={() => {
212
- eventBus.emit('annotation:cancel-pending', undefined);
212
+ eventBus.get('annotation:cancel-pending').next(undefined);
213
213
  setNewAssessmentText('');
214
214
  }}
215
215
  className="semiont-button semiont-button--secondary"