@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,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 { CommentEntry } from './CommentEntry';
8
- import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
9
10
  import { DetectSection } from './DetectSection';
10
11
  import { PanelHeader } from './PanelHeader';
11
12
  import './CommentsPanel.css';
@@ -38,45 +39,139 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
38
39
 
39
40
  interface CommentsPanelProps {
40
41
  annotations: Annotation[];
41
- onAnnotationClick: (annotation: Annotation) => void;
42
- onCreate: (commentText: string) => void;
43
- focusedAnnotationId: string | null;
44
- hoveredAnnotationId?: string | null;
45
- onAnnotationHover?: (annotationId: string | null) => void;
46
42
  pendingAnnotation: PendingAnnotation | null;
47
43
  annotateMode?: boolean;
48
- onDetect?: (instructions?: string, tone?: string) => void | Promise<void>;
49
44
  isDetecting?: boolean;
50
45
  detectionProgress?: {
51
46
  status: string;
52
47
  percentage?: number;
53
48
  message?: string;
54
49
  } | null;
50
+ scrollToAnnotationId?: string | null;
51
+ onScrollCompleted?: () => void;
52
+ hoveredAnnotationId?: string | null;
55
53
  }
56
54
 
55
+ /**
56
+ * Panel for managing comment annotations with text input
57
+ *
58
+ * @emits annotation:create - Create new comment annotation. Payload: { motivation: 'commenting', selector: Selector | Selector[], body: Body[] }
59
+ * @emits annotation:cancel-pending - Cancel pending comment annotation. Payload: undefined
60
+ * @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
61
+ */
57
62
  export function CommentsPanel({
58
63
  annotations,
59
- onAnnotationClick,
60
- onCreate,
61
- focusedAnnotationId,
62
- hoveredAnnotationId,
63
- onAnnotationHover,
64
64
  pendingAnnotation,
65
65
  annotateMode = true,
66
- onDetect,
67
66
  isDetecting = false,
68
67
  detectionProgress,
68
+ scrollToAnnotationId,
69
+ onScrollCompleted,
70
+ hoveredAnnotationId,
69
71
  }: CommentsPanelProps) {
70
72
  const t = useTranslations('CommentsPanel');
71
- const eventBus = useMakeMeaningEvents();
73
+ const eventBus = useEventBus();
72
74
  const [newCommentText, setNewCommentText] = useState('');
75
+ const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
76
+ const containerRef = useRef<HTMLDivElement>(null);
77
+
78
+ // Direct ref management - replace useAnnotationPanel hook
79
+ const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
80
+
81
+ // Sort annotations by their position in the resource
82
+ const sortedAnnotations = useMemo(() => {
83
+ return [...annotations].sort((a, b) => {
84
+ const aSelector = getTextPositionSelector(getTargetSelector(a.target));
85
+ const bSelector = getTextPositionSelector(getTargetSelector(b.target));
86
+ if (!aSelector || !bSelector) return 0;
87
+ return aSelector.start - bSelector.start;
88
+ });
89
+ }, [annotations]);
90
+
91
+ // Ref callback for entry components
92
+ const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
93
+ if (element) {
94
+ entryRefs.current.set(id, element);
95
+ } else {
96
+ entryRefs.current.delete(id);
97
+ }
98
+ }, []);
99
+
100
+ // Handle scrollToAnnotationId (click scroll)
101
+ useEffect(() => {
102
+ if (!scrollToAnnotationId) return;
103
+
104
+ const element = entryRefs.current.get(scrollToAnnotationId);
105
+
106
+ if (element && containerRef.current) {
107
+ // Calculate scroll position to center element in container
108
+ const elementTop = element.offsetTop;
109
+ const containerHeight = containerRef.current.clientHeight;
110
+ const elementHeight = element.offsetHeight;
111
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
112
+
113
+ // Scroll to center
114
+ containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
115
+
116
+ // Add pulse effect
117
+ element.classList.remove('semiont-annotation-pulse');
118
+ void element.offsetWidth; // Force reflow
119
+ element.classList.add('semiont-annotation-pulse');
120
+
121
+ // Notify completion
122
+ if (onScrollCompleted) {
123
+ onScrollCompleted();
124
+ }
125
+ }
126
+ }, [scrollToAnnotationId]);
127
+
128
+ // Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
129
+ useEffect(() => {
130
+ if (!hoveredAnnotationId) return;
131
+
132
+ const element = entryRefs.current.get(hoveredAnnotationId);
133
+
134
+ if (!element || !containerRef.current) return;
135
+
136
+ const container = containerRef.current;
137
+ const elementRect = element.getBoundingClientRect();
138
+ const containerRect = container.getBoundingClientRect();
139
+
140
+ // Only scroll if element is not fully visible
141
+ const isVisible =
142
+ elementRect.top >= containerRect.top &&
143
+ elementRect.bottom <= containerRect.bottom;
144
+
145
+ if (!isVisible) {
146
+ const elementTop = element.offsetTop;
147
+ const containerHeight = container.clientHeight;
148
+ const elementHeight = element.offsetHeight;
149
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
150
+
151
+ container.scrollTo({ top: scrollTo, behavior: 'smooth' });
152
+ }
153
+
154
+ // Pulse effect is handled by isHovered prop on CommentEntry
155
+ }, [hoveredAnnotationId]);
156
+
157
+ // Subscribe to click events - update focused state
158
+ // Event handler for annotation clicks (extracted to avoid inline arrow function)
159
+ const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
160
+ setFocusedAnnotationId(annotationId);
161
+ setTimeout(() => setFocusedAnnotationId(null), 3000);
162
+ }, []);
73
163
 
74
- const { sortedAnnotations, containerRef, handleAnnotationRef } =
75
- useAnnotationPanel(annotations, hoveredAnnotationId);
164
+ useEventSubscriptions({
165
+ 'annotation:click': handleAnnotationClick,
166
+ });
76
167
 
77
168
  const handleSaveNewComment = () => {
78
- if (newCommentText.trim()) {
79
- onCreate(newCommentText);
169
+ if (newCommentText.trim() && pendingAnnotation) {
170
+ eventBus.emit('annotation:create', {
171
+ motivation: 'commenting',
172
+ selector: pendingAnnotation.selector,
173
+ body: [{ type: 'TextualBody', value: newCommentText, purpose: 'commenting' }],
174
+ });
80
175
  setNewCommentText('');
81
176
  }
82
177
  };
@@ -87,14 +182,14 @@ export function CommentsPanel({
87
182
 
88
183
  const handleEscape = (e: KeyboardEvent) => {
89
184
  if (e.key === 'Escape') {
90
- eventBus.emit('ui:annotation:cancel-pending');
185
+ eventBus.emit('annotation:cancel-pending', undefined);
91
186
  setNewCommentText('');
92
187
  }
93
188
  };
94
189
 
95
190
  document.addEventListener('keydown', handleEscape);
96
191
  return () => document.removeEventListener('keydown', handleEscape);
97
- }, [pendingAnnotation, eventBus]);
192
+ }, [pendingAnnotation]);
98
193
 
99
194
  return (
100
195
  <div className="semiont-panel">
@@ -129,7 +224,7 @@ export function CommentsPanel({
129
224
  <div className="semiont-annotation-prompt__actions">
130
225
  <button
131
226
  onClick={() => {
132
- eventBus.emit('ui:annotation:cancel-pending');
227
+ eventBus.emit('annotation:cancel-pending', undefined);
133
228
  setNewCommentText('');
134
229
  }}
135
230
  className="semiont-button semiont-button--secondary"
@@ -153,12 +248,11 @@ export function CommentsPanel({
153
248
  {/* Scrollable content area */}
154
249
  <div ref={containerRef} className="semiont-panel__content">
155
250
  {/* Detection Section - only in Annotate mode and for text resources */}
156
- {annotateMode && onDetect && (
251
+ {annotateMode && (
157
252
  <DetectSection
158
253
  annotationType="comment"
159
254
  isDetecting={isDetecting}
160
255
  detectionProgress={detectionProgress}
161
- onDetect={onDetect}
162
256
  />
163
257
  )}
164
258
 
@@ -174,10 +268,9 @@ export function CommentsPanel({
174
268
  key={comment.id}
175
269
  comment={comment}
176
270
  isFocused={comment.id === focusedAnnotationId}
177
- onClick={() => onAnnotationClick(comment)}
178
- onCommentRef={handleAnnotationRef}
179
- {...(onAnnotationHover && { onCommentHover: onAnnotationHover })}
271
+ isHovered={comment.id === hoveredAnnotationId}
180
272
  annotateMode={annotateMode}
273
+ ref={(el) => setEntryRef(comment.id, el)}
181
274
  />
182
275
  ))
183
276
  )}
@@ -320,6 +320,7 @@
320
320
  display: flex;
321
321
  flex-direction: column;
322
322
  gap: 0.5rem;
323
+ position: relative;
323
324
  }
324
325
 
325
326
  .semiont-detection-progress__message {
@@ -329,13 +330,47 @@
329
330
  font-size: 0.875rem;
330
331
  color: var(--semiont-color-gray-900);
331
332
  font-weight: 500;
332
- padding: 0.75rem 1rem;
333
+ padding: 0.75rem 2.5rem 0.75rem 1rem; /* Extra right padding for close button */
333
334
  border-radius: 0.5rem;
334
335
  background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(37, 99, 235, 0.15));
335
336
  border: 2px solid rgba(59, 130, 246, 0.3);
336
337
  animation: semiont-detection-pulse 2s ease-in-out infinite;
337
338
  }
338
339
 
340
+ .semiont-detection-progress__close {
341
+ position: absolute;
342
+ top: 0.5rem;
343
+ right: 0.5rem;
344
+ width: 1.5rem;
345
+ height: 1.5rem;
346
+ border: none;
347
+ background-color: rgba(0, 0, 0, 0.1);
348
+ color: var(--semiont-color-gray-700);
349
+ font-size: 1.25rem;
350
+ line-height: 1;
351
+ border-radius: 0.25rem;
352
+ cursor: pointer;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ transition: all 0.2s ease;
357
+ }
358
+
359
+ .semiont-detection-progress__close:hover {
360
+ background-color: rgba(0, 0, 0, 0.2);
361
+ color: var(--semiont-color-gray-900);
362
+ }
363
+
364
+ [data-theme="dark"] .semiont-detection-progress__close {
365
+ background-color: rgba(255, 255, 255, 0.1);
366
+ color: var(--semiont-color-gray-300);
367
+ }
368
+
369
+ [data-theme="dark"] .semiont-detection-progress__close:hover {
370
+ background-color: rgba(255, 255, 255, 0.2);
371
+ color: var(--semiont-color-gray-100);
372
+ }
373
+
339
374
  [data-theme="dark"] .semiont-detection-progress__message {
340
375
  color: var(--semiont-color-gray-100);
341
376
  background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.25));
@@ -1,7 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect, useCallback } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
+ import { useEventBus } from '../../../contexts/EventBusContext';
6
+ import type { Motivation } from '@semiont/api-client';
5
7
  import './DetectSection.css';
6
8
 
7
9
  interface DetectSectionProps {
@@ -13,7 +15,6 @@ interface DetectSectionProps {
13
15
  message?: string;
14
16
  requestParams?: Array<{ label: string; value: string }>;
15
17
  } | null | undefined;
16
- onDetect: (instructions?: string, tone?: string, density?: number) => void | Promise<void>;
17
18
  }
18
19
 
19
20
  // Color schemes are now handled via CSS data attributes
@@ -26,19 +27,24 @@ interface DetectSectionProps {
26
27
  * - Optional tone selector (for comments)
27
28
  * - Detect button with sparkle animation
28
29
  * - Progress display during detection
30
+ *
31
+ * @emits detection:start - Start detection for annotation type. Payload: { motivation: Motivation, options: { instructions?: string, tone?: string, density?: number } }
32
+ * @emits detection:dismiss-progress - Dismiss the detection progress display
29
33
  */
30
34
  export function DetectSection({
31
35
  annotationType,
32
36
  isDetecting,
33
37
  detectionProgress,
34
- onDetect
35
38
  }: DetectSectionProps) {
39
+
36
40
  const panelName = annotationType === 'highlight' ? 'HighlightPanel' :
37
41
  annotationType === 'assessment' ? 'AssessmentPanel' :
38
42
  'CommentsPanel';
39
43
  const t = useTranslations(panelName);
44
+ const eventBus = useEventBus();
40
45
  const [instructions, setInstructions] = useState('');
41
- const [tone, setTone] = useState('');
46
+ type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
47
+ const [tone, setTone] = useState<ToneValue>('');
42
48
  // Default density depends on annotation type
43
49
  const defaultDensity = annotationType === 'comment' ? 5 : annotationType === 'assessment' ? 4 : annotationType === 'highlight' ? 5 : 5;
44
50
  const [density, setDensity] = useState(defaultDensity);
@@ -57,16 +63,31 @@ export function DetectSection({
57
63
  localStorage.setItem(`detect-section-expanded-${annotationType}`, String(isExpanded));
58
64
  }, [isExpanded, annotationType]);
59
65
 
60
- const handleDetect = () => {
61
- onDetect(
62
- instructions.trim() || undefined,
63
- (annotationType === 'comment' || annotationType === 'assessment') && tone ? tone : undefined,
64
- (annotationType === 'comment' || annotationType === 'assessment' || annotationType === 'highlight') && useDensity ? density : undefined
65
- );
66
+ const handleDetect = useCallback(() => {
67
+ // Map annotation type to motivation
68
+ const motivation: Motivation =
69
+ annotationType === 'highlight' ? 'highlighting' :
70
+ annotationType === 'assessment' ? 'assessing' :
71
+ 'commenting';
72
+
73
+ // Emit detection:start event with options
74
+ eventBus.emit('detection:start', {
75
+ motivation,
76
+ options: {
77
+ instructions: instructions.trim() || undefined,
78
+ tone: (annotationType === 'comment' || annotationType === 'assessment') && tone ? tone : undefined,
79
+ density: (annotationType === 'comment' || annotationType === 'assessment' || annotationType === 'highlight') && useDensity ? density : undefined,
80
+ },
81
+ });
82
+
66
83
  setInstructions('');
67
84
  setTone('');
68
85
  // Don't reset density/useDensity - persist across detections
69
- };
86
+ }, [annotationType, instructions, tone, useDensity, density]); // eventBus is stable singleton - never in deps
87
+
88
+ const handleDismissProgress = useCallback(() => {
89
+ eventBus.emit('detection:dismiss-progress', undefined);
90
+ }, []); // eventBus is stable singleton - never in deps
70
91
 
71
92
  return (
72
93
  <div className="semiont-panel__section">
@@ -91,7 +112,8 @@ export function DetectSection({
91
112
  data-detecting={isDetecting && detectionProgress ? 'true' : 'false'}
92
113
  data-type={annotationType}
93
114
  >
94
- {!isDetecting && !detectionProgress && (
115
+ {/* Show form when NOT detecting and NO progress to display */}
116
+ {!detectionProgress && (
95
117
  <>
96
118
  <div className="semiont-form-field">
97
119
  <label className="semiont-form-field__label">
@@ -118,7 +140,7 @@ export function DetectSection({
118
140
  </label>
119
141
  <select
120
142
  value={tone}
121
- onChange={(e) => setTone(e.target.value)}
143
+ onChange={(e) => setTone(e.target.value as ToneValue)}
122
144
  className="semiont-select"
123
145
  >
124
146
  <option value="">Default</option>
@@ -194,8 +216,8 @@ export function DetectSection({
194
216
  </>
195
217
  )}
196
218
 
197
- {/* Detection Progress */}
198
- {isDetecting && detectionProgress && (
219
+ {/* Detection Progress - show whenever we have progress (during or after detection) */}
220
+ {detectionProgress && (
199
221
  <div className="semiont-detection-progress" data-type={annotationType}>
200
222
  {/* Request Parameters */}
201
223
  {detectionProgress.requestParams && detectionProgress.requestParams.length > 0 && (
@@ -214,6 +236,18 @@ export function DetectSection({
214
236
  <span className="semiont-detection-progress__icon">✨</span>
215
237
  <span>{detectionProgress.message}</span>
216
238
  </div>
239
+ {/* Close button - shown after detection completes (when not actively detecting) */}
240
+ {!isDetecting && (
241
+ <button
242
+ onClick={handleDismissProgress}
243
+ className="semiont-detection-progress__close"
244
+ aria-label={t('closeProgress')}
245
+ title={t('closeProgress')}
246
+ type="button"
247
+ >
248
+ ×
249
+ </button>
250
+ )}
217
251
  </div>
218
252
  </div>
219
253
  )}
@@ -1,17 +1,16 @@
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
+ import { useEventBus } from '../../../contexts/EventBusContext';
6
7
 
7
8
  type Annotation = components['schemas']['Annotation'];
8
9
 
9
10
  interface HighlightEntryProps {
10
11
  highlight: Annotation;
11
12
  isFocused: boolean;
12
- onClick: () => void;
13
- onHighlightRef: (highlightId: string, el: HTMLElement | null) => void;
14
- onHighlightHover?: (highlightId: string | null) => void;
13
+ isHovered?: boolean;
15
14
  }
16
15
 
17
16
  function formatRelativeTime(isoString: string): string {
@@ -31,41 +30,34 @@ function formatRelativeTime(isoString: string): string {
31
30
  return date.toLocaleDateString();
32
31
  }
33
32
 
34
- export function HighlightEntry({
35
- highlight,
36
- isFocused,
37
- onClick,
38
- onHighlightRef,
39
- onHighlightHover,
40
- }: HighlightEntryProps) {
41
- const highlightRef = useRef<HTMLDivElement>(null);
42
-
43
- // Register ref with parent
44
- useEffect(() => {
45
- onHighlightRef(highlight.id, highlightRef.current);
46
- return () => {
47
- onHighlightRef(highlight.id, null);
48
- };
49
- }, [highlight.id, onHighlightRef]);
50
-
51
- // Scroll to highlight when focused
52
- useEffect(() => {
53
- if (isFocused && highlightRef.current) {
54
- highlightRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
55
- }
56
- }, [isFocused]);
33
+ export const HighlightEntry = forwardRef<HTMLDivElement, HighlightEntryProps>(
34
+ function HighlightEntry(
35
+ {
36
+ highlight,
37
+ isFocused,
38
+ isHovered = false,
39
+ },
40
+ ref
41
+ ) {
42
+ const eventBus = useEventBus();
57
43
 
58
44
  const selectedText = getAnnotationExactText(highlight);
59
45
 
60
46
  return (
61
47
  <div
62
- ref={highlightRef}
63
- className="semiont-annotation-entry"
48
+ ref={ref}
49
+ className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
64
50
  data-type="highlight"
65
51
  data-focused={isFocused ? 'true' : 'false'}
66
- onClick={onClick}
67
- onMouseEnter={() => onHighlightHover?.(highlight.id)}
68
- onMouseLeave={() => onHighlightHover?.(null)}
52
+ onClick={() => {
53
+ eventBus.emit('annotation:click', { annotationId: highlight.id, motivation: highlight.motivation });
54
+ }}
55
+ onMouseEnter={() => {
56
+ eventBus.emit('annotation:hover', { annotationId: highlight.id });
57
+ }}
58
+ onMouseLeave={() => {
59
+ eventBus.emit('annotation:hover', { annotationId: null });
60
+ }}
69
61
  >
70
62
  {/* Highlighted text */}
71
63
  {selectedText && (
@@ -80,4 +72,4 @@ export function HighlightEntry({
80
72
  </div>
81
73
  </div>
82
74
  );
83
- }
75
+ });
@@ -1,10 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { useEffect } from 'react';
3
+ import { useEffect, useState, 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 { HighlightEntry } from './HighlightEntry';
7
- import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
8
10
  import { DetectSection } from './DetectSection';
9
11
  import { PanelHeader } from './PanelHeader';
10
12
  import './HighlightPanel.css';
@@ -20,12 +22,6 @@ interface PendingAnnotation {
20
22
 
21
23
  interface HighlightPanelProps {
22
24
  annotations: Annotation[];
23
- onAnnotationClick: (annotation: Annotation) => void;
24
- focusedAnnotationId: string | null;
25
- hoveredAnnotationId?: string | null;
26
- onAnnotationHover?: (annotationId: string | null) => void;
27
- onDetect?: (instructions?: string) => void | Promise<void>;
28
- onCreate: (selector: Selector | Selector[]) => void;
29
25
  pendingAnnotation: PendingAnnotation | null;
30
26
  isDetecting?: boolean;
31
27
  detectionProgress?: {
@@ -34,34 +30,115 @@ interface HighlightPanelProps {
34
30
  message?: string;
35
31
  } | null;
36
32
  annotateMode?: boolean;
33
+ scrollToAnnotationId?: string | null;
34
+ onScrollCompleted?: () => void;
35
+ hoveredAnnotationId?: string | null;
37
36
  }
38
37
 
38
+ /**
39
+ * Panel for managing highlight annotations with auto-creation
40
+ *
41
+ * @emits annotation:create - Create new highlight annotation (auto-triggered). Payload: { motivation: 'highlighting', selector: Selector | Selector[], body: Body[] }
42
+ * @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
43
+ */
39
44
  export function HighlightPanel({
40
45
  annotations,
41
- onAnnotationClick,
42
- focusedAnnotationId,
43
- hoveredAnnotationId,
44
- onAnnotationHover,
45
- onDetect,
46
- onCreate,
47
46
  pendingAnnotation,
48
47
  isDetecting = false,
49
48
  detectionProgress,
50
49
  annotateMode = true,
50
+ scrollToAnnotationId,
51
+ onScrollCompleted,
52
+ hoveredAnnotationId,
51
53
  }: HighlightPanelProps) {
54
+
52
55
  const t = useTranslations('HighlightPanel');
56
+ const eventBus = useEventBus();
57
+ const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
58
+ const containerRef = useRef<HTMLDivElement>(null);
59
+
60
+ // Direct ref management
61
+ const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
62
+
63
+ // Sort annotations by their position in the resource
64
+ const sortedAnnotations = useMemo(() => {
65
+ return [...annotations].sort((a, b) => {
66
+ const aSelector = getTextPositionSelector(getTargetSelector(a.target));
67
+ const bSelector = getTextPositionSelector(getTargetSelector(b.target));
68
+ if (!aSelector || !bSelector) return 0;
69
+ return aSelector.start - bSelector.start;
70
+ });
71
+ }, [annotations]);
72
+
73
+ // Ref callback for entry components
74
+ const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
75
+ if (element) {
76
+ entryRefs.current.set(id, element);
77
+ } else {
78
+ entryRefs.current.delete(id);
79
+ }
80
+ }, []);
81
+
82
+ // Handle scrollToAnnotationId (click scroll)
83
+ useEffect(() => {
84
+ if (!scrollToAnnotationId) return;
85
+ const element = entryRefs.current.get(scrollToAnnotationId);
86
+ if (element && containerRef.current) {
87
+ const elementTop = element.offsetTop;
88
+ const containerHeight = containerRef.current.clientHeight;
89
+ const elementHeight = element.offsetHeight;
90
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
91
+ containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
92
+ element.classList.remove('semiont-annotation-pulse');
93
+ void element.offsetWidth;
94
+ element.classList.add('semiont-annotation-pulse');
95
+ if (onScrollCompleted) onScrollCompleted();
96
+ }
97
+ }, [scrollToAnnotationId]);
98
+
99
+ // Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
100
+ useEffect(() => {
101
+ if (!hoveredAnnotationId) return;
102
+ const element = entryRefs.current.get(hoveredAnnotationId);
103
+ if (!element || !containerRef.current) return;
104
+
105
+ const container = containerRef.current;
106
+ const elementRect = element.getBoundingClientRect();
107
+ const containerRect = container.getBoundingClientRect();
108
+ const isVisible = elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
109
+ if (!isVisible) {
110
+ const elementTop = element.offsetTop;
111
+ const containerHeight = container.clientHeight;
112
+ const elementHeight = element.offsetHeight;
113
+ const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
114
+ container.scrollTo({ top: scrollTo, behavior: 'smooth' });
115
+ }
116
+
117
+ // Pulse effect is handled by isHovered prop on HighlightEntry
118
+ }, [hoveredAnnotationId]);
119
+
120
+ // Subscribe to click events - update focused state
121
+ // Event handler for annotation clicks (extracted to avoid inline arrow function)
122
+ const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
123
+ setFocusedAnnotationId(annotationId);
124
+ setTimeout(() => setFocusedAnnotationId(null), 3000);
125
+ }, []);
53
126
 
54
- const { sortedAnnotations, containerRef, handleAnnotationRef } =
55
- useAnnotationPanel(annotations, hoveredAnnotationId);
127
+ useEventSubscriptions({
128
+ 'annotation:click': handleAnnotationClick,
129
+ });
56
130
 
57
131
  // Highlights auto-create: when pendingAnnotation arrives with highlighting motivation,
58
- // immediately call onCreate without showing a form
132
+ // immediately emit annotation:create event
59
133
  useEffect(() => {
60
134
  if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
61
- onCreate(pendingAnnotation.selector);
135
+ eventBus.emit('annotation:create', {
136
+ motivation: 'highlighting',
137
+ selector: pendingAnnotation.selector,
138
+ body: [],
139
+ });
62
140
  }
63
- // eslint-disable-next-line react-hooks/exhaustive-deps
64
- }, [pendingAnnotation]); // Only depend on pendingAnnotation, not onCreate (which is recreated on every render)
141
+ }, [pendingAnnotation]);
65
142
 
66
143
  return (
67
144
  <div className="semiont-panel">
@@ -70,12 +147,11 @@ export function HighlightPanel({
70
147
  {/* Scrollable content area */}
71
148
  <div ref={containerRef} className="semiont-panel__content">
72
149
  {/* Detection Section - only in Annotate mode and for text resources */}
73
- {annotateMode && onDetect && (
150
+ {annotateMode && (
74
151
  <DetectSection
75
152
  annotationType="highlight"
76
153
  isDetecting={isDetecting}
77
154
  detectionProgress={detectionProgress}
78
- onDetect={onDetect}
79
155
  />
80
156
  )}
81
157
 
@@ -91,9 +167,8 @@ export function HighlightPanel({
91
167
  key={highlight.id}
92
168
  highlight={highlight}
93
169
  isFocused={highlight.id === focusedAnnotationId}
94
- onClick={() => onAnnotationClick(highlight)}
95
- onHighlightRef={handleAnnotationRef}
96
- {...(onAnnotationHover && { onHighlightHover: onAnnotationHover })}
170
+ isHovered={highlight.id === hoveredAnnotationId}
171
+ ref={(el) => setEntryRef(highlight.id, el)}
97
172
  />
98
173
  ))
99
174
  )}