@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,30 +1,23 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useRef } from 'react';
4
- // useRouter removed - using window.location for navigation
5
4
  import { useTranslations } from '../../contexts/TranslationContext';
6
5
  import { AnnotateView, type SelectionMotivation, type ClickAction, type ShapeType } from './AnnotateView';
7
6
  import { BrowseView } from './BrowseView';
8
7
  import { PopupContainer } from '../annotation-popups/SharedPopupElements';
9
8
  import { JsonLdView } from '../annotation-popups/JsonLdView';
10
- import type { components, Selector } from '@semiont/api-client';
9
+ import type { components } from '@semiont/api-client';
11
10
  import { getExactText, getTargetSelector, resourceUri, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
12
- import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
13
- import { useMakeMeaningEvents } from '../../contexts/MakeMeaningEventBusContext';
11
+ import { useEventBus } from '../../contexts/EventBusContext';
12
+ import { useEventSubscriptions } from '../../contexts/useEventSubscription';
14
13
  import { useCacheManager } from '../../contexts/CacheContext';
15
- import type { Annotator } from '../../lib/annotation-registry';
14
+ import { useObservableExternalNavigation } from '../../hooks/useObservableNavigation';
15
+ import { ANNOTATORS } from '../../lib/annotation-registry';
16
16
  import type { AnnotationsCollection } from '../../types/annotation-props';
17
17
  import { getSelectorType, getSelectedShapeForSelectorType, saveSelectedShapeForSelectorType } from '../../lib/media-shapes';
18
18
 
19
19
  type Annotation = components['schemas']['Annotation'];
20
20
  type SemiontResource = components['schemas']['ResourceDescriptor'];
21
- type Motivation = components['schemas']['Motivation'];
22
-
23
- // Unified pending annotation type - all human-created annotations flow through this
24
- interface PendingAnnotation {
25
- selector: Selector | Selector[];
26
- motivation: Motivation;
27
- }
28
21
 
29
22
  /**
30
23
  * ResourceViewer - Display and interact with resource content and annotations
@@ -49,23 +42,37 @@ interface Props {
49
42
  annotations: AnnotationsCollection;
50
43
  generatingReferenceId?: string | null;
51
44
  showLineNumbers?: boolean;
52
- onAnnotationRequested?: (pending: PendingAnnotation) => void;
53
- annotators: Record<string, Annotator>;
45
+ hoveredAnnotationId?: string | null;
54
46
  }
55
47
 
48
+ /**
49
+ * @emits annotation:delete - User requested to delete annotation. Payload: { annotationId: string }
50
+ * @emits panel:open - Request to open panel with annotation. Payload: { panel: string, scrollToAnnotationId?: string, motivation?: Motivation }
51
+ *
52
+ * @subscribes view:mode-toggled - Toggles between browse and annotate mode. Payload: { mode: 'browse' | 'annotate' }
53
+ * @subscribes annotation:added - New annotation was added. Payload: { annotation: Annotation }
54
+ * @subscribes annotation:removed - Annotation was removed. Payload: { annotationId: string }
55
+ * @subscribes annotation:updated - Annotation was updated. Payload: { annotation: Annotation }
56
+ * @subscribes toolbar:selection-changed - Text selection tool changed. Payload: { selection: boolean }
57
+ * @subscribes toolbar:click-changed - Click annotation tool changed. Payload: { click: 'detail' | 'scroll' | null }
58
+ * @subscribes toolbar:shape-changed - Drawing shape changed. Payload: { shape: string }
59
+ * @subscribes annotation:click - User clicked on annotation. Payload: { annotationId: string }
60
+ */
56
61
  export function ResourceViewer({
57
62
  resource,
58
63
  annotations,
59
64
  generatingReferenceId,
60
65
  showLineNumbers = false,
61
- onAnnotationRequested,
62
- annotators
66
+ hoveredAnnotationId: hoveredAnnotationIdProp
63
67
  }: Props) {
64
68
  const t = useTranslations('ResourceViewer');
65
69
  const documentViewerRef = useRef<HTMLDivElement>(null);
66
70
 
67
71
  // Get unified event bus for emitting UI events
68
- const eventBus = useMakeMeaningEvents();
72
+ const eventBus = useEventBus();
73
+
74
+ // Get observable navigation for event-driven routing
75
+ const navigate = useObservableExternalNavigation();
69
76
 
70
77
  const { highlights, references, assessments, comments, tags } = annotations;
71
78
 
@@ -102,50 +109,35 @@ export function ResourceViewer({
102
109
  }
103
110
  }, [annotateMode]);
104
111
 
105
- // Toggle handler
106
- const toggleAnnotateMode = useCallback(() => {
112
+ // Event handlers (extracted to avoid inline arrow functions)
113
+ const handleViewModeToggle = useCallback(() => {
107
114
  setAnnotateMode(prev => !prev);
108
115
  }, []);
109
116
 
110
117
  // Determine active view based on annotate mode
111
118
  const activeView = annotateMode ? 'annotate' : 'browse';
112
- const {
113
- deleteAnnotation,
114
- createAnnotation
115
- } = useResourceAnnotations();
116
119
 
117
120
  // Event-based cache invalidation - subscribe to make-meaning events
118
121
  // This replaces manual onRefetchAnnotations calls with automatic updates
119
122
  const cacheManager = useCacheManager();
120
123
 
121
- useEffect(() => {
122
- if (!eventBus || !cacheManager) return;
123
-
124
- // Annotation events - invalidate cache when annotations change
125
- const handleAnnotationAdded = () => {
124
+ const handleAnnotationAdded = useCallback(() => {
125
+ if (cacheManager) {
126
126
  cacheManager.invalidateAnnotations(rUri);
127
- };
127
+ }
128
+ }, [cacheManager, rUri]);
128
129
 
129
- const handleAnnotationRemoved = () => {
130
+ const handleAnnotationRemoved = useCallback(() => {
131
+ if (cacheManager) {
130
132
  cacheManager.invalidateAnnotations(rUri);
131
- };
133
+ }
134
+ }, [cacheManager, rUri]);
132
135
 
133
- const handleAnnotationUpdated = () => {
136
+ const handleAnnotationUpdated = useCallback(() => {
137
+ if (cacheManager) {
134
138
  cacheManager.invalidateAnnotations(rUri);
135
- };
136
-
137
- // Subscribe to make-meaning annotation events
138
- eventBus.on('annotation:added', handleAnnotationAdded);
139
- eventBus.on('annotation:removed', handleAnnotationRemoved);
140
- eventBus.on('annotation:updated', handleAnnotationUpdated);
141
-
142
- // Cleanup subscriptions
143
- return () => {
144
- eventBus.off('annotation:added', handleAnnotationAdded);
145
- eventBus.off('annotation:removed', handleAnnotationRemoved);
146
- eventBus.off('annotation:updated', handleAnnotationUpdated);
147
- };
148
- }, [eventBus, cacheManager, rUri]);
139
+ }
140
+ }, [cacheManager, rUri]);
149
141
 
150
142
  // Annotation toolbar state - persisted in localStorage
151
143
  const [selectedMotivation, setSelectedMotivation] = useState<SelectionMotivation | null>(() => {
@@ -177,6 +169,19 @@ export function ResourceViewer({
177
169
  return getSelectedShapeForSelectorType(selectorType);
178
170
  });
179
171
 
172
+ // Toolbar event handlers (extracted to avoid inline arrow functions)
173
+ const handleToolbarSelectionChanged = useCallback(({ motivation }: { motivation: string | null }) => {
174
+ setSelectedMotivation(motivation as SelectionMotivation | null);
175
+ }, []);
176
+
177
+ const handleToolbarClickChanged = useCallback(({ action }: { action: string }) => {
178
+ setSelectedClick(action as ClickAction);
179
+ }, []);
180
+
181
+ const handleToolbarShapeChanged = useCallback(({ shape }: { shape: string }) => {
182
+ setSelectedShape(shape as ShapeType);
183
+ }, []);
184
+
180
185
  // Persist toolbar state to localStorage
181
186
  useEffect(() => {
182
187
  if (selectedMotivation === null) {
@@ -214,7 +219,8 @@ export function ResourceViewer({
214
219
  } | null>(null);
215
220
 
216
221
  // Internal UI state for hover, focus, and scroll
217
- const [hoveredAnnotationId, _setHoveredAnnotationId] = useState<string | null>(null);
222
+ // Use prop value when provided (controlled by parent), otherwise null
223
+ const hoveredAnnotationId = hoveredAnnotationIdProp ?? null;
218
224
  const [hoveredCommentId, _setHoveredCommentId] = useState<string | null>(null);
219
225
  const [scrollToAnnotationId, setScrollToAnnotationId] = useState<string | null>(null);
220
226
  const [_focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
@@ -241,19 +247,14 @@ export function ResourceViewer({
241
247
  };
242
248
  };
243
249
 
244
- // Handle deleting annotations - memoized
245
- const handleDeleteAnnotation = useCallback(async (id: string) => {
246
- try {
247
- await deleteAnnotation(id, rUri);
248
- // Cache invalidation now handled by annotation:removed event
249
- } catch (err) {
250
- console.error('Failed to delete annotation:', err);
251
- }
252
- }, [deleteAnnotation, rUri]);
250
+ // Handle deleting annotations - emit event instead of direct call
251
+ const handleDeleteAnnotation = useCallback((id: string) => {
252
+ eventBus.emit('annotation:delete', { annotationId: id });
253
+ }, []); // eventBus is stable
253
254
 
254
255
  // Handle annotation clicks - memoized
255
256
  const handleAnnotationClick = useCallback((annotation: Annotation, event?: React.MouseEvent) => {
256
- const metadata = Object.values(annotators).find(a => a.matchesAnnotation(annotation));
257
+ const metadata = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(annotation));
257
258
 
258
259
  // If annotation has a side panel, only open it when Detail mode is active
259
260
  // For delete/jsonld/follow modes, let those handlers below process it
@@ -276,10 +277,10 @@ export function ResourceViewer({
276
277
  if (selectedClick === 'follow' && isReference(annotation)) {
277
278
  const bodySource = getBodySource(annotation.body);
278
279
  if (bodySource) {
279
- // Navigate to the linked resource
280
+ // Navigate to the linked resource - emits 'navigation:external-navigate' event
280
281
  const resourceId = bodySource.split('/resources/')[1];
281
282
  if (resourceId) {
282
- window.location.href = `/know/resource/${resourceId}`;
283
+ navigate(`/know/resource/${resourceId}`, { resourceId });
283
284
  }
284
285
  }
285
286
  return;
@@ -305,202 +306,65 @@ export function ResourceViewer({
305
306
  setDeleteConfirmation({ annotation, position });
306
307
  return;
307
308
  }
308
- }, [annotateMode, selectedClick, handleDeleteAnnotation, annotators, focusAnnotation]);
309
-
310
- // Unified annotation creation handler - works for both text and images
311
- const handleAnnotationCreate = useCallback(async (params: import('../../types/annotation-props').UICreateAnnotationParams) => {
312
- const { motivation, selector } = params;
313
-
314
- try {
315
- switch (motivation) {
316
- case 'highlighting':
317
- case 'assessing':
318
- // Create highlight/assessment immediately using generic createAnnotation
319
- if (selector.type === 'TextQuoteSelector' && selector.exact) {
320
- // Build selectors array for text annotation
321
- const selectors: any[] = [
322
- {
323
- type: 'TextQuoteSelector',
324
- exact: selector.exact,
325
- ...(selector.prefix && { prefix: selector.prefix }),
326
- ...(selector.suffix && { suffix: selector.suffix })
327
- },
328
- {
329
- type: 'TextPositionSelector',
330
- start: selector.start || 0,
331
- end: selector.end || 0
332
- }
333
- ];
334
-
335
- const annotation = await createAnnotation(
336
- rUri,
337
- motivation,
338
- selectors,
339
- []
340
- );
341
-
342
- // Focus the new annotation to trigger panel tab switch
343
- if (annotation) {
344
- focusAnnotation(annotation.id);
345
- }
346
- // Cache invalidation now handled by annotation:added event
347
- } else if (selector.type === 'SvgSelector' && selector.value) {
348
- // Image annotations use generic createAnnotation
349
- await createAnnotation(
350
- rUri,
351
- motivation,
352
- { type: 'SvgSelector', value: selector.value },
353
- []
354
- );
355
- // Cache invalidation now handled by annotation:added event
356
- } else if (selector.type === 'FragmentSelector' && selector.value) {
357
- // PDF annotations use FragmentSelector
358
- await createAnnotation(
359
- rUri,
360
- motivation,
361
- {
362
- type: 'FragmentSelector',
363
- value: selector.value,
364
- ...(selector.conformsTo && { conformsTo: selector.conformsTo })
365
- },
366
- []
367
- );
368
- // Cache invalidation now handled by annotation:added event
369
- }
370
- break;
371
-
372
- case 'commenting':
373
- if (selector.type === 'TextQuoteSelector' && selector.exact) {
374
- // Text: emit UI event for comment creation
375
- eventBus.emit('ui:selection:comment-requested', {
376
- exact: selector.exact,
377
- start: selector.start || 0,
378
- end: selector.end || 0
379
- });
380
- } else if (selector.type === 'SvgSelector' && selector.value) {
381
- // Image: create annotation, then open panel
382
- const annotation = await createAnnotation(
383
- rUri,
384
- motivation,
385
- { type: 'SvgSelector', value: selector.value },
386
- []
387
- );
388
- if (annotation) {
389
- focusAnnotation(annotation.id);
390
- }
391
- // Cache invalidation now handled by annotation:added event
392
- } else if (selector.type === 'FragmentSelector' && selector.value) {
393
- // PDF: create annotation, then open panel
394
- const annotation = await createAnnotation(
395
- rUri,
396
- motivation,
397
- {
398
- type: 'FragmentSelector',
399
- value: selector.value,
400
- ...(selector.conformsTo && { conformsTo: selector.conformsTo })
401
- },
402
- []
403
- );
404
- if (annotation) {
405
- focusAnnotation(annotation.id);
406
- }
407
- // Cache invalidation now handled by annotation:added event
408
- }
409
- break;
410
-
411
- case 'tagging':
412
- if (selector.type === 'TextQuoteSelector' && selector.exact) {
413
- // Text: emit UI event for tag creation
414
- eventBus.emit('ui:selection:tag-requested', {
415
- exact: selector.exact,
416
- start: selector.start || 0,
417
- end: selector.end || 0
418
- });
419
- } else if (selector.type === 'SvgSelector' && selector.value) {
420
- // Image: create annotation, then open panel
421
- const annotation = await createAnnotation(
422
- rUri,
423
- motivation,
424
- { type: 'SvgSelector', value: selector.value },
425
- []
426
- );
427
- if (annotation) {
428
- focusAnnotation(annotation.id);
429
- }
430
- // Cache invalidation now handled by annotation:added event
431
- } else if (selector.type === 'FragmentSelector' && selector.value) {
432
- // PDF: create annotation, then open panel
433
- const annotation = await createAnnotation(
434
- rUri,
435
- motivation,
436
- {
437
- type: 'FragmentSelector',
438
- value: selector.value,
439
- ...(selector.conformsTo && { conformsTo: selector.conformsTo })
440
- },
441
- []
442
- );
443
- if (annotation) {
444
- focusAnnotation(annotation.id);
445
- }
446
- // Cache invalidation now handled by annotation:added event
447
- }
448
- break;
449
-
450
- case 'linking':
451
- // Emit UI event for reference creation (text, image, or PDF selections)
452
- if (selector.type === 'TextQuoteSelector' && selector.exact) {
453
- eventBus.emit('ui:selection:reference-requested', {
454
- exact: selector.exact,
455
- start: selector.start || 0,
456
- end: selector.end || 0,
457
- ...(selector.prefix && { prefix: selector.prefix }),
458
- ...(selector.suffix && { suffix: selector.suffix })
459
- });
460
- } else if (selector.type === 'SvgSelector' && selector.value) {
461
- eventBus.emit('ui:selection:reference-requested', {
462
- exact: '', // Images don't have exact text
463
- start: 0,
464
- end: 0,
465
- svgSelector: selector.value
466
- });
467
- } else if (selector.type === 'FragmentSelector' && selector.value) {
468
- eventBus.emit('ui:selection:reference-requested', {
469
- exact: '', // PDFs don't have exact text
470
- start: 0,
471
- end: 0,
472
- fragmentSelector: selector.value,
473
- ...(selector.conformsTo && { conformsTo: selector.conformsTo })
474
- });
475
- }
476
- break;
309
+ }, [annotateMode, selectedClick, focusAnnotation]);
310
+
311
+ // Annotation click coordinator - handles panel opening and scrolling
312
+ const handleAnnotationClickEvent = useCallback(({ annotationId, motivation }: {
313
+ annotationId: string;
314
+ motivation: components['schemas']['Motivation'];
315
+ }) => {
316
+ // Find the annotation metadata
317
+ const metadata = Object.values(ANNOTATORS).find(a => a.matchesAnnotation({ motivation } as Annotation));
318
+
319
+ if (!metadata?.hasSidePanel) {
320
+ // Annotation doesn't have a side panel - let handleAnnotationClick handle it
321
+ const allAnnotations = [...highlights, ...references, ...assessments, ...comments, ...tags];
322
+ const annotation = allAnnotations.find(a => a.id === annotationId);
323
+ if (annotation) {
324
+ handleAnnotationClick(annotation);
325
+ }
326
+ return;
327
+ }
328
+
329
+ if (selectedClick !== 'detail') {
330
+ // Only open panels in detail mode - for other modes, let handleAnnotationClick handle it
331
+ const allAnnotations = [...highlights, ...references, ...assessments, ...comments, ...tags];
332
+ const annotation = allAnnotations.find(a => a.id === annotationId);
333
+ if (annotation) {
334
+ handleAnnotationClick(annotation);
477
335
  }
478
- } catch (err) {
479
- console.error('Failed to create annotation:', err);
336
+ return;
480
337
  }
481
- }, [rUri, createAnnotation]);
482
338
 
483
- // Quick action: Delete annotation from widget
484
- const handleDeleteAnnotationWidget = useCallback(async (annotation: Annotation) => {
485
- await handleDeleteAnnotation(annotation.id);
486
- }, [handleDeleteAnnotation]);
339
+ // All annotations open the unified annotations panel
340
+ // The panel internally switches tabs based on the motivation → tab mapping in UnifiedAnnotationsPanel
341
+ eventBus.emit('panel:open', { panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
342
+ }, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick]);
343
+
344
+ // Event subscriptions - Combined into single useEventSubscriptions call to prevent hook ordering issues
345
+ // IMPORTANT: All event subscriptions MUST be in a single call to maintain consistent hook order between renders
346
+ useEventSubscriptions({
347
+ // View mode
348
+ 'view:mode-toggled': handleViewModeToggle,
349
+
350
+ // Annotation cache invalidation
351
+ 'annotation:added': handleAnnotationAdded,
352
+ 'annotation:removed': handleAnnotationRemoved,
353
+ 'annotation:updated': handleAnnotationUpdated,
354
+
355
+ // Toolbar state
356
+ 'toolbar:selection-changed': handleToolbarSelectionChanged,
357
+ 'toolbar:click-changed': handleToolbarClickChanged,
358
+ 'toolbar:shape-changed': handleToolbarShapeChanged,
359
+
360
+ // Annotation clicks
361
+ 'annotation:click': handleAnnotationClickEvent,
362
+ });
487
363
 
488
364
  // Prepare props for child components
489
365
  // Note: These objects are created inline - React's reconciliation handles re-renders efficiently
490
366
  const annotationsCollection = { highlights, references, assessments, comments, tags };
491
367
 
492
- const handlersForAnnotate = {
493
- onClick: handleAnnotationClick
494
- // Note: onHover/onCommentHover removed - component now manages hover state internally
495
- };
496
-
497
- const handlersForBrowse = {
498
- onClick: handleAnnotationClick
499
- // Note: onCommentHover removed - component now manages hover state internally
500
- };
501
-
502
- const creationHandler = { onCreate: handleAnnotationCreate };
503
-
504
368
  const uiState = {
505
369
  selectedMotivation,
506
370
  selectedClick,
@@ -509,6 +373,13 @@ export function ResourceViewer({
509
373
  scrollToAnnotationId
510
374
  };
511
375
 
376
+ // Define getTargetDocumentName callback OUTSIDE the conditional
377
+ // IMPORTANT: This must be defined before the return statement to avoid hook ordering violations
378
+ const getTargetDocumentName = useCallback((documentId: string) => {
379
+ const referencedResource = references.find((a: Annotation) => getBodySource(a.body) === documentId);
380
+ return referencedResource ? getExactText(getTargetSelector(referencedResource.target)) : undefined;
381
+ }, [references]);
382
+
512
383
  return (
513
384
  <div ref={documentViewerRef} className="semiont-resource-viewer">
514
385
  {/* Content */}
@@ -518,8 +389,6 @@ export function ResourceViewer({
518
389
  mimeType={mimeType}
519
390
  resourceUri={resource['@id']}
520
391
  annotations={annotationsCollection}
521
- handlers={handlersForAnnotate}
522
- creationHandler={creationHandler}
523
392
  uiState={uiState}
524
393
  onUIStateChange={(updates) => {
525
394
  if ('selectedMotivation' in updates) setSelectedMotivation(updates.selectedMotivation!);
@@ -527,17 +396,10 @@ export function ResourceViewer({
527
396
  if ('selectedShape' in updates) setSelectedShape(updates.selectedShape!);
528
397
  }}
529
398
  enableWidgets={true}
530
- onEntityTypeClick={(entityType) => {
531
- window.location.href = `/know?entityType=${encodeURIComponent(entityType)}`;
532
- }}
533
- onUnresolvedReferenceClick={handleAnnotationClick}
399
+ getTargetDocumentName={getTargetDocumentName}
534
400
  {...(generatingReferenceId !== undefined && { generatingReferenceId })}
535
- onDeleteAnnotation={handleDeleteAnnotationWidget}
536
401
  showLineNumbers={showLineNumbers}
537
402
  annotateMode={annotateMode}
538
- onAnnotateModeToggle={toggleAnnotateMode}
539
- {...(onAnnotationRequested && { onAnnotationRequested })}
540
- annotators={annotators}
541
403
  />
542
404
  ) : (
543
405
  <BrowseView
@@ -545,13 +407,9 @@ export function ResourceViewer({
545
407
  mimeType={mimeType}
546
408
  resourceUri={resource['@id']}
547
409
  annotations={annotationsCollection}
548
- handlers={handlersForBrowse}
549
410
  hoveredCommentId={hoveredCommentId}
550
411
  selectedClick={selectedClick}
551
- onClickChange={setSelectedClick}
552
412
  annotateMode={annotateMode}
553
- onAnnotateModeToggle={toggleAnnotateMode}
554
- annotators={annotators}
555
413
  />
556
414
  )}
557
415
 
@@ -576,7 +434,7 @@ export function ResourceViewer({
576
434
  {/* Delete Confirmation Modal */}
577
435
  {deleteConfirmation && (() => {
578
436
  const annotation = deleteConfirmation.annotation;
579
- const metadata = Object.values(annotators).find(a => a.matchesAnnotation(annotation));
437
+ const metadata = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(annotation));
580
438
  const targetSelector = getTargetSelector(annotation.target);
581
439
  const selectedText = getExactText(targetSelector);
582
440
  const motivationEmoji = metadata?.iconEmoji || '📝';
@@ -614,8 +472,8 @@ export function ResourceViewer({
614
472
  {t('deleteConfirmationCancel')}
615
473
  </button>
616
474
  <button
617
- onClick={async () => {
618
- await handleDeleteAnnotation(deleteConfirmation.annotation.id);
475
+ onClick={() => {
476
+ handleDeleteAnnotation(deleteConfirmation.annotation.id);
619
477
  setDeleteConfirmation(null);
620
478
  }}
621
479
  className="semiont-button semiont-button--danger"