@semiont/react-ui 0.2.34-build.91 → 0.2.34-build.92

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 (195) hide show
  1. package/dist/{EventBusContext-BmzEcGHZ.d.mts → EventBusContext-DMI4uwYk.d.mts} +2 -2
  2. package/dist/{PdfAnnotationCanvas.client-VLNA5O5M.mjs → PdfAnnotationCanvas.client-HNYRKFDS.mjs} +10 -9
  3. package/dist/PdfAnnotationCanvas.client-HNYRKFDS.mjs.map +1 -0
  4. package/dist/{ar-4ZEORRW2.mjs → ar-MDB7HC5S.mjs} +45 -49
  5. package/dist/ar-MDB7HC5S.mjs.map +1 -0
  6. package/dist/{bn-SEDE5BQJ.mjs → bn-3SAV2ZEM.mjs} +45 -49
  7. package/dist/bn-3SAV2ZEM.mjs.map +1 -0
  8. package/dist/{chunk-C63BARI7.mjs → chunk-3FIQXKQF.mjs} +31 -31
  9. package/dist/{chunk-D7NBW4RV.mjs → chunk-JH7BXE2P.mjs} +45 -49
  10. package/dist/chunk-JH7BXE2P.mjs.map +1 -0
  11. package/dist/{chunk-M7SZRRIE.mjs → chunk-MWQ5CNKW.mjs} +13 -13
  12. package/dist/chunk-MWQ5CNKW.mjs.map +1 -0
  13. package/dist/{chunk-ULIET3MW.mjs → chunk-YI5IX5ZA.mjs} +1 -1
  14. package/dist/{chunk-ULIET3MW.mjs.map → chunk-YI5IX5ZA.mjs.map} +1 -1
  15. package/dist/{cs-7W4WF5WD.mjs → cs-AWCETEUV.mjs} +45 -49
  16. package/dist/cs-AWCETEUV.mjs.map +1 -0
  17. package/dist/{da-75XGBCBK.mjs → da-UZZHXYLC.mjs} +45 -49
  18. package/dist/da-UZZHXYLC.mjs.map +1 -0
  19. package/dist/{de-ODJVFLHM.mjs → de-LQFWN6S5.mjs} +45 -49
  20. package/dist/de-LQFWN6S5.mjs.map +1 -0
  21. package/dist/{el-C4PM4WB3.mjs → el-IWOETBJ7.mjs} +45 -49
  22. package/dist/el-IWOETBJ7.mjs.map +1 -0
  23. package/dist/{en-KJCJQ4OO.mjs → en-XWEPVTB4.mjs} +2 -6
  24. package/dist/{es-WD33R7QL.mjs → es-726NTS53.mjs} +45 -49
  25. package/dist/es-726NTS53.mjs.map +1 -0
  26. package/dist/{fa-2BP6V56P.mjs → fa-BVEJZT5S.mjs} +45 -49
  27. package/dist/fa-BVEJZT5S.mjs.map +1 -0
  28. package/dist/{fi-USRRW24J.mjs → fi-JBNCGGA6.mjs} +45 -49
  29. package/dist/fi-JBNCGGA6.mjs.map +1 -0
  30. package/dist/{fr-EC5S6WVF.mjs → fr-OLH7PNGI.mjs} +45 -49
  31. package/dist/fr-OLH7PNGI.mjs.map +1 -0
  32. package/dist/{he-7TBVIKAA.mjs → he-KOJQ4HMA.mjs} +45 -49
  33. package/dist/he-KOJQ4HMA.mjs.map +1 -0
  34. package/dist/{hi-FO4VIZLA.mjs → hi-BKJFZXAY.mjs} +45 -49
  35. package/dist/hi-BKJFZXAY.mjs.map +1 -0
  36. package/dist/{id-7U7GGVWY.mjs → id-DPLHJVNP.mjs} +45 -49
  37. package/dist/id-DPLHJVNP.mjs.map +1 -0
  38. package/dist/index.css +80 -80
  39. package/dist/index.css.map +1 -1
  40. package/dist/index.d.mts +215 -201
  41. package/dist/index.mjs +810 -748
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/{it-Y4OPL6I2.mjs → it-JXHAM7NL.mjs} +45 -49
  44. package/dist/it-JXHAM7NL.mjs.map +1 -0
  45. package/dist/{ja-PK7SQL55.mjs → ja-DQRAO3PU.mjs} +45 -49
  46. package/dist/ja-DQRAO3PU.mjs.map +1 -0
  47. package/dist/{ko-L25PXMYD.mjs → ko-6IFCOP6F.mjs} +45 -49
  48. package/dist/ko-6IFCOP6F.mjs.map +1 -0
  49. package/dist/{ms-STH777QM.mjs → ms-KF5S2TLL.mjs} +45 -49
  50. package/dist/ms-KF5S2TLL.mjs.map +1 -0
  51. package/dist/{nl-Y7LECDDR.mjs → nl-2GUUZLQM.mjs} +45 -49
  52. package/dist/nl-2GUUZLQM.mjs.map +1 -0
  53. package/dist/{no-KEKCEWU6.mjs → no-2IBCZGEF.mjs} +45 -49
  54. package/dist/no-2IBCZGEF.mjs.map +1 -0
  55. package/dist/{pl-7A7OC75O.mjs → pl-ZEUBJ7YU.mjs} +45 -49
  56. package/dist/pl-ZEUBJ7YU.mjs.map +1 -0
  57. package/dist/{pt-35HTM7RA.mjs → pt-WLAFIZWQ.mjs} +45 -49
  58. package/dist/pt-WLAFIZWQ.mjs.map +1 -0
  59. package/dist/{ro-VAWL5KQA.mjs → ro-K56IXFGU.mjs} +45 -49
  60. package/dist/ro-K56IXFGU.mjs.map +1 -0
  61. package/dist/{sv-7ZK5EQEB.mjs → sv-VFJLMJRY.mjs} +45 -49
  62. package/dist/sv-VFJLMJRY.mjs.map +1 -0
  63. package/dist/test-utils.d.mts +2 -2
  64. package/dist/test-utils.mjs +3 -3
  65. package/dist/{th-UDWZ4X34.mjs → th-RICLQ2GW.mjs} +45 -49
  66. package/dist/th-RICLQ2GW.mjs.map +1 -0
  67. package/dist/{tr-4WMPK3UX.mjs → tr-SALXWE2M.mjs} +45 -49
  68. package/dist/tr-SALXWE2M.mjs.map +1 -0
  69. package/dist/{uk-SSLASQYJ.mjs → uk-3U3T3O2E.mjs} +45 -49
  70. package/dist/uk-3U3T3O2E.mjs.map +1 -0
  71. package/dist/{vi-IF42Z5PU.mjs → vi-LIVNZXOB.mjs} +45 -49
  72. package/dist/vi-LIVNZXOB.mjs.map +1 -0
  73. package/dist/{zh-HRQTNTAI.mjs → zh-KDUAZPX3.mjs} +45 -49
  74. package/dist/zh-KDUAZPX3.mjs.map +1 -0
  75. package/package.json +1 -1
  76. package/src/components/AnnotateReferencesProgressWidget.tsx +113 -0
  77. package/src/components/CodeMirrorRenderer.tsx +9 -27
  78. package/src/components/Toolbar.tsx +2 -2
  79. package/src/components/annotation/AnnotateToolbar.tsx +9 -9
  80. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +17 -17
  81. package/src/components/image-annotation/AnnotationOverlay.tsx +13 -11
  82. package/src/components/image-annotation/SvgDrawingCanvas.tsx +8 -4
  83. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +11 -9
  84. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -1
  85. package/src/components/resource/AnnotateView.tsx +17 -12
  86. package/src/components/resource/BrowseView.tsx +19 -50
  87. package/src/components/resource/ResourceViewer.tsx +28 -24
  88. package/src/components/resource/__tests__/BrowseView.test.tsx +27 -27
  89. package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
  90. package/src/components/resource/panels/AssessmentPanel.tsx +16 -16
  91. package/src/components/resource/panels/{DetectSection.css → AssistSection.css} +79 -79
  92. package/src/components/resource/panels/{DetectSection.tsx → AssistSection.tsx} +46 -46
  93. package/src/components/resource/panels/CommentEntry.tsx +1 -1
  94. package/src/components/resource/panels/CommentsPanel.tsx +16 -16
  95. package/src/components/resource/panels/HighlightEntry.tsx +1 -1
  96. package/src/components/resource/panels/HighlightPanel.tsx +14 -14
  97. package/src/components/resource/panels/ReferenceEntry.tsx +5 -5
  98. package/src/components/resource/panels/ReferencesPanel.tsx +90 -103
  99. package/src/components/resource/panels/ResourceInfoPanel.tsx +2 -2
  100. package/src/components/resource/panels/TagEntry.tsx +1 -1
  101. package/src/components/resource/panels/TaggingPanel.tsx +53 -53
  102. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +12 -20
  103. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +12 -12
  104. package/src/components/resource/panels/__tests__/{DetectSection.test.tsx → AssistSection.test.tsx} +109 -108
  105. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +8 -8
  106. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +3 -3
  107. package/src/components/resource/panels/__tests__/{HighlightPanel.detectionProgress.test.tsx → HighlightPanel.annotationProgress.test.tsx} +56 -56
  108. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +98 -95
  109. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +3 -3
  110. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +16 -16
  111. package/src/components/settings/SettingsPanel.tsx +29 -1
  112. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -0
  113. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +36 -26
  114. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +26 -16
  115. package/src/features/resource-viewer/__tests__/{DetectionProgressDismissal.test.tsx → AnnotationProgressDismissal.test.tsx} +48 -38
  116. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +59 -49
  117. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +55 -33
  118. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +24 -16
  119. package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +41 -31
  120. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +10 -10
  121. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +196 -0
  122. package/src/features/resource-viewer/__tests__/{detection-progress-flow.test.tsx → annotation-progress-flow.test.tsx} +51 -28
  123. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +56 -45
  124. package/src/styles/core/buttons.css +3 -3
  125. package/src/styles/core/forms.css +2 -2
  126. package/src/styles/index.css +1 -1
  127. package/src/styles/motivations/motivation-assessment.css +9 -9
  128. package/src/styles/motivations/motivation-comment.css +9 -9
  129. package/src/styles/motivations/motivation-highlight.css +9 -9
  130. package/src/styles/motivations/motivation-reference.css +9 -9
  131. package/src/styles/motivations/motivation-tag.css +9 -9
  132. package/src/styles/utilities/focus-extended.css +6 -6
  133. package/translations/ar.json +44 -44
  134. package/translations/bn.json +44 -44
  135. package/translations/cs.json +44 -44
  136. package/translations/da.json +44 -44
  137. package/translations/de.json +44 -44
  138. package/translations/el.json +44 -44
  139. package/translations/en.json +44 -44
  140. package/translations/es.json +44 -44
  141. package/translations/fa.json +44 -44
  142. package/translations/fi.json +44 -44
  143. package/translations/fr.json +44 -44
  144. package/translations/he.json +44 -44
  145. package/translations/hi.json +44 -44
  146. package/translations/id.json +44 -44
  147. package/translations/it.json +44 -44
  148. package/translations/ja.json +44 -44
  149. package/translations/ko.json +44 -44
  150. package/translations/ms.json +44 -44
  151. package/translations/nl.json +44 -44
  152. package/translations/no.json +44 -44
  153. package/translations/pl.json +44 -44
  154. package/translations/pt.json +44 -44
  155. package/translations/ro.json +44 -44
  156. package/translations/sv.json +44 -44
  157. package/translations/th.json +44 -44
  158. package/translations/tr.json +44 -44
  159. package/translations/uk.json +44 -44
  160. package/translations/vi.json +44 -44
  161. package/translations/zh.json +44 -44
  162. package/dist/PdfAnnotationCanvas.client-VLNA5O5M.mjs.map +0 -1
  163. package/dist/ar-4ZEORRW2.mjs.map +0 -1
  164. package/dist/bn-SEDE5BQJ.mjs.map +0 -1
  165. package/dist/chunk-D7NBW4RV.mjs.map +0 -1
  166. package/dist/chunk-M7SZRRIE.mjs.map +0 -1
  167. package/dist/cs-7W4WF5WD.mjs.map +0 -1
  168. package/dist/da-75XGBCBK.mjs.map +0 -1
  169. package/dist/de-ODJVFLHM.mjs.map +0 -1
  170. package/dist/el-C4PM4WB3.mjs.map +0 -1
  171. package/dist/es-WD33R7QL.mjs.map +0 -1
  172. package/dist/fa-2BP6V56P.mjs.map +0 -1
  173. package/dist/fi-USRRW24J.mjs.map +0 -1
  174. package/dist/fr-EC5S6WVF.mjs.map +0 -1
  175. package/dist/he-7TBVIKAA.mjs.map +0 -1
  176. package/dist/hi-FO4VIZLA.mjs.map +0 -1
  177. package/dist/id-7U7GGVWY.mjs.map +0 -1
  178. package/dist/it-Y4OPL6I2.mjs.map +0 -1
  179. package/dist/ja-PK7SQL55.mjs.map +0 -1
  180. package/dist/ko-L25PXMYD.mjs.map +0 -1
  181. package/dist/ms-STH777QM.mjs.map +0 -1
  182. package/dist/nl-Y7LECDDR.mjs.map +0 -1
  183. package/dist/no-KEKCEWU6.mjs.map +0 -1
  184. package/dist/pl-7A7OC75O.mjs.map +0 -1
  185. package/dist/pt-35HTM7RA.mjs.map +0 -1
  186. package/dist/ro-VAWL5KQA.mjs.map +0 -1
  187. package/dist/sv-7ZK5EQEB.mjs.map +0 -1
  188. package/dist/th-UDWZ4X34.mjs.map +0 -1
  189. package/dist/tr-4WMPK3UX.mjs.map +0 -1
  190. package/dist/uk-SSLASQYJ.mjs.map +0 -1
  191. package/dist/vi-IF42Z5PU.mjs.map +0 -1
  192. package/dist/zh-HRQTNTAI.mjs.map +0 -1
  193. package/src/components/DetectionProgressWidget.tsx +0 -113
  194. /package/dist/{chunk-C63BARI7.mjs.map → chunk-3FIQXKQF.mjs.map} +0 -0
  195. /package/dist/{en-KJCJQ4OO.mjs.map → en-XWEPVTB4.mjs.map} +0 -0
@@ -18,6 +18,7 @@ interface AnnotationOverlayProps {
18
18
  eventBus?: EventBus;
19
19
  hoveredAnnotationId?: string | null;
20
20
  selectedAnnotationId?: string | null;
21
+ hoverDelayMs: number;
21
22
  }
22
23
 
23
24
  /**
@@ -62,8 +63,8 @@ function getAnnotationTooltip(annotation: Annotation): string {
62
63
  /**
63
64
  * Render annotation overlay - displays existing annotations as SVG shapes
64
65
  *
65
- * @emits annotation:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
66
- * @emits annotation:click - Annotation clicked. Payload: { annotationId: string, motivation: Motivation }
66
+ * @emits attend:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
67
+ * @emits attend:click - Annotation clicked. Payload: { annotationId: string, motivation: Motivation }
67
68
  */
68
69
  export function AnnotationOverlay({
69
70
  annotations,
@@ -73,14 +74,15 @@ export function AnnotationOverlay({
73
74
  displayHeight,
74
75
  eventBus,
75
76
  hoveredAnnotationId,
76
- selectedAnnotationId
77
+ selectedAnnotationId,
78
+ hoverDelayMs
77
79
  }: AnnotationOverlayProps) {
78
80
  const scaleX = displayWidth / imageWidth;
79
81
  const scaleY = displayHeight / imageHeight;
80
82
 
81
83
  const { handleMouseEnter, handleMouseLeave } = useMemo(
82
- () => createHoverHandlers((annotationId) => eventBus?.get('annotation:hover').next({ annotationId })),
83
- [eventBus]
84
+ () => createHoverHandlers((annotationId) => eventBus?.get('attend:hover').next({ annotationId }), hoverDelayMs),
85
+ [eventBus, hoverDelayMs]
84
86
  );
85
87
 
86
88
  return (
@@ -129,7 +131,7 @@ export function AnnotationOverlay({
129
131
  className="semiont-annotation-overlay__shape"
130
132
  data-hovered={isHovered ? 'true' : 'false'}
131
133
  data-selected={isSelected ? 'true' : 'false'}
132
- onClick={() => eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
134
+ onClick={() => eventBus?.get('attend:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
133
135
  onMouseEnter={() => handleMouseEnter(annotation.id)}
134
136
  onMouseLeave={handleMouseLeave}
135
137
  />
@@ -142,7 +144,7 @@ export function AnnotationOverlay({
142
144
  style={{ userSelect: 'none' }}
143
145
  onClick={(e) => {
144
146
  e.stopPropagation();
145
- eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
147
+ eventBus?.get('attend:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
146
148
  }}
147
149
  onMouseEnter={() => handleMouseEnter(annotation.id)}
148
150
  onMouseLeave={handleMouseLeave}
@@ -176,7 +178,7 @@ export function AnnotationOverlay({
176
178
  className="semiont-annotation-overlay__shape"
177
179
  data-hovered={isHovered ? 'true' : 'false'}
178
180
  data-selected={isSelected ? 'true' : 'false'}
179
- onClick={() => eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
181
+ onClick={() => eventBus?.get('attend:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
180
182
  onMouseEnter={() => handleMouseEnter(annotation.id)}
181
183
  onMouseLeave={handleMouseLeave}
182
184
  />
@@ -189,7 +191,7 @@ export function AnnotationOverlay({
189
191
  style={{ userSelect: 'none' }}
190
192
  onClick={(e) => {
191
193
  e.stopPropagation();
192
- eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
194
+ eventBus?.get('attend:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
193
195
  }}
194
196
  onMouseEnter={() => handleMouseEnter(annotation.id)}
195
197
  onMouseLeave={handleMouseLeave}
@@ -236,7 +238,7 @@ export function AnnotationOverlay({
236
238
  className="semiont-annotation-overlay__shape"
237
239
  data-hovered={isHovered ? 'true' : 'false'}
238
240
  data-selected={isSelected ? 'true' : 'false'}
239
- onClick={() => eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
241
+ onClick={() => eventBus?.get('attend:click').next({ annotationId: annotation.id, motivation: annotation.motivation })}
240
242
  onMouseEnter={() => handleMouseEnter(annotation.id)}
241
243
  onMouseLeave={handleMouseLeave}
242
244
  />
@@ -249,7 +251,7 @@ export function AnnotationOverlay({
249
251
  style={{ userSelect: 'none' }}
250
252
  onClick={(e) => {
251
253
  e.stopPropagation();
252
- eventBus?.get('annotation:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
254
+ eventBus?.get('attend:click').next({ annotationId: annotation.id, motivation: annotation.motivation });
253
255
  }}
254
256
  onMouseEnter={() => handleMouseEnter(annotation.id)}
255
257
  onMouseLeave={handleMouseLeave}
@@ -6,6 +6,7 @@ import { createRectangleSvg, createCircleSvg, createPolygonSvg, scaleSvgToNative
6
6
  import { AnnotationOverlay } from './AnnotationOverlay';
7
7
  import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
8
8
  import type { EventBus } from "@semiont/core"
9
+ import { useHoverDelay } from '../../hooks/useHoverDelay';
9
10
 
10
11
  type Annotation = components['schemas']['Annotation'];
11
12
 
@@ -42,13 +43,14 @@ interface SvgDrawingCanvasProps {
42
43
  eventBus?: EventBus;
43
44
  hoveredAnnotationId?: string | null;
44
45
  selectedAnnotationId?: string | null;
46
+ hoverDelayMs?: number;
45
47
  }
46
48
 
47
49
  /**
48
50
  * SVG-based drawing canvas for creating image annotations with shapes
49
51
  *
50
- * @emits annotation:click - Annotation clicked on canvas. Payload: { annotationId: string, motivation: Motivation }
51
- * @emits annotation:requested - New annotation drawn on canvas. Payload: { selector: SvgSelector, motivation: SelectionMotivation }
52
+ * @emits attend:click - Annotation clicked on canvas. Payload: { annotationId: string, motivation: Motivation }
53
+ * @emits annotate:requested - New annotation drawn on canvas. Payload: { selector: SvgSelector, motivation: SelectionMotivation }
52
54
  */
53
55
  export function SvgDrawingCanvas({
54
56
  resourceUri,
@@ -59,6 +61,7 @@ export function SvgDrawingCanvas({
59
61
  hoveredAnnotationId,
60
62
  selectedAnnotationId
61
63
  }: SvgDrawingCanvasProps) {
64
+ const { hoverDelayMs } = useHoverDelay();
62
65
  const imageUrl = useMemo(() => {
63
66
  const resourceId = resourceUri.split('/').pop();
64
67
  return `/api/resources/${resourceId}`;
@@ -213,7 +216,7 @@ export function SvgDrawingCanvas({
213
216
  });
214
217
 
215
218
  if (clickedAnnotation) {
216
- eventBus?.get('annotation:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
219
+ eventBus?.get('attend:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
217
220
  setIsDrawing(false);
218
221
  setStartPoint(null);
219
222
  setCurrentPoint(null);
@@ -276,7 +279,7 @@ export function SvgDrawingCanvas({
276
279
 
277
280
  // Emit annotation:requested event with SvgSelector
278
281
  if (eventBus && selectedMotivation) {
279
- eventBus.get('annotation:requested').next({
282
+ eventBus.get('annotate:requested').next({
280
283
  selector: {
281
284
  type: 'SvgSelector',
282
285
  value: nativeSvg
@@ -338,6 +341,7 @@ export function SvgDrawingCanvas({
338
341
  imageHeight={imageDimensions.height}
339
342
  displayWidth={displayDimensions.width}
340
343
  displayHeight={displayDimensions.height}
344
+ hoverDelayMs={hoverDelayMs}
341
345
  {...(eventBus && { eventBus })}
342
346
  {...(hoveredAnnotationId !== undefined && { hoveredAnnotationId })}
343
347
  {...(selectedAnnotationId !== undefined && { selectedAnnotationId })}
@@ -55,14 +55,15 @@ interface PdfAnnotationCanvasProps {
55
55
  eventBus?: EventBus;
56
56
  hoveredAnnotationId?: string | null;
57
57
  selectedAnnotationId?: string | null;
58
+ hoverDelayMs?: number;
58
59
  }
59
60
 
60
61
  /**
61
62
  * PDF annotation canvas with page navigation and rectangle drawing
62
63
  *
63
- * @emits annotation:click - Annotation clicked on PDF. Payload: { annotationId: string, motivation: Motivation }
64
- * @emits annotation:requested - New annotation drawn on PDF. Payload: { selector: FragmentSelector, motivation: SelectionMotivation }
65
- * @emits annotation:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
64
+ * @emits attend:click - Annotation clicked on PDF. Payload: { annotationId: string, motivation: Motivation }
65
+ * @emits annotate:requested - New annotation drawn on PDF. Payload: { selector: FragmentSelector, motivation: SelectionMotivation }
66
+ * @emits attend:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
66
67
  */
67
68
  export function PdfAnnotationCanvas({
68
69
  resourceUri,
@@ -71,7 +72,8 @@ export function PdfAnnotationCanvas({
71
72
  selectedMotivation,
72
73
  eventBus,
73
74
  hoveredAnnotationId,
74
- selectedAnnotationId
75
+ selectedAnnotationId,
76
+ hoverDelayMs = 150
75
77
  }: PdfAnnotationCanvasProps) {
76
78
  const pdfUrl = useMemo(() => {
77
79
  const resourceId = resourceUri.split('/').pop();
@@ -285,7 +287,7 @@ export function PdfAnnotationCanvas({
285
287
  });
286
288
 
287
289
  if (clickedAnnotation) {
288
- eventBus?.get('annotation:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
290
+ eventBus?.get('attend:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
289
291
  setIsDrawing(false);
290
292
  setSelection(null);
291
293
  return;
@@ -324,7 +326,7 @@ export function PdfAnnotationCanvas({
324
326
 
325
327
  // Emit annotation:requested event with FragmentSelector
326
328
  if (selectedMotivation) {
327
- eventBus.get('annotation:requested').next({
329
+ eventBus.get('annotate:requested').next({
328
330
  selector: {
329
331
  type: 'FragmentSelector',
330
332
  conformsTo: 'http://tools.ietf.org/rfc/rfc3778',
@@ -362,8 +364,8 @@ export function PdfAnnotationCanvas({
362
364
 
363
365
  // Hover handlers with currentHover guard and dwell delay
364
366
  const { handleMouseEnter, handleMouseLeave } = useMemo(
365
- () => createHoverHandlers((annotationId) => eventBus?.get('annotation:hover').next({ annotationId })),
366
- [eventBus]
367
+ () => createHoverHandlers((annotationId) => eventBus?.get('attend:hover').next({ annotationId }), hoverDelayMs),
368
+ [eventBus, hoverDelayMs]
367
369
  );
368
370
 
369
371
  // Calculate motivation color
@@ -462,7 +464,7 @@ export function PdfAnnotationCanvas({
462
464
  cursor: 'pointer',
463
465
  opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7
464
466
  }}
465
- onClick={() => eventBus?.get('annotation:click').next({ annotationId: ann.id, motivation: ann.motivation })}
467
+ onClick={() => eventBus?.get('attend:click').next({ annotationId: ann.id, motivation: ann.motivation })}
466
468
  onMouseEnter={() => handleMouseEnter(ann.id)}
467
469
  onMouseLeave={handleMouseLeave}
468
470
  />
@@ -156,7 +156,7 @@ describe('PdfAnnotationCanvas', () => {
156
156
  expect(rects?.length).toBeGreaterThan(0);
157
157
  });
158
158
 
159
- test('emits annotation:requested via eventBus when drawing with sufficient drag', async () => {
159
+ test('emits annotate:requested via eventBus when drawing with sufficient drag', async () => {
160
160
  const mockEventBus = {
161
161
  emit: vi.fn(),
162
162
  on: vi.fn(),
@@ -41,6 +41,7 @@ interface Props {
41
41
  getTargetDocumentName?: (documentId: string) => string | undefined;
42
42
  generatingReferenceId?: string | null;
43
43
  showLineNumbers?: boolean;
44
+ hoverDelayMs?: number;
44
45
  annotateMode: boolean;
45
46
  }
46
47
 
@@ -127,11 +128,11 @@ function segmentTextWithAnnotations(content: string, annotations: Annotation[]):
127
128
  /**
128
129
  * View component for annotating resources with text selection and drawing
129
130
  *
130
- * @emits annotation:requested - User requested to create annotation. Payload: { selector: Selector | Selector[], motivation: SelectionMotivation }
131
- * @subscribes toolbar:selection-changed - Toolbar selection changed. Payload: { motivation: string | null }
132
- * @subscribes toolbar:click-changed - Toolbar click action changed. Payload: { action: string }
133
- * @subscribes toolbar:shape-changed - Toolbar shape changed. Payload: { shape: string }
134
- * @subscribes annotation:hover - Annotation hovered. Payload: { annotationId: string | null }
131
+ * @emits annotate:requested - User requested to create annotation. Payload: { selector: Selector | Selector[], motivation: SelectionMotivation }
132
+ * @subscribes annotate:selection-changed - Toolbar selection changed. Payload: { motivation: string | null }
133
+ * @subscribes annotate:click-changed - Toolbar click action changed. Payload: { action: string }
134
+ * @subscribes annotate:shape-changed - Toolbar shape changed. Payload: { shape: string }
135
+ * @subscribes attend:hover - Annotation hovered. Payload: { annotationId: string | null }
135
136
  */
136
137
  export function AnnotateView({
137
138
  content,
@@ -144,6 +145,7 @@ export function AnnotateView({
144
145
  getTargetDocumentName,
145
146
  generatingReferenceId,
146
147
  showLineNumbers = false,
148
+ hoverDelayMs = 150,
147
149
  annotateMode
148
150
  }: Props) {
149
151
  const { newAnnotationIds } = useResourceAnnotations();
@@ -183,10 +185,10 @@ export function AnnotateView({
183
185
 
184
186
  // Subscribe to toolbar events and annotation hover
185
187
  useEventSubscriptions({
186
- 'toolbar:selection-changed': handleToolbarSelectionChanged,
187
- 'toolbar:click-changed': handleToolbarClickChanged,
188
- 'toolbar:shape-changed': handleToolbarShapeChanged,
189
- 'annotation:hover': handleAnnotationHover,
188
+ 'annotate:selection-changed': handleToolbarSelectionChanged,
189
+ 'annotate:click-changed': handleToolbarClickChanged,
190
+ 'annotate:shape-changed': handleToolbarShapeChanged,
191
+ 'attend:hover': handleAnnotationHover,
190
192
  });
191
193
 
192
194
  // Handle text annotation with sparkle or immediate creation
@@ -208,7 +210,7 @@ export function AnnotateView({
208
210
 
209
211
  const handleMouseUp = (e: MouseEvent) => {
210
212
  // Skip if the mouseUp came from PDF or image canvas
211
- // (those components handle their own annotation:requested events)
213
+ // (those components handle their own annotate:requested events)
212
214
  const target = e.target as Element;
213
215
  if (target.closest('.semiont-pdf-annotation-canvas') ||
214
216
  target.closest('.semiont-svg-drawing-canvas')) {
@@ -248,7 +250,7 @@ export function AnnotateView({
248
250
 
249
251
  // Unified flow: all text annotations use BOTH TextPositionSelector and TextQuoteSelector
250
252
  if (selectedMotivation) {
251
- eventBus.get('annotation:requested').next({
253
+ eventBus.get('annotate:requested').next({
252
254
  selector: [
253
255
  {
254
256
  type: 'TextPositionSelector',
@@ -282,7 +284,7 @@ export function AnnotateView({
282
284
 
283
285
  // Unified flow: all text annotations use BOTH TextPositionSelector and TextQuoteSelector
284
286
  if (selectedMotivation) {
285
- eventBus.get('annotation:requested').next({
287
+ eventBus.get('annotate:requested').next({
286
288
  selector: [
287
289
  {
288
290
  type: 'TextPositionSelector',
@@ -338,6 +340,7 @@ export function AnnotateView({
338
340
  {...(scrollToAnnotationId !== undefined && { scrollToAnnotationId })}
339
341
  sourceView={true}
340
342
  showLineNumbers={showLineNumbers}
343
+ hoverDelayMs={hoverDelayMs}
341
344
  enableWidgets={enableWidgets}
342
345
  eventBus={eventBus}
343
346
  {...(getTargetDocumentName && { getTargetDocumentName })}
@@ -373,6 +376,7 @@ export function AnnotateView({
373
376
  selectedMotivation={selectedMotivation}
374
377
  eventBus={eventBus}
375
378
  hoveredAnnotationId={hoveredCommentId || hoveredAnnotationId || null}
379
+ hoverDelayMs={hoverDelayMs}
376
380
  />
377
381
  </Suspense>
378
382
  )}
@@ -402,6 +406,7 @@ export function AnnotateView({
402
406
  selectedMotivation={selectedMotivation}
403
407
  eventBus={eventBus}
404
408
  hoveredAnnotationId={hoveredCommentId || hoveredAnnotationId || null}
409
+ hoverDelayMs={hoverDelayMs}
405
410
  />
406
411
  )}
407
412
  </div>
@@ -10,6 +10,7 @@ import { resourceUri as toResourceUri } from '@semiont/core';
10
10
  import { getExactText, getTextPositionSelector, getTargetSelector, getBodySource, getMimeCategory, isPdfMimeType } from '@semiont/api-client';
11
11
  import { ANNOTATORS } from '../../lib/annotation-registry';
12
12
  import { createHoverHandlers } from '../../hooks/useAttentionFlow';
13
+ import { scrollAnnotationIntoView } from '../../lib/scroll-utils';
13
14
  import { ImageViewer } from '../viewers';
14
15
  import { AnnotateToolbar, type ClickAction } from '../annotation/AnnotateToolbar';
15
16
  import type { AnnotationsCollection } from '../../types/annotation-props';
@@ -31,6 +32,7 @@ interface Props {
31
32
  hoveredCommentId?: string | null;
32
33
  selectedClick?: ClickAction;
33
34
  annotateMode: boolean;
35
+ hoverDelayMs?: number;
34
36
  }
35
37
 
36
38
  /**
@@ -41,8 +43,8 @@ function prepareAnnotations(annotations: Annotation[]): PreparedAnnotation[] {
41
43
  /**
42
44
  * View component for browsing resources with rendered annotations
43
45
  *
44
- * @emits annotation:click - Annotation clicked in browse view. Payload: { annotationId: string, motivation: Motivation }
45
- * @emits annotation:hover - Annotation hovered in browse view. Payload: { annotationId: string | null }
46
+ * @emits attend:click - Annotation clicked in browse view. Payload: { annotationId: string, motivation: Motivation }
47
+ * @emits attend:hover - Annotation hovered in browse view. Payload: { annotationId: string | null }
46
48
  */
47
49
  return annotations
48
50
  .map(ann => {
@@ -68,11 +70,11 @@ function prepareAnnotations(annotations: Annotation[]): PreparedAnnotation[] {
68
70
  /**
69
71
  * View component for browsing annotated resources in read-only mode
70
72
  *
71
- * @emits annotation:click - User clicked on annotation. Payload: { annotationId: string, motivation: Motivation }
72
- * @emits annotation:hover - User hovered over annotation. Payload: { annotationId: string | null }
73
+ * @emits attend:click - User clicked on annotation. Payload: { annotationId: string, motivation: Motivation }
74
+ * @emits attend:hover - User hovered over annotation. Payload: { annotationId: string | null }
73
75
  *
74
- * @subscribes annotation:hover - Highlight annotation on hover. Payload: { annotationId: string | null }
75
- * @subscribes annotation:focus - Scroll to and highlight annotation. Payload: { annotationId: string }
76
+ * @subscribes attend:hover - Highlight annotation on hover. Payload: { annotationId: string | null }
77
+ * @subscribes attend:focus - Scroll to and highlight annotation. Payload: { annotationId: string }
76
78
  */
77
79
  export function BrowseView({
78
80
  content,
@@ -80,7 +82,8 @@ export function BrowseView({
80
82
  resourceUri,
81
83
  annotations,
82
84
  selectedClick = 'detail',
83
- annotateMode
85
+ annotateMode,
86
+ hoverDelayMs = 150
84
87
  }: Props) {
85
88
  const { newAnnotationIds } = useResourceAnnotations();
86
89
  const eventBus = useEventBus();
@@ -112,13 +115,14 @@ export function BrowseView({
112
115
  if (annotationId && annotationType === 'reference') {
113
116
  const annotation = allAnnotations.find(a => a.id === annotationId);
114
117
  if (annotation) {
115
- eventBus.get('annotation:click').next({ annotationId, motivation: annotation.motivation });
118
+ eventBus.get('attend:click').next({ annotationId, motivation: annotation.motivation });
116
119
  }
117
120
  }
118
121
  };
119
122
 
120
123
  const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
121
- (annotationId) => eventBus.get('annotation:hover').next({ annotationId })
124
+ (annotationId) => eventBus.get('attend:hover').next({ annotationId }),
125
+ hoverDelayMs
122
126
  );
123
127
 
124
128
  // Single mouseover handler for the container - fires once on enter
@@ -157,48 +161,13 @@ export function BrowseView({
157
161
  container.removeEventListener('mouseout', handleMouseOut);
158
162
  cleanupHover();
159
163
  };
160
- }, [content, allAnnotations, newAnnotationIds]);
164
+ }, [content, allAnnotations, newAnnotationIds, hoverDelayMs]);
161
165
 
162
166
  // Helper to scroll annotation into view with pulse effect
163
167
  const scrollToAnnotation = useCallback((annotationId: string | null, removePulse = false) => {
164
- if (!containerRef.current || !annotationId) return;
165
-
166
- const element = containerRef.current.querySelector(
167
- `[data-annotation-id="${CSS.escape(annotationId)}"]`
168
- ) as HTMLElement;
169
-
170
- if (!element) return;
171
-
172
- // Find the scroll container
173
- const scrollContainer = element.closest('.semiont-browse-view__content') as HTMLElement;
174
-
175
- if (scrollContainer) {
176
- // Check visibility within the scroll container
177
- const elementRect = element.getBoundingClientRect();
178
- const containerRect = scrollContainer.getBoundingClientRect();
179
-
180
- const isVisible =
181
- elementRect.top >= containerRect.top &&
182
- elementRect.bottom <= containerRect.bottom;
183
-
184
- if (!isVisible) {
185
- // Scroll using container.scrollTo to avoid scrolling ancestors
186
- const elementTop = element.offsetTop;
187
- const containerHeight = scrollContainer.clientHeight;
188
- const elementHeight = element.offsetHeight;
189
- const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
190
-
191
- scrollContainer.scrollTo({ top: scrollTo, behavior: 'smooth' });
192
- }
193
- }
194
-
195
- // Add pulse effect
196
- element.classList.add('annotation-pulse');
197
- if (removePulse) {
198
- setTimeout(() => {
199
- element.classList.remove('annotation-pulse');
200
- }, 2000);
201
- }
168
+ if (!containerRef.current) return;
169
+ // removePulse = true means "add pulse and auto-remove after 2s"
170
+ scrollAnnotationIntoView(annotationId, containerRef.current, { pulse: removePulse });
202
171
  }, []);
203
172
 
204
173
  // Handle hover events for scrolling
@@ -212,8 +181,8 @@ export function BrowseView({
212
181
  }, [scrollToAnnotation]);
213
182
 
214
183
  useEventSubscriptions({
215
- 'annotation:hover': handleAnnotationHover,
216
- 'annotation:focus': handleAnnotationFocus,
184
+ 'attend:hover': handleAnnotationHover,
185
+ 'attend:focus': handleAnnotationFocus,
217
186
  });
218
187
 
219
188
  // Route to appropriate viewer based on MIME type category
@@ -24,7 +24,7 @@ type SemiontResource = components['schemas']['ResourceDescriptor'];
24
24
  * ResourceViewer - Display and interact with resource content and annotations
25
25
  *
26
26
  * This component uses event-driven architecture for real-time updates:
27
- * - Subscribes to make-meaning events (annotation:added, annotation:removed, annotation:updated)
27
+ * - Subscribes to make-meaning events (annotate:added, annotate:removed, annotate:body-updated)
28
28
  * - Automatically invalidates cache when annotations change
29
29
  * - No manual refetch needed - events handle cache invalidation
30
30
  *
@@ -43,27 +43,29 @@ interface Props {
43
43
  annotations: AnnotationsCollection;
44
44
  generatingReferenceId?: string | null;
45
45
  showLineNumbers?: boolean;
46
+ hoverDelayMs?: number;
46
47
  hoveredAnnotationId?: string | null;
47
48
  }
48
49
 
49
50
  /**
50
- * @emits annotation:delete - User requested to delete annotation. Payload: { annotationId: string }
51
- * @emits panel:open - Request to open panel with annotation. Payload: { panel: string, scrollToAnnotationId?: string, motivation?: Motivation }
51
+ * @emits annotate:delete - User requested to delete annotation. Payload: { annotationId: string }
52
+ * @emits attend:panel-open - Request to open panel with annotation. Payload: { panel: string, scrollToAnnotationId?: string, motivation?: Motivation }
52
53
  *
53
- * @subscribes view:mode-toggled - Toggles between browse and annotate mode. Payload: { mode: 'browse' | 'annotate' }
54
- * @subscribes annotation:added - New annotation was added. Payload: { annotation: Annotation }
55
- * @subscribes annotation:removed - Annotation was removed. Payload: { annotationId: string }
56
- * @subscribes annotation:updated - Annotation was updated. Payload: { annotation: Annotation }
57
- * @subscribes toolbar:selection-changed - Text selection tool changed. Payload: { selection: boolean }
58
- * @subscribes toolbar:click-changed - Click annotation tool changed. Payload: { click: 'detail' | 'scroll' | null }
59
- * @subscribes toolbar:shape-changed - Drawing shape changed. Payload: { shape: string }
60
- * @subscribes annotation:click - User clicked on annotation. Payload: { annotationId: string }
54
+ * @subscribes annotate:mode-toggled - Toggles between browse and annotate mode. Payload: { mode: 'browse' | 'annotate' }
55
+ * @subscribes annotate:added - New annotation was added. Payload: { annotation: Annotation }
56
+ * @subscribes annotate:removed - Annotation was removed. Payload: { annotationId: string }
57
+ * @subscribes annotate:body-updated - Annotation was updated. Payload: { annotation: Annotation }
58
+ * @subscribes annotate:selection-changed - Text selection tool changed. Payload: { selection: boolean }
59
+ * @subscribes annotate:click-changed - Click annotation tool changed. Payload: { click: 'detail' | 'scroll' | null }
60
+ * @subscribes annotate:shape-changed - Drawing shape changed. Payload: { shape: string }
61
+ * @subscribes attend:click - User clicked on annotation. Payload: { annotationId: string }
61
62
  */
62
63
  export function ResourceViewer({
63
64
  resource,
64
65
  annotations,
65
66
  generatingReferenceId,
66
67
  showLineNumbers = false,
68
+ hoverDelayMs,
67
69
  hoveredAnnotationId: hoveredAnnotationIdProp
68
70
  }: Props) {
69
71
  const t = useTranslations('ResourceViewer');
@@ -122,19 +124,19 @@ export function ResourceViewer({
122
124
  // This replaces manual onRefetchAnnotations calls with automatic updates
123
125
  const cacheManager = useCacheManager();
124
126
 
125
- const handleAnnotationAdded = useCallback(() => {
127
+ const handleAnnotateAdded = useCallback(() => {
126
128
  if (cacheManager) {
127
129
  cacheManager.invalidateAnnotations(rUri);
128
130
  }
129
131
  }, [cacheManager, rUri]);
130
132
 
131
- const handleAnnotationRemoved = useCallback(() => {
133
+ const handleAnnotateRemoved = useCallback(() => {
132
134
  if (cacheManager) {
133
135
  cacheManager.invalidateAnnotations(rUri);
134
136
  }
135
137
  }, [cacheManager, rUri]);
136
138
 
137
- const handleAnnotationUpdated = useCallback(() => {
139
+ const handleAnnotateBodyUpdated = useCallback(() => {
138
140
  if (cacheManager) {
139
141
  cacheManager.invalidateAnnotations(rUri);
140
142
  }
@@ -250,7 +252,7 @@ export function ResourceViewer({
250
252
 
251
253
  // Handle deleting annotations - emit event instead of direct call
252
254
  const handleDeleteAnnotation = useCallback((id: string) => {
253
- eventBus.get('annotation:delete').next({ annotationId: id });
255
+ eventBus.get('annotate:delete').next({ annotationId: id });
254
256
  }, []); // eventBus is stable
255
257
 
256
258
  // Handle annotation clicks - memoized
@@ -339,27 +341,27 @@ export function ResourceViewer({
339
341
 
340
342
  // All annotations open the unified annotations panel
341
343
  // The panel internally switches tabs based on the motivation → tab mapping in UnifiedAnnotationsPanel
342
- eventBus.get('panel:open').next({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
344
+ eventBus.get('attend:panel-open').next({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
343
345
  }, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick]);
344
346
 
345
347
  // Event subscriptions - Combined into single useEventSubscriptions call to prevent hook ordering issues
346
348
  // IMPORTANT: All event subscriptions MUST be in a single call to maintain consistent hook order between renders
347
349
  useEventSubscriptions({
348
350
  // View mode
349
- 'view:mode-toggled': handleViewModeToggle,
351
+ 'annotate:mode-toggled': handleViewModeToggle,
350
352
 
351
353
  // Annotation cache invalidation
352
- 'annotation:added': handleAnnotationAdded,
353
- 'annotation:removed': handleAnnotationRemoved,
354
- 'annotation:updated': handleAnnotationUpdated,
354
+ 'annotate:added': handleAnnotateAdded,
355
+ 'annotate:removed': handleAnnotateRemoved,
356
+ 'annotate:body-updated': handleAnnotateBodyUpdated,
355
357
 
356
358
  // Toolbar state
357
- 'toolbar:selection-changed': handleToolbarSelectionChanged,
358
- 'toolbar:click-changed': handleToolbarClickChanged,
359
- 'toolbar:shape-changed': handleToolbarShapeChanged,
359
+ 'annotate:selection-changed': handleToolbarSelectionChanged,
360
+ 'annotate:click-changed': handleToolbarClickChanged,
361
+ 'annotate:shape-changed': handleToolbarShapeChanged,
360
362
 
361
363
  // Annotation clicks
362
- 'annotation:click': handleAnnotationClickEvent,
364
+ 'attend:click': handleAnnotationClickEvent,
363
365
  });
364
366
 
365
367
  // Prepare props for child components
@@ -400,6 +402,7 @@ export function ResourceViewer({
400
402
  getTargetDocumentName={getTargetDocumentName}
401
403
  {...(generatingReferenceId !== undefined && { generatingReferenceId })}
402
404
  showLineNumbers={showLineNumbers}
405
+ hoverDelayMs={hoverDelayMs}
403
406
  annotateMode={annotateMode}
404
407
  />
405
408
  ) : (
@@ -410,6 +413,7 @@ export function ResourceViewer({
410
413
  annotations={annotationsCollection}
411
414
  hoveredCommentId={hoveredCommentId}
412
415
  selectedClick={selectedClick}
416
+ hoverDelayMs={hoverDelayMs}
413
417
  annotateMode={annotateMode}
414
418
  />
415
419
  )}