@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,10 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect, useRef, 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 { components, Selector } from '@semiont/api-client';
8
+ import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
6
9
  import { TagEntry } from './TagEntry';
7
- import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
8
10
  import { PanelHeader } from './PanelHeader';
9
11
  import { getAllTagSchemas } from '../../../lib/tag-schemas';
10
12
  import './TaggingPanel.css';
@@ -37,13 +39,7 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
37
39
 
38
40
  interface TaggingPanelProps {
39
41
  annotations: Annotation[];
40
- onAnnotationClick: (annotation: Annotation) => void;
41
- focusedAnnotationId: string | null;
42
- hoveredAnnotationId?: string | null;
43
- onAnnotationHover?: (annotationId: string | null) => void;
44
42
  annotateMode?: boolean;
45
- onDetect?: (schemaId: string, categories: string[]) => void | Promise<void>;
46
- onCreate: (schemaId: string, category: string) => void | Promise<void>;
47
43
  isDetecting?: boolean;
48
44
  detectionProgress?: {
49
45
  status: string;
@@ -55,24 +51,35 @@ interface TaggingPanelProps {
55
51
  requestParams?: Array<{ label: string; value: string }>;
56
52
  } | null;
57
53
  pendingAnnotation: PendingAnnotation | null;
54
+ scrollToAnnotationId?: string | null;
55
+ onScrollCompleted?: () => void;
56
+ hoveredAnnotationId?: string | null;
58
57
  }
59
58
 
59
+ /**
60
+ * Panel for managing tag annotations with schema-based detection
61
+ *
62
+ * @emits detection:start - Start tag detection. Payload: { motivation: 'tagging', options: { schemaId: string, categories: string[] } }
63
+ * @emits annotation:cancel-pending - Cancel pending tag annotation. Payload: undefined
64
+ * @emits annotation:create - Create new tag annotation. Payload: { motivation: 'tagging', selector: Selector | Selector[], body: Body[] }
65
+ * @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
66
+ */
60
67
  export function TaggingPanel({
61
68
  annotations,
62
- onAnnotationClick,
63
- focusedAnnotationId,
64
- hoveredAnnotationId,
65
- onAnnotationHover,
66
69
  annotateMode = true,
67
- onDetect,
68
- onCreate,
69
70
  isDetecting = false,
70
71
  detectionProgress,
71
- pendingAnnotation
72
+ pendingAnnotation,
73
+ scrollToAnnotationId,
74
+ onScrollCompleted,
75
+ hoveredAnnotationId,
72
76
  }: TaggingPanelProps) {
73
77
  const t = useTranslations('TaggingPanel');
78
+ const eventBus = useEventBus();
74
79
  const [selectedSchemaId, setSelectedSchemaId] = useState<string>('legal-irac');
75
80
  const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
81
+ const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
82
+ const containerRef = useRef<HTMLDivElement>(null);
76
83
 
77
84
  // Collapsible detection section state - load from localStorage, default expanded
78
85
  const [isDetectExpanded, setIsDetectExpanded] = useState(() => {
@@ -87,8 +94,76 @@ export function TaggingPanel({
87
94
  localStorage.setItem('detect-section-expanded-tag', String(isDetectExpanded));
88
95
  }, [isDetectExpanded]);
89
96
 
90
- const { sortedAnnotations, containerRef, handleAnnotationRef } =
91
- useAnnotationPanel(annotations, hoveredAnnotationId);
97
+ // Subscribe to click events - update focused state
98
+ // Event handler for annotation clicks (extracted to avoid inline arrow function)
99
+ const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
100
+ setFocusedAnnotationId(annotationId);
101
+ setTimeout(() => setFocusedAnnotationId(null), 3000);
102
+ }, []);
103
+
104
+ useEventSubscriptions({
105
+ 'annotation:click': handleAnnotationClick,
106
+ });
107
+
108
+ // Direct ref management
109
+ const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
110
+
111
+ // Sort annotations by their position in the resource
112
+ const sortedAnnotations = useMemo(() => {
113
+ return [...annotations].sort((a, b) => {
114
+ const aSelector = getTextPositionSelector(getTargetSelector(a.target));
115
+ const bSelector = getTextPositionSelector(getTargetSelector(b.target));
116
+ if (!aSelector || !bSelector) return 0;
117
+ return aSelector.start - bSelector.start;
118
+ });
119
+ }, [annotations]);
120
+
121
+ // Ref callback for entry components
122
+ const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
123
+ if (element) {
124
+ entryRefs.current.set(id, element);
125
+ } else {
126
+ entryRefs.current.delete(id);
127
+ }
128
+ }, []);
129
+
130
+ // Handle scrollToAnnotationId (click scroll)
131
+ useEffect(() => {
132
+ if (!scrollToAnnotationId) return;
133
+ const element = entryRefs.current.get(scrollToAnnotationId);
134
+ if (element && containerRef.current) {
135
+ const elementTop = element.offsetTop;
136
+ const containerHeight = containerRef.current.clientHeight;
137
+ const elementHeight = element.offsetHeight;
138
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
139
+ containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
140
+ element.classList.remove('semiont-annotation-pulse');
141
+ void element.offsetWidth;
142
+ element.classList.add('semiont-annotation-pulse');
143
+ if (onScrollCompleted) onScrollCompleted();
144
+ }
145
+ }, [scrollToAnnotationId]);
146
+
147
+ // Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
148
+ useEffect(() => {
149
+ if (!hoveredAnnotationId) return;
150
+ const element = entryRefs.current.get(hoveredAnnotationId);
151
+ if (!element || !containerRef.current) return;
152
+
153
+ const container = containerRef.current;
154
+ const elementRect = element.getBoundingClientRect();
155
+ const containerRect = container.getBoundingClientRect();
156
+ const isVisible = elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
157
+ if (!isVisible) {
158
+ const elementTop = element.offsetTop;
159
+ const containerHeight = container.clientHeight;
160
+ const elementHeight = element.offsetHeight;
161
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
162
+ container.scrollTo({ top: scrollTo, behavior: 'smooth' });
163
+ }
164
+
165
+ // Pulse effect is handled by isHovered prop on TagEntry
166
+ }, [hoveredAnnotationId]);
92
167
 
93
168
  const schemas = getAllTagSchemas();
94
169
  const selectedSchema = schemas.find(s => s.id === selectedSchemaId);
@@ -119,12 +194,32 @@ export function TaggingPanel({
119
194
  };
120
195
 
121
196
  const handleDetect = () => {
122
- if (onDetect && selectedCategories.size > 0) {
123
- onDetect(selectedSchemaId, Array.from(selectedCategories));
197
+ if (selectedCategories.size > 0) {
198
+ eventBus.emit('detection:start', {
199
+ motivation: 'tagging',
200
+ options: {
201
+ schemaId: selectedSchemaId,
202
+ categories: Array.from(selectedCategories),
203
+ },
204
+ });
124
205
  setSelectedCategories(new Set()); // Reset after detection
125
206
  }
126
207
  };
127
208
 
209
+ // Escape key handler for cancelling pending annotation
210
+ useEffect(() => {
211
+ if (!pendingAnnotation) return;
212
+
213
+ const handleEscape = (e: KeyboardEvent) => {
214
+ if (e.key === 'Escape') {
215
+ eventBus.emit('annotation:cancel-pending', undefined);
216
+ }
217
+ };
218
+
219
+ document.addEventListener('keydown', handleEscape);
220
+ return () => document.removeEventListener('keydown', handleEscape);
221
+ }, [pendingAnnotation]);
222
+
128
223
  // Color schemes are now handled via CSS data attributes
129
224
 
130
225
  return (
@@ -178,8 +273,19 @@ export function TaggingPanel({
178
273
  <select
179
274
  className="semiont-select"
180
275
  onChange={(e) => {
181
- if (e.target.value) {
182
- onCreate(selectedSchemaId, e.target.value);
276
+ if (e.target.value && pendingAnnotation) {
277
+ eventBus.emit('annotation:create', {
278
+ motivation: 'tagging',
279
+ selector: pendingAnnotation.selector,
280
+ body: [
281
+ {
282
+ type: 'TextualBody',
283
+ value: e.target.value,
284
+ purpose: 'tagging',
285
+ schema: selectedSchemaId,
286
+ },
287
+ ],
288
+ });
183
289
  }
184
290
  }}
185
291
  defaultValue=""
@@ -191,11 +297,22 @@ export function TaggingPanel({
191
297
  </select>
192
298
  </div>
193
299
  )}
300
+
301
+ {/* Cancel button */}
302
+ <div className="semiont-annotation-prompt__footer">
303
+ <button
304
+ onClick={() => eventBus.emit('annotation:cancel-pending', undefined)}
305
+ className="semiont-button semiont-button--secondary"
306
+ data-type="tag"
307
+ >
308
+ {t('cancel')}
309
+ </button>
310
+ </div>
194
311
  </div>
195
312
  )}
196
313
 
197
314
  {/* Detection Section - only in Annotate mode */}
198
- {annotateMode && onDetect && (
315
+ {annotateMode && (
199
316
  <div className="semiont-panel__section">
200
317
  <button
201
318
  onClick={() => setIsDetectExpanded(!isDetectExpanded)}
@@ -363,9 +480,8 @@ export function TaggingPanel({
363
480
  key={tag.id}
364
481
  tag={tag}
365
482
  isFocused={tag.id === focusedAnnotationId}
366
- onClick={() => onAnnotationClick(tag)}
367
- onTagRef={handleAnnotationRef}
368
- {...(onAnnotationHover && { onTagHover: onAnnotationHover })}
483
+ isHovered={tag.id === hoveredAnnotationId}
484
+ ref={(el) => setEntryRef(tag.id, el)}
369
485
  />
370
486
  ))
371
487
  )}
@@ -5,7 +5,6 @@ import { useTranslations } from '../../../contexts/TranslationContext';
5
5
  import type { components, Selector } from '@semiont/api-client';
6
6
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
7
7
  import type { Annotator } from '../../../lib/annotation-registry';
8
- import { createDetectionHandler } from '../../../lib/annotation-registry';
9
8
  import { StatisticsPanel } from './StatisticsPanel';
10
9
  import { HighlightPanel } from './HighlightPanel';
11
10
  import { ReferencesPanel } from './ReferencesPanel';
@@ -28,13 +27,13 @@ interface PendingAnnotation {
28
27
  const TAB_ORDER: TabKey[] = ['statistics', 'reference', 'highlight', 'assessment', 'comment', 'tag'];
29
28
 
30
29
  /**
31
- * Simplified UnifiedAnnotationsPanel using Annotator abstraction
30
+ * Simplified UnifiedAnnotationsPanel using event-driven architecture
32
31
  *
33
32
  * Key simplifications:
34
33
  * - Single annotations array (grouped internally by motivation)
35
34
  * - Single focusedAnnotationId (motivation-agnostic)
36
- * - Single hoveredAnnotationId (motivation-agnostic)
37
- * - Single onCreateAnnotation handler (motivation-based dispatch)
35
+ * - Hover state managed via event bus (no props needed)
36
+ * - All operations managed via event bus (no callback props)
38
37
  */
39
38
  interface UnifiedAnnotationsPanelProps {
40
39
  // All annotations (grouped internally by motivation)
@@ -43,29 +42,6 @@ interface UnifiedAnnotationsPanelProps {
43
42
  // Annotators (pure static data - no handlers)
44
43
  annotators: Record<string, Annotator>;
45
44
 
46
- // Detection context (passed separately so annotators remain stable)
47
- detectionContext?: {
48
- client: any;
49
- rUri: any;
50
- setDetectingMotivation: (motivation: Motivation | null) => void;
51
- setMotivationDetectionProgress: (progress: any) => void;
52
- detectionStreamRef: any;
53
- cacheManager: any;
54
- showSuccess: (message: string) => void;
55
- showError: (message: string) => void;
56
- };
57
-
58
- // Unified state (motivation-agnostic)
59
- focusedAnnotationId: string | null;
60
- hoveredAnnotationId?: string | null;
61
-
62
- // Shared UI handlers (same for all annotation types)
63
- onAnnotationClick: (annotation: Annotation) => void;
64
- onAnnotationHover?: (annotationId: string | null) => void;
65
-
66
- // Single generic creation handler
67
- onCreateAnnotation: (motivation: Motivation, ...args: any[]) => void;
68
-
69
45
  // Mode
70
46
  annotateMode?: boolean;
71
47
 
@@ -84,20 +60,23 @@ interface UnifiedAnnotationsPanelProps {
84
60
  // Unified pending annotation (for creating new annotations)
85
61
  pendingAnnotation: PendingAnnotation | null;
86
62
 
87
- // Reference-specific props (TODO: refactor these into annotator handlers)
63
+ // Reference-specific props
88
64
  allEntityTypes?: string[];
89
65
  generatingReferenceId?: string | null;
90
- onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
91
- onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
92
- onSearchDocuments?: (referenceId: string, searchTerm: string) => void;
93
- onCancelDetection?: () => void;
94
- mediaType?: string;
95
66
  referencedBy?: any[];
96
67
  referencedByLoading?: boolean;
97
68
 
98
69
  // Resource context
99
70
  resourceId?: string;
100
71
  initialTab?: TabKey;
72
+ initialTabGeneration?: number; // Generation counter for tab switching
73
+
74
+ // Scroll coordination (one-time action, will be cleared after use)
75
+ scrollToAnnotationId?: string | null;
76
+ onScrollCompleted?: () => void;
77
+
78
+ // Hover coordination (for bidirectional hover highlighting)
79
+ hoveredAnnotationId?: string | null;
101
80
 
102
81
  // Routing
103
82
  Link: React.ComponentType<LinkComponentProps>;
@@ -108,27 +87,25 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
108
87
  const t = useTranslations('UnifiedAnnotationsPanel');
109
88
 
110
89
  // Group annotations by type using annotators
111
- const grouped = React.useMemo(() => {
112
- const groups: Record<string, Annotation[]> = {
113
- highlight: [],
114
- comment: [],
115
- assessment: [],
116
- reference: [],
117
- tag: []
118
- };
119
-
120
- for (const ann of props.annotations) {
121
- const annotator = Object.values(props.annotators).find(a => a.matchesAnnotation(ann));
122
- if (annotator) {
123
- if (!groups[annotator.internalType]) {
124
- groups[annotator.internalType] = [];
125
- }
126
- groups[annotator.internalType].push(ann);
90
+ const groups: Record<string, Annotation[]> = {
91
+ highlight: [],
92
+ comment: [],
93
+ assessment: [],
94
+ reference: [],
95
+ tag: []
96
+ };
97
+
98
+ for (const ann of props.annotations) {
99
+ const annotator = Object.values(props.annotators).find(a => a.matchesAnnotation(ann));
100
+ if (annotator) {
101
+ if (!groups[annotator.internalType]) {
102
+ groups[annotator.internalType] = [];
127
103
  }
104
+ groups[annotator.internalType].push(ann);
128
105
  }
106
+ }
129
107
 
130
- return groups;
131
- }, [props.annotations, props.annotators]);
108
+ const grouped = groups;
132
109
 
133
110
  // Load tab from localStorage (per-resource)
134
111
  const [activeTab, setActiveTab] = useState<TabKey>(() => {
@@ -157,23 +134,14 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
157
134
  localStorage.setItem(storageKey, activeTab);
158
135
  }, [activeTab, props.resourceId]);
159
136
 
160
- // Auto-switch to the appropriate tab when an annotation is focused
161
- useEffect(() => {
162
- if (!props.focusedAnnotationId) return;
163
-
164
- // Find which annotation type this focused annotation belongs to
165
- const focusedAnnotation = props.annotations.find(ann => ann.id === props.focusedAnnotationId);
166
- if (!focusedAnnotation) return;
167
137
 
168
- // Determine the annotator key for this annotation
169
- for (const [key, annotator] of Object.entries(props.annotators)) {
170
- if (annotator.matchesAnnotation(focusedAnnotation)) {
171
- setActiveTab(key as TabKey);
172
- break;
173
- }
138
+ // Switch to initialTab when generation counter changes (e.g., when clicking annotations with different motivations)
139
+ // Using generation counter ensures we can switch to the same tab multiple times
140
+ useEffect(() => {
141
+ if (props.initialTab && props.initialTabGeneration !== undefined) {
142
+ setActiveTab(props.initialTab);
174
143
  }
175
- // eslint-disable-next-line react-hooks/exhaustive-deps
176
- }, [props.focusedAnnotationId]);
144
+ }, [props.initialTabGeneration]); // Only watch generation counter, not the tab itself
177
145
 
178
146
  // Auto-switch to the appropriate tab when creating a new annotation
179
147
  useEffect(() => {
@@ -267,29 +235,22 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
267
235
 
268
236
  const annotations = grouped[activeTab] || [];
269
237
  const isDetecting = props.detectingMotivation === annotator.motivation;
270
- const detectionProgress = isDetecting ? props.detectionProgress : null;
238
+ // Pass through detectionProgress even when not actively detecting
239
+ // This allows final progress message to display after detection completes
240
+ const detectionProgress = props.detectionProgress;
271
241
 
272
- // Common props for all annotation panels
273
- // Create detection handler on-demand if detection is supported and context is provided
274
- const onDetect = (annotator.detection && props.detectionContext)
275
- ? createDetectionHandler(annotator, props.detectionContext)
276
- : undefined;
277
-
278
- // Create wrapper function that calls onCreateAnnotation with the annotator's motivation
279
- const onCreate = (...args: any[]) => props.onCreateAnnotation(annotator.motivation, ...args);
242
+ console.log('[UnifiedAnnotationsPanel] activeTab:', activeTab, 'annotator.motivation:', annotator.motivation, 'props.detectingMotivation:', props.detectingMotivation, 'isDetecting:', isDetecting, 'detectionProgress:', detectionProgress);
280
243
 
244
+ // Common props for all annotation panels
281
245
  const commonProps = {
282
246
  annotations,
283
- onAnnotationClick: props.onAnnotationClick,
284
- focusedAnnotationId: props.focusedAnnotationId,
285
- hoveredAnnotationId: props.hoveredAnnotationId,
286
- onAnnotationHover: props.onAnnotationHover,
287
- onDetect,
288
- onCreate,
289
247
  pendingAnnotation: props.pendingAnnotation,
290
248
  isDetecting,
291
249
  detectionProgress,
292
- annotateMode: props.annotateMode
250
+ annotateMode: props.annotateMode,
251
+ scrollToAnnotationId: props.scrollToAnnotationId,
252
+ onScrollCompleted: props.onScrollCompleted,
253
+ hoveredAnnotationId: props.hoveredAnnotationId
293
254
  };
294
255
 
295
256
  // Render specific panel based on activeTab with full type safety
@@ -297,8 +258,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
297
258
  return (
298
259
  <HighlightPanel
299
260
  {...commonProps}
300
- onAnnotationClick={commonProps.onAnnotationClick!}
301
- onCreate={onCreate}
302
261
  />
303
262
  );
304
263
  }
@@ -307,23 +266,15 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
307
266
  return (
308
267
  <ReferencesPanel
309
268
  annotations={commonProps.annotations}
310
- onAnnotationClick={commonProps.onAnnotationClick!}
311
- focusedAnnotationId={commonProps.focusedAnnotationId}
312
- hoveredAnnotationId={commonProps.hoveredAnnotationId}
313
- onAnnotationHover={commonProps.onAnnotationHover}
314
- onDetect={onDetect}
315
- onCreate={onCreate}
316
269
  pendingAnnotation={commonProps.pendingAnnotation}
317
270
  isDetecting={commonProps.isDetecting}
318
271
  detectionProgress={commonProps.detectionProgress}
319
272
  annotateMode={commonProps.annotateMode}
273
+ scrollToAnnotationId={commonProps.scrollToAnnotationId}
274
+ onScrollCompleted={commonProps.onScrollCompleted}
275
+ hoveredAnnotationId={commonProps.hoveredAnnotationId}
320
276
  allEntityTypes={props.allEntityTypes || []}
321
- onCancelDetection={props.onCancelDetection || (() => {})}
322
- onGenerateDocument={props.onGenerateDocument}
323
- onCreateDocument={props.onCreateDocument}
324
- onSearchDocuments={props.onSearchDocuments}
325
277
  generatingReferenceId={props.generatingReferenceId}
326
- mediaType={props.mediaType}
327
278
  referencedBy={props.referencedBy}
328
279
  referencedByLoading={props.referencedByLoading}
329
280
  Link={props.Link}
@@ -336,8 +287,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
336
287
  return (
337
288
  <AssessmentPanel
338
289
  {...commonProps}
339
- onAnnotationClick={commonProps.onAnnotationClick!}
340
- onCreate={onCreate}
341
290
  />
342
291
  );
343
292
  }
@@ -346,8 +295,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
346
295
  return (
347
296
  <CommentsPanel
348
297
  {...commonProps}
349
- onAnnotationClick={commonProps.onAnnotationClick!}
350
- onCreate={onCreate}
351
298
  />
352
299
  );
353
300
  }
@@ -356,8 +303,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
356
303
  return (
357
304
  <TaggingPanel
358
305
  {...commonProps}
359
- onAnnotationClick={commonProps.onAnnotationClick!}
360
- onCreate={onCreate}
361
306
  />
362
307
  );
363
308
  }