@semiont/react-ui 0.4.2 → 0.4.3

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 (26) hide show
  1. package/dist/{PdfAnnotationCanvas.client-PVTVPDBQ.mjs → PdfAnnotationCanvas.client-LF6DDTCV.mjs} +3 -3
  2. package/dist/chunk-5JZFKRLW.mjs +62 -0
  3. package/dist/chunk-5JZFKRLW.mjs.map +1 -0
  4. package/dist/{chunk-PFQYNPQJ.mjs → chunk-F74ZQJMA.mjs} +31 -62
  5. package/dist/chunk-F74ZQJMA.mjs.map +1 -0
  6. package/dist/{chunk-ZPV43WN2.mjs → chunk-XMCUHQ2Y.mjs} +72 -3
  7. package/dist/chunk-XMCUHQ2Y.mjs.map +1 -0
  8. package/dist/index.d.mts +26 -9
  9. package/dist/index.mjs +1140 -1149
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/test-utils.mjs +3 -3
  12. package/package.json +3 -5
  13. package/src/components/resource/BrowseView.tsx +2 -2
  14. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +6 -6
  15. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +4 -4
  16. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -17
  17. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +4 -4
  18. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +14 -14
  19. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
  20. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +2 -2
  21. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +4 -4
  22. package/dist/chunk-2HGWOLVN.mjs +0 -31
  23. package/dist/chunk-2HGWOLVN.mjs.map +0 -1
  24. package/dist/chunk-PFQYNPQJ.mjs.map +0 -1
  25. package/dist/chunk-ZPV43WN2.mjs.map +0 -1
  26. /package/dist/{PdfAnnotationCanvas.client-PVTVPDBQ.mjs.map → PdfAnnotationCanvas.client-LF6DDTCV.mjs.map} +0 -0
@@ -1,16 +1,16 @@
1
1
  'use client';
2
2
  import {
3
- ApiClientProvider,
4
3
  OpenResourcesProvider,
5
4
  SessionProvider,
6
5
  ToastProvider,
7
6
  TranslationProvider
8
- } from "./chunk-PFQYNPQJ.mjs";
7
+ } from "./chunk-F74ZQJMA.mjs";
9
8
  import {
9
+ ApiClientProvider,
10
10
  EventBusProvider,
11
11
  resetEventBusForTesting,
12
12
  useEventBus
13
- } from "./chunk-2HGWOLVN.mjs";
13
+ } from "./chunk-5JZFKRLW.mjs";
14
14
  import "./chunk-VVCCMJS7.mjs";
15
15
  import {
16
16
  __commonJS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/react-ui",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "React components and hooks for Semiont",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -89,8 +89,7 @@
89
89
  "jsdom": "^28.0.0",
90
90
  "postcss-import": "^16.1.1",
91
91
  "tsup": "^8.0.1",
92
- "typescript": "^5.6.3",
93
- "vitest": "^4.0.18"
92
+ "typescript": "^5.6.3"
94
93
  },
95
94
  "publishConfig": {
96
95
  "access": "public"
@@ -104,7 +103,6 @@
104
103
  },
105
104
  "dependencies": {
106
105
  "@semiont/api-client": "*",
107
- "@semiont/core": "*",
108
- "@vitest/ui": "4.0.18"
106
+ "@semiont/core": "*"
109
107
  }
110
108
  }
@@ -196,8 +196,8 @@ export const BrowseView = memo(function BrowseView({
196
196
  scrollToAnnotation(annotationId);
197
197
  }, [scrollToAnnotation]);
198
198
 
199
- const handleAnnotationFocus = useCallback(({ annotationId }: { annotationId: string | null }) => {
200
- scrollToAnnotation(annotationId, true);
199
+ const handleAnnotationFocus = useCallback(({ annotationId }: { annotationId?: string | null }) => {
200
+ scrollToAnnotation(annotationId ?? null, true);
201
201
  }, [scrollToAnnotation]);
202
202
 
203
203
  useEventSubscriptions({
@@ -103,13 +103,13 @@ function renderDetectionFlow(testUri: string) {
103
103
  // ─── Tests ────────────────────────────────────────────────────────────────────
104
104
 
105
105
  describe('Annotation creation clears pendingAnnotation', () => {
106
- let createAnnotationSpy: ReturnType<typeof vi.spyOn>;
106
+ let markAnnotationSpy: ReturnType<typeof vi.spyOn>;
107
107
 
108
108
  beforeEach(() => {
109
109
  vi.clearAllMocks();
110
110
  resetEventBusForTesting();
111
- createAnnotationSpy = vi
112
- .spyOn(SemiontApiClient.prototype, 'createAnnotation')
111
+ markAnnotationSpy = vi
112
+ .spyOn(SemiontApiClient.prototype, 'markAnnotation')
113
113
  .mockResolvedValue({ annotationId: MOCK_ANNOTATION.id } as any);
114
114
  });
115
115
 
@@ -143,7 +143,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
143
143
  expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
144
144
  });
145
145
 
146
- expect(createAnnotationSpy).toHaveBeenCalledTimes(1);
146
+ expect(markAnnotationSpy).toHaveBeenCalledTimes(1);
147
147
  });
148
148
 
149
149
  it('clears pendingAnnotation after creating an assessment (assessing)', async () => {
@@ -272,7 +272,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
272
272
  });
273
273
 
274
274
  it('does NOT clear pendingAnnotation if API call fails', async () => {
275
- createAnnotationSpy.mockRejectedValueOnce(new Error('Network error'));
275
+ markAnnotationSpy.mockRejectedValueOnce(new Error('Network error'));
276
276
 
277
277
  const { emit } = renderDetectionFlow(TEST_URI);
278
278
 
@@ -319,6 +319,6 @@ describe('Annotation creation clears pendingAnnotation', () => {
319
319
  });
320
320
 
321
321
  // API should NOT have been called on cancel
322
- expect(createAnnotationSpy).not.toHaveBeenCalled();
322
+ expect(markAnnotationSpy).not.toHaveBeenCalled();
323
323
  });
324
324
  });
@@ -50,10 +50,10 @@ describe('Detection Progress Dismissal Bug', () => {
50
50
  close: vi.fn(),
51
51
  };
52
52
 
53
- vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue(mockStream);
54
- vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue(mockStream);
55
- vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue(mockStream);
56
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue(mockStream);
53
+ vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream);
54
+ vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream);
55
+ vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream);
56
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream);
57
57
  });
58
58
 
59
59
  afterEach(() => {
@@ -2,10 +2,10 @@
2
2
  * Layer 3: Feature Integration Test - Bind Flow (body update)
3
3
  *
4
4
  * Tests the write side of useBindFlow:
5
- * - bind:update-body → calls updateAnnotationBody API
5
+ * - bind:update-body → calls bindAnnotation API
6
6
  * - bind:update-body → emits bind:body-updated on success
7
7
  * - bind:update-body → emits bind:body-update-failed on error
8
- * - auth token passed to updateAnnotationBody
8
+ * - auth token passed to bindAnnotation
9
9
  *
10
10
  * The wizard modal (ReferenceWizardModal) handles modal state, context
11
11
  * gathering, search configuration, and result display. This test covers
@@ -21,7 +21,7 @@ import { useBindFlow } from '../../../hooks/useBindFlow';
21
21
  import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
22
22
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
23
23
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
24
- import { SemiontApiClient } from '@semiont/api-client';
24
+ import { SSEClient } from '@semiont/api-client';
25
25
  import { resourceId, accessToken, annotationId } from '@semiont/core';
26
26
 
27
27
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -35,7 +35,7 @@ vi.mock('../../../components/Toast', () => ({
35
35
  }));
36
36
 
37
37
  describe('Bind Flow - Body Update Integration', () => {
38
- let updateAnnotationBodySpy: ReturnType<typeof vi.fn>;
38
+ let bindAnnotationSpy: ReturnType<typeof vi.fn>;
39
39
  const testId = resourceId('test-resource');
40
40
  const testToken = 'test-resolution-token';
41
41
  const testBaseUrl = 'http://localhost:4000';
@@ -44,8 +44,11 @@ describe('Bind Flow - Body Update Integration', () => {
44
44
  vi.clearAllMocks();
45
45
  resetEventBusForTesting();
46
46
 
47
- updateAnnotationBodySpy = vi.fn().mockResolvedValue({ success: true });
48
- vi.spyOn(SemiontApiClient.prototype, 'updateAnnotationBody').mockImplementation(updateAnnotationBodySpy);
47
+ bindAnnotationSpy = vi.fn().mockImplementation((_rId: any, annId: any, _req: any, opts: any) => {
48
+ queueMicrotask(() => opts.eventBus.get('bind:finished').next({ annotationId: annId }));
49
+ return { close: vi.fn() };
50
+ });
51
+ vi.spyOn(SSEClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
49
52
  });
50
53
 
51
54
  afterEach(() => {
@@ -80,17 +83,17 @@ describe('Bind Flow - Body Update Integration', () => {
80
83
 
81
84
  // ─── bind:update-body ──────────────────────────────────────────────────
82
85
 
83
- it('bind:update-body calls updateAnnotationBody API', async () => {
86
+ it('bind:update-body calls bindAnnotation API', async () => {
84
87
  const { getEventBus } = renderBindFlow();
85
88
 
86
89
  act(() => { getEventBus().get('bind:update-body').next({
87
90
  annotationId: annotationId('ann-body-1'),
88
91
  resourceId: resourceId('linked-resource-id'),
89
- operations: [{ op: 'add', item: { id: 'linked-resource-id' } }],
92
+ operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'linked-resource-id' } }],
90
93
  }); });
91
94
 
92
95
  await waitFor(() => {
93
- expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
96
+ expect(bindAnnotationSpy).toHaveBeenCalledTimes(1);
94
97
  });
95
98
  });
96
99
 
@@ -100,14 +103,14 @@ describe('Bind Flow - Body Update Integration', () => {
100
103
  act(() => { getEventBus().get('bind:update-body').next({
101
104
  annotationId: annotationId('ann-auth'),
102
105
  resourceId: resourceId('resource-id'),
103
- operations: [{ op: 'replace', newItem: { id: 'resource-id' } }],
106
+ operations: [{ op: 'replace', newItem: { type: 'SpecificResource' as const, source: 'resource-id' } }],
104
107
  }); });
105
108
 
106
109
  await waitFor(() => {
107
- expect(updateAnnotationBodySpy).toHaveBeenCalled();
110
+ expect(bindAnnotationSpy).toHaveBeenCalled();
108
111
  });
109
112
 
110
- const callArgs = updateAnnotationBodySpy.mock.calls[0];
113
+ const callArgs = bindAnnotationSpy.mock.calls[0];
111
114
  expect(callArgs[3]).toHaveProperty('auth');
112
115
  expect(callArgs[3].auth).toBe(accessToken(testToken));
113
116
  });
@@ -121,7 +124,7 @@ describe('Bind Flow - Body Update Integration', () => {
121
124
  act(() => { getEventBus().get('bind:update-body').next({
122
125
  annotationId: annotationId('ann-success'),
123
126
  resourceId: resourceId('resource-id'),
124
- operations: [{ op: 'add', item: { id: 'resource-id' } }],
127
+ operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
125
128
  }); });
126
129
 
127
130
  await waitFor(() => {
@@ -136,7 +139,10 @@ describe('Bind Flow - Body Update Integration', () => {
136
139
  });
137
140
 
138
141
  it('bind:update-body emits bind:body-update-failed on API error', async () => {
139
- updateAnnotationBodySpy.mockRejectedValue(new Error('Update failed'));
142
+ bindAnnotationSpy.mockImplementation((_rId: any, _annId: any, _req: any, opts: any) => {
143
+ queueMicrotask(() => opts.eventBus.get('bind:failed').next({ error: new Error('Update failed') }));
144
+ return { close: vi.fn() };
145
+ });
140
146
 
141
147
  const { getEventBus } = renderBindFlow();
142
148
  const bodyUpdateFailedSpy = vi.fn();
@@ -146,7 +152,7 @@ describe('Bind Flow - Body Update Integration', () => {
146
152
  act(() => { getEventBus().get('bind:update-body').next({
147
153
  annotationId: annotationId('ann-fail'),
148
154
  resourceId: resourceId('resource-id'),
149
- operations: [{ op: 'remove', item: { id: 'old-id' } }],
155
+ operations: [{ op: 'remove', item: { type: 'SpecificResource' as const, source: 'old-id' } }],
150
156
  }); });
151
157
 
152
158
  await waitFor(() => {
@@ -166,11 +172,11 @@ describe('Bind Flow - Body Update Integration', () => {
166
172
  act(() => { getEventBus().get('bind:update-body').next({
167
173
  annotationId: annotationId('ann-dedup'),
168
174
  resourceId: resourceId('resource-id'),
169
- operations: [{ op: 'add', item: { id: 'resource-id' } }],
175
+ operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
170
176
  }); });
171
177
 
172
178
  await waitFor(() => {
173
- expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
179
+ expect(bindAnnotationSpy).toHaveBeenCalledTimes(1);
174
180
  });
175
181
  });
176
182
  });
@@ -35,10 +35,10 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
35
35
  vi.clearAllMocks();
36
36
 
37
37
  // Minimal mock - SSE streams not needed for this test
38
- vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
- vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
- vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
41
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
38
+ vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
+ vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
+ vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
41
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
42
42
  });
43
43
 
44
44
  afterEach(() => {
@@ -54,8 +54,8 @@ const createMockSSEStream = () => {
54
54
 
55
55
  describe('Detection Flow - Feature Integration', () => {
56
56
  let mockStream: ReturnType<typeof createMockSSEStream>;
57
- let annotateReferencesSpy: any;
58
- let annotateHighlightsSpy: any;
57
+ let markReferencesSpy: any;
58
+ let markHighlightsSpy: any;
59
59
  let detectCommentsSpy: any;
60
60
 
61
61
  beforeEach(() => {
@@ -66,10 +66,10 @@ describe('Detection Flow - Feature Integration', () => {
66
66
  mockStream = createMockSSEStream();
67
67
 
68
68
  // Spy on SSEClient prototype methods
69
- annotateReferencesSpy = vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue(mockStream as any);
70
- annotateHighlightsSpy = vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue(mockStream as any);
71
- detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue(mockStream as any);
72
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue(mockStream as any);
69
+ markReferencesSpy = vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
70
+ markHighlightsSpy = vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
71
+ detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
72
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
73
73
  });
74
74
 
75
75
  afterEach(() => {
@@ -93,11 +93,11 @@ describe('Detection Flow - Feature Integration', () => {
93
93
  // CRITICAL ASSERTION: API called exactly once (not twice!)
94
94
  // This would FAIL if useBindFlow was called in multiple places
95
95
  await waitFor(() => {
96
- expect(annotateReferencesSpy).toHaveBeenCalledTimes(1);
96
+ expect(markReferencesSpy).toHaveBeenCalledTimes(1);
97
97
  });
98
98
 
99
99
  // Verify correct parameters (eventBus is passed but we don't need to verify its exact value)
100
- expect(annotateReferencesSpy).toHaveBeenCalledWith(
100
+ expect(markReferencesSpy).toHaveBeenCalledWith(
101
101
  testId,
102
102
  {
103
103
  entityTypes: ['Person', 'Organization'],
@@ -122,7 +122,7 @@ describe('Detection Flow - Feature Integration', () => {
122
122
 
123
123
  // Wait for stream to be created
124
124
  await waitFor(() => {
125
- expect(annotateReferencesSpy).toHaveBeenCalled();
125
+ expect(markReferencesSpy).toHaveBeenCalled();
126
126
  });
127
127
 
128
128
  // Simulate SSE progress event being emitted to EventBus (how SSE actually works now)
@@ -156,7 +156,7 @@ describe('Detection Flow - Feature Integration', () => {
156
156
  });
157
157
 
158
158
  await waitFor(() => {
159
- expect(annotateHighlightsSpy).toHaveBeenCalledTimes(1);
159
+ expect(markHighlightsSpy).toHaveBeenCalledTimes(1);
160
160
  });
161
161
 
162
162
  // First progress update via EventBus
@@ -291,8 +291,8 @@ describe('Detection Flow - Feature Integration', () => {
291
291
  });
292
292
 
293
293
  await waitFor(() => {
294
- expect(annotateHighlightsSpy).toHaveBeenCalledTimes(1);
295
- expect(annotateHighlightsSpy).toHaveBeenCalledWith(testId, {
294
+ expect(markHighlightsSpy).toHaveBeenCalledTimes(1);
295
+ expect(markHighlightsSpy).toHaveBeenCalledWith(testId, {
296
296
  instructions: 'Find important text',
297
297
  }, expect.objectContaining({ auth: undefined }));
298
298
  });
@@ -337,11 +337,11 @@ describe('Detection Flow - Feature Integration', () => {
337
337
 
338
338
  // Wait for operation to complete
339
339
  await waitFor(() => {
340
- expect(annotateReferencesSpy).toHaveBeenCalled();
340
+ expect(markReferencesSpy).toHaveBeenCalled();
341
341
  });
342
342
 
343
343
  // VERIFY: API called exactly once, even though multiple listeners exist
344
- expect(annotateReferencesSpy).toHaveBeenCalledTimes(1);
344
+ expect(markReferencesSpy).toHaveBeenCalledTimes(1);
345
345
 
346
346
  // VERIFY: Our additional listener was called (events work)
347
347
  expect(additionalListener).toHaveBeenCalledTimes(1);
@@ -91,7 +91,7 @@ useDebouncedCallback: (fn: any) => fn,
91
91
  useResourceAnnotations: () => ({
92
92
  clearNewAnnotationId: vi.fn(),
93
93
  newAnnotationIds: new Set(),
94
- createAnnotation: vi.fn(),
94
+ markAnnotation: vi.fn(),
95
95
  deleteAnnotation: vi.fn(),
96
96
  triggerSparkleAnimation: vi.fn(),
97
97
  }),
@@ -111,7 +111,7 @@ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
111
111
  useResourceAnnotations: () => ({
112
112
  clearNewAnnotationId: vi.fn(),
113
113
  newAnnotationIds: new Set(),
114
- createAnnotation: vi.fn(),
114
+ markAnnotation: vi.fn(),
115
115
  deleteAnnotation: vi.fn(),
116
116
  triggerSparkleAnimation: vi.fn(),
117
117
  }),
@@ -64,7 +64,7 @@ describe('Generation Flow - Feature Integration', () => {
64
64
  mockStream = createMockGenerationStream();
65
65
 
66
66
  // Spy on SSEClient prototype method
67
- generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResourceFromAnnotation').mockReturnValue(mockStream as any);
67
+ generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResource').mockReturnValue(mockStream as any);
68
68
 
69
69
  // Mock callbacks
70
70
  mockShowSuccess = vi.fn();
@@ -76,7 +76,7 @@ describe('Generation Flow - Feature Integration', () => {
76
76
  vi.restoreAllMocks();
77
77
  });
78
78
 
79
- it('should call yieldResourceFromAnnotation exactly ONCE when generation starts', async () => {
79
+ it('should call yieldResource exactly ONCE when generation starts', async () => {
80
80
  const testResourceId = resourceId('test-resource');
81
81
  const testAnnotationId = annotationId('test-annotation');
82
82
 
@@ -158,10 +158,10 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
158
158
  mockStream = new MockSSEStream(eventBus);
159
159
 
160
160
  // Spy on SSEClient prototype methods to inject mock stream
161
- vi.spyOn(SSEClient.prototype, 'annotateHighlights').mockReturnValue(mockStream as any);
162
- vi.spyOn(SSEClient.prototype, 'annotateAssessments').mockReturnValue(mockStream as any);
163
- vi.spyOn(SSEClient.prototype, 'annotateComments').mockReturnValue(mockStream as any);
164
- vi.spyOn(SSEClient.prototype, 'annotateReferences').mockReturnValue(mockStream as any);
161
+ vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
162
+ vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
163
+ vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
164
+ vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
165
165
 
166
166
  mockAnnotations = [];
167
167
  });
@@ -1,31 +0,0 @@
1
- 'use client';
2
-
3
- // src/contexts/EventBusContext.tsx
4
- import { createContext, useContext, useMemo } from "react";
5
- import { EventBus } from "@semiont/core";
6
- import { jsx } from "react/jsx-runtime";
7
- var EventBusContext = createContext(null);
8
- var globalEventBus = new EventBus();
9
- function resetEventBusForTesting() {
10
- globalEventBus.destroy();
11
- globalEventBus = new EventBus();
12
- return globalEventBus;
13
- }
14
- function EventBusProvider({ children }) {
15
- const eventBus = useMemo(() => globalEventBus, []);
16
- return /* @__PURE__ */ jsx(EventBusContext.Provider, { value: eventBus, children });
17
- }
18
- function useEventBus() {
19
- const eventBus = useContext(EventBusContext);
20
- if (!eventBus) {
21
- throw new Error("useEventBus must be used within EventBusProvider");
22
- }
23
- return eventBus;
24
- }
25
-
26
- export {
27
- resetEventBusForTesting,
28
- EventBusProvider,
29
- useEventBus
30
- };
31
- //# sourceMappingURL=chunk-2HGWOLVN.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/contexts/EventBusContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { EventBus } from '@semiont/core';\n\nconst EventBusContext = createContext<EventBus | null>(null);\n\n/**\n * Global singleton event bus.\n *\n * Uses RxJS-based EventBus from @semiont/core for framework-agnostic event routing.\n *\n * This ensures all components in the application share the same event bus instance,\n * which is critical for cross-component communication (e.g., hovering an annotation\n * in one component scrolls the panel in another component).\n *\n * FUTURE: Multi-Window Support\n * When we need to support multiple document windows (e.g., pop-out resource viewers),\n * we'll need to transition to a per-window event bus architecture:\n *\n * Option 1: Window-scoped event bus\n * - Create a new event bus for each window/portal\n * - Pass windowId or documentId to EventBusProvider\n * - Store Map<windowId, EventBus> instead of single global\n * - Components use useEventBus(windowId) to get correct bus\n *\n * Option 2: Event bus hierarchy\n * - Global event bus for app-wide events (settings, navigation)\n * - Per-document event bus for document-specific events (annotation hover)\n * - Components subscribe to both buses as needed\n *\n * Option 3: Cross-window event bridge\n * - Keep per-window buses isolated\n * - Use BroadcastChannel or postMessage for cross-window events\n * - Bridge pattern to sync certain events across windows\n *\n * For now, single global bus is correct for single-window app.\n */\nlet globalEventBus = new EventBus();\n\n/**\n * Reset the global event bus - FOR TESTING ONLY.\n *\n * Call this in test setup (beforeEach) to ensure test isolation.\n * Each test gets a fresh event bus with no lingering subscriptions.\n *\n * @returns The new EventBus instance\n *\n * @example\n * ```typescript\n * beforeEach(() => {\n * const eventBus = resetEventBusForTesting();\n * });\n * ```\n */\nexport function resetEventBusForTesting(): EventBus {\n globalEventBus.destroy();\n globalEventBus = new EventBus();\n return globalEventBus;\n}\n\nexport interface EventBusProviderProps {\n children: ReactNode;\n}\n\n/**\n * Unified event bus provider for all application events\n *\n * Consolidates three previous event buses:\n * - MakeMeaningEventBus (document/annotation operations)\n * - NavigationEventBus (navigation and sidebar UI)\n * - GlobalSettingsEventBus (app-wide settings)\n *\n * Benefits:\n * - Single import: useEventBus()\n * - No decision fatigue about which bus to use\n * - Easier cross-domain coordination\n * - Simpler provider hierarchy\n *\n * NOTE: This provider uses a global singleton event bus to ensure all components\n * share the same instance. Multiple providers in the tree will all reference the\n * same global bus.\n *\n * Operation handlers (API calls triggered by events) are set up separately via\n * the useBindFlow hook, which should be called at the resource page level.\n */\nexport function EventBusProvider({ children }: EventBusProviderProps) {\n const eventBus = useMemo(() => globalEventBus, []);\n\n return (\n <EventBusContext.Provider value={eventBus}>\n {children}\n </EventBusContext.Provider>\n );\n}\n\n/**\n * Hook to access the unified event bus\n *\n * Use this everywhere instead of:\n * - useMakeMeaningEvents()\n * - useNavigationEvents()\n * - useGlobalSettingsEvents()\n *\n * @example\n * ```typescript\n * const eventBus = useEventBus();\n *\n * // Emit any event\n * eventBus.get('beckon:hover').next({ annotationId: '123' });\n * eventBus.get('browse:sidebar-toggle').next(undefined);\n * eventBus.get('settings:theme-changed').next({ theme: 'dark' });\n *\n * // Subscribe to any event\n * useEffect(() => {\n * const unsubscribe = eventBus.on('beckon:hover', ({ annotationId }) => {\n * console.log(annotationId);\n * });\n * return () => unsubscribe();\n * }, []);\n * ```\n */\nexport function useEventBus(): EventBus {\n const eventBus = useContext(EventBusContext);\n if (!eventBus) {\n throw new Error('useEventBus must be used within EventBusProvider');\n }\n return eventBus;\n}\n"],"mappings":";;;AAEA,SAAS,eAAe,YAAY,eAA+B;AACnE,SAAS,gBAAgB;AAuFrB;AArFJ,IAAM,kBAAkB,cAA+B,IAAI;AAiC3D,IAAI,iBAAiB,IAAI,SAAS;AAiB3B,SAAS,0BAAoC;AAClD,iBAAe,QAAQ;AACvB,mBAAiB,IAAI,SAAS;AAC9B,SAAO;AACT;AA2BO,SAAS,iBAAiB,EAAE,SAAS,GAA0B;AACpE,QAAM,WAAW,QAAQ,MAAM,gBAAgB,CAAC,CAAC;AAEjD,SACE,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,UAC9B,UACH;AAEJ;AA4BO,SAAS,cAAwB;AACtC,QAAM,WAAW,WAAW,eAAe;AAC3C,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/contexts/ApiClientContext.tsx","../src/contexts/SessionContext.tsx","../src/components/Toast.tsx","../src/contexts/OpenResourcesContext.tsx","../src/contexts/TranslationContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, ReactNode, useMemo } from 'react';\nimport { baseUrl } from '@semiont/core';\nimport { SemiontApiClient } from '@semiont/api-client';\n\nconst ApiClientContext = createContext<SemiontApiClient | undefined>(undefined);\n\nexport interface ApiClientProviderProps {\n baseUrl: string;\n children: ReactNode;\n}\n\n/**\n * Provider for API client - creates a stateless singleton client\n * The client instance never changes (no token dependency)\n * Auth tokens are passed per-request via useAuthToken() in calling code\n */\nexport function ApiClientProvider({\n baseUrl: url,\n children,\n}: ApiClientProviderProps) {\n // Client created once and never recreated (no token dependency)\n const client = useMemo(\n () => new SemiontApiClient({\n baseUrl: baseUrl(url),\n // Use no timeout in test environment to avoid AbortController issues with ky + vitest\n ...(process.env.NODE_ENV !== 'test' && { timeout: 30000 }),\n }),\n [url] // Only baseUrl in deps, token removed\n );\n\n return (\n <ApiClientContext.Provider value={client}>\n {children}\n </ApiClientContext.Provider>\n );\n}\n\n/**\n * Hook to access the stateless API client singleton\n * Must be used within an ApiClientProvider\n * @returns Stateless SemiontApiClient instance\n */\nexport function useApiClient(): SemiontApiClient {\n const context = useContext(ApiClientContext);\n\n if (context === undefined) {\n throw new Error('useApiClient must be used within an ApiClientProvider');\n }\n\n return context;\n}\n","'use client';\n\nimport { createContext, useContext, ReactNode } from 'react';\nimport type { SessionManager } from '../types/SessionManager';\n\nconst SessionContext = createContext<SessionManager | null>(null);\n\n/**\n * Provider Pattern: Accepts SessionManager implementation as prop\n * and makes it available to child components via Context.\n *\n * Apps provide their own implementation (next-auth, custom auth, etc.)\n * and pass it to this provider at the root level.\n *\n * @example\n * ```tsx\n * // In app root\n * const sessionManager = useSessionManager(); // App's implementation\n *\n * <SessionProvider sessionManager={sessionManager}>\n * <App />\n * </SessionProvider>\n * ```\n */\nexport function SessionProvider({\n sessionManager,\n children\n}: {\n sessionManager: SessionManager;\n children: ReactNode;\n}) {\n return (\n <SessionContext.Provider value={sessionManager}>\n {children}\n </SessionContext.Provider>\n );\n}\n\n/**\n * Hook to access SessionManager from Context\n * Components use this hook to access session state and expiry information\n */\nexport function useSessionContext() {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSessionContext must be used within SessionProvider');\n }\n return context;\n}","'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport './Toast.css';\n\nexport type ToastType = 'success' | 'error' | 'info' | 'warning';\n\nexport interface ToastMessage {\n id: string;\n message: string;\n type: ToastType;\n duration?: number;\n}\n\ninterface ToastProps {\n toast: ToastMessage;\n onClose: (id: string) => void;\n}\n\nconst icons = {\n success: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n warning: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n ),\n info: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n};\n\nfunction Toast({ toast, onClose }: ToastProps) {\n useEffect(() => {\n const timer = setTimeout(() => {\n onClose(toast.id);\n }, toast.duration || 3000);\n\n return () => clearTimeout(timer);\n }, [toast, onClose]);\n\n return (\n <div\n className=\"semiont-toast\"\n data-variant={toast.type}\n role=\"alert\"\n >\n <div className=\"semiont-toast-icon-wrapper\">{icons[toast.type]}</div>\n <p className=\"semiont-toast-message\">{toast.message}</p>\n <button\n onClick={() => onClose(toast.id)}\n className=\"semiont-toast-close\"\n aria-label=\"Close\"\n >\n <svg className=\"semiont-toast-close-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n );\n}\n\ninterface ToastContainerProps {\n toasts: ToastMessage[];\n onClose: (id: string) => void;\n}\n\nexport function ToastContainer({ toasts, onClose }: ToastContainerProps) {\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n return () => setMounted(false);\n }, []);\n\n if (!mounted) return null;\n\n return createPortal(\n <div className=\"semiont-toast-container\">\n {toasts.map((toast) => (\n <Toast key={toast.id} toast={toast} onClose={onClose} />\n ))}\n </div>,\n document.body\n );\n}\n\n// Toast context and hook for global toast management\ninterface ToastContextType {\n showToast: (message: string, type?: ToastType, duration?: number) => void;\n showSuccess: (message: string, duration?: number) => void;\n showError: (message: string, duration?: number) => void;\n showWarning: (message: string, duration?: number) => void;\n showInfo: (message: string, duration?: number) => void;\n}\n\nconst ToastContext = React.createContext<ToastContextType | undefined>(undefined);\n\nexport function ToastProvider({ children }: { children: React.ReactNode }) {\n const [toasts, setToasts] = useState<ToastMessage[]>([]);\n\n const showToast = React.useCallback((message: string, type: ToastType = 'info', duration?: number) => {\n const id = Date.now().toString();\n const newToast: ToastMessage = duration !== undefined\n ? { id, message, type, duration }\n : { id, message, type };\n setToasts((prev) => [...prev, newToast]);\n }, []);\n\n const showSuccess = React.useCallback((message: string, duration?: number) => showToast(message, 'success', duration), [showToast]);\n const showError = React.useCallback((message: string, duration?: number) => showToast(message, 'error', duration), [showToast]);\n const showWarning = React.useCallback((message: string, duration?: number) => showToast(message, 'warning', duration), [showToast]);\n const showInfo = React.useCallback((message: string, duration?: number) => showToast(message, 'info', duration), [showToast]);\n\n const handleClose = React.useCallback((id: string) => {\n setToasts((prev) => prev.filter((toast) => toast.id !== id));\n }, []);\n\n const contextValue = React.useMemo(\n () => ({ showToast, showSuccess, showError, showWarning, showInfo }),\n [showToast, showSuccess, showError, showWarning, showInfo]\n );\n\n return (\n <ToastContext.Provider value={contextValue}>\n {children}\n <ToastContainer toasts={toasts} onClose={handleClose} />\n </ToastContext.Provider>\n );\n}\n\nexport function useToast() {\n const context = React.useContext(ToastContext);\n if (context === undefined) {\n throw new Error('useToast must be used within a ToastProvider');\n }\n return context;\n}","'use client';\n\nimport React, { createContext, useContext } from 'react';\nimport type { OpenResourcesManager } from '../types/OpenResourcesManager';\n\nconst OpenResourcesContext = createContext<OpenResourcesManager | undefined>(undefined);\n\n/**\n * Provider Pattern: Accepts OpenResourcesManager implementation as prop\n * and makes it available to child components via Context.\n *\n * Apps provide their own implementation (localStorage, sessionStorage, database, etc.)\n * and pass it to this provider at the root level.\n *\n * @example\n * ```tsx\n * // In app root\n * const openResourcesManager = useOpenResourcesManager(); // App's implementation\n *\n * <OpenResourcesProvider openResourcesManager={openResourcesManager}>\n * <App />\n * </OpenResourcesProvider>\n * ```\n */\nexport function OpenResourcesProvider({\n openResourcesManager,\n children\n}: {\n openResourcesManager: OpenResourcesManager;\n children: React.ReactNode;\n}) {\n return (\n <OpenResourcesContext.Provider value={openResourcesManager}>\n {children}\n </OpenResourcesContext.Provider>\n );\n}\n\n/**\n * Hook to access OpenResourcesManager from Context\n * Components use this hook to access open resources functionality\n */\nexport function useOpenResources(): OpenResourcesManager {\n const context = useContext(OpenResourcesContext);\n if (context === undefined) {\n throw new Error('useOpenResources must be used within an OpenResourcesProvider');\n }\n return context;\n}","'use client';\n\nimport { createContext, useContext, ReactNode, useState, useEffect, useMemo } from 'react';\nimport type { TranslationManager } from '../types/TranslationManager';\n\n// Static import for default English only - always needed as fallback\nimport enTranslations from '../../translations/en.json';\n\nconst TranslationContext = createContext<TranslationManager | null>(null);\n\n// Cache for dynamically loaded translations\nconst translationCache = new Map<string, any>();\n\n/**\n * Process ICU MessageFormat plural syntax\n * Supports: {count, plural, =0 {text} =1 {text} other {text}}\n */\nfunction processPluralFormat(text: string, params: Record<string, any>): string {\n // Match {paramName, plural, ...} with proper brace counting\n const pluralMatch = text.match(/\\{(\\w+),\\s*plural,\\s*/);\n if (!pluralMatch) {\n return text;\n }\n\n const paramName = pluralMatch[1];\n const count = params[paramName];\n if (count === undefined) {\n return text;\n }\n\n // Find the matching closing brace by counting\n let startPos = pluralMatch[0].length;\n let braceCount = 1; // We're inside the first {\n let endPos = startPos;\n\n for (let i = startPos; i < text.length; i++) {\n if (text[i] === '{') braceCount++;\n else if (text[i] === '}') {\n braceCount--;\n if (braceCount === 0) {\n endPos = i;\n break;\n }\n }\n }\n\n const pluralCases = text.substring(startPos, endPos);\n\n // Parse plural cases: =0 {text} =1 {text} other {text}\n const cases: Record<string, string> = {};\n const caseRegex = /(?:=(\\d+)|(\\w+))\\s*\\{([^}]+)\\}/g;\n let caseMatch;\n\n while ((caseMatch = caseRegex.exec(pluralCases)) !== null) {\n const [, exactNumber, keyword, textContent] = caseMatch;\n const key = exactNumber !== undefined ? `=${exactNumber}` : keyword;\n cases[key] = textContent;\n }\n\n // Select appropriate case\n const exactMatch = cases[`=${count}`];\n if (exactMatch !== undefined) {\n const result = exactMatch.replace(/#/g, String(count));\n return text.substring(0, pluralMatch.index!) + result + text.substring(endPos + 1);\n }\n\n const otherCase = cases['other'];\n if (otherCase !== undefined) {\n const result = otherCase.replace(/#/g, String(count));\n return text.substring(0, pluralMatch.index!) + result + text.substring(endPos + 1);\n }\n\n return text;\n}\n\n// List of available locales (can be extended without importing all files)\nexport const AVAILABLE_LOCALES = [\n 'ar', // Arabic\n 'bn', // Bengali\n 'cs', // Czech\n 'da', // Danish\n 'de', // German\n 'el', // Greek\n 'en', // English\n 'es', // Spanish\n 'fa', // Persian/Farsi\n 'fi', // Finnish\n 'fr', // French\n 'he', // Hebrew\n 'hi', // Hindi\n 'id', // Indonesian\n 'it', // Italian\n 'ja', // Japanese\n 'ko', // Korean\n 'ms', // Malay\n 'nl', // Dutch\n 'no', // Norwegian\n 'pl', // Polish\n 'pt', // Portuguese\n 'ro', // Romanian\n 'sv', // Swedish\n 'th', // Thai\n 'tr', // Turkish\n 'uk', // Ukrainian\n 'vi', // Vietnamese\n 'zh', // Chinese\n] as const;\nexport type AvailableLocale = typeof AVAILABLE_LOCALES[number];\n\n// Lazy load translations for a specific locale\nasync function loadTranslations(locale: string): Promise<any> {\n // Check cache first\n if (translationCache.has(locale)) {\n return translationCache.get(locale);\n }\n\n // English is already loaded statically\n if (locale === 'en') {\n translationCache.set('en', enTranslations);\n return enTranslations;\n }\n\n try {\n // Dynamic import for all other locales\n const translations = await import(`../../translations/${locale}.json`);\n const translationData = translations.default || translations;\n translationCache.set(locale, translationData);\n return translationData;\n } catch (error) {\n console.error(`Failed to load translations for locale: ${locale}`, error);\n // Fall back to English\n return enTranslations;\n }\n}\n\n// Default English translation manager (using static import)\nconst defaultTranslationManager: TranslationManager = {\n t: (namespace: string, key: string, params?: Record<string, any>) => {\n const translations = enTranslations as Record<string, Record<string, string>>;\n const translation = translations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n },\n};\n\nexport interface TranslationProviderProps {\n /**\n * Option 1: Provide a complete TranslationManager implementation\n */\n translationManager?: TranslationManager;\n\n /**\n * Option 2: Use built-in translations by specifying a locale\n * When adding new locales, just add the JSON file and update AVAILABLE_LOCALES\n */\n locale?: string;\n\n /**\n * Loading component to show while translations are being loaded\n * Only relevant when using dynamic locale loading\n */\n loadingComponent?: ReactNode;\n\n children: ReactNode;\n}\n\n/**\n * Provider for translation management with dynamic loading\n *\n * Three modes of operation:\n * 1. No provider: Components use default English strings\n * 2. With locale prop: Dynamically loads translations for that locale\n * 3. With translationManager: Use custom translation implementation\n */\nexport function TranslationProvider({\n translationManager,\n locale,\n loadingComponent = null,\n children,\n}: TranslationProviderProps) {\n const [loadedTranslations, setLoadedTranslations] = useState<any>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n // Load translations when locale changes\n useEffect(() => {\n if (locale && !translationManager) {\n setIsLoading(true);\n loadTranslations(locale)\n .then(translations => {\n setLoadedTranslations(translations);\n setIsLoading(false);\n })\n .catch(error => {\n console.error('Failed to load translations:', error);\n setLoadedTranslations(enTranslations); // Fall back to English\n setIsLoading(false);\n });\n }\n }, [locale, translationManager]);\n\n // Create translation manager from loaded translations\n const localeManager = useMemo<TranslationManager | null>(() => {\n if (!loadedTranslations) return null;\n\n return {\n t: (namespace: string, key: string, params?: Record<string, any>) => {\n const translation = loadedTranslations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key} in locale ${locale}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n },\n };\n }, [loadedTranslations, locale]);\n\n // If custom translation manager provided, use it\n if (translationManager) {\n return (\n <TranslationContext.Provider value={translationManager}>\n {children}\n </TranslationContext.Provider>\n );\n }\n\n // If locale provided and still loading, show loading component\n if (locale && isLoading) {\n return <>{loadingComponent}</>;\n }\n\n // If locale provided and translations loaded, use them\n if (locale && localeManager) {\n return (\n <TranslationContext.Provider value={localeManager}>\n {children}\n </TranslationContext.Provider>\n );\n }\n\n // Default: use English translations\n return (\n <TranslationContext.Provider value={defaultTranslationManager}>\n {children}\n </TranslationContext.Provider>\n );\n}\n\n/**\n * Hook to access translations within a namespace\n *\n * Works in three modes:\n * 1. Without provider: Returns default English translations\n * 2. With provider using locale: Returns dynamically loaded translations for that locale\n * 3. With custom provider: Uses the custom translation manager\n *\n * @param namespace - Translation namespace (e.g., 'Toolbar', 'ResourceViewer')\n * @returns Function to translate keys within the namespace\n */\nexport function useTranslations(namespace: string) {\n const context = useContext(TranslationContext);\n\n // If no context (no provider), use default English translations\n if (!context) {\n return (key: string, params?: Record<string, any>) => {\n const translations = enTranslations as Record<string, Record<string, string>>;\n const translation = translations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n };\n }\n\n // Return a function that translates keys within this namespace\n return (key: string, params?: Record<string, any>) => context.t(namespace, key, params);\n}\n\n/**\n * Hook to preload translations for a locale\n * Useful for preloading translations before navigation\n */\nexport function usePreloadTranslations() {\n return {\n preload: async (locale: string) => {\n try {\n await loadTranslations(locale);\n return true;\n } catch (error) {\n console.error(`Failed to preload translations for ${locale}:`, error);\n return false;\n }\n },\n isLoaded: (locale: string) => translationCache.has(locale),\n };\n}"],"mappings":"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","names":["createContext","useContext","jsx","jsx","createContext","useContext","jsx","createContext","useContext","useState","useEffect","useMemo","jsx","createContext","useState","useEffect","useMemo","jsx","useContext"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/contexts/useEventSubscription.ts","../src/hooks/useBeckonFlow.ts"],"sourcesContent":["import { useEffect, useRef, useMemo } from 'react';\nimport type { EventMap } from '@semiont/core';\nimport { useEventBus } from './EventBusContext';\n\n/**\n * Subscribe to an event bus event with automatic cleanup.\n *\n * This hook solves the \"stale closure\" problem by always using the latest\n * version of the handler without re-subscribing.\n *\n * @example\n * ```tsx\n * useEventSubscription('mark:created', ({ annotation }) => {\n * // This always uses the latest props/state\n * triggerSparkleAnimation(annotation.id);\n * });\n * ```\n */\nexport function useEventSubscription<K extends keyof EventMap>(\n eventName: K,\n handler: (payload: EventMap[K]) => void\n): void {\n const eventBus = useEventBus();\n\n // Store the latest handler in a ref to avoid stale closures\n const handlerRef = useRef(handler);\n\n // Update ref on every render (no re-subscription needed)\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n // Subscribe once, using a stable wrapper that calls the current handler\n useEffect(() => {\n const stableHandler = (payload: EventMap[K]) => {\n handlerRef.current(payload);\n };\n\n // RxJS EventBus.get() returns Subject, subscribe returns Subscription\n const subscription = eventBus.get(eventName).subscribe(stableHandler);\n\n return () => {\n subscription.unsubscribe();\n };\n }, [eventName, eventBus]); // eventBus is stable, only re-subscribe if event name changes\n}\n\n/**\n * Subscribe to multiple events at once.\n *\n * @example\n * ```tsx\n * useEventSubscriptions({\n * 'mark:created': ({ annotation }) => setNewAnnotation(annotation),\n * 'mark:deleted': ({ annotationId }) => removeAnnotation(annotationId),\n * });\n * ```\n */\nexport function useEventSubscriptions(\n subscriptions: {\n [K in keyof EventMap]?: (payload: EventMap[K]) => void;\n }\n): void {\n const eventBus = useEventBus();\n\n // Store the latest handlers in refs\n const handlersRef = useRef(subscriptions);\n\n // Update refs on every render\n useEffect(() => {\n handlersRef.current = subscriptions;\n });\n\n // Get stable list of event names to subscribe to\n const eventNames = useMemo(\n () => Object.keys(subscriptions).sort(),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [Object.keys(subscriptions).sort().join(',')]\n );\n\n // Subscribe once per event - only re-subscribe if event names actually change\n useEffect(() => {\n const subscriptions: Array<{ unsubscribe: () => void }> = [];\n\n // Create stable wrappers for each subscription\n for (const eventName of eventNames) {\n const stableHandler = (payload: any) => {\n const currentHandler = handlersRef.current[eventName as keyof EventMap];\n if (currentHandler) {\n currentHandler(payload);\n } else {\n console.warn('[useEventSubscriptions] No current handler found for:', eventName);\n }\n };\n\n // RxJS EventBus.get() returns Subject, subscribe returns Subscription\n const subscription = eventBus.get(eventName as keyof EventMap).subscribe(stableHandler);\n subscriptions.push(subscription);\n }\n\n // Cleanup: unsubscribe from all subscriptions\n return () => {\n for (const subscription of subscriptions) {\n subscription.unsubscribe();\n }\n };\n }, [eventNames, eventBus]); // eventBus is stable singleton - never in deps; only re-subscribe if event names change\n}\n","/**\n * useBeckonFlow — Annotation attention / pointer coordination hook\n *\n * Manages which annotation currently has the user's attention:\n * - Hover state (hoveredAnnotationId)\n * - Hover → sparkle relay\n * - Click → focus relay\n *\n * Follows react-rxjs-guide.md Layer 2 pattern: Hook bridge that\n * subscribes to events and pushes values into React state.\n *\n * Note: beckon:sparkle visual effect (triggerSparkleAnimation) is owned by\n * ResourceViewerPage, which subscribes to beckon:sparkle and delegates to\n * ResourceAnnotationsContext. This hook emits the signal; it does not render the effect.\n *\n * @subscribes beckon:hover - Sets hoveredAnnotationId; emits beckon:sparkle\n * @subscribes browse:click - Emits beckon:focus (attention relay only)\n * @emits beckon:sparkle\n * @emits beckon:focus\n */\n\n/**\n * useHoverEmitter / createHoverHandlers — annotation hover emission utilities\n *\n * Centralises two hover quality-of-life behaviours:\n *\n * 1. currentHover guard — suppresses redundant emissions when the mouse\n * moves within the same annotation element (prevents event bus noise).\n *\n * 2. Debounce delay (HOVER_DELAY_MS) — a short timer before emitting\n * beckon:hover, so that transient pass-through movements (user dragging\n * the mouse across the panel to reach a button elsewhere) do not trigger\n * sparkle animations or cross-highlight effects.\n * The delay is cancelled immediately on mouseLeave, so leaving is always instant.\n *\n * Two forms are provided:\n *\n * useHoverEmitter(annotationId)\n * React hook. Returns { onMouseEnter, onMouseLeave } props for JSX elements.\n * Use in panel entries (HighlightEntry, CommentEntry, …).\n *\n * createHoverHandlers(emit)\n * Plain factory. Returns { handleMouseEnter(id), handleMouseLeave(), cleanup }.\n * Use inside useEffect / imperative setup code where hooks cannot be called\n * (BrowseView, CodeMirrorRenderer, AnnotationOverlay, PdfAnnotationCanvas).\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { useEventBus } from '../contexts/EventBusContext';\nimport { useEventSubscriptions } from '../contexts/useEventSubscription';\n\n// ─── useBeckonFlow ─────────────────────────────────────────────────────────\n\nexport interface BeckonFlowState {\n hoveredAnnotationId: string | null;\n}\n\nexport function useBeckonFlow(): BeckonFlowState {\n const eventBus = useEventBus();\n const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null);\n\n const handleAnnotationHover = useCallback(({ annotationId }: { annotationId: string | null }) => {\n setHoveredAnnotationId(annotationId);\n if (annotationId) {\n eventBus.get('beckon:sparkle').next({ annotationId });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {\n eventBus.get('beckon:focus').next({ annotationId });\n // Scroll to annotation handled by BrowseView via beckon:focus subscription\n }, []); // eventBus is stable singleton - never in deps\n\n useEventSubscriptions({\n 'beckon:hover': handleAnnotationHover,\n 'browse:click': handleAnnotationClick,\n });\n\n return { hoveredAnnotationId };\n}\n\n// ─── createHoverHandlers (use inside useEffect / imperative setup) ────────────\n\n/** Default milliseconds the mouse must dwell before beckon:hover is emitted. */\nexport const HOVER_DELAY_MS = 150;\n\ntype EmitHover = (annotationId: string | null) => void;\n\nexport interface HoverHandlers {\n /** Call with the annotation ID when the mouse enters an annotation element. */\n handleMouseEnter: (annotationId: string) => void;\n /** Call when the mouse leaves the annotation element. */\n handleMouseLeave: () => void;\n /** Cancel any pending timer — call in the useEffect cleanup. */\n cleanup: () => void;\n}\n\n/**\n * Creates hover handlers for imperative code (non-hook contexts).\n * @param emit - Callback to emit hover events\n * @param delayMs - Hover delay in milliseconds\n */\nexport function createHoverHandlers(emit: EmitHover, delayMs: number): HoverHandlers {\n let currentHover: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const cancelTimer = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const handleMouseEnter = (annotationId: string) => {\n if (currentHover === annotationId) return; // already hovering this one\n cancelTimer();\n timer = setTimeout(() => {\n timer = null;\n currentHover = annotationId;\n emit(annotationId);\n }, delayMs);\n };\n\n const handleMouseLeave = () => {\n cancelTimer();\n if (currentHover !== null) {\n currentHover = null;\n emit(null);\n }\n };\n\n return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };\n}\n\n// ─── useHoverEmitter (use in JSX onMouseEnter / onMouseLeave props) ───────────\n\nexport interface HoverEmitterProps {\n onMouseEnter: () => void;\n onMouseLeave: () => void;\n}\n\n/**\n * React hook that returns onMouseEnter / onMouseLeave props for a single\n * annotation entry element.\n *\n * @param annotationId - The ID of the annotation this element represents.\n * @param hoverDelayMs - Hover delay in milliseconds (defaults to HOVER_DELAY_MS for panel entries)\n */\nexport function useHoverEmitter(annotationId: string, hoverDelayMs: number = HOVER_DELAY_MS): HoverEmitterProps {\n const eventBus = useEventBus();\n const currentHoverRef = useRef<string | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const onMouseEnter = useCallback(() => {\n if (currentHoverRef.current === annotationId) return;\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n }\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n currentHoverRef.current = annotationId;\n eventBus.get('beckon:hover').next({ annotationId });\n }, hoverDelayMs);\n }, [annotationId, hoverDelayMs]); // eventBus is stable singleton - never in deps\n\n const onMouseLeave = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n if (currentHoverRef.current !== null) {\n currentHoverRef.current = null;\n eventBus.get('beckon:hover').next({ annotationId: null });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, []);\n\n return { onMouseEnter, onMouseLeave };\n}\n"],"mappings":"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","names":["subscriptions","useRef","useEffect","useRef","useEffect"]}