@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
@@ -27,32 +27,16 @@ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
28
  import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
29
29
  import { SSEClient } from '@semiont/api-client';
30
- import type { ResourceUri, AnnotationUri } from '@semiont/api-client';
31
- import { resourceUri, annotationUri } from '@semiont/api-client';
30
+ import type { ResourceUri, AnnotationUri } from '@semiont/core';
31
+ import { resourceUri, annotationUri } from '@semiont/core';
32
32
  import type { Emitter } from 'mitt';
33
- import type { EventMap } from '../../../contexts/EventBusContext';
33
+ import type { EventMap } from '@semiont/core';
34
34
 
35
- // Mock SSE stream that we can control in tests
35
+ // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
36
36
  const createMockGenerationStream = () => {
37
- const stream = {
38
- onProgressCallback: null as ((chunk: any) => void) | null,
39
- onCompleteCallback: null as ((finalChunk: any) => void) | null,
40
- onErrorCallback: null as ((error: Error) => void) | null,
41
- onProgress: vi.fn((callback: (chunk: any) => void) => {
42
- stream.onProgressCallback = callback;
43
- return stream;
44
- }),
45
- onComplete: vi.fn((callback: (finalChunk: any) => void) => {
46
- stream.onCompleteCallback = callback;
47
- return stream;
48
- }),
49
- onError: vi.fn((callback: (error: Error) => void) => {
50
- stream.onErrorCallback = callback;
51
- return stream;
52
- }),
37
+ return {
53
38
  close: vi.fn(),
54
39
  };
55
- return stream;
56
40
  };
57
41
 
58
42
  describe('Generation Flow - Feature Integration', () => {
@@ -107,7 +91,7 @@ describe('Generation Flow - Feature Integration', () => {
107
91
  const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
108
92
  const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
109
93
 
110
- const { emitGenerationStart } = renderGenerationFlow(
94
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
111
95
  testResourceUri
112
96
  );
113
97
 
@@ -146,7 +130,7 @@ describe('Generation Flow - Feature Integration', () => {
146
130
  entityTypes: ['Person', 'Organization'],
147
131
  },
148
132
  },
149
- { auth: undefined }
133
+ expect.objectContaining({ auth: undefined })
150
134
  );
151
135
  });
152
136
 
@@ -154,7 +138,7 @@ describe('Generation Flow - Feature Integration', () => {
154
138
  const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
155
139
  const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
156
140
 
157
- const { emitGenerationStart } = renderGenerationFlow(
141
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
158
142
  testResourceUri
159
143
  );
160
144
 
@@ -173,7 +157,7 @@ describe('Generation Flow - Feature Integration', () => {
173
157
 
174
158
  // Simulate SSE progress callback being invoked
175
159
  act(() => {
176
- mockStream.onProgressCallback!({
160
+ getEventBus().get('generation:progress').next({
177
161
  status: 'generating',
178
162
  message: 'Generating content...',
179
163
  percentage: 25,
@@ -191,7 +175,7 @@ describe('Generation Flow - Feature Integration', () => {
191
175
  const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
192
176
  const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
193
177
 
194
- const { emitGenerationStart } = renderGenerationFlow(
178
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
195
179
  testResourceUri
196
180
  );
197
181
 
@@ -209,7 +193,7 @@ describe('Generation Flow - Feature Integration', () => {
209
193
 
210
194
  // First progress update
211
195
  act(() => {
212
- mockStream.onProgressCallback!({
196
+ getEventBus().get('generation:progress').next({
213
197
  status: 'started',
214
198
  message: 'Starting generation...',
215
199
  percentage: 0,
@@ -222,7 +206,7 @@ describe('Generation Flow - Feature Integration', () => {
222
206
 
223
207
  // Second progress update
224
208
  act(() => {
225
- mockStream.onProgressCallback!({
209
+ getEventBus().get('generation:progress').next({
226
210
  status: 'generating',
227
211
  message: 'Creating document structure...',
228
212
  percentage: 50,
@@ -235,11 +219,14 @@ describe('Generation Flow - Feature Integration', () => {
235
219
 
236
220
  // Final progress update via onComplete
237
221
  act(() => {
238
- mockStream.onCompleteCallback!({
239
- status: 'complete',
240
- message: 'Document created successfully',
241
- percentage: 100,
242
- resourceName: 'Generated Document',
222
+ getEventBus().get('generation:complete').next({
223
+ annotationUri: testAnnotationUri,
224
+ progress: {
225
+ status: 'complete',
226
+ message: 'Document created successfully',
227
+ percentage: 100,
228
+ resourceName: 'Generated Document',
229
+ }
243
230
  });
244
231
  });
245
232
 
@@ -271,7 +258,7 @@ describe('Generation Flow - Feature Integration', () => {
271
258
 
272
259
  // Simulate completion with final chunk
273
260
  act(() => {
274
- mockStream.onProgressCallback!({
261
+ getEventBus().get('generation:progress').next({
275
262
  status: 'complete',
276
263
  message: 'Complete',
277
264
  resourceName: 'Generated Document',
@@ -280,7 +267,7 @@ describe('Generation Flow - Feature Integration', () => {
280
267
 
281
268
  // Emit completion event
282
269
  act(() => {
283
- getEventBus().emit('generation:complete', {
270
+ getEventBus().get('generation:complete').next({
284
271
  annotationUri: testAnnotationUri,
285
272
  progress: {
286
273
  status: 'complete',
@@ -311,7 +298,7 @@ describe('Generation Flow - Feature Integration', () => {
311
298
 
312
299
  // Add some progress
313
300
  act(() => {
314
- mockStream.onProgressCallback!({
301
+ getEventBus().get('generation:progress').next({
315
302
  status: 'generating',
316
303
  message: 'Generating...',
317
304
  });
@@ -323,7 +310,7 @@ describe('Generation Flow - Feature Integration', () => {
323
310
 
324
311
  // Emit failure
325
312
  act(() => {
326
- getEventBus().emit('generation:failed', { error: new Error('Network error') });
313
+ getEventBus().get('generation:failed').next({ error: new Error('Network error') });
327
314
  });
328
315
 
329
316
  // Verify: progress cleared and not generating
@@ -343,7 +330,7 @@ describe('Generation Flow - Feature Integration', () => {
343
330
 
344
331
  // Add an additional event listener (simulating multiple subscribers)
345
332
  const additionalListener = vi.fn();
346
- getEventBus().on('generation:start', additionalListener);
333
+ const subscription = getEventBus().get('generation:start').subscribe(additionalListener);
347
334
 
348
335
  // Trigger generation
349
336
  act(() => {
@@ -363,13 +350,15 @@ describe('Generation Flow - Feature Integration', () => {
363
350
 
364
351
  // VERIFY: Our additional listener was called (events work)
365
352
  expect(additionalListener).toHaveBeenCalledTimes(1);
353
+
354
+ subscription.unsubscribe();
366
355
  });
367
356
 
368
357
  it('should forward final chunk as progress before emitting complete', async () => {
369
358
  const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
370
359
  const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
371
360
 
372
- const { emitGenerationStart } = renderGenerationFlow(
361
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
373
362
  testResourceUri
374
363
  );
375
364
 
@@ -387,11 +376,14 @@ describe('Generation Flow - Feature Integration', () => {
387
376
 
388
377
  // Simulate onComplete with final chunk
389
378
  act(() => {
390
- mockStream.onCompleteCallback!({
391
- status: 'complete',
392
- message: 'Document created: My Document',
393
- resourceName: 'My Document',
394
- percentage: 100,
379
+ getEventBus().get('generation:complete').next({
380
+ annotationUri: testAnnotationUri,
381
+ progress: {
382
+ status: 'complete',
383
+ message: 'Document created: My Document',
384
+ resourceName: 'My Document',
385
+ percentage: 100,
386
+ }
395
387
  });
396
388
  });
397
389
 
@@ -470,7 +462,7 @@ function renderGenerationFlow(
470
462
  resourceUri: ResourceUri,
471
463
  defaultTitle: string
472
464
  ) => {
473
- eventBusInstance.emit('generation:modal-open', {
465
+ eventBusInstance.get('generation:modal-open').next({
474
466
  annotationUri,
475
467
  resourceUri,
476
468
  defaultTitle,
@@ -488,7 +480,7 @@ function renderGenerationFlow(
488
480
  context: any;
489
481
  }
490
482
  ) => {
491
- eventBusInstance.emit('generation:start', {
483
+ eventBusInstance.get('generation:start').next({
492
484
  annotationUri,
493
485
  resourceUri,
494
486
  options,
@@ -22,7 +22,8 @@ import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
22
22
  import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
23
23
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
24
24
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
25
- import { SemiontApiClient, resourceUri, accessToken } from '@semiont/api-client';
25
+ import { SemiontApiClient } from '@semiont/api-client';
26
+ import { resourceUri, accessToken } from '@semiont/core';
26
27
 
27
28
  describe('Resolution Flow - Search Modal & Body Update Integration', () => {
28
29
  let updateAnnotationBodySpy: ReturnType<typeof vi.fn>;
@@ -66,15 +67,7 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
66
67
 
67
68
  return {
68
69
  getState: () => lastState!,
69
- emit: (event: Parameters<typeof eventBusInstance.emit>[0], payload: Parameters<typeof eventBusInstance.emit>[1]) => {
70
- act(() => { eventBusInstance!.emit(event as any, payload as any); });
71
- },
72
- on: (event: Parameters<typeof eventBusInstance.on>[0], handler: (payload: any) => void) => {
73
- eventBusInstance!.on(event as any, handler);
74
- },
75
- off: (event: Parameters<typeof eventBusInstance.off>[0], handler: (payload: any) => void) => {
76
- eventBusInstance!.off(event as any, handler);
77
- },
70
+ getEventBus: () => eventBusInstance!,
78
71
  };
79
72
  }
80
73
 
@@ -89,12 +82,12 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
89
82
  // ─── reference:link ─────────────────────────────────────────────────────────
90
83
 
91
84
  it('reference:link emits resolution:search-requested with referenceId and searchTerm', () => {
92
- const { emit, on, off } = renderResolutionFlow();
85
+ const { getEventBus } = renderResolutionFlow();
93
86
  const searchRequestedSpy = vi.fn();
94
87
 
95
- on('resolution:search-requested', searchRequestedSpy);
96
- emit('reference:link', { annotationUri: 'ann-uri-123', searchTerm: 'climate change' });
97
- off('resolution:search-requested', searchRequestedSpy);
88
+ const subscription = getEventBus().get('resolution:search-requested').subscribe(searchRequestedSpy);
89
+ act(() => { getEventBus().get('reference:link').next({ annotationUri: 'ann-uri-123', searchTerm: 'climate change' }); });
90
+ subscription.unsubscribe();
98
91
 
99
92
  expect(searchRequestedSpy).toHaveBeenCalledTimes(1);
100
93
  expect(searchRequestedSpy).toHaveBeenCalledWith({
@@ -106,11 +99,11 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
106
99
  // ─── resolution:search-requested ────────────────────────────────────────────
107
100
 
108
101
  it('resolution:search-requested opens the search modal', async () => {
109
- const { getState, emit } = renderResolutionFlow();
102
+ const { getState, getEventBus } = renderResolutionFlow();
110
103
 
111
104
  expect(getState().searchModalOpen).toBe(false);
112
105
 
113
- emit('resolution:search-requested', { referenceId: 'ref-abc', searchTerm: 'oceans' });
106
+ act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-abc', searchTerm: 'oceans' }); });
114
107
 
115
108
  await waitFor(() => {
116
109
  expect(getState().searchModalOpen).toBe(true);
@@ -118,9 +111,9 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
118
111
  });
119
112
 
120
113
  it('resolution:search-requested sets pendingReferenceId', async () => {
121
- const { getState, emit } = renderResolutionFlow();
114
+ const { getState, getEventBus } = renderResolutionFlow();
122
115
 
123
- emit('resolution:search-requested', { referenceId: 'ref-xyz', searchTerm: 'forests' });
116
+ act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-xyz', searchTerm: 'forests' }); });
124
117
 
125
118
  await waitFor(() => {
126
119
  expect(getState().pendingReferenceId).toBe('ref-xyz');
@@ -128,10 +121,10 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
128
121
  });
129
122
 
130
123
  it('reference:link → resolution:search-requested chain opens modal end-to-end', async () => {
131
- const { getState, emit } = renderResolutionFlow();
124
+ const { getState, getEventBus } = renderResolutionFlow();
132
125
 
133
126
  // Simulate the full user journey: user clicks "Link Document" on a reference entry
134
- emit('reference:link', { annotationUri: 'ann-full-chain', searchTerm: 'biodiversity' });
127
+ act(() => { getEventBus().get('reference:link').next({ annotationUri: 'ann-full-chain', searchTerm: 'biodiversity' }); });
135
128
 
136
129
  await waitFor(() => {
137
130
  expect(getState().searchModalOpen).toBe(true);
@@ -142,9 +135,9 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
142
135
  // ─── onCloseSearchModal ──────────────────────────────────────────────────────
143
136
 
144
137
  it('onCloseSearchModal closes the search modal', async () => {
145
- const { getState, emit } = renderResolutionFlow();
138
+ const { getState, getEventBus } = renderResolutionFlow();
146
139
 
147
- emit('resolution:search-requested', { referenceId: 'ref-close', searchTerm: 'test' });
140
+ act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-close', searchTerm: 'test' }); });
148
141
 
149
142
  await waitFor(() => expect(getState().searchModalOpen).toBe(true));
150
143
 
@@ -156,9 +149,9 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
156
149
  });
157
150
 
158
151
  it('onCloseSearchModal does not clear pendingReferenceId (preserves for re-open)', async () => {
159
- const { getState, emit } = renderResolutionFlow();
152
+ const { getState, getEventBus } = renderResolutionFlow();
160
153
 
161
- emit('resolution:search-requested', { referenceId: 'ref-persist', searchTerm: 'test' });
154
+ act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-persist', searchTerm: 'test' }); });
162
155
  await waitFor(() => expect(getState().searchModalOpen).toBe(true));
163
156
 
164
157
  act(() => { getState().onCloseSearchModal(); });
@@ -171,13 +164,13 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
171
164
  // ─── annotation:update-body ──────────────────────────────────────────────────
172
165
 
173
166
  it('annotation:update-body calls updateAnnotationBody API', async () => {
174
- const { emit } = renderResolutionFlow();
167
+ const { getEventBus } = renderResolutionFlow();
175
168
 
176
- emit('annotation:update-body', {
169
+ act(() => { getEventBus().get('annotation:update-body').next({
177
170
  annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-body-1',
178
171
  resourceId: 'linked-resource-id',
179
172
  operations: [{ op: 'add', item: { id: 'linked-resource-id' } }],
180
- });
173
+ }); });
181
174
 
182
175
  await waitFor(() => {
183
176
  expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
@@ -185,13 +178,13 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
185
178
  });
186
179
 
187
180
  it('annotation:update-body passes auth token to API call', async () => {
188
- const { emit } = renderResolutionFlow();
181
+ const { getEventBus } = renderResolutionFlow();
189
182
 
190
- emit('annotation:update-body', {
183
+ act(() => { getEventBus().get('annotation:update-body').next({
191
184
  annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-auth',
192
185
  resourceId: 'resource-id',
193
186
  operations: [{ op: 'replace', newItem: { id: 'resource-id' } }],
194
- });
187
+ }); });
195
188
 
196
189
  await waitFor(() => {
197
190
  expect(updateAnnotationBodySpy).toHaveBeenCalled();
@@ -203,22 +196,22 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
203
196
  });
204
197
 
205
198
  it('annotation:update-body emits annotation:body-updated on success', async () => {
206
- const { emit, on, off } = renderResolutionFlow();
199
+ const { getEventBus } = renderResolutionFlow();
207
200
  const bodyUpdatedSpy = vi.fn();
208
201
 
209
- on('annotation:body-updated', bodyUpdatedSpy);
202
+ const subscription = getEventBus().get('annotation:body-updated').subscribe(bodyUpdatedSpy);
210
203
 
211
- emit('annotation:update-body', {
204
+ act(() => { getEventBus().get('annotation:update-body').next({
212
205
  annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-success',
213
206
  resourceId: 'resource-id',
214
207
  operations: [{ op: 'add', item: { id: 'resource-id' } }],
215
- });
208
+ }); });
216
209
 
217
210
  await waitFor(() => {
218
211
  expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
219
212
  });
220
213
 
221
- off('annotation:body-updated', bodyUpdatedSpy);
214
+ subscription.unsubscribe();
222
215
 
223
216
  expect(bodyUpdatedSpy).toHaveBeenCalledWith({
224
217
  annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-success',
@@ -228,22 +221,22 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
228
221
  it('annotation:update-body emits annotation:body-update-failed on API error', async () => {
229
222
  updateAnnotationBodySpy.mockRejectedValue(new Error('Update failed'));
230
223
 
231
- const { emit, on, off } = renderResolutionFlow();
224
+ const { getEventBus } = renderResolutionFlow();
232
225
  const bodyUpdateFailedSpy = vi.fn();
233
226
 
234
- on('annotation:body-update-failed', bodyUpdateFailedSpy);
227
+ const subscription = getEventBus().get('annotation:body-update-failed').subscribe(bodyUpdateFailedSpy);
235
228
 
236
- emit('annotation:update-body', {
229
+ act(() => { getEventBus().get('annotation:update-body').next({
237
230
  annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-fail',
238
231
  resourceId: 'resource-id',
239
232
  operations: [{ op: 'remove', item: { id: 'old-id' } }],
240
- });
233
+ }); });
241
234
 
242
235
  await waitFor(() => {
243
236
  expect(bodyUpdateFailedSpy).toHaveBeenCalledTimes(1);
244
237
  });
245
238
 
246
- off('annotation:body-update-failed', bodyUpdateFailedSpy);
239
+ subscription.unsubscribe();
247
240
 
248
241
  expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
249
242
  error: expect.any(Error),
@@ -251,13 +244,13 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
251
244
  });
252
245
 
253
246
  it('annotation:update-body called ONCE — no duplicate subscriptions', async () => {
254
- const { emit } = renderResolutionFlow();
247
+ const { getEventBus } = renderResolutionFlow();
255
248
 
256
- emit('annotation:update-body', {
249
+ act(() => { getEventBus().get('annotation:update-body').next({
257
250
  annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-dedup',
258
251
  resourceId: 'resource-id',
259
252
  operations: [{ op: 'add', item: { id: 'resource-id' } }],
260
- });
253
+ }); });
261
254
 
262
255
  await waitFor(() => {
263
256
  expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
@@ -25,14 +25,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
25
25
  import { render, waitFor } from '@testing-library/react';
26
26
  import { act } from 'react';
27
27
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
28
- import { SemiontApiClient, resourceUri, accessToken } from '@semiont/api-client';
28
+ import { SemiontApiClient } from '@semiont/api-client';
29
+ import { resourceUri, accessToken } from '@semiont/core';
29
30
  import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
30
31
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
31
32
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
32
33
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
33
34
  import { useResources } from '../../../lib/api-hooks';
34
- import type { EventMap } from '../../../contexts/EventBusContext';
35
- import type { Emitter } from 'mitt';
35
+ import type { EventMap, EventBus } from '@semiont/core';
36
36
 
37
37
  // ─── Constants ────────────────────────────────────────────────────────────────
38
38
 
@@ -50,10 +50,10 @@ const CLONE_TOKEN = 'generated-clone-token-xyz';
50
50
  * The critical invariant under test: useMutation() is called at hook level
51
51
  * (inside useResources), not inside the useCallback bodies.
52
52
  */
53
- function ResourceMutationHarness({ onEventBus }: { onEventBus: (bus: Emitter<EventMap>) => void }) {
53
+ function ResourceMutationHarness({ onEventBus }: { onEventBus: (eventBus: EventBus) => void }) {
54
54
  const eventBus = useEventBus();
55
55
 
56
- // Capture the bus for the test to emit events
56
+ // Capture the eventBus for the test to emit events
57
57
  React.useEffect(() => {
58
58
  onEventBus(eventBus);
59
59
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
@@ -91,7 +91,7 @@ function ResourceMutationHarness({ onEventBus }: { onEventBus: (bus: Emitter<Eve
91
91
  // ─── Test setup ───────────────────────────────────────────────────────────────
92
92
 
93
93
  function renderHarness() {
94
- let capturedBus: Emitter<EventMap> | null = null;
94
+ let capturedEventBus: EventBus | null = null;
95
95
 
96
96
  const queryClient = new QueryClient({
97
97
  defaultOptions: {
@@ -105,7 +105,7 @@ function renderHarness() {
105
105
  <ApiClientProvider baseUrl={BASE_URL}>
106
106
  <QueryClientProvider client={queryClient}>
107
107
  <EventBusProvider>
108
- <ResourceMutationHarness onEventBus={(bus) => { capturedBus = bus; }} />
108
+ <ResourceMutationHarness onEventBus={(eventBus) => { capturedEventBus = eventBus; }} />
109
109
  </EventBusProvider>
110
110
  </QueryClientProvider>
111
111
  </ApiClientProvider>
@@ -113,7 +113,7 @@ function renderHarness() {
113
113
  );
114
114
 
115
115
  const emit = <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
116
- act(() => { capturedBus!.emit(event, payload); });
116
+ act(() => { capturedEventBus!.get(event).next(payload); });
117
117
  };
118
118
 
119
119
  return { emit };
@@ -32,7 +32,7 @@ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/Eve
32
32
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
33
33
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
34
34
  import { SSEClient } from '@semiont/api-client';
35
- import type { components } from '@semiont/api-client';
35
+ import type { components } from '@semiont/core';
36
36
 
37
37
  type Annotation = components['schemas']['Annotation'];
38
38
 
@@ -60,33 +60,26 @@ vi.mock('../../../contexts/TranslationContext', () => ({
60
60
 
61
61
  // Create a mock SSE stream that we can control
62
62
  class MockSSEStream {
63
- private progressHandlers: Array<(chunk: any) => void> = [];
64
- private completeHandlers: Array<(finalChunk?: any) => void> = [];
65
- private errorHandlers: Array<(error: Error) => void> = [];
63
+ constructor(private eventBus: any) {}
66
64
 
67
- onProgress(handler: (chunk: any) => void) {
68
- this.progressHandlers.push(handler);
65
+ close() {
66
+ // Mock close method
69
67
  }
70
68
 
71
- onComplete(handler: (finalChunk?: any) => void) {
72
- this.completeHandlers.push(handler);
73
- }
74
-
75
- onError(handler: (error: Error) => void) {
76
- this.errorHandlers.push(handler);
77
- }
78
-
79
- // Test helper methods
69
+ // Test helper methods that emit to EventBus
80
70
  emitProgress(chunk: any) {
81
- this.progressHandlers.forEach(handler => handler(chunk));
71
+ this.eventBus.get('detection:progress').next(chunk);
82
72
  }
83
73
 
84
74
  emitComplete(finalChunk?: any) {
85
- this.completeHandlers.forEach(handler => handler(finalChunk));
75
+ if (finalChunk) {
76
+ this.eventBus.get('detection:progress').next(finalChunk);
77
+ }
78
+ this.eventBus.get('detection:complete').next({});
86
79
  }
87
80
 
88
81
  emitError(error: Error) {
89
- this.errorHandlers.forEach(handler => handler(error));
82
+ this.eventBus.get('detection:failed').next({ error });
90
83
  }
91
84
  }
92
85
 
@@ -135,11 +128,11 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
135
128
 
136
129
  beforeEach(() => {
137
130
  // Reset event bus for test isolation
138
- resetEventBusForTesting();
131
+ const eventBus = resetEventBusForTesting();
139
132
  vi.clearAllMocks();
140
133
 
141
- // Reset mocks
142
- mockStream = new MockSSEStream();
134
+ // Reset mocks - create stream with eventBus
135
+ mockStream = new MockSSEStream(eventBus);
143
136
 
144
137
  // Spy on SSEClient prototype methods to inject mock stream
145
138
  vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
@@ -7,8 +7,9 @@
7
7
 
8
8
  import React, { useState, useEffect, useCallback, useMemo } from 'react';
9
9
  import { useQueryClient } from '@tanstack/react-query';
10
- import type { components, ResourceUri } from '@semiont/api-client';
11
- import { getLanguage, getPrimaryRepresentation, resourceAnnotationUri, getPrimaryMediaType } from '@semiont/api-client';
10
+ import type { components, ResourceUri } from '@semiont/core';
11
+ import { resourceAnnotationUri } from '@semiont/core';
12
+ import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType } from '@semiont/api-client';
12
13
  import { uriToAnnotationId } from '@semiont/core';
13
14
  import { ANNOTATORS } from '@semiont/react-ui';
14
15
  import { ErrorBoundary } from '@semiont/react-ui';
@@ -308,7 +309,7 @@ export function ResourceViewerPage({
308
309
  try {
309
310
  const result = await generateCloneTokenMutation.mutateAsync(rUri);
310
311
  const token = result.token;
311
- eventBus.emit('navigation:router-push', { path: `/know/compose?mode=clone&token=${token}`, reason: 'clone' });
312
+ eventBus.get('navigation:router-push').next({ path: `/know/compose?mode=clone&token=${token}`, reason: 'clone' });
312
313
  } catch (err) {
313
314
  console.error('Failed to generate clone token:', err);
314
315
  showError('Failed to generate clone link');
@@ -345,14 +346,14 @@ export function ResourceViewerPage({
345
346
  const handleReferenceNavigate = useCallback(({ documentId }: { documentId: string }) => {
346
347
  if (routes.resource) {
347
348
  const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
348
- eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
349
+ eventBus.get('navigation:router-push').next({ path, reason: 'reference-link' });
349
350
  }
350
351
  }, [routes.resource]); // eventBus is stable singleton - never in deps
351
352
 
352
353
  const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
353
354
  if (routes.know) {
354
355
  const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
355
- eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
356
+ eventBus.get('navigation:router-push').next({ path, reason: 'entity-type-filter' });
356
357
  }
357
358
  }, [routes.know]); // eventBus is stable singleton - never in deps
358
359
 
@@ -436,7 +437,7 @@ export function ResourceViewerPage({
436
437
  // Handlers for AnnotationHistory (legacy event-based interaction)
437
438
  const handleEventHover = useCallback((annotationId: string | null) => {
438
439
  if (annotationId) {
439
- eventBus.emit('annotation:sparkle', { annotationId });
440
+ eventBus.get('annotation:sparkle').next({ annotationId });
440
441
  }
441
442
  }, []); // eventBus is stable singleton - never in deps
442
443
 
@@ -603,7 +604,7 @@ export function ResourceViewerPage({
603
604
  const resourceIdSegment = rUri.split('/').pop() || '';
604
605
  const nestedUri = `${window.location.origin}/resources/${resourceIdSegment}/annotations/${annotationIdShort}`;
605
606
 
606
- eventBus.emit('annotation:update-body', {
607
+ eventBus.get('annotation:update-body').next({
607
608
  annotationUri: resourceAnnotationUri(nestedUri),
608
609
  resourceId: resourceIdSegment,
609
610
  operations: [{