@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.81

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 (213) hide show
  1. package/dist/EventBusContext-CJjL_cCf.d.mts +462 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-JZIO2A3B.mjs → chunk-QB52Q7EQ.mjs} +206 -146
  11. package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
  12. package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
  34. package/dist/id-7U7GGVWY.mjs.map +1 -0
  35. package/dist/index.css +123 -85
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.mts +715 -574
  38. package/dist/index.mjs +3898 -3575
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
  59. package/dist/sv-7ZK5EQEB.mjs.map +1 -0
  60. package/dist/test-utils.d.mts +18 -8
  61. package/dist/test-utils.mjs +36 -14
  62. package/dist/test-utils.mjs.map +1 -1
  63. package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +3 -1
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/ResizeHandle.tsx +10 -4
  77. package/src/components/SessionExpiryBanner.tsx +2 -3
  78. package/src/components/SessionTimer.tsx +3 -3
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +33 -33
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -134
  100. package/src/components/resource/BrowseView.tsx +86 -166
  101. package/src/components/resource/HistoryEvent.tsx +13 -7
  102. package/src/components/resource/ResourceViewer.tsx +122 -264
  103. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  104. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  105. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  106. package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
  107. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  108. package/src/components/resource/panels/CommentsPanel.tsx +121 -28
  109. package/src/components/resource/panels/DetectSection.css +36 -1
  110. package/src/components/resource/panels/DetectSection.tsx +49 -15
  111. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  112. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  113. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  114. package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  116. package/src/components/resource/panels/TagEntry.tsx +25 -33
  117. package/src/components/resource/panels/TaggingPanel.tsx +118 -30
  118. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
  119. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
  120. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  121. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
  122. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  123. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  124. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
  125. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  126. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
  127. package/src/components/settings/SettingsPanel.tsx +15 -12
  128. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  129. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  130. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  131. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  132. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  133. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  134. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  135. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  136. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  137. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  138. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  139. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  140. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  141. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  142. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  143. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
  144. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +234 -0
  145. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  146. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  147. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  148. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +503 -0
  149. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +139 -93
  150. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  151. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +341 -524
  152. package/translations/ar.json +6 -3
  153. package/translations/bn.json +6 -3
  154. package/translations/cs.json +6 -3
  155. package/translations/da.json +6 -3
  156. package/translations/de.json +6 -3
  157. package/translations/el.json +6 -3
  158. package/translations/en.json +6 -3
  159. package/translations/es.json +6 -3
  160. package/translations/fa.json +6 -3
  161. package/translations/fi.json +6 -3
  162. package/translations/fr.json +6 -3
  163. package/translations/he.json +6 -3
  164. package/translations/hi.json +6 -3
  165. package/translations/id.json +6 -3
  166. package/translations/it.json +6 -3
  167. package/translations/ja.json +6 -3
  168. package/translations/ko.json +6 -3
  169. package/translations/ms.json +6 -3
  170. package/translations/nl.json +6 -3
  171. package/translations/no.json +6 -3
  172. package/translations/pl.json +6 -3
  173. package/translations/pt.json +6 -3
  174. package/translations/ro.json +6 -3
  175. package/translations/sv.json +6 -3
  176. package/translations/th.json +6 -3
  177. package/translations/tr.json +6 -3
  178. package/translations/uk.json +6 -3
  179. package/translations/vi.json +6 -3
  180. package/translations/zh.json +6 -3
  181. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  182. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  183. package/dist/ar-EMHEHPCJ.mjs.map +0 -1
  184. package/dist/bn-OVCI4F6X.mjs.map +0 -1
  185. package/dist/chunk-JZIO2A3B.mjs.map +0 -1
  186. package/dist/chunk-LIHZTECW.mjs.map +0 -1
  187. package/dist/cs-FAN66Q2F.mjs.map +0 -1
  188. package/dist/da-YBBIHI2O.mjs.map +0 -1
  189. package/dist/de-MAYU33LB.mjs.map +0 -1
  190. package/dist/el-MKGSWN4O.mjs.map +0 -1
  191. package/dist/es-52LHUWJD.mjs.map +0 -1
  192. package/dist/fa-FJICRANB.mjs.map +0 -1
  193. package/dist/fi-O455XFCR.mjs.map +0 -1
  194. package/dist/fr-TXIXHOOE.mjs.map +0 -1
  195. package/dist/he-JBSOX5IN.mjs.map +0 -1
  196. package/dist/hi-KGHI3XVT.mjs.map +0 -1
  197. package/dist/id-5OCPPZLO.mjs.map +0 -1
  198. package/dist/it-PNBBZSM2.mjs.map +0 -1
  199. package/dist/ja-LDD7R3TJ.mjs.map +0 -1
  200. package/dist/ko-F47ZDEY3.mjs.map +0 -1
  201. package/dist/ms-Z7LMXJWL.mjs.map +0 -1
  202. package/dist/nl-6SJFBPJ3.mjs.map +0 -1
  203. package/dist/no-YXPBPSGF.mjs.map +0 -1
  204. package/dist/pl-P4AZ2QME.mjs.map +0 -1
  205. package/dist/pt-LHWUS6U6.mjs.map +0 -1
  206. package/dist/ro-EA5J2ZON.mjs.map +0 -1
  207. package/dist/sv-DATBS3UQ.mjs.map +0 -1
  208. package/dist/th-WTFJRWPT.mjs.map +0 -1
  209. package/dist/tr-IKO3RXOX.mjs.map +0 -1
  210. package/dist/uk-CF6CTTRK.mjs.map +0 -1
  211. package/dist/vi-AJLTXPZQ.mjs.map +0 -1
  212. package/dist/zh-U3ORHHYH.mjs.map +0 -1
  213. /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
2
  import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
3
- import { vi } from 'vitest';
3
+ import { vi, beforeEach, describe, it, expect } from 'vitest';
4
4
  import { NextIntlClientProvider } from 'next-intl';
5
5
  import { AnnotateToolbar, type SelectionMotivation, type ClickAction } from '../AnnotateToolbar';
6
6
  import { ANNOTATORS } from '../../../lib/annotation-registry';
7
+ import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../contexts/EventBusContext';
7
8
 
8
9
  // Mock translations
9
10
  const messages = {
@@ -29,11 +30,75 @@ const messages = {
29
30
  }
30
31
  };
31
32
 
32
- const renderWithIntl = (component: React.ReactElement) => {
33
+ // Composition-based event tracker
34
+ interface TrackedEvent {
35
+ event: string;
36
+ payload: any;
37
+ }
38
+
39
+ function createEventTracker() {
40
+ const events: TrackedEvent[] = [];
41
+
42
+ function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
43
+ const eventBus = useEventBus();
44
+
45
+ React.useEffect(() => {
46
+ const handlers: Array<() => void> = [];
47
+
48
+ // Track toolbar-related events
49
+ const trackEvent = (eventName: string) => (payload: any) => {
50
+ events.push({ event: eventName, payload });
51
+ };
52
+
53
+ const toolbarEvents = [
54
+ 'view:mode-toggled',
55
+ 'toolbar:click-changed',
56
+ 'toolbar:selection-changed',
57
+ 'toolbar:shape-changed',
58
+ ];
59
+
60
+ toolbarEvents.forEach(eventName => {
61
+ const handler = trackEvent(eventName);
62
+ eventBus.on(eventName, handler);
63
+ handlers.push(() => eventBus.off(eventName, handler));
64
+ });
65
+
66
+ return () => {
67
+ handlers.forEach(cleanup => cleanup());
68
+ };
69
+ }, [eventBus]);
70
+
71
+ return <>{children}</>;
72
+ }
73
+
74
+ return {
75
+ EventTrackingWrapper,
76
+ events,
77
+ clear: () => {
78
+ events.length = 0;
79
+ },
80
+ };
81
+ }
82
+
83
+ const renderWithIntl = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
84
+ if (tracker) {
85
+ return render(
86
+ <EventBusProvider>
87
+ <NextIntlClientProvider locale="en" messages={messages}>
88
+ <tracker.EventTrackingWrapper>
89
+ {component}
90
+ </tracker.EventTrackingWrapper>
91
+ </NextIntlClientProvider>
92
+ </EventBusProvider>
93
+ );
94
+ }
95
+
33
96
  return render(
34
- <NextIntlClientProvider locale="en" messages={messages}>
35
- {component}
36
- </NextIntlClientProvider>
97
+ <EventBusProvider>
98
+ <NextIntlClientProvider locale="en" messages={messages}>
99
+ {component}
100
+ </NextIntlClientProvider>
101
+ </EventBusProvider>
37
102
  );
38
103
  };
39
104
 
@@ -44,11 +109,11 @@ describe('AnnotateToolbar', () => {
44
109
  onSelectionChange: vi.fn(),
45
110
  onClickChange: vi.fn(),
46
111
  annotateMode: false,
47
- onAnnotateModeToggle: vi.fn(),
48
112
  annotators: ANNOTATORS
49
113
  };
50
114
 
51
115
  beforeEach(() => {
116
+ resetEventBusForTesting();
52
117
  vi.clearAllMocks();
53
118
  });
54
119
 
@@ -113,19 +178,19 @@ describe('AnnotateToolbar', () => {
113
178
  <AnnotateToolbar
114
179
  {...defaultProps}
115
180
  annotateMode={false}
116
- onAnnotateModeToggle={vi.fn()}
117
181
  />
118
182
  );
119
183
  expect(screen.getByText('Browse')).toBeInTheDocument();
120
184
 
121
185
  rerender(
122
- <NextIntlClientProvider locale="en" messages={messages}>
123
- <AnnotateToolbar
124
- {...defaultProps}
125
- annotateMode={true}
126
- onAnnotateModeToggle={vi.fn()}
127
- />
128
- </NextIntlClientProvider>
186
+ <EventBusProvider>
187
+ <NextIntlClientProvider locale="en" messages={messages}>
188
+ <AnnotateToolbar
189
+ {...defaultProps}
190
+ annotateMode={true}
191
+ />
192
+ </NextIntlClientProvider>
193
+ </EventBusProvider>
129
194
  );
130
195
  expect(screen.getByText('Annotate')).toBeInTheDocument();
131
196
  });
@@ -135,7 +200,6 @@ describe('AnnotateToolbar', () => {
135
200
  <AnnotateToolbar
136
201
  {...defaultProps}
137
202
  annotateMode={false}
138
- onAnnotateModeToggle={vi.fn()}
139
203
  />
140
204
  );
141
205
 
@@ -151,14 +215,14 @@ describe('AnnotateToolbar', () => {
151
215
  });
152
216
  });
153
217
 
154
- it('calls onAnnotateModeToggle when Browse is clicked in Annotate mode', async () => {
155
- const handleToggle = vi.fn();
218
+ it('emits view:mode-toggled event when Browse is clicked in Annotate mode', async () => {
219
+ const tracker = createEventTracker();
156
220
  renderWithIntl(
157
221
  <AnnotateToolbar
158
222
  {...defaultProps}
159
223
  annotateMode={true}
160
- onAnnotateModeToggle={handleToggle}
161
- />
224
+ />,
225
+ tracker
162
226
  );
163
227
 
164
228
  const modeGroup = screen.getByLabelText('Mode');
@@ -169,17 +233,19 @@ describe('AnnotateToolbar', () => {
169
233
  fireEvent.click(browseButton);
170
234
  });
171
235
 
172
- expect(handleToggle).toHaveBeenCalledTimes(1);
236
+ await waitFor(() => {
237
+ expect(tracker.events.some(e => e.event === 'view:mode-toggled')).toBe(true);
238
+ });
173
239
  });
174
240
 
175
- it('calls onAnnotateModeToggle when Annotate is clicked in Browse mode', async () => {
176
- const handleToggle = vi.fn();
241
+ it('emits view:mode-toggled event when Annotate is clicked in Browse mode', async () => {
242
+ const tracker = createEventTracker();
177
243
  renderWithIntl(
178
244
  <AnnotateToolbar
179
245
  {...defaultProps}
180
246
  annotateMode={false}
181
- onAnnotateModeToggle={handleToggle}
182
- />
247
+ />,
248
+ tracker
183
249
  );
184
250
 
185
251
  const modeGroup = screen.getByLabelText('Mode');
@@ -190,17 +256,19 @@ describe('AnnotateToolbar', () => {
190
256
  fireEvent.click(annotateButton);
191
257
  });
192
258
 
193
- expect(handleToggle).toHaveBeenCalledTimes(1);
259
+ await waitFor(() => {
260
+ expect(tracker.events.some(e => e.event === 'view:mode-toggled')).toBe(true);
261
+ });
194
262
  });
195
263
 
196
264
  it('closes dropdown after selection', async () => {
197
- const handleToggle = vi.fn();
265
+ const tracker = createEventTracker();
198
266
  const { rerender } = renderWithIntl(
199
267
  <AnnotateToolbar
200
268
  {...defaultProps}
201
269
  annotateMode={false}
202
- onAnnotateModeToggle={handleToggle}
203
- />
270
+ />,
271
+ tracker
204
272
  );
205
273
 
206
274
  const modeGroup = screen.getByLabelText('Mode');
@@ -213,18 +281,23 @@ describe('AnnotateToolbar', () => {
213
281
  const annotateButton = screen.getByText('Annotate');
214
282
  fireEvent.click(annotateButton);
215
283
 
216
- // Verify the toggle was called
217
- expect(handleToggle).toHaveBeenCalledTimes(1);
284
+ // Verify the event was emitted
285
+ await waitFor(() => {
286
+ expect(tracker.events.some(e => e.event === 'view:mode-toggled')).toBe(true);
287
+ });
218
288
 
219
289
  // Simulate mode change by rerendering with new mode
220
290
  rerender(
221
- <NextIntlClientProvider locale="en" messages={messages}>
222
- <AnnotateToolbar
223
- {...defaultProps}
224
- annotateMode={true}
225
- onAnnotateModeToggle={handleToggle}
226
- />
227
- </NextIntlClientProvider>
291
+ <EventBusProvider>
292
+ <NextIntlClientProvider locale="en" messages={messages}>
293
+ <tracker.EventTrackingWrapper>
294
+ <AnnotateToolbar
295
+ {...defaultProps}
296
+ annotateMode={true}
297
+ />
298
+ </tracker.EventTrackingWrapper>
299
+ </NextIntlClientProvider>
300
+ </EventBusProvider>
228
301
  );
229
302
 
230
303
  // After mode change, the collapsed content should show "Annotate"
@@ -238,10 +311,11 @@ describe('AnnotateToolbar', () => {
238
311
  });
239
312
 
240
313
  describe('CLICK Group Interactions', () => {
241
- it('calls onClickChange when clicking an action', async () => {
242
- const handleChange = vi.fn();
314
+ it('emits toolbar:click-changed event when clicking an action', async () => {
315
+ const tracker = createEventTracker();
243
316
  renderWithIntl(
244
- <AnnotateToolbar {...defaultProps} onClickChange={handleChange} />
317
+ <AnnotateToolbar {...defaultProps} />,
318
+ tracker
245
319
  );
246
320
 
247
321
  const clickGroup = screen.getByLabelText('Click');
@@ -251,8 +325,13 @@ describe('AnnotateToolbar', () => {
251
325
  expect(screen.getByText('Follow')).toBeInTheDocument();
252
326
  });
253
327
 
328
+ tracker.clear();
254
329
  fireEvent.click(screen.getByText('Follow'));
255
- expect(handleChange).toHaveBeenCalledWith('follow');
330
+ await waitFor(() => {
331
+ expect(tracker.events.some(e =>
332
+ e.event === 'toolbar:click-changed' && e.payload?.action === 'follow'
333
+ )).toBe(true);
334
+ });
256
335
  });
257
336
 
258
337
  it('displays selected action', () => {
@@ -264,10 +343,11 @@ describe('AnnotateToolbar', () => {
264
343
  });
265
344
 
266
345
  describe('MOTIVATION Group Interactions', () => {
267
- it('calls onSelectionChange when clicking a motivation', async () => {
268
- const handleChange = vi.fn();
346
+ it('emits toolbar:selection-changed event when clicking a motivation', async () => {
347
+ const tracker = createEventTracker();
269
348
  renderWithIntl(
270
- <AnnotateToolbar {...defaultProps} onSelectionChange={handleChange} />
349
+ <AnnotateToolbar {...defaultProps} />,
350
+ tracker
271
351
  );
272
352
 
273
353
  const motivationGroup = screen.getByLabelText('Motivation');
@@ -277,18 +357,23 @@ describe('AnnotateToolbar', () => {
277
357
  expect(screen.getByText('Reference')).toBeInTheDocument();
278
358
  });
279
359
 
360
+ tracker.clear();
280
361
  fireEvent.click(screen.getByText('Reference'));
281
- expect(handleChange).toHaveBeenCalledWith('linking');
362
+ await waitFor(() => {
363
+ expect(tracker.events.some(e =>
364
+ e.event === 'toolbar:selection-changed' && e.payload?.motivation === 'linking'
365
+ )).toBe(true);
366
+ });
282
367
  });
283
368
 
284
369
  it('toggles motivation on/off', async () => {
285
- const handleChange = vi.fn();
370
+ const tracker = createEventTracker();
286
371
  const { rerender } = renderWithIntl(
287
372
  <AnnotateToolbar
288
373
  {...defaultProps}
289
374
  selectedMotivation={null}
290
- onSelectionChange={handleChange}
291
- />
375
+ />,
376
+ tracker
292
377
  );
293
378
 
294
379
  const motivationGroup = screen.getByLabelText('Motivation');
@@ -300,18 +385,26 @@ describe('AnnotateToolbar', () => {
300
385
  });
301
386
 
302
387
  const dropdown = screen.getByRole('menu');
388
+ tracker.clear();
303
389
  fireEvent.click(within(dropdown).getByText('Highlight'));
304
- expect(handleChange).toHaveBeenCalledWith('highlighting');
390
+ await waitFor(() => {
391
+ expect(tracker.events.some(e =>
392
+ e.event === 'toolbar:selection-changed' && e.payload?.motivation === 'highlighting'
393
+ )).toBe(true);
394
+ });
305
395
 
306
396
  // Simulate selection
307
397
  rerender(
308
- <NextIntlClientProvider locale="en" messages={messages}>
309
- <AnnotateToolbar
310
- {...defaultProps}
311
- selectedMotivation="highlighting"
312
- onSelectionChange={handleChange}
313
- />
314
- </NextIntlClientProvider>
398
+ <EventBusProvider>
399
+ <NextIntlClientProvider locale="en" messages={messages}>
400
+ <tracker.EventTrackingWrapper>
401
+ <AnnotateToolbar
402
+ {...defaultProps}
403
+ selectedMotivation="highlighting"
404
+ />
405
+ </tracker.EventTrackingWrapper>
406
+ </NextIntlClientProvider>
407
+ </EventBusProvider>
315
408
  );
316
409
 
317
410
  // Click again to deselect
@@ -321,21 +414,26 @@ describe('AnnotateToolbar', () => {
321
414
  expect(within(dropdown).getByText('Highlight')).toBeInTheDocument();
322
415
  });
323
416
  const dropdown2 = screen.getByRole('menu');
417
+ tracker.clear();
324
418
  fireEvent.click(within(dropdown2).getByText('Highlight'));
325
- expect(handleChange).toHaveBeenCalledWith(null);
419
+ await waitFor(() => {
420
+ expect(tracker.events.some(e =>
421
+ e.event === 'toolbar:selection-changed' && e.payload?.motivation === null
422
+ )).toBe(true);
423
+ });
326
424
  });
327
425
  });
328
426
 
329
427
  describe('SHAPE Group Interactions', () => {
330
- it('calls onShapeChange when clicking a shape', async () => {
331
- const handleChange = vi.fn();
428
+ it('emits toolbar:shape-changed event when clicking a shape', async () => {
429
+ const tracker = createEventTracker();
332
430
  renderWithIntl(
333
431
  <AnnotateToolbar
334
432
  {...defaultProps}
335
433
  showShapeGroup={true}
336
434
  selectedShape="rectangle"
337
- onShapeChange={handleChange}
338
- />
435
+ />,
436
+ tracker
339
437
  );
340
438
 
341
439
  const shapeGroup = screen.getByLabelText('Shape');
@@ -345,8 +443,13 @@ describe('AnnotateToolbar', () => {
345
443
  expect(screen.getByText('Circle')).toBeInTheDocument();
346
444
  });
347
445
 
446
+ tracker.clear();
348
447
  fireEvent.click(screen.getByText('Circle'));
349
- expect(handleChange).toHaveBeenCalledWith('circle');
448
+ await waitFor(() => {
449
+ expect(tracker.events.some(e =>
450
+ e.event === 'toolbar:shape-changed' && e.payload?.shape === 'circle'
451
+ )).toBe(true);
452
+ });
350
453
  });
351
454
  });
352
455
 
@@ -356,7 +459,6 @@ describe('AnnotateToolbar', () => {
356
459
  <AnnotateToolbar
357
460
  {...defaultProps}
358
461
  annotateMode={false}
359
- onAnnotateModeToggle={vi.fn()}
360
462
  />
361
463
  );
362
464
 
@@ -28,6 +28,16 @@
28
28
  z-index: 10;
29
29
  }
30
30
 
31
+ /* Pulse effect for hover synchronization */
32
+ .semiont-annotation-pulse {
33
+ background: var(--semiont-bg-hover) !important;
34
+ transition: background 0.3s ease-in-out;
35
+ }
36
+
37
+ [data-theme="dark"] .semiont-annotation-pulse {
38
+ background: var(--semiont-bg-hover) !important;
39
+ }
40
+
31
41
  /* Motivation-specific styles are handled in /styles/motivations/*.css */
32
42
 
33
43
  /* Entry structure */
@@ -22,17 +22,23 @@ export function JsonLdView({ annotation, onBack }: JsonLdViewProps) {
22
22
  const viewRef = useRef<EditorView | null>(null);
23
23
  const { showLineNumbers } = useLineNumbers();
24
24
 
25
+ // Store callback in ref to avoid including in dependency arrays
26
+ const onBackRef = useRef(onBack);
27
+ useEffect(() => {
28
+ onBackRef.current = onBack;
29
+ });
30
+
25
31
  // Handle escape key
26
32
  useEffect(() => {
27
33
  const handleKeyDown = (e: KeyboardEvent) => {
28
34
  if (e.key === 'Escape') {
29
- onBack();
35
+ onBackRef.current();
30
36
  }
31
37
  };
32
38
 
33
39
  window.addEventListener('keydown', handleKeyDown);
34
40
  return () => window.removeEventListener('keydown', handleKeyDown);
35
- }, [onBack]);
41
+ }, []);
36
42
 
37
43
  // Initialize CodeMirror
38
44
  useEffect(() => {
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
 
3
+ import { useRef } from 'react';
3
4
  import type { components } from '@semiont/api-client';
4
5
  import { getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/api-client';
5
6
  import { parseSvgSelector } from '@semiont/api-client';
7
+ import type { EventBus } from '../../contexts/EventBusContext';
6
8
 
7
9
  type Annotation = components['schemas']['Annotation'];
8
10
 
@@ -12,8 +14,7 @@ interface AnnotationOverlayProps {
12
14
  imageHeight: number;
13
15
  displayWidth: number;
14
16
  displayHeight: number;
15
- onAnnotationClick?: (annotation: Annotation) => void;
16
- onAnnotationHover?: (annotationId: string | null) => void;
17
+ eventBus?: EventBus;
17
18
  hoveredAnnotationId?: string | null;
18
19
  selectedAnnotationId?: string | null;
19
20
  }
@@ -59,6 +60,9 @@ function getAnnotationTooltip(annotation: Annotation): string {
59
60
 
60
61
  /**
61
62
  * Render annotation overlay - displays existing annotations as SVG shapes
63
+ *
64
+ * @emits annotation:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
65
+ * @emits annotation:click - Annotation clicked. Payload: { annotationId: string, motivation: Motivation }
62
66
  */
63
67
  export function AnnotationOverlay({
64
68
  annotations,
@@ -66,14 +70,30 @@ export function AnnotationOverlay({
66
70
  imageHeight,
67
71
  displayWidth,
68
72
  displayHeight,
69
- onAnnotationClick,
70
- onAnnotationHover,
73
+ eventBus,
71
74
  hoveredAnnotationId,
72
75
  selectedAnnotationId
73
76
  }: AnnotationOverlayProps) {
74
77
  const scaleX = displayWidth / imageWidth;
75
78
  const scaleY = displayHeight / imageHeight;
76
79
 
80
+ // Track current hover state to prevent redundant emissions
81
+ const currentHover = useRef<string | null>(null);
82
+
83
+ const handleMouseEnter = (annotationId: string) => {
84
+ if (currentHover.current !== annotationId) {
85
+ currentHover.current = annotationId;
86
+ eventBus?.emit('annotation:hover', { annotationId });
87
+ }
88
+ };
89
+
90
+ const handleMouseLeave = () => {
91
+ if (currentHover.current !== null) {
92
+ currentHover.current = null;
93
+ eventBus?.emit('annotation:hover', { annotationId: null });
94
+ }
95
+ };
96
+
77
97
  return (
78
98
  <svg
79
99
  className="semiont-annotation-overlay"
@@ -120,9 +140,9 @@ export function AnnotationOverlay({
120
140
  className="semiont-annotation-overlay__shape"
121
141
  data-hovered={isHovered ? 'true' : 'false'}
122
142
  data-selected={isSelected ? 'true' : 'false'}
123
- onClick={() => onAnnotationClick?.(annotation)}
124
- onMouseEnter={() => onAnnotationHover?.(annotation.id)}
125
- onMouseLeave={() => onAnnotationHover?.(null)}
143
+ onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
144
+ onMouseEnter={() => handleMouseEnter(annotation.id)}
145
+ onMouseLeave={handleMouseLeave}
126
146
  />
127
147
  {statusEmoji && (
128
148
  <text
@@ -133,10 +153,10 @@ export function AnnotationOverlay({
133
153
  style={{ userSelect: 'none' }}
134
154
  onClick={(e) => {
135
155
  e.stopPropagation();
136
- onAnnotationClick?.(annotation);
156
+ eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
137
157
  }}
138
- onMouseEnter={() => onAnnotationHover?.(annotation.id)}
139
- onMouseLeave={() => onAnnotationHover?.(null)}
158
+ onMouseEnter={() => handleMouseEnter(annotation.id)}
159
+ onMouseLeave={handleMouseLeave}
140
160
  >
141
161
  {statusEmoji}
142
162
  </text>
@@ -167,9 +187,9 @@ export function AnnotationOverlay({
167
187
  className="semiont-annotation-overlay__shape"
168
188
  data-hovered={isHovered ? 'true' : 'false'}
169
189
  data-selected={isSelected ? 'true' : 'false'}
170
- onClick={() => onAnnotationClick?.(annotation)}
171
- onMouseEnter={() => onAnnotationHover?.(annotation.id)}
172
- onMouseLeave={() => onAnnotationHover?.(null)}
190
+ onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
191
+ onMouseEnter={() => handleMouseEnter(annotation.id)}
192
+ onMouseLeave={handleMouseLeave}
173
193
  />
174
194
  {statusEmoji && (
175
195
  <text
@@ -180,10 +200,10 @@ export function AnnotationOverlay({
180
200
  style={{ userSelect: 'none' }}
181
201
  onClick={(e) => {
182
202
  e.stopPropagation();
183
- onAnnotationClick?.(annotation);
203
+ eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
184
204
  }}
185
- onMouseEnter={() => onAnnotationHover?.(annotation.id)}
186
- onMouseLeave={() => onAnnotationHover?.(null)}
205
+ onMouseEnter={() => handleMouseEnter(annotation.id)}
206
+ onMouseLeave={handleMouseLeave}
187
207
  >
188
208
  {statusEmoji}
189
209
  </text>
@@ -227,9 +247,9 @@ export function AnnotationOverlay({
227
247
  className="semiont-annotation-overlay__shape"
228
248
  data-hovered={isHovered ? 'true' : 'false'}
229
249
  data-selected={isSelected ? 'true' : 'false'}
230
- onClick={() => onAnnotationClick?.(annotation)}
231
- onMouseEnter={() => onAnnotationHover?.(annotation.id)}
232
- onMouseLeave={() => onAnnotationHover?.(null)}
250
+ onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
251
+ onMouseEnter={() => handleMouseEnter(annotation.id)}
252
+ onMouseLeave={handleMouseLeave}
233
253
  />
234
254
  {statusEmoji && (
235
255
  <text
@@ -240,10 +260,10 @@ export function AnnotationOverlay({
240
260
  style={{ userSelect: 'none' }}
241
261
  onClick={(e) => {
242
262
  e.stopPropagation();
243
- onAnnotationClick?.(annotation);
263
+ eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
244
264
  }}
245
- onMouseEnter={() => onAnnotationHover?.(annotation.id)}
246
- onMouseLeave={() => onAnnotationHover?.(null)}
265
+ onMouseEnter={() => handleMouseEnter(annotation.id)}
266
+ onMouseLeave={handleMouseLeave}
247
267
  >
248
268
  {statusEmoji}
249
269
  </text>