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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/EventBusContext-7GvDyO0d.d.mts +414 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -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 +699 -529
  38. package/dist/index.mjs +4291 -3491
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -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-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +8 -2
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/LiveRegion.tsx +18 -18
  77. package/src/components/ResizeHandle.tsx +10 -4
  78. package/src/components/SessionTimer.tsx +2 -2
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +9 -9
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -138
  100. package/src/components/resource/AnnotationHistory.tsx +12 -13
  101. package/src/components/resource/BrowseView.tsx +89 -177
  102. package/src/components/resource/HistoryEvent.tsx +16 -11
  103. package/src/components/resource/ResourceViewer.tsx +201 -370
  104. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  105. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  106. package/src/components/resource/event-formatting.ts +316 -0
  107. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  108. package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
  109. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  110. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  111. package/src/components/resource/panels/CommentsPanel.tsx +153 -31
  112. package/src/components/resource/panels/DetectSection.css +36 -1
  113. package/src/components/resource/panels/DetectSection.tsx +38 -10
  114. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  115. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  116. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  117. package/src/components/resource/panels/ReferencesPanel.tsx +166 -49
  118. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  119. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  120. package/src/components/resource/panels/TagEntry.tsx +25 -33
  121. package/src/components/resource/panels/TaggingPanel.tsx +141 -25
  122. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
  123. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
  124. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  125. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
  126. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  127. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  128. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +228 -103
  129. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  130. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
  131. package/src/components/settings/SettingsPanel.tsx +15 -12
  132. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  133. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  134. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  135. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  136. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  137. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  138. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  139. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  140. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  141. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  142. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  143. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  144. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  145. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  146. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  147. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +16 -27
  148. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  149. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  150. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  151. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  152. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  153. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +145 -91
  154. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  155. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
  156. package/src/styles/motivations/motivation-assessment.css +28 -0
  157. package/src/styles/patterns/panel-helpers.css +26 -0
  158. package/translations/ar.json +7 -3
  159. package/translations/bn.json +7 -3
  160. package/translations/cs.json +7 -3
  161. package/translations/da.json +7 -3
  162. package/translations/de.json +7 -3
  163. package/translations/el.json +7 -3
  164. package/translations/en.json +7 -3
  165. package/translations/es.json +7 -3
  166. package/translations/fa.json +7 -3
  167. package/translations/fi.json +7 -3
  168. package/translations/fr.json +7 -3
  169. package/translations/he.json +7 -3
  170. package/translations/hi.json +7 -3
  171. package/translations/id.json +7 -3
  172. package/translations/it.json +7 -3
  173. package/translations/ja.json +7 -3
  174. package/translations/ko.json +7 -3
  175. package/translations/ms.json +7 -3
  176. package/translations/nl.json +7 -3
  177. package/translations/no.json +7 -3
  178. package/translations/pl.json +7 -3
  179. package/translations/pt.json +7 -3
  180. package/translations/ro.json +7 -3
  181. package/translations/sv.json +7 -3
  182. package/translations/th.json +7 -3
  183. package/translations/tr.json +7 -3
  184. package/translations/uk.json +7 -3
  185. package/translations/vi.json +7 -3
  186. package/translations/zh.json +7 -3
  187. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  188. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  189. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  190. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  191. package/dist/chunk-35LLVRFK.mjs.map +0 -1
  192. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  193. package/dist/cs-RSV675WU.mjs.map +0 -1
  194. package/dist/da-CHXNPWJC.mjs.map +0 -1
  195. package/dist/de-KPEZ53D4.mjs.map +0 -1
  196. package/dist/el-MW2BME5T.mjs.map +0 -1
  197. package/dist/es-HQ24NYS3.mjs.map +0 -1
  198. package/dist/fa-W34LRLHG.mjs.map +0 -1
  199. package/dist/fi-3U44IGOA.mjs.map +0 -1
  200. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  201. package/dist/he-CS4WRXN3.mjs.map +0 -1
  202. package/dist/hi-GJDY46KA.mjs.map +0 -1
  203. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  204. package/dist/it-VDNDMZPU.mjs.map +0 -1
  205. package/dist/ja-5PEH56J5.mjs.map +0 -1
  206. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  207. package/dist/ms-5PZVW76T.mjs.map +0 -1
  208. package/dist/nl-YXES36KM.mjs.map +0 -1
  209. package/dist/no-XRA2UCQD.mjs.map +0 -1
  210. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  211. package/dist/pt-7GAG57BM.mjs.map +0 -1
  212. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  213. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  214. package/dist/th-LPKYLBX5.mjs.map +0 -1
  215. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  216. package/dist/uk-36UHTDDI.mjs.map +0 -1
  217. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  218. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  219. /package/dist/{en-EVMIX24Y.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -1,14 +1,15 @@
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 { useEventBus } from '../../../contexts/EventBusContext';
6
+ import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
5
7
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
6
8
  import { DetectionProgressWidget } from '../../DetectionProgressWidget';
7
9
  import { ReferenceEntry } from './ReferenceEntry';
8
10
  import type { components, paths, Selector } from '@semiont/api-client';
9
- import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
11
+ import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
10
12
  import { PanelHeader } from './PanelHeader';
11
- import { supportsDetection } from '../../../lib/resource-utils';
12
13
  import './ReferencesPanel.css';
13
14
 
14
15
  type Annotation = components['schemas']['Annotation'];
@@ -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,47 +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
- mediaType?: string | undefined;
70
60
  referencedBy?: ReferencedBy[];
71
61
  referencedByLoading?: boolean;
72
62
  pendingAnnotation: PendingAnnotation | null;
63
+ scrollToAnnotationId?: string | null;
64
+ onScrollCompleted?: () => void;
65
+ hoveredAnnotationId?: string | null;
73
66
  }
74
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
+ */
75
76
  export function ReferencesPanel({
76
77
  annotations = [],
77
- onAnnotationClick,
78
- focusedAnnotationId,
79
- hoveredAnnotationId,
80
- onAnnotationHover,
81
- onDetect,
82
- onCreate,
83
78
  isDetecting,
84
79
  detectionProgress,
85
80
  annotateMode = true,
86
81
  Link,
87
82
  routes,
88
83
  allEntityTypes,
89
- onCancelDetection,
90
- onSearchDocuments,
91
- onGenerateDocument,
92
- onCreateDocument,
93
84
  generatingReferenceId,
94
- mediaType,
95
85
  referencedBy = [],
96
86
  referencedByLoading = false,
97
87
  pendingAnnotation,
88
+ scrollToAnnotationId,
89
+ onScrollCompleted,
90
+ hoveredAnnotationId,
98
91
  }: Props) {
99
92
  const t = useTranslations('DetectPanel');
100
93
  const tRef = useTranslations('ReferencesPanel');
94
+ const eventBus = useEventBus();
101
95
  const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
102
96
  const [lastDetectionLog, setLastDetectionLog] = useState<DetectionLog[] | null>(null);
103
97
  const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
104
98
  const [includeDescriptiveReferences, setIncludeDescriptiveReferences] = useState(false);
99
+ const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
100
+ const containerRef = useRef<HTMLDivElement>(null);
105
101
 
106
102
  // Collapsible detection section state - load from localStorage, default expanded
107
103
  const [isDetectExpanded, setIsDetectExpanded] = useState(() => {
@@ -116,17 +112,108 @@ export function ReferencesPanel({
116
112
  localStorage.setItem('detect-section-expanded-reference', String(isDetectExpanded));
117
113
  }, [isDetectExpanded]);
118
114
 
119
- const { sortedAnnotations, containerRef, handleAnnotationRef } =
120
- 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]);
121
195
 
122
- // Check if detection is supported for this media type
123
- const isTextResource = supportsDetection(mediaType);
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
+ });
124
206
 
125
207
  // Clear log when starting new detection
126
208
  const handleDetect = () => {
127
- if (!onDetect) return;
128
209
  setLastDetectionLog(null);
129
- onDetect(selectedEntityTypes, includeDescriptiveReferences);
210
+ eventBus.emit('detection:start', {
211
+ motivation: 'linking',
212
+ options: {
213
+ entityTypes: selectedEntityTypes,
214
+ includeDescriptiveReferences,
215
+ },
216
+ });
130
217
  };
131
218
 
132
219
  // Track whether we've already saved the log for the current detection run
@@ -161,11 +248,32 @@ export function ReferencesPanel({
161
248
  };
162
249
 
163
250
  const handleCreateReference = () => {
164
- const entityType = pendingEntityTypes.join(',') || undefined;
165
- onCreate(entityType);
166
- 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
+ }
167
260
  };
168
261
 
262
+ // Escape key handler for cancelling pending annotation
263
+ useEffect(() => {
264
+ if (!pendingAnnotation) return;
265
+
266
+ const handleEscape = (e: KeyboardEvent) => {
267
+ if (e.key === 'Escape') {
268
+ eventBus.emit('annotation:cancel-pending', undefined);
269
+ setPendingEntityTypes([]);
270
+ }
271
+ };
272
+
273
+ document.addEventListener('keydown', handleEscape);
274
+ return () => document.removeEventListener('keydown', handleEscape);
275
+ }, [pendingAnnotation]);
276
+
169
277
  return (
170
278
  <div className="semiont-panel">
171
279
  <PanelHeader annotationType="reference" count={annotations.length} title={tRef('referencesTitle')} />
@@ -205,20 +313,34 @@ export function ReferencesPanel({
205
313
  </div>
206
314
  )}
207
315
 
208
- <button
209
- onClick={handleCreateReference}
210
- className="semiont-button semiont-button--primary"
211
- data-type="reference"
212
- >
213
- 🔗 {tRef('createReference')}
214
- </button>
316
+ <div className="semiont-annotation-prompt__footer">
317
+ <div className="semiont-annotation-prompt__actions">
318
+ <button
319
+ onClick={() => {
320
+ eventBus.emit('annotation:cancel-pending', undefined);
321
+ setPendingEntityTypes([]);
322
+ }}
323
+ className="semiont-button semiont-button--secondary"
324
+ data-type="reference"
325
+ >
326
+ {tRef('cancel')}
327
+ </button>
328
+ <button
329
+ onClick={handleCreateReference}
330
+ className="semiont-button semiont-button--primary"
331
+ data-type="reference"
332
+ >
333
+ 🔗 {tRef('createReference')}
334
+ </button>
335
+ </div>
336
+ </div>
215
337
  </div>
216
338
  )}
217
339
 
218
340
  {/* Scrollable content area */}
219
341
  <div ref={containerRef} className="semiont-panel__content">
220
342
  {/* Detection Section - only in Annotate mode and for text resources */}
221
- {annotateMode && isTextResource && (
343
+ {annotateMode && (
222
344
  <div className="semiont-panel__section">
223
345
  <button
224
346
  onClick={() => setIsDetectExpanded(!isDetectExpanded)}
@@ -313,7 +435,6 @@ export function ReferencesPanel({
313
435
  {detectionProgress && (
314
436
  <DetectionProgressWidget
315
437
  progress={detectionProgress}
316
- onCancel={onCancelDetection}
317
438
  annotationType="reference"
318
439
  />
319
440
  )}
@@ -364,15 +485,11 @@ export function ReferencesPanel({
364
485
  key={reference.id}
365
486
  reference={reference}
366
487
  isFocused={reference.id === focusedAnnotationId}
367
- onClick={() => onAnnotationClick?.(reference)}
488
+ isHovered={reference.id === hoveredAnnotationId}
368
489
  routes={routes}
369
- onReferenceRef={handleAnnotationRef}
370
490
  annotateMode={annotateMode}
371
491
  isGenerating={reference.id === generatingReferenceId}
372
- {...(onAnnotationHover && { onReferenceHover: onAnnotationHover })}
373
- {...(onGenerateDocument && { onGenerateDocument })}
374
- {...(onCreateDocument && { onCreateDocument })}
375
- {...(onSearchDocuments && { onSearchDocuments })}
492
+ ref={(el) => setEntryRef(reference.id, el)}
376
493
  />
377
494
  ))
378
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
  }
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
4
3
  import { useTranslations } from '../../../contexts/TranslationContext';
5
4
  import type { components } from '@semiont/api-client';
6
5
  import { isBodyResolved } from '@semiont/api-client';
@@ -27,28 +26,19 @@ export function StatisticsPanel({
27
26
  const t = useTranslations('StatisticsPanel');
28
27
 
29
28
  // Count stub vs resolved references
30
- const stubCount = useMemo(
31
- () => references.filter((r) => !isBodyResolved(r.body)).length,
32
- [references]
33
- );
34
-
35
- const resolvedCount = useMemo(
36
- () => references.filter((r) => isBodyResolved(r.body)).length,
37
- [references]
38
- );
29
+ const stubCount = references.filter((r) => !isBodyResolved(r.body)).length;
30
+ const resolvedCount = references.filter((r) => isBodyResolved(r.body)).length;
39
31
 
40
32
  // Count entity types from references (at annotation level)
41
- const entityTypesList = useMemo(() => {
42
- const entityTypeCounts = new Map<string, number>();
43
- references.forEach((ref) => {
44
- const entityTypes = getEntityTypes(ref);
45
- entityTypes.forEach((type: string) => {
46
- entityTypeCounts.set(type, (entityTypeCounts.get(type) || 0) + 1);
47
- });
33
+ const entityTypeCounts = new Map<string, number>();
34
+ references.forEach((ref) => {
35
+ const entityTypes = getEntityTypes(ref);
36
+ entityTypes.forEach((type: string) => {
37
+ entityTypeCounts.set(type, (entityTypeCounts.get(type) || 0) + 1);
48
38
  });
39
+ });
49
40
 
50
- return Array.from(entityTypeCounts.entries()).sort((a, b) => b[1] - a[1]); // Sort by count descending
51
- }, [references]);
41
+ const entityTypesList = Array.from(entityTypeCounts.entries()).sort((a, b) => b[1] - a[1]); // Sort by count descending
52
42
 
53
43
  return (
54
44
  <div className="semiont-statistics-panel">
@@ -1,44 +1,30 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef } from 'react';
3
+ import { forwardRef } from 'react';
4
4
  import type { components } from '@semiont/api-client';
5
5
  import { getAnnotationExactText } from '@semiont/api-client';
6
6
  import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
7
7
  import { getTagSchema } from '../../../lib/tag-schemas';
8
+ import { useEventBus } from '../../../contexts/EventBusContext';
8
9
 
9
10
  type Annotation = components['schemas']['Annotation'];
10
11
 
11
12
  interface TagEntryProps {
12
13
  tag: Annotation;
13
14
  isFocused: boolean;
14
- onClick: () => void;
15
- onTagRef: (tagId: string, el: HTMLElement | null) => void;
16
- onTagHover?: (tagId: string | null) => void;
15
+ isHovered?: boolean;
17
16
  }
18
17
 
19
- export function TagEntry({
20
- tag,
21
- isFocused,
22
- onClick,
23
- onTagRef,
24
- onTagHover,
25
- }: TagEntryProps) {
26
- const tagRef = useRef<HTMLDivElement>(null);
27
-
28
- // Register ref with parent
29
- useEffect(() => {
30
- onTagRef(tag.id, tagRef.current);
31
- return () => {
32
- onTagRef(tag.id, null);
33
- };
34
- }, [tag.id, onTagRef]);
35
-
36
- // Scroll to tag when focused
37
- useEffect(() => {
38
- if (isFocused && tagRef.current) {
39
- tagRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
40
- }
41
- }, [isFocused]);
18
+ export const TagEntry = forwardRef<HTMLDivElement, TagEntryProps>(
19
+ function TagEntry(
20
+ {
21
+ tag,
22
+ isFocused,
23
+ isHovered = false,
24
+ },
25
+ ref
26
+ ) {
27
+ const eventBus = useEventBus();
42
28
 
43
29
  const selectedText = getAnnotationExactText(tag);
44
30
  const category = getTagCategory(tag);
@@ -47,11 +33,17 @@ export function TagEntry({
47
33
 
48
34
  return (
49
35
  <div
50
- ref={tagRef}
51
- onClick={onClick}
52
- onMouseEnter={() => onTagHover?.(tag.id)}
53
- onMouseLeave={() => onTagHover?.(null)}
54
- className="semiont-annotation-entry"
36
+ ref={ref}
37
+ onClick={() => {
38
+ eventBus.emit('annotation:click', { annotationId: tag.id, motivation: tag.motivation });
39
+ }}
40
+ onMouseEnter={() => {
41
+ eventBus.emit('annotation:hover', { annotationId: tag.id });
42
+ }}
43
+ onMouseLeave={() => {
44
+ eventBus.emit('annotation:hover', { annotationId: null });
45
+ }}
46
+ className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
55
47
  data-type="tag"
56
48
  data-focused={isFocused ? 'true' : 'false'}
57
49
  >
@@ -73,4 +65,4 @@ export function TagEntry({
73
65
  </div>
74
66
  </div>
75
67
  );
76
- }
68
+ });