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

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-7GvDyO0d.d.mts +414 -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-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.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 +645 -471
  38. package/dist/index.mjs +3461 -3025
  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/LiveRegion.tsx +18 -18
  77. package/src/components/ResizeHandle.tsx +10 -4
  78. package/src/components/SessionTimer.tsx +2 -2
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +9 -9
  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 +38 -10
  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 +119 -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 +231 -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 +504 -0
  149. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +135 -88
  150. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  151. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +308 -528
  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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useState, useRef, useEffect } from 'react';
4
4
  import { useTranslations } from '../../contexts/TranslationContext';
5
+ import { useEventBus } from '../../contexts/EventBusContext';
5
6
  import { getSupportedShapes } from '../../lib/media-shapes';
6
7
  import type { Annotator } from '../../lib/annotation-registry';
7
8
  import './annotations.css';
@@ -15,18 +16,14 @@ export type ShapeType = 'rectangle' | 'circle' | 'polygon';
15
16
  interface AnnotateToolbarProps {
16
17
  selectedMotivation: SelectionMotivation | null;
17
18
  selectedClick: ClickAction;
18
- onSelectionChange: (motivation: SelectionMotivation | null) => void;
19
- onClickChange: (motivation: ClickAction) => void;
20
19
  showSelectionGroup?: boolean;
21
20
  showDeleteButton?: boolean;
22
21
  showShapeGroup?: boolean;
23
22
  selectedShape?: ShapeType;
24
- onShapeChange?: (shape: ShapeType) => void;
25
23
  mediaType?: string | null; // MIME type to determine supported shapes
26
24
 
27
25
  // Mode props
28
26
  annotateMode: boolean;
29
- onAnnotateModeToggle: () => void;
30
27
 
31
28
  // Annotators for emoji lookup
32
29
  annotators: Record<string, Annotator>;
@@ -102,22 +99,27 @@ function DropdownGroup({
102
99
  );
103
100
  }
104
101
 
102
+ /**
103
+ * Toolbar for annotation controls with mode, selection, click, and shape options
104
+ *
105
+ * @emits toolbar:selection-changed - Selection motivation changed. Payload: { motivation: SelectionMotivation | null }
106
+ * @emits toolbar:click-changed - Click action mode changed. Payload: { action: ClickAction }
107
+ * @emits toolbar:shape-changed - Drawing shape changed. Payload: { shape: ShapeType }
108
+ * @emits view:mode-toggled - View mode toggled between browse and annotate. Payload: undefined
109
+ */
105
110
  export function AnnotateToolbar({
106
111
  selectedMotivation,
107
112
  selectedClick,
108
- onSelectionChange,
109
- onClickChange,
110
113
  showSelectionGroup = true,
111
114
  showDeleteButton = true,
112
115
  showShapeGroup = false,
113
116
  selectedShape = 'rectangle',
114
- onShapeChange,
115
117
  mediaType,
116
118
  annotateMode = false,
117
- onAnnotateModeToggle,
118
119
  annotators
119
120
  }: AnnotateToolbarProps) {
120
121
  const t = useTranslations('AnnotateToolbar');
122
+ const eventBus = useEventBus();
121
123
 
122
124
  // Helper to get emoji from annotators by motivation (with fallback for safety)
123
125
  const getMotivationEmoji = (motivation: SelectionMotivation): string => {
@@ -186,9 +188,11 @@ export function AnnotateToolbar({
186
188
  const handleSelectionClick = (motivation: SelectionMotivation | null) => {
187
189
  // If null is clicked, always deselect. Otherwise toggle.
188
190
  if (motivation === null) {
189
- onSelectionChange(null);
191
+ eventBus.emit('toolbar:selection-changed', { motivation: null });
190
192
  } else {
191
- onSelectionChange(selectedMotivation === motivation ? null : motivation);
193
+ eventBus.emit('toolbar:selection-changed', {
194
+ motivation: selectedMotivation === motivation ? null : motivation
195
+ });
192
196
  }
193
197
  // Close dropdown after selection
194
198
  setSelectionPinned(false);
@@ -196,23 +200,21 @@ export function AnnotateToolbar({
196
200
  };
197
201
 
198
202
  const handleClickClick = (action: ClickAction) => {
199
- onClickChange(action);
203
+ eventBus.emit('toolbar:click-changed', { action });
200
204
  // Close dropdown after selection
201
205
  setClickPinned(false);
202
206
  setClickHovered(false);
203
207
  };
204
208
 
205
209
  const handleShapeClick = (shape: ShapeType) => {
206
- if (onShapeChange) {
207
- onShapeChange(shape);
208
- }
210
+ eventBus.emit('toolbar:shape-changed', { shape });
209
211
  // Close dropdown after selection
210
212
  setShapePinned(false);
211
213
  setShapeHovered(false);
212
214
  };
213
215
 
214
216
  const handleModeToggle = () => {
215
- onAnnotateModeToggle();
217
+ eventBus.emit('view:mode-toggled', undefined);
216
218
  setModePinned(false);
217
219
  setModeHovered(false);
218
220
  };
@@ -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(() => {