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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/EventBusContext-7GvDyO0d.d.mts +414 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -4
  34. package/dist/id-7U7GGVWY.mjs.map +1 -0
  35. package/dist/index.css +123 -85
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.mts +699 -529
  38. package/dist/index.mjs +4291 -3491
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -4
  59. package/dist/sv-7ZK5EQEB.mjs.map +1 -0
  60. package/dist/test-utils.d.mts +18 -8
  61. package/dist/test-utils.mjs +36 -14
  62. package/dist/test-utils.mjs.map +1 -1
  63. package/dist/{th-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +8 -2
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/LiveRegion.tsx +18 -18
  77. package/src/components/ResizeHandle.tsx +10 -4
  78. package/src/components/SessionTimer.tsx +2 -2
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +9 -9
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -138
  100. package/src/components/resource/AnnotationHistory.tsx +12 -13
  101. package/src/components/resource/BrowseView.tsx +89 -177
  102. package/src/components/resource/HistoryEvent.tsx +16 -11
  103. package/src/components/resource/ResourceViewer.tsx +201 -370
  104. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  105. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  106. package/src/components/resource/event-formatting.ts +316 -0
  107. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  108. package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
  109. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  110. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  111. package/src/components/resource/panels/CommentsPanel.tsx +153 -31
  112. package/src/components/resource/panels/DetectSection.css +36 -1
  113. package/src/components/resource/panels/DetectSection.tsx +38 -10
  114. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  115. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  116. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  117. package/src/components/resource/panels/ReferencesPanel.tsx +166 -49
  118. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  119. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  120. package/src/components/resource/panels/TagEntry.tsx +25 -33
  121. package/src/components/resource/panels/TaggingPanel.tsx +141 -25
  122. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
  123. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
  124. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  125. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
  126. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  127. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  128. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +228 -103
  129. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  130. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
  131. package/src/components/settings/SettingsPanel.tsx +15 -12
  132. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  133. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  134. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  135. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  136. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  137. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  138. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  139. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  140. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  141. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  142. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  143. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  144. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  145. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  146. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  147. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +16 -27
  148. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  149. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  150. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  151. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  152. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  153. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +145 -91
  154. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  155. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
  156. package/src/styles/motivations/motivation-assessment.css +28 -0
  157. package/src/styles/patterns/panel-helpers.css +26 -0
  158. package/translations/ar.json +7 -3
  159. package/translations/bn.json +7 -3
  160. package/translations/cs.json +7 -3
  161. package/translations/da.json +7 -3
  162. package/translations/de.json +7 -3
  163. package/translations/el.json +7 -3
  164. package/translations/en.json +7 -3
  165. package/translations/es.json +7 -3
  166. package/translations/fa.json +7 -3
  167. package/translations/fi.json +7 -3
  168. package/translations/fr.json +7 -3
  169. package/translations/he.json +7 -3
  170. package/translations/hi.json +7 -3
  171. package/translations/id.json +7 -3
  172. package/translations/it.json +7 -3
  173. package/translations/ja.json +7 -3
  174. package/translations/ko.json +7 -3
  175. package/translations/ms.json +7 -3
  176. package/translations/nl.json +7 -3
  177. package/translations/no.json +7 -3
  178. package/translations/pl.json +7 -3
  179. package/translations/pt.json +7 -3
  180. package/translations/ro.json +7 -3
  181. package/translations/sv.json +7 -3
  182. package/translations/th.json +7 -3
  183. package/translations/tr.json +7 -3
  184. package/translations/uk.json +7 -3
  185. package/translations/vi.json +7 -3
  186. package/translations/zh.json +7 -3
  187. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  188. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  189. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  190. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  191. package/dist/chunk-35LLVRFK.mjs.map +0 -1
  192. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  193. package/dist/cs-RSV675WU.mjs.map +0 -1
  194. package/dist/da-CHXNPWJC.mjs.map +0 -1
  195. package/dist/de-KPEZ53D4.mjs.map +0 -1
  196. package/dist/el-MW2BME5T.mjs.map +0 -1
  197. package/dist/es-HQ24NYS3.mjs.map +0 -1
  198. package/dist/fa-W34LRLHG.mjs.map +0 -1
  199. package/dist/fi-3U44IGOA.mjs.map +0 -1
  200. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  201. package/dist/he-CS4WRXN3.mjs.map +0 -1
  202. package/dist/hi-GJDY46KA.mjs.map +0 -1
  203. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  204. package/dist/it-VDNDMZPU.mjs.map +0 -1
  205. package/dist/ja-5PEH56J5.mjs.map +0 -1
  206. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  207. package/dist/ms-5PZVW76T.mjs.map +0 -1
  208. package/dist/nl-YXES36KM.mjs.map +0 -1
  209. package/dist/no-XRA2UCQD.mjs.map +0 -1
  210. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  211. package/dist/pt-7GAG57BM.mjs.map +0 -1
  212. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  213. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  214. package/dist/th-LPKYLBX5.mjs.map +0 -1
  215. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  216. package/dist/uk-36UHTDDI.mjs.map +0 -1
  217. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  218. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  219. /package/dist/{en-EVMIX24Y.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -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));
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useState, useEffect } 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,17 +27,20 @@ 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 } }
29
32
  */
30
33
  export function DetectSection({
31
34
  annotationType,
32
35
  isDetecting,
33
36
  detectionProgress,
34
- onDetect
35
37
  }: DetectSectionProps) {
38
+
36
39
  const panelName = annotationType === 'highlight' ? 'HighlightPanel' :
37
40
  annotationType === 'assessment' ? 'AssessmentPanel' :
38
41
  'CommentsPanel';
39
42
  const t = useTranslations(panelName);
43
+ const eventBus = useEventBus();
40
44
  const [instructions, setInstructions] = useState('');
41
45
  const [tone, setTone] = useState('');
42
46
  // Default density depends on annotation type
@@ -58,11 +62,22 @@ export function DetectSection({
58
62
  }, [isExpanded, annotationType]);
59
63
 
60
64
  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
- );
65
+ // Map annotation type to motivation
66
+ const motivation: Motivation =
67
+ annotationType === 'highlight' ? 'highlighting' :
68
+ annotationType === 'assessment' ? 'assessing' :
69
+ 'commenting';
70
+
71
+ // Emit detection:start event with options
72
+ eventBus.emit('detection:start', {
73
+ motivation,
74
+ options: {
75
+ instructions: instructions.trim() || undefined,
76
+ tone: (annotationType === 'comment' || annotationType === 'assessment') && tone ? tone as any : undefined,
77
+ density: (annotationType === 'comment' || annotationType === 'assessment' || annotationType === 'highlight') && useDensity ? density : undefined,
78
+ },
79
+ });
80
+
66
81
  setInstructions('');
67
82
  setTone('');
68
83
  // Don't reset density/useDensity - persist across detections
@@ -91,7 +106,8 @@ export function DetectSection({
91
106
  data-detecting={isDetecting && detectionProgress ? 'true' : 'false'}
92
107
  data-type={annotationType}
93
108
  >
94
- {!isDetecting && !detectionProgress && (
109
+ {/* Show form when NOT detecting and NO progress to display */}
110
+ {!detectionProgress && (
95
111
  <>
96
112
  <div className="semiont-form-field">
97
113
  <label className="semiont-form-field__label">
@@ -194,8 +210,8 @@ export function DetectSection({
194
210
  </>
195
211
  )}
196
212
 
197
- {/* Detection Progress */}
198
- {isDetecting && detectionProgress && (
213
+ {/* Detection Progress - show whenever we have progress (during or after detection) */}
214
+ {detectionProgress && (
199
215
  <div className="semiont-detection-progress" data-type={annotationType}>
200
216
  {/* Request Parameters */}
201
217
  {detectionProgress.requestParams && detectionProgress.requestParams.length > 0 && (
@@ -214,6 +230,18 @@ export function DetectSection({
214
230
  <span className="semiont-detection-progress__icon">✨</span>
215
231
  <span>{detectionProgress.message}</span>
216
232
  </div>
233
+ {/* Close button - shown after detection completes (when not actively detecting) */}
234
+ {!isDetecting && (
235
+ <button
236
+ onClick={() => eventBus.emit('detection:dismiss-progress', undefined)}
237
+ className="semiont-detection-progress__close"
238
+ aria-label={t('closeProgress')}
239
+ title={t('closeProgress')}
240
+ type="button"
241
+ >
242
+ ×
243
+ </button>
244
+ )}
217
245
  </div>
218
246
  </div>
219
247
  )}
@@ -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
  )}
@@ -1,12 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef } from 'react';
3
+ import { forwardRef } from 'react';
4
4
  import type { RouteBuilder } from '../../../contexts/RoutingContext';
5
5
  import { useTranslations } from '../../../contexts/TranslationContext';
6
6
  import type { components } from '@semiont/api-client';
7
7
  import { getAnnotationExactText, isBodyResolved, getBodySource, getFragmentSelector, getSvgSelector, getTargetSelector } from '@semiont/api-client';
8
8
  import { getEntityTypes } from '@semiont/ontology';
9
9
  import { getResourceIcon } from '../../../lib/resource-utils';
10
+ import { useEventBus } from '../../../contexts/EventBusContext';
11
+ import { useObservableExternalNavigation } from '../../../hooks/useObservableNavigation';
10
12
 
11
13
  type Annotation = components['schemas']['Annotation'];
12
14
 
@@ -19,68 +21,27 @@ interface EnrichedAnnotation extends Annotation {
19
21
  interface ReferenceEntryProps {
20
22
  reference: Annotation;
21
23
  isFocused: boolean;
22
- onClick: () => void;
24
+ isHovered?: boolean;
23
25
  routes: RouteBuilder;
24
- onReferenceRef: (referenceId: string, el: HTMLElement | null) => void;
25
- onReferenceHover?: (referenceId: string | null) => void;
26
- onGenerateDocument?: (referenceId: string, options: { title: string; prompt?: string }) => void;
27
- onCreateDocument?: (annotationUri: string, title: string, entityTypes: string[]) => void;
28
- onSearchDocuments?: (referenceId: string, searchTerm: string) => void;
29
- onUpdateReference?: (referenceId: string, updates: Partial<Annotation>) => void;
30
26
  annotateMode?: boolean;
31
27
  isGenerating?: boolean;
32
28
  }
33
29
 
34
- export function ReferenceEntry({
35
- reference,
36
- isFocused,
37
- onClick,
38
- routes,
39
- onReferenceRef,
40
- onReferenceHover,
41
- onGenerateDocument,
42
- onCreateDocument,
43
- onSearchDocuments,
44
- onUpdateReference,
45
- annotateMode = true,
46
- isGenerating = false,
47
- }: ReferenceEntryProps) {
30
+ export const ReferenceEntry = forwardRef<HTMLDivElement, ReferenceEntryProps>(
31
+ function ReferenceEntry(
32
+ {
33
+ reference,
34
+ isFocused,
35
+ isHovered = false,
36
+ routes,
37
+ annotateMode = true,
38
+ isGenerating = false,
39
+ },
40
+ ref
41
+ ) {
48
42
  const t = useTranslations('ReferencesPanel');
49
- const referenceRef = useRef<HTMLDivElement>(null);
50
-
51
- // Register ref with parent
52
- useEffect(() => {
53
- onReferenceRef(reference.id, referenceRef.current);
54
- return () => {
55
- onReferenceRef(reference.id, null);
56
- };
57
- }, [reference.id, onReferenceRef]);
58
-
59
- // Scroll to reference when focused - use container.scrollTo to avoid scrolling ancestors
60
- useEffect(() => {
61
- if (isFocused && referenceRef.current) {
62
- const element = referenceRef.current;
63
- const container = element.closest('.semiont-toolbar-panels__content') as HTMLElement;
64
-
65
- if (container) {
66
- const elementRect = element.getBoundingClientRect();
67
- const containerRect = container.getBoundingClientRect();
68
-
69
- const isVisible =
70
- elementRect.top >= containerRect.top &&
71
- elementRect.bottom <= containerRect.bottom;
72
-
73
- if (!isVisible) {
74
- const elementTop = element.offsetTop;
75
- const containerHeight = container.clientHeight;
76
- const elementHeight = element.offsetHeight;
77
- const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
78
-
79
- container.scrollTo({ top: scrollTo, behavior: 'smooth' });
80
- }
81
- }
82
- }
83
- }, [isFocused]);
43
+ const eventBus = useEventBus();
44
+ const navigate = useObservableExternalNavigation();
84
45
 
85
46
  const selectedText = getAnnotationExactText(reference) || '';
86
47
  const isResolved = isBodyResolved(reference.body);
@@ -103,44 +64,69 @@ export function ReferenceEntry({
103
64
  if (resolvedResourceUri) {
104
65
  const resourceId = resolvedResourceUri.split('/resources/')[1];
105
66
  if (resourceId) {
106
- window.location.href = routes.resourceDetail(resourceId);
67
+ // Use observable navigation - emits 'navigation:external-navigate' event
68
+ navigate(routes.resourceDetail(resourceId), { resourceId });
107
69
  }
108
70
  }
109
71
  };
110
72
 
111
73
  const handleComposeDocument = () => {
112
- if (onCreateDocument) {
113
- onCreateDocument(reference.id, selectedText, entityTypes);
114
- }
74
+ eventBus.emit('reference:create-manual', {
75
+ annotationUri: reference.id,
76
+ title: selectedText,
77
+ entityTypes,
78
+ });
115
79
  };
116
80
 
117
81
  const handleUnlink = () => {
118
- if (onUpdateReference) {
119
- onUpdateReference(reference.id, { body: [] });
82
+ // Unlinking removes all body items from the reference annotation
83
+ const sourceUri = typeof reference.target === 'object' && 'source' in reference.target
84
+ ? reference.target.source
85
+ : '';
86
+ if (sourceUri) {
87
+ eventBus.emit('annotation:update-body', {
88
+ annotationUri: reference.id,
89
+ resourceId: sourceUri.split('/resources/')[1] || '',
90
+ operations: [{ op: 'remove', item: null }], // Remove all body items
91
+ });
120
92
  }
121
93
  };
122
94
 
123
95
  const handleGenerate = () => {
124
- if (onGenerateDocument) {
125
- onGenerateDocument(reference.id, { title: selectedText });
126
- }
96
+ const resourceUri = typeof reference.target === 'object' && 'source' in reference.target
97
+ ? reference.target.source
98
+ : '';
99
+
100
+ // Emit request to open generation modal
101
+ eventBus.emit('generation:modal-open', {
102
+ annotationUri: reference.id,
103
+ resourceUri,
104
+ defaultTitle: selectedText,
105
+ });
127
106
  };
128
107
 
129
108
  const handleSearch = () => {
130
- if (onSearchDocuments) {
131
- onSearchDocuments(reference.id, selectedText);
132
- }
109
+ eventBus.emit('reference:link', {
110
+ annotationUri: reference.id,
111
+ searchTerm: selectedText,
112
+ });
133
113
  };
134
114
 
135
115
  return (
136
116
  <div
137
- ref={referenceRef}
138
- className="semiont-annotation-entry"
117
+ ref={ref}
118
+ className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
139
119
  data-type="reference"
140
120
  data-focused={isFocused ? 'true' : 'false'}
141
- onClick={onClick}
142
- onMouseEnter={() => onReferenceHover?.(reference.id)}
143
- onMouseLeave={() => onReferenceHover?.(null)}
121
+ onClick={() => {
122
+ eventBus.emit('annotation:click', { annotationId: reference.id, motivation: reference.motivation });
123
+ }}
124
+ onMouseEnter={() => {
125
+ eventBus.emit('annotation:hover', { annotationId: reference.id });
126
+ }}
127
+ onMouseLeave={() => {
128
+ eventBus.emit('annotation:hover', { annotationId: null });
129
+ }}
144
130
  >
145
131
  {/* Status indicator and text quote */}
146
132
  <div className="semiont-annotation-entry__header">
@@ -235,4 +221,4 @@ export function ReferenceEntry({
235
221
  </div>
236
222
  </div>
237
223
  );
238
- }
224
+ });