@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,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
+ });
@@ -1,11 +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 { useMakeMeaningEvents } from '../../../contexts/MakeMeaningEventBusContext';
5
+ import { useEventBus } from '../../../contexts/EventBusContext';
6
+ import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
6
7
  import type { components, Selector } from '@semiont/api-client';
8
+ import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
7
9
  import { TagEntry } from './TagEntry';
8
- import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
9
10
  import { PanelHeader } from './PanelHeader';
10
11
  import { getAllTagSchemas } from '../../../lib/tag-schemas';
11
12
  import './TaggingPanel.css';
@@ -38,13 +39,7 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
38
39
 
39
40
  interface TaggingPanelProps {
40
41
  annotations: Annotation[];
41
- onAnnotationClick: (annotation: Annotation) => void;
42
- focusedAnnotationId: string | null;
43
- hoveredAnnotationId?: string | null;
44
- onAnnotationHover?: (annotationId: string | null) => void;
45
42
  annotateMode?: boolean;
46
- onDetect?: (schemaId: string, categories: string[]) => void | Promise<void>;
47
- onCreate: (schemaId: string, category: string) => void | Promise<void>;
48
43
  isDetecting?: boolean;
49
44
  detectionProgress?: {
50
45
  status: string;
@@ -56,25 +51,35 @@ interface TaggingPanelProps {
56
51
  requestParams?: Array<{ label: string; value: string }>;
57
52
  } | null;
58
53
  pendingAnnotation: PendingAnnotation | null;
54
+ scrollToAnnotationId?: string | null;
55
+ onScrollCompleted?: () => void;
56
+ hoveredAnnotationId?: string | null;
59
57
  }
60
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
+ */
61
67
  export function TaggingPanel({
62
68
  annotations,
63
- onAnnotationClick,
64
- focusedAnnotationId,
65
- hoveredAnnotationId,
66
- onAnnotationHover,
67
69
  annotateMode = true,
68
- onDetect,
69
- onCreate,
70
70
  isDetecting = false,
71
71
  detectionProgress,
72
- pendingAnnotation
72
+ pendingAnnotation,
73
+ scrollToAnnotationId,
74
+ onScrollCompleted,
75
+ hoveredAnnotationId,
73
76
  }: TaggingPanelProps) {
74
77
  const t = useTranslations('TaggingPanel');
75
- const eventBus = useMakeMeaningEvents();
78
+ const eventBus = useEventBus();
76
79
  const [selectedSchemaId, setSelectedSchemaId] = useState<string>('legal-irac');
77
80
  const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
81
+ const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
82
+ const containerRef = useRef<HTMLDivElement>(null);
78
83
 
79
84
  // Collapsible detection section state - load from localStorage, default expanded
80
85
  const [isDetectExpanded, setIsDetectExpanded] = useState(() => {
@@ -89,8 +94,76 @@ export function TaggingPanel({
89
94
  localStorage.setItem('detect-section-expanded-tag', String(isDetectExpanded));
90
95
  }, [isDetectExpanded]);
91
96
 
92
- const { sortedAnnotations, containerRef, handleAnnotationRef } =
93
- 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]);
94
167
 
95
168
  const schemas = getAllTagSchemas();
96
169
  const selectedSchema = schemas.find(s => s.id === selectedSchemaId);
@@ -121,8 +194,14 @@ export function TaggingPanel({
121
194
  };
122
195
 
123
196
  const handleDetect = () => {
124
- if (onDetect && selectedCategories.size > 0) {
125
- 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
+ });
126
205
  setSelectedCategories(new Set()); // Reset after detection
127
206
  }
128
207
  };
@@ -133,13 +212,13 @@ export function TaggingPanel({
133
212
 
134
213
  const handleEscape = (e: KeyboardEvent) => {
135
214
  if (e.key === 'Escape') {
136
- eventBus.emit('ui:annotation:cancel-pending');
215
+ eventBus.emit('annotation:cancel-pending', undefined);
137
216
  }
138
217
  };
139
218
 
140
219
  document.addEventListener('keydown', handleEscape);
141
220
  return () => document.removeEventListener('keydown', handleEscape);
142
- }, [pendingAnnotation, eventBus]);
221
+ }, [pendingAnnotation]);
143
222
 
144
223
  // Color schemes are now handled via CSS data attributes
145
224
 
@@ -194,8 +273,18 @@ export function TaggingPanel({
194
273
  <select
195
274
  className="semiont-select"
196
275
  onChange={(e) => {
197
- if (e.target.value) {
198
- 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' as const,
283
+ value: e.target.value,
284
+ purpose: 'tagging' as const,
285
+ },
286
+ ],
287
+ });
199
288
  }
200
289
  }}
201
290
  defaultValue=""
@@ -211,7 +300,7 @@ export function TaggingPanel({
211
300
  {/* Cancel button */}
212
301
  <div className="semiont-annotation-prompt__footer">
213
302
  <button
214
- onClick={() => eventBus.emit('ui:annotation:cancel-pending')}
303
+ onClick={() => eventBus.emit('annotation:cancel-pending', undefined)}
215
304
  className="semiont-button semiont-button--secondary"
216
305
  data-type="tag"
217
306
  >
@@ -222,7 +311,7 @@ export function TaggingPanel({
222
311
  )}
223
312
 
224
313
  {/* Detection Section - only in Annotate mode */}
225
- {annotateMode && onDetect && (
314
+ {annotateMode && (
226
315
  <div className="semiont-panel__section">
227
316
  <button
228
317
  onClick={() => setIsDetectExpanded(!isDetectExpanded)}
@@ -390,9 +479,8 @@ export function TaggingPanel({
390
479
  key={tag.id}
391
480
  tag={tag}
392
481
  isFocused={tag.id === focusedAnnotationId}
393
- onClick={() => onAnnotationClick(tag)}
394
- onTagRef={handleAnnotationRef}
395
- {...(onAnnotationHover && { onTagHover: onAnnotationHover })}
482
+ isHovered={tag.id === hoveredAnnotationId}
483
+ ref={(el) => setEntryRef(tag.id, el)}
396
484
  />
397
485
  ))
398
486
  )}
@@ -5,8 +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
- import { supportsDetection } from '../../../lib/resource-utils';
10
8
  import { StatisticsPanel } from './StatisticsPanel';
11
9
  import { HighlightPanel } from './HighlightPanel';
12
10
  import { ReferencesPanel } from './ReferencesPanel';
@@ -29,13 +27,13 @@ interface PendingAnnotation {
29
27
  const TAB_ORDER: TabKey[] = ['statistics', 'reference', 'highlight', 'assessment', 'comment', 'tag'];
30
28
 
31
29
  /**
32
- * Simplified UnifiedAnnotationsPanel using Annotator abstraction
30
+ * Simplified UnifiedAnnotationsPanel using event-driven architecture
33
31
  *
34
32
  * Key simplifications:
35
33
  * - Single annotations array (grouped internally by motivation)
36
34
  * - Single focusedAnnotationId (motivation-agnostic)
37
- * - Single hoveredAnnotationId (motivation-agnostic)
38
- * - 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)
39
37
  */
40
38
  interface UnifiedAnnotationsPanelProps {
41
39
  // All annotations (grouped internally by motivation)
@@ -44,29 +42,6 @@ interface UnifiedAnnotationsPanelProps {
44
42
  // Annotators (pure static data - no handlers)
45
43
  annotators: Record<string, Annotator>;
46
44
 
47
- // Detection context (passed separately so annotators remain stable)
48
- detectionContext?: {
49
- client: any;
50
- rUri: any;
51
- setDetectingMotivation: (motivation: Motivation | null) => void;
52
- setMotivationDetectionProgress: (progress: any) => void;
53
- detectionStreamRef: any;
54
- cacheManager: any;
55
- showSuccess: (message: string) => void;
56
- showError: (message: string) => void;
57
- };
58
-
59
- // Unified state (motivation-agnostic)
60
- focusedAnnotationId: string | null;
61
- hoveredAnnotationId?: string | null;
62
-
63
- // Shared UI handlers (same for all annotation types)
64
- onAnnotationClick: (annotation: Annotation) => void;
65
- onAnnotationHover?: (annotationId: string | null) => void;
66
-
67
- // Single generic creation handler
68
- onCreateAnnotation: (motivation: Motivation, ...args: any[]) => void;
69
-
70
45
  // Mode
71
46
  annotateMode?: boolean;
72
47
 
@@ -85,20 +60,23 @@ interface UnifiedAnnotationsPanelProps {
85
60
  // Unified pending annotation (for creating new annotations)
86
61
  pendingAnnotation: PendingAnnotation | null;
87
62
 
88
- // Reference-specific props (TODO: refactor these into annotator handlers)
63
+ // Reference-specific props
89
64
  allEntityTypes?: string[];
90
65
  generatingReferenceId?: string | null;
91
- onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
92
- onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
93
- onSearchDocuments?: (referenceId: string, searchTerm: string) => void;
94
- onCancelDetection?: () => void;
95
- mediaType?: string;
96
66
  referencedBy?: any[];
97
67
  referencedByLoading?: boolean;
98
68
 
99
69
  // Resource context
100
70
  resourceId?: string;
101
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;
102
80
 
103
81
  // Routing
104
82
  Link: React.ComponentType<LinkComponentProps>;
@@ -156,23 +134,14 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
156
134
  localStorage.setItem(storageKey, activeTab);
157
135
  }, [activeTab, props.resourceId]);
158
136
 
159
- // Auto-switch to the appropriate tab when an annotation is focused
160
- useEffect(() => {
161
- if (!props.focusedAnnotationId) return;
162
-
163
- // Find which annotation type this focused annotation belongs to
164
- const focusedAnnotation = props.annotations.find(ann => ann.id === props.focusedAnnotationId);
165
- if (!focusedAnnotation) return;
166
137
 
167
- // Determine the annotator key for this annotation
168
- for (const [key, annotator] of Object.entries(props.annotators)) {
169
- if (annotator.matchesAnnotation(focusedAnnotation)) {
170
- setActiveTab(key as TabKey);
171
- break;
172
- }
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);
173
143
  }
174
- // eslint-disable-next-line react-hooks/exhaustive-deps
175
- }, [props.focusedAnnotationId]);
144
+ }, [props.initialTabGeneration]); // Only watch generation counter, not the tab itself
176
145
 
177
146
  // Auto-switch to the appropriate tab when creating a new annotation
178
147
  useEffect(() => {
@@ -266,38 +235,22 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
266
235
 
267
236
  const annotations = grouped[activeTab] || [];
268
237
  const isDetecting = props.detectingMotivation === annotator.motivation;
269
- 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;
270
241
 
271
- // Common props for all annotation panels
272
- // Create detection handler on-demand if:
273
- // 1. Annotator supports detection (has detection config)
274
- // 2. Detection context is provided (API client, state handlers)
275
- // 3. API client is available (not undefined/null)
276
- // 4. Resource supports detection (is a text/* media type)
277
- const onDetect = (
278
- annotator.detection &&
279
- props.detectionContext &&
280
- props.detectionContext.client &&
281
- supportsDetection(props.mediaType)
282
- )
283
- ? createDetectionHandler(annotator, props.detectionContext)
284
- : undefined;
285
-
286
- // Create wrapper function that calls onCreateAnnotation with the annotator's motivation
287
- 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);
288
243
 
244
+ // Common props for all annotation panels
289
245
  const commonProps = {
290
246
  annotations,
291
- onAnnotationClick: props.onAnnotationClick,
292
- focusedAnnotationId: props.focusedAnnotationId,
293
- hoveredAnnotationId: props.hoveredAnnotationId,
294
- onAnnotationHover: props.onAnnotationHover,
295
- onDetect,
296
- onCreate,
297
247
  pendingAnnotation: props.pendingAnnotation,
298
248
  isDetecting,
299
249
  detectionProgress,
300
- annotateMode: props.annotateMode
250
+ annotateMode: props.annotateMode,
251
+ scrollToAnnotationId: props.scrollToAnnotationId,
252
+ onScrollCompleted: props.onScrollCompleted,
253
+ hoveredAnnotationId: props.hoveredAnnotationId
301
254
  };
302
255
 
303
256
  // Render specific panel based on activeTab with full type safety
@@ -305,8 +258,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
305
258
  return (
306
259
  <HighlightPanel
307
260
  {...commonProps}
308
- onAnnotationClick={commonProps.onAnnotationClick!}
309
- onCreate={onCreate}
310
261
  />
311
262
  );
312
263
  }
@@ -315,21 +266,14 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
315
266
  return (
316
267
  <ReferencesPanel
317
268
  annotations={commonProps.annotations}
318
- onAnnotationClick={commonProps.onAnnotationClick!}
319
- focusedAnnotationId={commonProps.focusedAnnotationId}
320
- hoveredAnnotationId={commonProps.hoveredAnnotationId}
321
- onAnnotationHover={commonProps.onAnnotationHover}
322
- onDetect={onDetect}
323
- onCreate={onCreate}
324
269
  pendingAnnotation={commonProps.pendingAnnotation}
325
270
  isDetecting={commonProps.isDetecting}
326
271
  detectionProgress={commonProps.detectionProgress}
327
272
  annotateMode={commonProps.annotateMode}
273
+ scrollToAnnotationId={commonProps.scrollToAnnotationId}
274
+ onScrollCompleted={commonProps.onScrollCompleted}
275
+ hoveredAnnotationId={commonProps.hoveredAnnotationId}
328
276
  allEntityTypes={props.allEntityTypes || []}
329
- onCancelDetection={props.onCancelDetection || (() => {})}
330
- onGenerateDocument={props.onGenerateDocument}
331
- onCreateDocument={props.onCreateDocument}
332
- onSearchDocuments={props.onSearchDocuments}
333
277
  generatingReferenceId={props.generatingReferenceId}
334
278
  referencedBy={props.referencedBy}
335
279
  referencedByLoading={props.referencedByLoading}
@@ -343,8 +287,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
343
287
  return (
344
288
  <AssessmentPanel
345
289
  {...commonProps}
346
- onAnnotationClick={commonProps.onAnnotationClick!}
347
- onCreate={onCreate}
348
290
  />
349
291
  );
350
292
  }
@@ -353,8 +295,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
353
295
  return (
354
296
  <CommentsPanel
355
297
  {...commonProps}
356
- onAnnotationClick={commonProps.onAnnotationClick!}
357
- onCreate={onCreate}
358
298
  />
359
299
  );
360
300
  }
@@ -363,8 +303,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
363
303
  return (
364
304
  <TaggingPanel
365
305
  {...commonProps}
366
- onAnnotationClick={commonProps.onAnnotationClick!}
367
- onCreate={onCreate}
368
306
  />
369
307
  );
370
308
  }