@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,12 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef } from 'react';
3
+ import { forwardRef } from 'react';
4
4
  import type { RouteBuilder } from '../../../contexts/RoutingContext';
5
5
  import { useTranslations } from '../../../contexts/TranslationContext';
6
6
  import type { components } from '@semiont/api-client';
7
7
  import { getAnnotationExactText, isBodyResolved, getBodySource, getFragmentSelector, getSvgSelector, getTargetSelector } from '@semiont/api-client';
8
8
  import { getEntityTypes } from '@semiont/ontology';
9
9
  import { getResourceIcon } from '../../../lib/resource-utils';
10
+ import { useEventBus } from '../../../contexts/EventBusContext';
11
+ import { useObservableExternalNavigation } from '../../../hooks/useObservableNavigation';
10
12
 
11
13
  type Annotation = components['schemas']['Annotation'];
12
14
 
@@ -19,68 +21,27 @@ interface EnrichedAnnotation extends Annotation {
19
21
  interface ReferenceEntryProps {
20
22
  reference: Annotation;
21
23
  isFocused: boolean;
22
- onClick: () => void;
24
+ isHovered?: boolean;
23
25
  routes: RouteBuilder;
24
- onReferenceRef: (referenceId: string, el: HTMLElement | null) => void;
25
- onReferenceHover?: (referenceId: string | null) => void;
26
- onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
27
- onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
28
- onSearchDocuments?: (referenceId: string, searchTerm: string) => void;
29
- onUpdateReference?: (referenceId: string, updates: Partial<Annotation>) => void;
30
26
  annotateMode?: boolean;
31
27
  isGenerating?: boolean;
32
28
  }
33
29
 
34
- export function ReferenceEntry({
35
- reference,
36
- isFocused,
37
- onClick,
38
- routes,
39
- onReferenceRef,
40
- onReferenceHover,
41
- onGenerateDocument,
42
- onCreateDocument,
43
- onSearchDocuments,
44
- onUpdateReference,
45
- annotateMode = true,
46
- isGenerating = false,
47
- }: ReferenceEntryProps) {
30
+ export const ReferenceEntry = forwardRef<HTMLDivElement, ReferenceEntryProps>(
31
+ function ReferenceEntry(
32
+ {
33
+ reference,
34
+ isFocused,
35
+ isHovered = false,
36
+ routes,
37
+ annotateMode = true,
38
+ isGenerating = false,
39
+ },
40
+ ref
41
+ ) {
48
42
  const t = useTranslations('ReferencesPanel');
49
- const referenceRef = useRef<HTMLDivElement>(null);
50
-
51
- // Register ref with parent
52
- useEffect(() => {
53
- onReferenceRef(reference.id, referenceRef.current);
54
- return () => {
55
- onReferenceRef(reference.id, null);
56
- };
57
- }, [reference.id, onReferenceRef]);
58
-
59
- // Scroll to reference when focused - use container.scrollTo to avoid scrolling ancestors
60
- useEffect(() => {
61
- if (isFocused && referenceRef.current) {
62
- const element = referenceRef.current;
63
- const container = element.closest('.semiont-toolbar-panels__content') as HTMLElement;
64
-
65
- if (container) {
66
- const elementRect = element.getBoundingClientRect();
67
- const containerRect = container.getBoundingClientRect();
68
-
69
- const isVisible =
70
- elementRect.top >= containerRect.top &&
71
- elementRect.bottom <= containerRect.bottom;
72
-
73
- if (!isVisible) {
74
- const elementTop = element.offsetTop;
75
- const containerHeight = container.clientHeight;
76
- const elementHeight = element.offsetHeight;
77
- const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
78
-
79
- container.scrollTo({ top: scrollTo, behavior: 'smooth' });
80
- }
81
- }
82
- }
83
- }, [isFocused]);
43
+ const eventBus = useEventBus();
44
+ const navigate = useObservableExternalNavigation();
84
45
 
85
46
  const selectedText = getAnnotationExactText(reference) || '';
86
47
  const isResolved = isBodyResolved(reference.body);
@@ -103,44 +64,69 @@ export function ReferenceEntry({
103
64
  if (resolvedResourceUri) {
104
65
  const resourceId = resolvedResourceUri.split('/resources/')[1];
105
66
  if (resourceId) {
106
- window.location.href = routes.resourceDetail(resourceId);
67
+ // Use observable navigation - emits 'navigation:external-navigate' event
68
+ navigate(routes.resourceDetail(resourceId), { resourceId });
107
69
  }
108
70
  }
109
71
  };
110
72
 
111
73
  const handleComposeDocument = () => {
112
- if (onCreateDocument) {
113
- onCreateDocument(reference.id, selectedText, entityTypes);
114
- }
74
+ eventBus.emit('reference:create-manual', {
75
+ annotationUri: reference.id,
76
+ title: selectedText,
77
+ entityTypes,
78
+ });
115
79
  };
116
80
 
117
81
  const handleUnlink = () => {
118
- if (onUpdateReference) {
119
- onUpdateReference(reference.id, { body: [] });
82
+ // Unlinking removes all body items from the reference annotation
83
+ const sourceUri = typeof reference.target === 'object' && 'source' in reference.target
84
+ ? reference.target.source
85
+ : '';
86
+ if (sourceUri) {
87
+ eventBus.emit('annotation:update-body', {
88
+ annotationUri: reference.id,
89
+ resourceId: sourceUri.split('/resources/')[1] || '',
90
+ operations: [{ op: 'remove' }], // Remove all body items
91
+ });
120
92
  }
121
93
  };
122
94
 
123
95
  const handleGenerate = () => {
124
- if (onGenerateDocument) {
125
- onGenerateDocument(reference.id, { title: selectedText });
126
- }
96
+ const resourceUri = typeof reference.target === 'object' && 'source' in reference.target
97
+ ? reference.target.source
98
+ : '';
99
+
100
+ // Emit request to open generation modal
101
+ eventBus.emit('generation:modal-open', {
102
+ annotationUri: reference.id,
103
+ resourceUri,
104
+ defaultTitle: selectedText,
105
+ });
127
106
  };
128
107
 
129
108
  const handleSearch = () => {
130
- if (onSearchDocuments) {
131
- onSearchDocuments(reference.id, selectedText);
132
- }
109
+ eventBus.emit('reference:link', {
110
+ annotationUri: reference.id,
111
+ searchTerm: selectedText,
112
+ });
133
113
  };
134
114
 
135
115
  return (
136
116
  <div
137
- ref={referenceRef}
138
- className="semiont-annotation-entry"
117
+ ref={ref}
118
+ className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
139
119
  data-type="reference"
140
120
  data-focused={isFocused ? 'true' : 'false'}
141
- onClick={onClick}
142
- onMouseEnter={() => onReferenceHover?.(reference.id)}
143
- onMouseLeave={() => onReferenceHover?.(null)}
121
+ onClick={() => {
122
+ eventBus.emit('annotation:click', { annotationId: reference.id, motivation: reference.motivation });
123
+ }}
124
+ onMouseEnter={() => {
125
+ eventBus.emit('annotation:hover', { annotationId: reference.id });
126
+ }}
127
+ onMouseLeave={() => {
128
+ eventBus.emit('annotation:hover', { annotationId: null });
129
+ }}
144
130
  >
145
131
  {/* Status indicator and text quote */}
146
132
  <div className="semiont-annotation-entry__header">
@@ -235,4 +221,4 @@ export function ReferenceEntry({
235
221
  </div>
236
222
  </div>
237
223
  );
238
- }
224
+ });
@@ -1,13 +1,14 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useRef, useEffect } from 'react';
3
+ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useMakeMeaningEvents } from '../../../contexts/MakeMeaningEventBusContext';
5
+ import { useEventBus } from '../../../contexts/EventBusContext';
6
+ import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
6
7
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
7
8
  import { DetectionProgressWidget } from '../../DetectionProgressWidget';
8
9
  import { ReferenceEntry } from './ReferenceEntry';
9
10
  import type { components, paths, Selector } from '@semiont/api-client';
10
- import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
11
+ import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
11
12
  import { PanelHeader } from './PanelHeader';
12
13
  import './ReferencesPanel.css';
13
14
 
@@ -47,12 +48,6 @@ interface DetectionLog {
47
48
  interface Props {
48
49
  // Generic panel props
49
50
  annotations?: Annotation[];
50
- onAnnotationClick?: (annotation: Annotation) => void;
51
- focusedAnnotationId?: string | null;
52
- hoveredAnnotationId?: string | null;
53
- onAnnotationHover?: (annotationId: string | null) => void;
54
- onDetect?: (selectedTypes: string[], includeDescriptiveReferences?: boolean) => void;
55
- onCreate: (entityType?: string) => void;
56
51
  isDetecting: boolean;
57
52
  detectionProgress: any; // TODO: type this properly
58
53
  annotateMode?: boolean;
@@ -61,46 +56,48 @@ interface Props {
61
56
 
62
57
  // Reference-specific props
63
58
  allEntityTypes: string[];
64
- onCancelDetection: () => void;
65
- onSearchDocuments?: (referenceId: string, searchTerm: string) => void;
66
- onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
67
- onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
68
59
  generatingReferenceId?: string | null;
69
60
  referencedBy?: ReferencedBy[];
70
61
  referencedByLoading?: boolean;
71
62
  pendingAnnotation: PendingAnnotation | null;
63
+ scrollToAnnotationId?: string | null;
64
+ onScrollCompleted?: () => void;
65
+ hoveredAnnotationId?: string | null;
72
66
  }
73
67
 
68
+ /**
69
+ * Panel for managing reference annotations with entity type detection
70
+ *
71
+ * @emits detection:start - Start reference detection. Payload: { motivation: 'linking', options: { entityTypes: string[], includeDescriptiveReferences: boolean } }
72
+ * @emits annotation:create - Create new reference annotation. Payload: { motivation: 'linking', selector: Selector | Selector[], body: Body[] }
73
+ * @emits annotation:cancel-pending - Cancel pending reference annotation. Payload: undefined
74
+ * @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
75
+ */
74
76
  export function ReferencesPanel({
75
77
  annotations = [],
76
- onAnnotationClick,
77
- focusedAnnotationId,
78
- hoveredAnnotationId,
79
- onAnnotationHover,
80
- onDetect,
81
- onCreate,
82
78
  isDetecting,
83
79
  detectionProgress,
84
80
  annotateMode = true,
85
81
  Link,
86
82
  routes,
87
83
  allEntityTypes,
88
- onCancelDetection,
89
- onSearchDocuments,
90
- onGenerateDocument,
91
- onCreateDocument,
92
84
  generatingReferenceId,
93
85
  referencedBy = [],
94
86
  referencedByLoading = false,
95
87
  pendingAnnotation,
88
+ scrollToAnnotationId,
89
+ onScrollCompleted,
90
+ hoveredAnnotationId,
96
91
  }: Props) {
97
92
  const t = useTranslations('DetectPanel');
98
93
  const tRef = useTranslations('ReferencesPanel');
99
- const eventBus = useMakeMeaningEvents();
94
+ const eventBus = useEventBus();
100
95
  const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
101
96
  const [lastDetectionLog, setLastDetectionLog] = useState<DetectionLog[] | null>(null);
102
97
  const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
103
98
  const [includeDescriptiveReferences, setIncludeDescriptiveReferences] = useState(false);
99
+ const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
100
+ const containerRef = useRef<HTMLDivElement>(null);
104
101
 
105
102
  // Collapsible detection section state - load from localStorage, default expanded
106
103
  const [isDetectExpanded, setIsDetectExpanded] = useState(() => {
@@ -115,14 +112,108 @@ export function ReferencesPanel({
115
112
  localStorage.setItem('detect-section-expanded-reference', String(isDetectExpanded));
116
113
  }, [isDetectExpanded]);
117
114
 
118
- const { sortedAnnotations, containerRef, handleAnnotationRef } =
119
- useAnnotationPanel(annotations, hoveredAnnotationId);
115
+ // Direct ref management - replace useAnnotationPanel hook
116
+ const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
117
+
118
+ // Sort annotations by their position in the resource
119
+ const sortedAnnotations = useMemo(() => {
120
+ return [...annotations].sort((a, b) => {
121
+ const aSelector = getTextPositionSelector(getTargetSelector(a.target));
122
+ const bSelector = getTextPositionSelector(getTargetSelector(b.target));
123
+ if (!aSelector || !bSelector) return 0;
124
+ return aSelector.start - bSelector.start;
125
+ });
126
+ }, [annotations]);
127
+
128
+ // Ref callback for entry components
129
+ const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
130
+ if (element) {
131
+ entryRefs.current.set(id, element);
132
+ } else {
133
+ entryRefs.current.delete(id);
134
+ }
135
+ }, []);
136
+
137
+ // Handle scrollToAnnotationId (click scroll)
138
+ useEffect(() => {
139
+ if (!scrollToAnnotationId) return;
140
+
141
+ const element = entryRefs.current.get(scrollToAnnotationId);
142
+
143
+ if (element && containerRef.current) {
144
+ // Calculate scroll position to center element in container
145
+ const elementTop = element.offsetTop;
146
+ const containerHeight = containerRef.current.clientHeight;
147
+ const elementHeight = element.offsetHeight;
148
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
149
+
150
+ // Scroll to center
151
+ containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
152
+
153
+ // Add pulse effect
154
+ element.classList.remove('semiont-annotation-pulse');
155
+ void element.offsetWidth; // Force reflow
156
+ element.classList.add('semiont-annotation-pulse');
157
+
158
+ // Notify completion
159
+ if (onScrollCompleted) {
160
+ onScrollCompleted();
161
+ }
162
+ } else {
163
+ console.warn('[ReferencesPanel] Element not found for scrollToAnnotationId:', scrollToAnnotationId);
164
+ }
165
+ }, [scrollToAnnotationId]);
166
+
167
+ // Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
168
+ useEffect(() => {
169
+ if (!hoveredAnnotationId) return;
170
+
171
+ const element = entryRefs.current.get(hoveredAnnotationId);
172
+
173
+ if (!element || !containerRef.current) return;
174
+
175
+ const container = containerRef.current;
176
+ const elementRect = element.getBoundingClientRect();
177
+ const containerRect = container.getBoundingClientRect();
178
+
179
+ // Only scroll if element is not fully visible
180
+ const isVisible =
181
+ elementRect.top >= containerRect.top &&
182
+ elementRect.bottom <= containerRect.bottom;
183
+
184
+ if (!isVisible) {
185
+ const elementTop = element.offsetTop;
186
+ const containerHeight = container.clientHeight;
187
+ const elementHeight = element.offsetHeight;
188
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
189
+
190
+ container.scrollTo({ top: scrollTo, behavior: 'smooth' });
191
+ }
192
+
193
+ // Pulse effect is handled by isHovered prop on ReferenceEntry
194
+ }, [hoveredAnnotationId]);
195
+
196
+ // Subscribe to click events - update focused state
197
+ // Event handler for annotation clicks (extracted to avoid inline arrow function)
198
+ const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
199
+ setFocusedAnnotationId(annotationId);
200
+ setTimeout(() => setFocusedAnnotationId(null), 3000);
201
+ }, []);
202
+
203
+ useEventSubscriptions({
204
+ 'annotation:click': handleAnnotationClick,
205
+ });
120
206
 
121
207
  // Clear log when starting new detection
122
208
  const handleDetect = () => {
123
- if (!onDetect) return;
124
209
  setLastDetectionLog(null);
125
- onDetect(selectedEntityTypes, includeDescriptiveReferences);
210
+ eventBus.emit('detection:start', {
211
+ motivation: 'linking',
212
+ options: {
213
+ entityTypes: selectedEntityTypes,
214
+ includeDescriptiveReferences,
215
+ },
216
+ });
126
217
  };
127
218
 
128
219
  // Track whether we've already saved the log for the current detection run
@@ -157,9 +248,15 @@ export function ReferencesPanel({
157
248
  };
158
249
 
159
250
  const handleCreateReference = () => {
160
- const entityType = pendingEntityTypes.join(',') || undefined;
161
- onCreate(entityType);
162
- setPendingEntityTypes([]);
251
+ if (pendingAnnotation) {
252
+ const entityType = pendingEntityTypes.join(',') || undefined;
253
+ eventBus.emit('annotation:create', {
254
+ motivation: 'linking',
255
+ selector: pendingAnnotation.selector,
256
+ body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
257
+ });
258
+ setPendingEntityTypes([]);
259
+ }
163
260
  };
164
261
 
165
262
  // Escape key handler for cancelling pending annotation
@@ -168,14 +265,14 @@ export function ReferencesPanel({
168
265
 
169
266
  const handleEscape = (e: KeyboardEvent) => {
170
267
  if (e.key === 'Escape') {
171
- eventBus.emit('ui:annotation:cancel-pending');
268
+ eventBus.emit('annotation:cancel-pending', undefined);
172
269
  setPendingEntityTypes([]);
173
270
  }
174
271
  };
175
272
 
176
273
  document.addEventListener('keydown', handleEscape);
177
274
  return () => document.removeEventListener('keydown', handleEscape);
178
- }, [pendingAnnotation, eventBus]);
275
+ }, [pendingAnnotation]);
179
276
 
180
277
  return (
181
278
  <div className="semiont-panel">
@@ -220,7 +317,7 @@ export function ReferencesPanel({
220
317
  <div className="semiont-annotation-prompt__actions">
221
318
  <button
222
319
  onClick={() => {
223
- eventBus.emit('ui:annotation:cancel-pending');
320
+ eventBus.emit('annotation:cancel-pending', undefined);
224
321
  setPendingEntityTypes([]);
225
322
  }}
226
323
  className="semiont-button semiont-button--secondary"
@@ -243,7 +340,7 @@ export function ReferencesPanel({
243
340
  {/* Scrollable content area */}
244
341
  <div ref={containerRef} className="semiont-panel__content">
245
342
  {/* Detection Section - only in Annotate mode and for text resources */}
246
- {annotateMode && onDetect && (
343
+ {annotateMode && (
247
344
  <div className="semiont-panel__section">
248
345
  <button
249
346
  onClick={() => setIsDetectExpanded(!isDetectExpanded)}
@@ -338,7 +435,6 @@ export function ReferencesPanel({
338
435
  {detectionProgress && (
339
436
  <DetectionProgressWidget
340
437
  progress={detectionProgress}
341
- onCancel={onCancelDetection}
342
438
  annotationType="reference"
343
439
  />
344
440
  )}
@@ -389,15 +485,11 @@ export function ReferencesPanel({
389
485
  key={reference.id}
390
486
  reference={reference}
391
487
  isFocused={reference.id === focusedAnnotationId}
392
- onClick={() => onAnnotationClick?.(reference)}
488
+ isHovered={reference.id === hoveredAnnotationId}
393
489
  routes={routes}
394
- onReferenceRef={handleAnnotationRef}
395
490
  annotateMode={annotateMode}
396
491
  isGenerating={reference.id === generatingReferenceId}
397
- {...(onAnnotationHover && { onReferenceHover: onAnnotationHover })}
398
- {...(onGenerateDocument && { onGenerateDocument })}
399
- {...(onCreateDocument && { onCreateDocument })}
400
- {...(onSearchDocuments && { onSearchDocuments })}
492
+ ref={(el) => setEntryRef(reference.id, el)}
401
493
  />
402
494
  ))
403
495
  )}
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useTranslations } from '../../../contexts/TranslationContext';
4
+ import { useEventBus } from '../../../contexts/EventBusContext';
4
5
  import { formatLocaleDisplay } from '@semiont/api-client';
5
6
  import './ResourceInfoPanel.css';
6
7
 
@@ -10,22 +11,24 @@ interface Props {
10
11
  primaryMediaType?: string | undefined;
11
12
  primaryByteSize?: number | undefined;
12
13
  isArchived?: boolean;
13
- onArchive?: () => void;
14
- onUnarchive?: () => void;
15
- onClone?: () => void;
16
14
  }
17
15
 
16
+ /**
17
+ * Panel for displaying resource metadata and management actions
18
+ *
19
+ * @emits resource:clone - Clone this resource. Payload: undefined
20
+ * @emits resource:unarchive - Unarchive this resource. Payload: undefined
21
+ * @emits resource:archive - Archive this resource. Payload: undefined
22
+ */
18
23
  export function ResourceInfoPanel({
19
24
  documentEntityTypes,
20
25
  documentLocale,
21
26
  primaryMediaType,
22
27
  primaryByteSize,
23
28
  isArchived = false,
24
- onArchive,
25
- onUnarchive,
26
- onClone
27
29
  }: Props) {
28
30
  const t = useTranslations('ResourceInfoPanel');
31
+ const eventBus = useEventBus();
29
32
 
30
33
  return (
31
34
  <div className="semiont-resource-info-panel">
@@ -92,50 +95,46 @@ export function ResourceInfoPanel({
92
95
  )}
93
96
 
94
97
  {/* Clone Action */}
95
- {onClone && (
96
- <div className="semiont-resource-info-panel__action-section">
97
- <button
98
- onClick={onClone}
99
- className="semiont-resource-button semiont-resource-button--secondary"
100
- >
101
- 🔗 {t('clone')}
102
- </button>
103
- <p className="semiont-resource-info-panel__description">
104
- {t('cloneDescription')}
105
- </p>
106
- </div>
107
- )}
98
+ <div className="semiont-resource-info-panel__action-section">
99
+ <button
100
+ onClick={() => eventBus.emit('resource:clone', undefined)}
101
+ className="semiont-resource-button semiont-resource-button--secondary"
102
+ >
103
+ 🔗 {t('clone')}
104
+ </button>
105
+ <p className="semiont-resource-info-panel__description">
106
+ {t('cloneDescription')}
107
+ </p>
108
+ </div>
108
109
 
109
110
  {/* Archive/Unarchive Actions */}
110
- {(onArchive || onUnarchive) && (
111
- <div className="semiont-resource-info-panel__action-section">
112
- {isArchived ? (
113
- <>
114
- <button
115
- onClick={onUnarchive}
116
- className="semiont-resource-button semiont-resource-button--secondary"
117
- >
118
- 📤 {t('unarchive')}
119
- </button>
120
- <p className="semiont-resource-info-panel__description">
121
- {t('unarchiveDescription')}
122
- </p>
123
- </>
124
- ) : (
125
- <>
126
- <button
127
- onClick={onArchive}
128
- className="semiont-resource-button semiont-resource-button--archive"
129
- >
130
- 📦 {t('archive')}
131
- </button>
132
- <p className="semiont-resource-info-panel__description">
133
- {t('archiveDescription')}
134
- </p>
135
- </>
136
- )}
137
- </div>
138
- )}
111
+ <div className="semiont-resource-info-panel__action-section">
112
+ {isArchived ? (
113
+ <>
114
+ <button
115
+ onClick={() => eventBus.emit('resource:unarchive', undefined)}
116
+ className="semiont-resource-button semiont-resource-button--secondary"
117
+ >
118
+ 📤 {t('unarchive')}
119
+ </button>
120
+ <p className="semiont-resource-info-panel__description">
121
+ {t('unarchiveDescription')}
122
+ </p>
123
+ </>
124
+ ) : (
125
+ <>
126
+ <button
127
+ onClick={() => eventBus.emit('resource:archive', undefined)}
128
+ className="semiont-resource-button semiont-resource-button--archive"
129
+ >
130
+ 📦 {t('archive')}
131
+ </button>
132
+ <p className="semiont-resource-info-panel__description">
133
+ {t('archiveDescription')}
134
+ </p>
135
+ </>
136
+ )}
137
+ </div>
139
138
  </div>
140
139
  );
141
140
  }