@semiont/react-ui 0.2.33-build.77 → 0.2.33-build.79

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 (147) hide show
  1. package/dist/{ar-RNNSPLQB.mjs → ar-EMHEHPCJ.mjs} +2 -1
  2. package/dist/ar-EMHEHPCJ.mjs.map +1 -0
  3. package/dist/{bn-S2CDL7EC.mjs → bn-OVCI4F6X.mjs} +2 -1
  4. package/dist/bn-OVCI4F6X.mjs.map +1 -0
  5. package/dist/{chunk-35LLVRFK.mjs → chunk-JZIO2A3B.mjs} +31 -31
  6. package/dist/{chunk-UDX2Q35T.mjs → chunk-LIHZTECW.mjs} +2 -1
  7. package/dist/chunk-LIHZTECW.mjs.map +1 -0
  8. package/dist/{cs-RSV675WU.mjs → cs-FAN66Q2F.mjs} +2 -1
  9. package/dist/cs-FAN66Q2F.mjs.map +1 -0
  10. package/dist/{da-CHXNPWJC.mjs → da-YBBIHI2O.mjs} +2 -1
  11. package/dist/da-YBBIHI2O.mjs.map +1 -0
  12. package/dist/{de-KPEZ53D4.mjs → de-MAYU33LB.mjs} +2 -1
  13. package/dist/de-MAYU33LB.mjs.map +1 -0
  14. package/dist/{el-MW2BME5T.mjs → el-MKGSWN4O.mjs} +2 -1
  15. package/dist/el-MKGSWN4O.mjs.map +1 -0
  16. package/dist/{en-EVMIX24Y.mjs → en-DDLIXJCU.mjs} +2 -2
  17. package/dist/{es-HQ24NYS3.mjs → es-52LHUWJD.mjs} +2 -1
  18. package/dist/es-52LHUWJD.mjs.map +1 -0
  19. package/dist/{fa-W34LRLHG.mjs → fa-FJICRANB.mjs} +2 -1
  20. package/dist/fa-FJICRANB.mjs.map +1 -0
  21. package/dist/{fi-3U44IGOA.mjs → fi-O455XFCR.mjs} +2 -1
  22. package/dist/fi-O455XFCR.mjs.map +1 -0
  23. package/dist/{fr-N7DKX6NN.mjs → fr-TXIXHOOE.mjs} +2 -1
  24. package/dist/fr-TXIXHOOE.mjs.map +1 -0
  25. package/dist/{he-CS4WRXN3.mjs → he-JBSOX5IN.mjs} +2 -1
  26. package/dist/he-JBSOX5IN.mjs.map +1 -0
  27. package/dist/{hi-GJDY46KA.mjs → hi-KGHI3XVT.mjs} +2 -1
  28. package/dist/hi-KGHI3XVT.mjs.map +1 -0
  29. package/dist/{id-WAEZJK2Y.mjs → id-5OCPPZLO.mjs} +2 -1
  30. package/dist/id-5OCPPZLO.mjs.map +1 -0
  31. package/dist/index.d.mts +102 -106
  32. package/dist/index.mjs +1814 -1450
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/{it-VDNDMZPU.mjs → it-PNBBZSM2.mjs} +2 -1
  35. package/dist/it-PNBBZSM2.mjs.map +1 -0
  36. package/dist/{ja-5PEH56J5.mjs → ja-LDD7R3TJ.mjs} +2 -1
  37. package/dist/ja-LDD7R3TJ.mjs.map +1 -0
  38. package/dist/{ko-JYPL3WVA.mjs → ko-F47ZDEY3.mjs} +2 -1
  39. package/dist/ko-F47ZDEY3.mjs.map +1 -0
  40. package/dist/{ms-5PZVW76T.mjs → ms-Z7LMXJWL.mjs} +2 -1
  41. package/dist/ms-Z7LMXJWL.mjs.map +1 -0
  42. package/dist/{nl-YXES36KM.mjs → nl-6SJFBPJ3.mjs} +2 -1
  43. package/dist/nl-6SJFBPJ3.mjs.map +1 -0
  44. package/dist/{no-XRA2UCQD.mjs → no-YXPBPSGF.mjs} +2 -1
  45. package/dist/no-YXPBPSGF.mjs.map +1 -0
  46. package/dist/{pl-WH6LJA5G.mjs → pl-P4AZ2QME.mjs} +2 -1
  47. package/dist/pl-P4AZ2QME.mjs.map +1 -0
  48. package/dist/{pt-7GAG57BM.mjs → pt-LHWUS6U6.mjs} +2 -1
  49. package/dist/pt-LHWUS6U6.mjs.map +1 -0
  50. package/dist/{ro-BTDDRB7N.mjs → ro-EA5J2ZON.mjs} +2 -1
  51. package/dist/ro-EA5J2ZON.mjs.map +1 -0
  52. package/dist/{sv-7V5C2IT4.mjs → sv-DATBS3UQ.mjs} +2 -1
  53. package/dist/sv-DATBS3UQ.mjs.map +1 -0
  54. package/dist/test-utils.mjs +2 -2
  55. package/dist/{th-LPKYLBX5.mjs → th-WTFJRWPT.mjs} +2 -1
  56. package/dist/th-WTFJRWPT.mjs.map +1 -0
  57. package/dist/{tr-DU4RQL4M.mjs → tr-IKO3RXOX.mjs} +2 -1
  58. package/dist/tr-IKO3RXOX.mjs.map +1 -0
  59. package/dist/{uk-36UHTDDI.mjs → uk-CF6CTTRK.mjs} +2 -1
  60. package/dist/uk-CF6CTTRK.mjs.map +1 -0
  61. package/dist/{vi-GDHOUZDH.mjs → vi-AJLTXPZQ.mjs} +2 -1
  62. package/dist/vi-AJLTXPZQ.mjs.map +1 -0
  63. package/dist/{zh-TYUID4XZ.mjs → zh-U3ORHHYH.mjs} +2 -1
  64. package/dist/zh-U3ORHHYH.mjs.map +1 -0
  65. package/package.json +6 -2
  66. package/src/components/resource/AnnotateView.tsx +0 -4
  67. package/src/components/resource/AnnotationHistory.tsx +12 -13
  68. package/src/components/resource/BrowseView.tsx +8 -16
  69. package/src/components/resource/HistoryEvent.tsx +3 -4
  70. package/src/components/resource/ResourceViewer.tsx +174 -201
  71. package/src/components/resource/event-formatting.ts +316 -0
  72. package/src/components/resource/panels/AssessmentPanel.tsx +37 -9
  73. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  74. package/src/components/resource/panels/CommentsPanel.tsx +38 -9
  75. package/src/components/resource/panels/ReferencesPanel.tsx +39 -14
  76. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  77. package/src/components/resource/panels/TaggingPanel.tsx +27 -0
  78. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +28 -21
  79. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +547 -0
  80. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +10 -0
  81. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +10 -0
  82. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +564 -0
  83. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +8 -15
  84. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +13 -6
  85. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +147 -78
  86. package/src/styles/motivations/motivation-assessment.css +28 -0
  87. package/src/styles/patterns/panel-helpers.css +26 -0
  88. package/translations/ar.json +1 -0
  89. package/translations/bn.json +1 -0
  90. package/translations/cs.json +1 -0
  91. package/translations/da.json +1 -0
  92. package/translations/de.json +1 -0
  93. package/translations/el.json +1 -0
  94. package/translations/en.json +1 -0
  95. package/translations/es.json +1 -0
  96. package/translations/fa.json +1 -0
  97. package/translations/fi.json +1 -0
  98. package/translations/fr.json +1 -0
  99. package/translations/he.json +1 -0
  100. package/translations/hi.json +1 -0
  101. package/translations/id.json +1 -0
  102. package/translations/it.json +1 -0
  103. package/translations/ja.json +1 -0
  104. package/translations/ko.json +1 -0
  105. package/translations/ms.json +1 -0
  106. package/translations/nl.json +1 -0
  107. package/translations/no.json +1 -0
  108. package/translations/pl.json +1 -0
  109. package/translations/pt.json +1 -0
  110. package/translations/ro.json +1 -0
  111. package/translations/sv.json +1 -0
  112. package/translations/th.json +1 -0
  113. package/translations/tr.json +1 -0
  114. package/translations/uk.json +1 -0
  115. package/translations/vi.json +1 -0
  116. package/translations/zh.json +1 -0
  117. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  118. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  119. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  120. package/dist/cs-RSV675WU.mjs.map +0 -1
  121. package/dist/da-CHXNPWJC.mjs.map +0 -1
  122. package/dist/de-KPEZ53D4.mjs.map +0 -1
  123. package/dist/el-MW2BME5T.mjs.map +0 -1
  124. package/dist/es-HQ24NYS3.mjs.map +0 -1
  125. package/dist/fa-W34LRLHG.mjs.map +0 -1
  126. package/dist/fi-3U44IGOA.mjs.map +0 -1
  127. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  128. package/dist/he-CS4WRXN3.mjs.map +0 -1
  129. package/dist/hi-GJDY46KA.mjs.map +0 -1
  130. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  131. package/dist/it-VDNDMZPU.mjs.map +0 -1
  132. package/dist/ja-5PEH56J5.mjs.map +0 -1
  133. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  134. package/dist/ms-5PZVW76T.mjs.map +0 -1
  135. package/dist/nl-YXES36KM.mjs.map +0 -1
  136. package/dist/no-XRA2UCQD.mjs.map +0 -1
  137. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  138. package/dist/pt-7GAG57BM.mjs.map +0 -1
  139. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  140. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  141. package/dist/th-LPKYLBX5.mjs.map +0 -1
  142. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  143. package/dist/uk-36UHTDDI.mjs.map +0 -1
  144. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  145. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  146. /package/dist/{chunk-35LLVRFK.mjs.map → chunk-JZIO2A3B.mjs.map} +0 -0
  147. /package/dist/{en-EVMIX24Y.mjs.map → en-DDLIXJCU.mjs.map} +0 -0
@@ -1,10 +1,12 @@
1
1
  'use client';
2
2
 
3
- import React, { useMemo, useEffect, useRef } from 'react';
3
+ import React, { useEffect, useRef } from 'react';
4
4
  import { useTranslations } from '../../contexts/TranslationContext';
5
5
  import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
6
6
  import { useResources } from '../../lib/api-hooks';
7
- import { type StoredEvent, type ResourceUri, getAnnotationUriFromEvent } from '@semiont/api-client';
7
+ import type { ResourceUri } from '@semiont/api-client';
8
+ import type { StoredEvent } from '@semiont/core';
9
+ import { getAnnotationUriFromEvent } from '@semiont/core';
8
10
  import { HistoryEvent } from './HistoryEvent';
9
11
 
10
12
  interface Props {
@@ -36,17 +38,14 @@ export function AnnotationHistory({ rUri, hoveredAnnotationId, onEventHover, onE
36
38
 
37
39
  // Sort events by oldest first (most recent at bottom)
38
40
  // Filter out all job events - they're represented by annotation.body.updated events instead
39
- const events = useMemo(() => {
40
- if (!eventsData?.events) return [];
41
- return [...eventsData.events]
42
- .filter((e: StoredEvent) => {
43
- const eventType = e.event.type;
44
- return eventType !== 'job.started' && eventType !== 'job.progress' && eventType !== 'job.completed';
45
- })
46
- .sort((a: StoredEvent, b: StoredEvent) =>
47
- a.metadata.sequenceNumber - b.metadata.sequenceNumber
48
- );
49
- }, [eventsData]);
41
+ const events = !eventsData?.events ? [] : [...eventsData.events]
42
+ .filter((e: StoredEvent) => {
43
+ const eventType = e.event.type;
44
+ return eventType !== 'job.started' && eventType !== 'job.progress' && eventType !== 'job.completed';
45
+ })
46
+ .sort((a: StoredEvent, b: StoredEvent) =>
47
+ a.metadata.sequenceNumber - b.metadata.sequenceNumber
48
+ );
50
49
 
51
50
  // Scroll to bottom when History is first shown or when events change
52
51
  useEffect(() => {
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useMemo, useEffect, useRef, useCallback, lazy, Suspense } from 'react';
3
+ import { useEffect, useRef, useCallback, lazy, Suspense } from 'react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
6
  import { remarkAnnotations, type PreparedAnnotation } from '../../lib/remark-annotations';
@@ -85,24 +85,16 @@ export function BrowseView({
85
85
  const onAnnotationHover = handlers?.onHover;
86
86
  const onCommentHover = handlers?.onCommentHover;
87
87
 
88
- const allAnnotations = useMemo(() =>
89
- [...highlights, ...references, ...assessments, ...comments, ...tags],
90
- [highlights, references, assessments, comments, tags]
91
- );
88
+ const allAnnotations = [...highlights, ...references, ...assessments, ...comments, ...tags];
92
89
 
93
- const preparedAnnotations = useMemo(() =>
94
- prepareAnnotations(allAnnotations, annotators),
95
- [allAnnotations, annotators]
96
- );
90
+ const preparedAnnotations = prepareAnnotations(allAnnotations, annotators);
97
91
 
98
92
  // Create a map of annotation ID -> full annotation for click handling
99
- const annotationMap = useMemo(() => {
100
- const map = new Map<string, Annotation>();
101
- for (const ann of allAnnotations) {
102
- map.set(ann.id, ann);
103
- }
104
- return map;
105
- }, [allAnnotations]);
93
+ const map = new Map<string, Annotation>();
94
+ for (const ann of allAnnotations) {
95
+ map.set(ann.id, ann);
96
+ }
97
+ const annotationMap = map;
106
98
 
107
99
  // Wrapper for annotation hover that routes based on registry metadata
108
100
  const handleAnnotationHover = useCallback((annotationId: string | null) => {
@@ -2,17 +2,16 @@
2
2
 
3
3
  import React, { useRef, useCallback } from 'react';
4
4
  import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
5
+ import type { StoredEvent, ResourceEventType } from '@semiont/core';
6
+ import { getAnnotationUriFromEvent } from '@semiont/core';
5
7
  import {
6
- type StoredEvent,
7
- type ResourceEventType,
8
8
  formatEventType,
9
9
  getEventEmoji,
10
10
  formatRelativeTime,
11
11
  getEventDisplayContent,
12
12
  getEventEntityTypes,
13
13
  getResourceCreationDetails,
14
- getAnnotationUriFromEvent,
15
- } from '@semiont/api-client';
14
+ } from './event-formatting';
16
15
 
17
16
  type TranslateFn = (key: string, params?: Record<string, string | number>) => string;
18
17
 
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
4
4
  // useRouter removed - using window.location for navigation
5
5
  import { useTranslations } from '../../contexts/TranslationContext';
6
6
  import { AnnotateView, type SelectionMotivation, type ClickAction, type ShapeType } from './AnnotateView';
@@ -10,6 +10,8 @@ import { JsonLdView } from '../annotation-popups/JsonLdView';
10
10
  import type { components, Selector } from '@semiont/api-client';
11
11
  import { getExactText, getTargetSelector, resourceUri, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
12
12
  import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
13
+ import { useMakeMeaningEvents } from '../../contexts/MakeMeaningEventBusContext';
14
+ import { useCacheManager } from '../../contexts/CacheContext';
13
15
  import type { Annotator } from '../../lib/annotation-registry';
14
16
  import type { AnnotationsCollection } from '../../types/annotation-props';
15
17
  import { getSelectorType, getSelectedShapeForSelectorType, saveSelectedShapeForSelectorType } from '../../lib/media-shapes';
@@ -24,92 +26,46 @@ interface PendingAnnotation {
24
26
  motivation: Motivation;
25
27
  }
26
28
 
29
+ /**
30
+ * ResourceViewer - Display and interact with resource content and annotations
31
+ *
32
+ * This component uses event-driven architecture for real-time updates:
33
+ * - Subscribes to make-meaning events (annotation:added, annotation:removed, annotation:updated)
34
+ * - Automatically invalidates cache when annotations change
35
+ * - No manual refetch needed - events handle cache invalidation
36
+ *
37
+ * Requirements:
38
+ * - Must be wrapped in MakeMeaningEventBusProvider (provides event bus)
39
+ * - Must be wrapped in CacheContext (provides cache manager)
40
+ *
41
+ * Event flow:
42
+ * make-meaning → EventLog → SSE → EventBus → ResourceViewer → Cache invalidation
43
+ *
44
+ * Phase 2 complete: Event-based cache invalidation replaces manual refetch
45
+ * Phase 3 complete: Fully event-driven - all user interactions use unified event bus
46
+ */
27
47
  interface Props {
28
48
  resource: SemiontResource & { content: string };
29
49
  annotations: AnnotationsCollection;
30
- onRefetchAnnotations?: () => void;
31
- annotateMode: boolean;
32
- onAnnotateModeToggle: () => void;
33
50
  generatingReferenceId?: string | null;
34
- onAnnotationHover?: (annotationId: string | null) => void;
35
- onCommentHover?: (commentId: string | null) => void;
36
- hoveredAnnotationId?: string | null;
37
- hoveredCommentId?: string | null;
38
- scrollToAnnotationId?: string | null;
39
51
  showLineNumbers?: boolean;
40
52
  onAnnotationRequested?: (pending: PendingAnnotation) => void;
41
- onCommentCreationRequested?: (selection: { exact: string; start: number; end: number; svgSelector?: string; fragmentSelector?: string; conformsTo?: string }) => void;
42
- onTagCreationRequested?: (selection: { exact: string; start: number; end: number; svgSelector?: string; fragmentSelector?: string; conformsTo?: string }) => void;
43
- onAssessmentCreationRequested?: (selection: { exact: string; start: number; end: number; svgSelector?: string; fragmentSelector?: string; conformsTo?: string }) => void;
44
- onReferenceCreationRequested?: (selection: {
45
- exact: string;
46
- start: number;
47
- end: number;
48
- prefix?: string;
49
- suffix?: string;
50
- svgSelector?: string;
51
- fragmentSelector?: string;
52
- conformsTo?: string;
53
- }) => void;
54
- onCommentClick?: (commentId: string) => void;
55
- onReferenceClick?: (referenceId: string) => void;
56
- onHighlightClick?: (highlightId: string) => void;
57
- onAssessmentClick?: (assessmentId: string) => void;
58
- onTagClick?: (tagId: string) => void;
59
53
  annotators: Record<string, Annotator>;
60
54
  }
61
55
 
62
56
  export function ResourceViewer({
63
57
  resource,
64
58
  annotations,
65
- onRefetchAnnotations,
66
- annotateMode,
67
- onAnnotateModeToggle,
68
59
  generatingReferenceId,
69
- onAnnotationHover,
70
- onCommentHover,
71
- hoveredAnnotationId,
72
- hoveredCommentId,
73
- scrollToAnnotationId,
74
60
  showLineNumbers = false,
75
61
  onAnnotationRequested,
76
- onCommentCreationRequested,
77
- onTagCreationRequested,
78
- onAssessmentCreationRequested,
79
- onReferenceCreationRequested,
80
- onCommentClick,
81
- onReferenceClick,
82
- onHighlightClick,
83
- onAssessmentClick,
84
- onTagClick,
85
62
  annotators
86
63
  }: Props) {
87
64
  const t = useTranslations('ResourceViewer');
88
65
  const documentViewerRef = useRef<HTMLDivElement>(null);
89
66
 
90
- // Use refs for function props to prevent infinite rerenders
91
- const onRefetchAnnotationsRef = useRef(onRefetchAnnotations);
92
- const onCommentCreationRequestedRef = useRef(onCommentCreationRequested);
93
- const onTagCreationRequestedRef = useRef(onTagCreationRequested);
94
- const onReferenceCreationRequestedRef = useRef(onReferenceCreationRequested);
95
- const onCommentClickRef = useRef(onCommentClick);
96
- const onReferenceClickRef = useRef(onReferenceClick);
97
- const onHighlightClickRef = useRef(onHighlightClick);
98
- const onAssessmentClickRef = useRef(onAssessmentClick);
99
- const onTagClickRef = useRef(onTagClick);
100
-
101
- // Keep refs up to date
102
- useEffect(() => {
103
- onRefetchAnnotationsRef.current = onRefetchAnnotations;
104
- onCommentCreationRequestedRef.current = onCommentCreationRequested;
105
- onTagCreationRequestedRef.current = onTagCreationRequested;
106
- onReferenceCreationRequestedRef.current = onReferenceCreationRequested;
107
- onCommentClickRef.current = onCommentClick;
108
- onReferenceClickRef.current = onReferenceClick;
109
- onHighlightClickRef.current = onHighlightClick;
110
- onAssessmentClickRef.current = onAssessmentClick;
111
- onTagClickRef.current = onTagClick;
112
- });
67
+ // Get unified event bus for emitting UI events
68
+ const eventBus = useMakeMeaningEvents();
113
69
 
114
70
  const { highlights, references, assessments, comments, tags } = annotations;
115
71
 
@@ -131,13 +87,66 @@ export function ResourceViewer({
131
87
 
132
88
  const mimeType = getMimeType();
133
89
 
134
- // Use prop directly instead of internal state
90
+ // Annotate mode state - persisted in localStorage
91
+ const [annotateMode, setAnnotateMode] = useState<boolean>(() => {
92
+ if (typeof window !== 'undefined') {
93
+ return localStorage.getItem('annotateMode') === 'true';
94
+ }
95
+ return false;
96
+ });
97
+
98
+ // Persist annotateMode to localStorage
99
+ useEffect(() => {
100
+ if (typeof window !== 'undefined') {
101
+ localStorage.setItem('annotateMode', annotateMode.toString());
102
+ }
103
+ }, [annotateMode]);
104
+
105
+ // Toggle handler
106
+ const toggleAnnotateMode = useCallback(() => {
107
+ setAnnotateMode(prev => !prev);
108
+ }, []);
109
+
110
+ // Determine active view based on annotate mode
135
111
  const activeView = annotateMode ? 'annotate' : 'browse';
136
112
  const {
137
113
  deleteAnnotation,
138
114
  createAnnotation
139
115
  } = useResourceAnnotations();
140
116
 
117
+ // Event-based cache invalidation - subscribe to make-meaning events
118
+ // This replaces manual onRefetchAnnotations calls with automatic updates
119
+ const cacheManager = useCacheManager();
120
+
121
+ useEffect(() => {
122
+ if (!eventBus || !cacheManager) return;
123
+
124
+ // Annotation events - invalidate cache when annotations change
125
+ const handleAnnotationAdded = () => {
126
+ cacheManager.invalidateAnnotations(rUri);
127
+ };
128
+
129
+ const handleAnnotationRemoved = () => {
130
+ cacheManager.invalidateAnnotations(rUri);
131
+ };
132
+
133
+ const handleAnnotationUpdated = () => {
134
+ cacheManager.invalidateAnnotations(rUri);
135
+ };
136
+
137
+ // Subscribe to make-meaning annotation events
138
+ eventBus.on('annotation:added', handleAnnotationAdded);
139
+ eventBus.on('annotation:removed', handleAnnotationRemoved);
140
+ eventBus.on('annotation:updated', handleAnnotationUpdated);
141
+
142
+ // Cleanup subscriptions
143
+ return () => {
144
+ eventBus.off('annotation:added', handleAnnotationAdded);
145
+ eventBus.off('annotation:removed', handleAnnotationRemoved);
146
+ eventBus.off('annotation:updated', handleAnnotationUpdated);
147
+ };
148
+ }, [eventBus, cacheManager, rUri]);
149
+
141
150
  // Annotation toolbar state - persisted in localStorage
142
151
  const [selectedMotivation, setSelectedMotivation] = useState<SelectionMotivation | null>(() => {
143
152
  if (typeof window !== 'undefined') {
@@ -204,8 +213,23 @@ export function ResourceViewer({
204
213
  position: { x: number; y: number };
205
214
  } | null>(null);
206
215
 
216
+ // Internal UI state for hover, focus, and scroll
217
+ const [hoveredAnnotationId, _setHoveredAnnotationId] = useState<string | null>(null);
218
+ const [hoveredCommentId, _setHoveredCommentId] = useState<string | null>(null);
219
+ const [scrollToAnnotationId, setScrollToAnnotationId] = useState<string | null>(null);
220
+ const [_focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
221
+
222
+ // Focus annotation helper
223
+ const focusAnnotation = useCallback((annotationId: string) => {
224
+ setFocusedAnnotationId(annotationId);
225
+ setScrollToAnnotationId(annotationId);
226
+
227
+ // Clear focus after 3 seconds
228
+ setTimeout(() => setFocusedAnnotationId(null), 3000);
229
+ }, []);
230
+
207
231
  // Calculate centered position for JSON-LD modal
208
- const jsonLdModalPosition = useMemo(() => {
232
+ const getJsonLdModalPosition = () => {
209
233
  if (typeof window === 'undefined') return { x: 0, y: 0 };
210
234
 
211
235
  const popupWidth = 800;
@@ -215,13 +239,13 @@ export function ResourceViewer({
215
239
  x: Math.max(0, (window.innerWidth - popupWidth) / 2),
216
240
  y: Math.max(0, (window.innerHeight - popupHeight) / 2),
217
241
  };
218
- }, []);
242
+ };
219
243
 
220
244
  // Handle deleting annotations - memoized
221
245
  const handleDeleteAnnotation = useCallback(async (id: string) => {
222
246
  try {
223
247
  await deleteAnnotation(id, rUri);
224
- onRefetchAnnotationsRef.current?.();
248
+ // Cache invalidation now handled by annotation:removed event
225
249
  } catch (err) {
226
250
  console.error('Failed to delete annotation:', err);
227
251
  }
@@ -235,27 +259,9 @@ export function ResourceViewer({
235
259
  // For delete/jsonld/follow modes, let those handlers below process it
236
260
  if (metadata?.hasSidePanel) {
237
261
  if (selectedClick === 'detail') {
238
- // Route to appropriate panel based on annotation type
239
- if (isComment(annotation) && onCommentClickRef.current) {
240
- onCommentClickRef.current(annotation.id);
241
- return;
242
- }
243
- if (isReference(annotation) && onReferenceClickRef.current) {
244
- onReferenceClickRef.current(annotation.id);
245
- return;
246
- }
247
- if (isHighlight(annotation) && onHighlightClickRef.current) {
248
- onHighlightClickRef.current(annotation.id);
249
- return;
250
- }
251
- if (isAssessment(annotation) && onAssessmentClickRef.current) {
252
- onAssessmentClickRef.current(annotation.id);
253
- return;
254
- }
255
- if (isTag(annotation) && onTagClickRef.current) {
256
- onTagClickRef.current(annotation.id);
257
- return;
258
- }
262
+ // Focus annotation (sets internal focus and scroll state, plus calls parent callback for backward compat)
263
+ focusAnnotation(annotation.id);
264
+ return;
259
265
  }
260
266
  // Don't return early for delete/jsonld/follow modes - let them be handled below
261
267
  if (selectedClick !== 'deleting' && selectedClick !== 'jsonld' && selectedClick !== 'follow') {
@@ -299,7 +305,7 @@ export function ResourceViewer({
299
305
  setDeleteConfirmation({ annotation, position });
300
306
  return;
301
307
  }
302
- }, [annotateMode, selectedClick, handleDeleteAnnotation, annotators]);
308
+ }, [annotateMode, selectedClick, handleDeleteAnnotation, annotators, focusAnnotation]);
303
309
 
304
310
  // Unified annotation creation handler - works for both text and images
305
311
  const handleAnnotationCreate = useCallback(async (params: import('../../types/annotation-props').UICreateAnnotationParams) => {
@@ -335,13 +341,9 @@ export function ResourceViewer({
335
341
 
336
342
  // Focus the new annotation to trigger panel tab switch
337
343
  if (annotation) {
338
- if (motivation === 'highlighting' && onHighlightClickRef.current) {
339
- onHighlightClickRef.current(annotation.id);
340
- } else if (motivation === 'assessing' && onAssessmentClickRef.current) {
341
- onAssessmentClickRef.current(annotation.id);
342
- }
344
+ focusAnnotation(annotation.id);
343
345
  }
344
- onRefetchAnnotationsRef.current?.();
346
+ // Cache invalidation now handled by annotation:added event
345
347
  } else if (selector.type === 'SvgSelector' && selector.value) {
346
348
  // Image annotations use generic createAnnotation
347
349
  await createAnnotation(
@@ -350,7 +352,7 @@ export function ResourceViewer({
350
352
  { type: 'SvgSelector', value: selector.value },
351
353
  []
352
354
  );
353
- onRefetchAnnotationsRef.current?.();
355
+ // Cache invalidation now handled by annotation:added event
354
356
  } else if (selector.type === 'FragmentSelector' && selector.value) {
355
357
  // PDF annotations use FragmentSelector
356
358
  await createAnnotation(
@@ -363,20 +365,18 @@ export function ResourceViewer({
363
365
  },
364
366
  []
365
367
  );
366
- onRefetchAnnotationsRef.current?.();
368
+ // Cache invalidation now handled by annotation:added event
367
369
  }
368
370
  break;
369
371
 
370
372
  case 'commenting':
371
373
  if (selector.type === 'TextQuoteSelector' && selector.exact) {
372
- // Text: notify parent to open Comments Panel
373
- if (onCommentCreationRequestedRef.current) {
374
- onCommentCreationRequestedRef.current({
375
- exact: selector.exact,
376
- start: selector.start || 0,
377
- end: selector.end || 0
378
- });
379
- }
374
+ // Text: emit UI event for comment creation
375
+ eventBus.emit('ui:selection:comment-requested', {
376
+ exact: selector.exact,
377
+ start: selector.start || 0,
378
+ end: selector.end || 0
379
+ });
380
380
  } else if (selector.type === 'SvgSelector' && selector.value) {
381
381
  // Image: create annotation, then open panel
382
382
  const annotation = await createAnnotation(
@@ -385,10 +385,10 @@ export function ResourceViewer({
385
385
  { type: 'SvgSelector', value: selector.value },
386
386
  []
387
387
  );
388
- if (annotation && onCommentClickRef.current) {
389
- onCommentClickRef.current(annotation.id);
388
+ if (annotation) {
389
+ focusAnnotation(annotation.id);
390
390
  }
391
- onRefetchAnnotationsRef.current?.();
391
+ // Cache invalidation now handled by annotation:added event
392
392
  } else if (selector.type === 'FragmentSelector' && selector.value) {
393
393
  // PDF: create annotation, then open panel
394
394
  const annotation = await createAnnotation(
@@ -401,23 +401,21 @@ export function ResourceViewer({
401
401
  },
402
402
  []
403
403
  );
404
- if (annotation && onCommentClickRef.current) {
405
- onCommentClickRef.current(annotation.id);
404
+ if (annotation) {
405
+ focusAnnotation(annotation.id);
406
406
  }
407
- onRefetchAnnotationsRef.current?.();
407
+ // Cache invalidation now handled by annotation:added event
408
408
  }
409
409
  break;
410
410
 
411
411
  case 'tagging':
412
412
  if (selector.type === 'TextQuoteSelector' && selector.exact) {
413
- // Text: notify parent to open Tags Panel
414
- if (onTagCreationRequestedRef.current) {
415
- onTagCreationRequestedRef.current({
416
- exact: selector.exact,
417
- start: selector.start || 0,
418
- end: selector.end || 0
419
- });
420
- }
413
+ // Text: emit UI event for tag creation
414
+ eventBus.emit('ui:selection:tag-requested', {
415
+ exact: selector.exact,
416
+ start: selector.start || 0,
417
+ end: selector.end || 0
418
+ });
421
419
  } else if (selector.type === 'SvgSelector' && selector.value) {
422
420
  // Image: create annotation, then open panel
423
421
  const annotation = await createAnnotation(
@@ -426,10 +424,10 @@ export function ResourceViewer({
426
424
  { type: 'SvgSelector', value: selector.value },
427
425
  []
428
426
  );
429
- if (annotation && onTagClickRef.current) {
430
- onTagClickRef.current(annotation.id);
427
+ if (annotation) {
428
+ focusAnnotation(annotation.id);
431
429
  }
432
- onRefetchAnnotationsRef.current?.();
430
+ // Cache invalidation now handled by annotation:added event
433
431
  } else if (selector.type === 'FragmentSelector' && selector.value) {
434
432
  // PDF: create annotation, then open panel
435
433
  const annotation = await createAnnotation(
@@ -442,43 +440,38 @@ export function ResourceViewer({
442
440
  },
443
441
  []
444
442
  );
445
- if (annotation && onTagClickRef.current) {
446
- onTagClickRef.current(annotation.id);
443
+ if (annotation) {
444
+ focusAnnotation(annotation.id);
447
445
  }
448
- onRefetchAnnotationsRef.current?.();
446
+ // Cache invalidation now handled by annotation:added event
449
447
  }
450
448
  break;
451
449
 
452
450
  case 'linking':
453
- // Call onReferenceCreationRequested for text, image, and PDF selections
454
- if (onReferenceCreationRequestedRef.current) {
455
- if (selector.type === 'TextQuoteSelector' && selector.exact) {
456
- const selection = {
457
- exact: selector.exact,
458
- start: selector.start || 0,
459
- end: selector.end || 0,
460
- ...(selector.prefix && { prefix: selector.prefix }),
461
- ...(selector.suffix && { suffix: selector.suffix })
462
- };
463
- onReferenceCreationRequestedRef.current(selection);
464
- } else if (selector.type === 'SvgSelector' && selector.value) {
465
- const selection = {
466
- exact: '', // Images don't have exact text
467
- start: 0,
468
- end: 0,
469
- svgSelector: selector.value
470
- };
471
- onReferenceCreationRequestedRef.current(selection);
472
- } else if (selector.type === 'FragmentSelector' && selector.value) {
473
- const selection = {
474
- exact: '', // PDFs don't have exact text
475
- start: 0,
476
- end: 0,
477
- fragmentSelector: selector.value,
478
- ...(selector.conformsTo && { conformsTo: selector.conformsTo })
479
- };
480
- onReferenceCreationRequestedRef.current(selection);
481
- }
451
+ // Emit UI event for reference creation (text, image, or PDF selections)
452
+ if (selector.type === 'TextQuoteSelector' && selector.exact) {
453
+ eventBus.emit('ui:selection:reference-requested', {
454
+ exact: selector.exact,
455
+ start: selector.start || 0,
456
+ end: selector.end || 0,
457
+ ...(selector.prefix && { prefix: selector.prefix }),
458
+ ...(selector.suffix && { suffix: selector.suffix })
459
+ });
460
+ } else if (selector.type === 'SvgSelector' && selector.value) {
461
+ eventBus.emit('ui:selection:reference-requested', {
462
+ exact: '', // Images don't have exact text
463
+ start: 0,
464
+ end: 0,
465
+ svgSelector: selector.value
466
+ });
467
+ } else if (selector.type === 'FragmentSelector' && selector.value) {
468
+ eventBus.emit('ui:selection:reference-requested', {
469
+ exact: '', // PDFs don't have exact text
470
+ start: 0,
471
+ end: 0,
472
+ fragmentSelector: selector.value,
473
+ ...(selector.conformsTo && { conformsTo: selector.conformsTo })
474
+ });
482
475
  }
483
476
  break;
484
477
  }
@@ -492,45 +485,29 @@ export function ResourceViewer({
492
485
  await handleDeleteAnnotation(annotation.id);
493
486
  }, [handleDeleteAnnotation]);
494
487
 
495
- // Memoize objects to prevent infinite re-renders
496
- const annotationsCollection = useMemo(
497
- () => ({ highlights, references, assessments, comments, tags }),
498
- [highlights, references, assessments, comments, tags]
499
- );
488
+ // Prepare props for child components
489
+ // Note: These objects are created inline - React's reconciliation handles re-renders efficiently
490
+ const annotationsCollection = { highlights, references, assessments, comments, tags };
500
491
 
501
- const handlersForAnnotate = useMemo(
502
- () => ({
503
- onClick: handleAnnotationClick,
504
- ...(onAnnotationHover && { onHover: onAnnotationHover }),
505
- ...(onCommentHover && { onCommentHover })
506
- }),
507
- [handleAnnotationClick, onAnnotationHover, onCommentHover]
508
- );
492
+ const handlersForAnnotate = {
493
+ onClick: handleAnnotationClick
494
+ // Note: onHover/onCommentHover removed - component now manages hover state internally
495
+ };
509
496
 
510
- const handlersForBrowse = useMemo(
511
- () => ({
512
- onClick: handleAnnotationClick,
513
- ...(onCommentHover && { onCommentHover })
514
- }),
515
- [handleAnnotationClick, onCommentHover]
516
- );
497
+ const handlersForBrowse = {
498
+ onClick: handleAnnotationClick
499
+ // Note: onCommentHover removed - component now manages hover state internally
500
+ };
517
501
 
518
- const creationHandler = useMemo(
519
- () => ({ onCreate: handleAnnotationCreate }),
520
- [handleAnnotationCreate]
521
- );
502
+ const creationHandler = { onCreate: handleAnnotationCreate };
522
503
 
523
- const uiState = useMemo(
524
- () => ({
525
- selectedMotivation,
526
- selectedClick,
527
- selectedShape,
528
- ...(hoveredAnnotationId !== undefined && { hoveredAnnotationId }),
529
- ...(hoveredCommentId !== undefined && { hoveredCommentId }),
530
- ...(scrollToAnnotationId !== undefined && { scrollToAnnotationId })
531
- }),
532
- [selectedMotivation, selectedClick, selectedShape, hoveredAnnotationId, hoveredCommentId, scrollToAnnotationId]
533
- );
504
+ const uiState = {
505
+ selectedMotivation,
506
+ selectedClick,
507
+ selectedShape,
508
+ hoveredAnnotationId,
509
+ scrollToAnnotationId
510
+ };
534
511
 
535
512
  return (
536
513
  <div ref={documentViewerRef} className="semiont-resource-viewer">
@@ -558,12 +535,8 @@ export function ResourceViewer({
558
535
  onDeleteAnnotation={handleDeleteAnnotationWidget}
559
536
  showLineNumbers={showLineNumbers}
560
537
  annotateMode={annotateMode}
561
- onAnnotateModeToggle={onAnnotateModeToggle}
538
+ onAnnotateModeToggle={toggleAnnotateMode}
562
539
  {...(onAnnotationRequested && { onAnnotationRequested })}
563
- {...(onCommentCreationRequested && { onCommentCreationRequested })}
564
- {...(onTagCreationRequested && { onTagCreationRequested })}
565
- {...(onAssessmentCreationRequested && { onAssessmentCreationRequested })}
566
- {...(onReferenceCreationRequested && { onReferenceCreationRequested })}
567
540
  annotators={annotators}
568
541
  />
569
542
  ) : (
@@ -573,11 +546,11 @@ export function ResourceViewer({
573
546
  resourceUri={resource['@id']}
574
547
  annotations={annotationsCollection}
575
548
  handlers={handlersForBrowse}
576
- {...(hoveredCommentId !== undefined && { hoveredCommentId })}
549
+ hoveredCommentId={hoveredCommentId}
577
550
  selectedClick={selectedClick}
578
551
  onClickChange={setSelectedClick}
579
552
  annotateMode={annotateMode}
580
- onAnnotateModeToggle={onAnnotateModeToggle}
553
+ onAnnotateModeToggle={toggleAnnotateMode}
581
554
  annotators={annotators}
582
555
  />
583
556
  )}
@@ -587,7 +560,7 @@ export function ResourceViewer({
587
560
  <PopupContainer
588
561
  isOpen={showJsonLdView}
589
562
  onClose={() => setShowJsonLdView(false)}
590
- position={jsonLdModalPosition}
563
+ position={getJsonLdModalPosition()}
591
564
  wide={true}
592
565
  >
593
566
  <JsonLdView