@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
@@ -7,37 +7,41 @@ import { remarkAnnotations, type PreparedAnnotation } from '../../lib/remark-ann
7
7
  import { rehypeRenderAnnotations } from '../../lib/rehype-render-annotations';
8
8
  import type { components } from '@semiont/api-client';
9
9
  import { getExactText, getTextPositionSelector, getTargetSelector, getBodySource, getMimeCategory, isPdfMimeType, resourceUri as toResourceUri } from '@semiont/api-client';
10
- import type { Annotator } from '../../lib/annotation-registry';
10
+ import { ANNOTATORS } from '../../lib/annotation-registry';
11
11
  import { ImageViewer } from '../viewers';
12
12
  import { AnnotateToolbar, type ClickAction } from '../annotation/AnnotateToolbar';
13
- import type { AnnotationsCollection, AnnotationHandlers } from '../../types/annotation-props';
13
+ import type { AnnotationsCollection } from '../../types/annotation-props';
14
14
 
15
15
  // Lazy load PDF component to avoid SSR issues with browser PDF.js loading
16
16
  const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
17
17
 
18
18
  type Annotation = components['schemas']['Annotation'];
19
19
  import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
20
+ import { useEventBus } from '../../contexts/EventBusContext';
21
+ import { useEventSubscriptions } from '../../contexts/useEventSubscription';
20
22
 
21
23
  interface Props {
22
24
  content: string;
23
25
  mimeType: string;
24
26
  resourceUri: string;
25
27
  annotations: AnnotationsCollection;
26
- handlers?: AnnotationHandlers;
27
28
  hoveredAnnotationId?: string | null;
28
29
  hoveredCommentId?: string | null;
29
30
  selectedClick?: ClickAction;
30
- onClickChange?: (motivation: ClickAction) => void;
31
31
  annotateMode: boolean;
32
- onAnnotateModeToggle: () => void;
33
- annotators: Record<string, Annotator>;
34
32
  }
35
33
 
36
34
  /**
37
35
  * Convert W3C Annotations to simplified format for remark plugin.
38
36
  * Extracts position info and converts start/end to offset/length.
39
37
  */
40
- function prepareAnnotations(annotations: Annotation[], annotators: Record<string, Annotator>): PreparedAnnotation[] {
38
+ function prepareAnnotations(annotations: Annotation[]): PreparedAnnotation[] {
39
+ /**
40
+ * View component for browsing resources with rendered annotations
41
+ *
42
+ * @emits annotation:click - Annotation clicked in browse view. Payload: { annotationId: string, motivation: Motivation }
43
+ * @emits annotation:hover - Annotation hovered in browse view. Payload: { annotationId: string | null }
44
+ */
41
45
  return annotations
42
46
  .map(ann => {
43
47
  const targetSelector = getTargetSelector(ann.target);
@@ -45,8 +49,8 @@ function prepareAnnotations(annotations: Annotation[], annotators: Record<string
45
49
  const start = posSelector?.start ?? 0;
46
50
  const end = posSelector?.end ?? 0;
47
51
 
48
- // Use annotators to determine type
49
- const type = Object.values(annotators).find(a => a.matchesAnnotation(ann))?.internalType || 'highlight';
52
+ // Use ANNOTATORS registry to determine type
53
+ const type = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(ann))?.internalType || 'highlight';
50
54
 
51
55
  return {
52
56
  id: ann.id,
@@ -59,121 +63,82 @@ function prepareAnnotations(annotations: Annotation[], annotators: Record<string
59
63
  });
60
64
  }
61
65
 
66
+ /**
67
+ * View component for browsing annotated resources in read-only mode
68
+ *
69
+ * @emits annotation:click - User clicked on annotation. Payload: { annotationId: string, motivation: Motivation }
70
+ * @emits annotation:hover - User hovered over annotation. Payload: { annotationId: string | null }
71
+ *
72
+ * @subscribes annotation:hover - Highlight annotation on hover. Payload: { annotationId: string | null }
73
+ * @subscribes annotation:focus - Scroll to and highlight annotation. Payload: { annotationId: string }
74
+ */
62
75
  export function BrowseView({
63
76
  content,
64
77
  mimeType,
65
78
  resourceUri,
66
79
  annotations,
67
- handlers,
68
- hoveredAnnotationId,
69
- hoveredCommentId,
70
80
  selectedClick = 'detail',
71
- onClickChange,
72
- annotateMode,
73
- onAnnotateModeToggle,
74
- annotators
81
+ annotateMode
75
82
  }: Props) {
76
83
  const { newAnnotationIds } = useResourceAnnotations();
84
+ const eventBus = useEventBus();
77
85
  const containerRef = useRef<HTMLDivElement>(null);
78
86
 
79
87
  const category = getMimeCategory(mimeType);
80
88
 
81
89
  const { highlights, references, assessments, comments, tags } = annotations;
82
90
 
83
- // Extract individual handlers from grouped object
84
- const onAnnotationClick = handlers?.onClick;
85
- const onAnnotationHover = handlers?.onHover;
86
- const onCommentHover = handlers?.onCommentHover;
87
-
88
91
  const allAnnotations = [...highlights, ...references, ...assessments, ...comments, ...tags];
89
92
 
90
- const preparedAnnotations = prepareAnnotations(allAnnotations, annotators);
91
-
92
- // Create a map of annotation ID -> full annotation for click handling
93
- const map = new Map<string, Annotation>();
94
- for (const ann of allAnnotations) {
95
- map.set(ann.id, ann);
96
- }
97
- const annotationMap = map;
98
-
99
- // Wrapper for annotation hover that routes based on registry metadata
100
- const handleAnnotationHover = useCallback((annotationId: string | null) => {
101
- if (annotationId) {
102
- const annotation = annotationMap.get(annotationId);
103
- const metadata = annotation ? Object.values(annotators).find(a => a.matchesAnnotation(annotation!)) : null;
104
-
105
- // Route to side panel if annotation type has one
106
- if (metadata?.hasSidePanel) {
107
- // Clear the other hover state when switching
108
- if (onAnnotationHover) onAnnotationHover(null);
109
- if (onCommentHover) onCommentHover(annotationId);
110
- return;
111
- } else {
112
- // Clear the other hover state when switching
113
- if (onCommentHover) onCommentHover(null);
114
- if (onAnnotationHover) onAnnotationHover(annotationId);
115
- return;
116
- }
117
- }
118
- // Clear both when null
119
- if (onAnnotationHover) onAnnotationHover(null);
120
- if (onCommentHover) onCommentHover(null);
121
- }, [annotationMap, onAnnotationHover, onCommentHover, annotators]);
93
+ const preparedAnnotations = prepareAnnotations(allAnnotations);
122
94
 
123
- // Attach click handlers, hover handlers, and animations after render
95
+ // Attach click handler, hover handler, and animations after render
124
96
  useEffect(() => {
125
97
  if (!containerRef.current) return;
126
98
 
127
99
  const container = containerRef.current;
128
100
 
129
- // Find all annotation spans
130
- const annotationSpans = container.querySelectorAll('[data-annotation-id]');
101
+ // Single click handler for the container
102
+ const handleClick = (e: MouseEvent) => {
103
+ const target = e.target as HTMLElement;
104
+ const annotationElement = target.closest('[data-annotation-id]');
105
+ if (!annotationElement) return;
131
106
 
132
- // Attach click handlers
133
- const handleClick = (e: Event) => {
134
- const target = e.currentTarget as HTMLElement;
135
- const annotationId = target.getAttribute('data-annotation-id');
136
- const annotationType = target.getAttribute('data-annotation-type');
107
+ const annotationId = annotationElement.getAttribute('data-annotation-id');
108
+ const annotationType = annotationElement.getAttribute('data-annotation-type');
137
109
 
138
- if (annotationId && annotationType === 'reference' && onAnnotationClick) {
139
- const annotation = annotationMap.get(annotationId);
110
+ if (annotationId && annotationType === 'reference') {
111
+ const annotation = allAnnotations.find(a => a.id === annotationId);
140
112
  if (annotation) {
141
- onAnnotationClick(annotation);
113
+ eventBus.emit('annotation:click', { annotationId, motivation: annotation.motivation });
142
114
  }
143
115
  }
144
116
  };
145
117
 
146
- // Attach hover handlers
147
- const handleMouseEnter = (e: Event) => {
148
- const target = e.currentTarget as HTMLElement;
149
- const annotationId = target.getAttribute('data-annotation-id');
118
+ // Single mouseover handler for the container - fires once on enter
119
+ const handleMouseOver = (e: MouseEvent) => {
120
+ const target = e.target as HTMLElement;
121
+ const annotationElement = target.closest('[data-annotation-id]');
122
+ const annotationId = annotationElement?.getAttribute('data-annotation-id');
123
+
150
124
  if (annotationId) {
151
- handleAnnotationHover(annotationId);
125
+ eventBus.emit('annotation:hover', { annotationId });
152
126
  }
153
127
  };
154
128
 
155
- const handleMouseLeave = () => {
156
- handleAnnotationHover(null);
157
- };
129
+ // Single mouseout handler for the container - fires once on exit
130
+ const handleMouseOut = (e: MouseEvent) => {
131
+ const target = e.target as HTMLElement;
132
+ const annotationElement = target.closest('[data-annotation-id]');
158
133
 
159
- const clickHandlers: Array<{ element: Element; handler: EventListener }> = [];
160
- const hoverHandlers: Array<{ element: Element; enterHandler: EventListener; leaveHandler: EventListener }> = [];
161
-
162
- annotationSpans.forEach((span) => {
163
- const annotationType = span.getAttribute('data-annotation-type');
164
- if (annotationType === 'reference') {
165
- span.addEventListener('click', handleClick);
166
- clickHandlers.push({ element: span, handler: handleClick });
134
+ if (annotationElement) {
135
+ eventBus.emit('annotation:hover', { annotationId: null });
167
136
  }
168
-
169
- // Add hover handlers for all annotation types
170
- span.addEventListener('mouseenter', handleMouseEnter);
171
- span.addEventListener('mouseleave', handleMouseLeave);
172
- hoverHandlers.push({ element: span, enterHandler: handleMouseEnter, leaveHandler: handleMouseLeave });
173
- });
137
+ };
174
138
 
175
139
  // Apply animation classes to new annotations
176
140
  if (newAnnotationIds) {
141
+ const annotationSpans = container.querySelectorAll('[data-annotation-id]');
177
142
  annotationSpans.forEach((span) => {
178
143
  const annotationId = span.getAttribute('data-annotation-id');
179
144
  if (annotationId && newAnnotationIds.has(annotationId)) {
@@ -182,27 +147,26 @@ export function BrowseView({
182
147
  });
183
148
  }
184
149
 
185
- // Cleanup
150
+ container.addEventListener('click', handleClick);
151
+ container.addEventListener('mouseover', handleMouseOver);
152
+ container.addEventListener('mouseout', handleMouseOut);
153
+
186
154
  return () => {
187
- clickHandlers.forEach(({ element, handler }) => {
188
- element.removeEventListener('click', handler);
189
- });
190
- hoverHandlers.forEach(({ element, enterHandler, leaveHandler }) => {
191
- element.removeEventListener('mouseenter', enterHandler);
192
- element.removeEventListener('mouseleave', leaveHandler);
193
- });
155
+ container.removeEventListener('click', handleClick);
156
+ container.removeEventListener('mouseover', handleMouseOver);
157
+ container.removeEventListener('mouseout', handleMouseOut);
194
158
  };
195
- }, [content, allAnnotations, onAnnotationClick, annotationMap, newAnnotationIds, handleAnnotationHover]);
159
+ }, [content, allAnnotations, newAnnotationIds]);
196
160
 
197
- // Handle hoveredAnnotationId - scroll and pulse
198
- useEffect(() => {
199
- if (!containerRef.current || !hoveredAnnotationId) return undefined;
161
+ // Helper to scroll annotation into view with pulse effect
162
+ const scrollToAnnotation = useCallback((annotationId: string | null, removePulse = false) => {
163
+ if (!containerRef.current || !annotationId) return;
200
164
 
201
165
  const element = containerRef.current.querySelector(
202
- `[data-annotation-id="${CSS.escape(hoveredAnnotationId)}"]`
166
+ `[data-annotation-id="${CSS.escape(annotationId)}"]`
203
167
  ) as HTMLElement;
204
168
 
205
- if (!element) return undefined;
169
+ if (!element) return;
206
170
 
207
171
  // Find the scroll container
208
172
  const scrollContainer = element.closest('.semiont-browse-view__content') as HTMLElement;
@@ -228,59 +192,28 @@ export function BrowseView({
228
192
  }
229
193
 
230
194
  // Add pulse effect
231
- const timeoutId = setTimeout(() => {
232
- element.classList.add('annotation-pulse');
233
- }, 100);
234
-
235
- return () => {
236
- clearTimeout(timeoutId);
237
- element.classList.remove('annotation-pulse');
238
- };
239
- }, [hoveredAnnotationId]);
240
-
241
- // Handle hoveredCommentId - scroll and pulse
242
- useEffect(() => {
243
- if (!containerRef.current || !hoveredCommentId) return undefined;
244
-
245
- const element = containerRef.current.querySelector(
246
- `[data-annotation-id="${CSS.escape(hoveredCommentId)}"]`
247
- ) as HTMLElement;
248
-
249
- if (!element) return undefined;
250
-
251
- // Find the scroll container
252
- const scrollContainer = element.closest('.semiont-browse-view__content') as HTMLElement;
253
-
254
- if (scrollContainer) {
255
- // Check visibility within the scroll container
256
- const elementRect = element.getBoundingClientRect();
257
- const containerRect = scrollContainer.getBoundingClientRect();
258
-
259
- const isVisible =
260
- elementRect.top >= containerRect.top &&
261
- elementRect.bottom <= containerRect.bottom;
262
-
263
- if (!isVisible) {
264
- // Scroll using container.scrollTo to avoid scrolling ancestors
265
- const elementTop = element.offsetTop;
266
- const containerHeight = scrollContainer.clientHeight;
267
- const elementHeight = element.offsetHeight;
268
- const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
269
-
270
- scrollContainer.scrollTo({ top: scrollTo, behavior: 'smooth' });
271
- }
195
+ element.classList.add('annotation-pulse');
196
+ if (removePulse) {
197
+ setTimeout(() => {
198
+ element.classList.remove('annotation-pulse');
199
+ }, 2000);
272
200
  }
201
+ }, []);
273
202
 
274
- // Add pulse effect
275
- const timeoutId = setTimeout(() => {
276
- element.classList.add('annotation-pulse');
277
- }, 100);
203
+ // Handle hover events for scrolling
204
+ // Event handlers (extracted to avoid inline arrow functions)
205
+ const handleAnnotationHover = useCallback(({ annotationId }: { annotationId: string | null }) => {
206
+ scrollToAnnotation(annotationId);
207
+ }, [scrollToAnnotation]);
278
208
 
279
- return () => {
280
- clearTimeout(timeoutId);
281
- element.classList.remove('annotation-pulse');
282
- };
283
- }, [hoveredCommentId]);
209
+ const handleAnnotationFocus = useCallback(({ annotationId }: { annotationId: string | null }) => {
210
+ scrollToAnnotation(annotationId, true);
211
+ }, [scrollToAnnotation]);
212
+
213
+ useEventSubscriptions({
214
+ 'annotation:hover': handleAnnotationHover,
215
+ 'annotation:focus': handleAnnotationFocus,
216
+ });
284
217
 
285
218
  // Route to appropriate viewer based on MIME type category
286
219
  switch (category) {
@@ -290,13 +223,10 @@ export function BrowseView({
290
223
  <AnnotateToolbar
291
224
  selectedMotivation={null}
292
225
  selectedClick={selectedClick}
293
- onSelectionChange={() => {}}
294
- onClickChange={onClickChange || (() => {})}
295
226
  showSelectionGroup={false}
296
227
  showDeleteButton={false}
297
228
  annotateMode={annotateMode}
298
- onAnnotateModeToggle={onAnnotateModeToggle}
299
- annotators={annotators}
229
+ annotators={ANNOTATORS}
300
230
  />
301
231
  <div ref={containerRef} className="semiont-browse-view__content">
302
232
  <ReactMarkdown
@@ -322,13 +252,10 @@ export function BrowseView({
322
252
  <AnnotateToolbar
323
253
  selectedMotivation={null}
324
254
  selectedClick={selectedClick}
325
- onSelectionChange={() => {}}
326
- onClickChange={onClickChange || (() => {})}
327
255
  showSelectionGroup={false}
328
256
  showDeleteButton={false}
329
257
  annotateMode={annotateMode}
330
- onAnnotateModeToggle={onAnnotateModeToggle}
331
- annotators={annotators}
258
+ annotators={ANNOTATORS}
332
259
  />
333
260
  <div ref={containerRef} className="semiont-browse-view__content">
334
261
  <Suspense fallback={<div className="semiont-browse-view__loading">Loading PDF viewer...</div>}>
@@ -337,10 +264,6 @@ export function BrowseView({
337
264
  existingAnnotations={allAnnotations}
338
265
  drawingMode={null}
339
266
  selectedMotivation={null}
340
- onAnnotationCreate={() => {}}
341
- {...(onAnnotationClick && { onAnnotationClick })}
342
- {...(onAnnotationHover && { onAnnotationHover })}
343
- hoveredAnnotationId={hoveredCommentId || hoveredAnnotationId || null}
344
267
  />
345
268
  </Suspense>
346
269
  </div>
@@ -354,13 +277,10 @@ export function BrowseView({
354
277
  <AnnotateToolbar
355
278
  selectedMotivation={null}
356
279
  selectedClick={selectedClick}
357
- onSelectionChange={() => {}}
358
- onClickChange={onClickChange || (() => {})}
359
280
  showSelectionGroup={false}
360
281
  showDeleteButton={false}
361
282
  annotateMode={annotateMode}
362
- onAnnotateModeToggle={onAnnotateModeToggle}
363
- annotators={annotators}
283
+ annotators={ANNOTATORS}
364
284
  />
365
285
  <div ref={containerRef} className="semiont-browse-view__content">
366
286
  <ImageViewer
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useRef, useCallback } from 'react';
3
+ import React, { useRef, useCallback, useEffect } from 'react';
4
4
  import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
5
5
  import type { StoredEvent, ResourceEventType } from '@semiont/core';
6
6
  import { getAnnotationUriFromEvent } from '@semiont/core';
@@ -46,9 +46,15 @@ export function HistoryEvent({
46
46
  const entityTypes = getEventEntityTypes(event);
47
47
  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
48
48
 
49
+ // Store callback in ref to avoid including in dependency arrays
50
+ const onEventHoverRef = useRef(onEventHover);
51
+ useEffect(() => {
52
+ onEventHoverRef.current = onEventHover;
53
+ });
54
+
49
55
  // Handle hover on emoji icon with 300ms delay
50
56
  const handleEmojiMouseEnter = useCallback(() => {
51
- if (!annotationUri || !onEventHover) return;
57
+ if (!annotationUri || !onEventHoverRef.current) return;
52
58
 
53
59
  // Clear any existing timeout
54
60
  if (hoverTimeoutRef.current) {
@@ -57,9 +63,9 @@ export function HistoryEvent({
57
63
 
58
64
  // Set new timeout for 300ms delay
59
65
  hoverTimeoutRef.current = setTimeout(() => {
60
- onEventHover(annotationUri);
66
+ onEventHoverRef.current?.(annotationUri);
61
67
  }, 300);
62
- }, [annotationUri, onEventHover]);
68
+ }, [annotationUri]);
63
69
 
64
70
  const handleEmojiMouseLeave = useCallback(() => {
65
71
  // Clear the timeout if mouse leaves before 500ms
@@ -69,10 +75,10 @@ export function HistoryEvent({
69
75
  }
70
76
 
71
77
  // Clear the hover state
72
- if (onEventHover) {
73
- onEventHover(null);
78
+ if (onEventHoverRef.current) {
79
+ onEventHoverRef.current(null);
74
80
  }
75
- }, [onEventHover]);
81
+ }, []);
76
82
 
77
83
  // Interactive events should be buttons for keyboard accessibility
78
84
  const EventWrapper = annotationUri ? 'button' : 'div';