@semiont/react-ui 0.5.10 → 0.5.12

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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/{PdfAnnotationCanvas.client-P37SXBWQ.js → PdfAnnotationCanvas.client-75GY2EDR.js} +15 -6
  3. package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
  4. package/dist/{chunk-5LFFAVDI.js → chunk-PFUXQPWJ.js} +96 -96
  5. package/dist/{chunk-5LFFAVDI.js.map → chunk-PFUXQPWJ.js.map} +1 -1
  6. package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
  7. package/dist/chunk-VXASXU4K.js.map +1 -0
  8. package/dist/index.css +150 -112
  9. package/dist/index.css.map +1 -1
  10. package/dist/index.d.ts +334 -48
  11. package/dist/index.js +2494 -2201
  12. package/dist/index.js.map +1 -1
  13. package/dist/test-utils.d.ts +5 -1
  14. package/dist/test-utils.js +10 -4
  15. package/dist/test-utils.js.map +1 -1
  16. package/package.json +7 -7
  17. package/src/components/ResourceTagsInline.tsx +94 -11
  18. package/src/components/StatusDisplay.css +36 -0
  19. package/src/components/__tests__/ResourceTagsInline.test.tsx +101 -3
  20. package/src/components/annotation/AnnotateToolbar.tsx +53 -28
  21. package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +76 -0
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +124 -360
  23. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  24. package/src/components/image-annotation/SvgDrawingCanvas.tsx +17 -5
  25. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
  26. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
  27. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +17 -4
  28. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +143 -7
  29. package/src/components/resource/AnnotateView.tsx +59 -38
  30. package/src/components/resource/BrowseView.tsx +172 -138
  31. package/src/components/resource/ResourceViewer.tsx +123 -130
  32. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +55 -0
  33. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +49 -0
  34. package/src/components/resource/__tests__/BrowseView.links.test.tsx +60 -0
  35. package/src/components/resource/__tests__/BrowseView.overlay-async.test.tsx +113 -0
  36. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +161 -0
  37. package/src/components/resource/__tests__/BrowseView.registry.test.tsx +43 -0
  38. package/src/components/resource/__tests__/BrowseView.test.tsx +133 -53
  39. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +102 -0
  40. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +12 -7
  41. package/src/components/resource/__tests__/ResourceViewer.toolbar-opt-out.test.tsx +135 -0
  42. package/src/components/resource/__tests__/ResourceViewer.toolbar-prefs.test.tsx +200 -0
  43. package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
  44. package/src/components/resource/__tests__/browse-renderers.test.tsx +34 -0
  45. package/src/components/resource/__tests__/inline-embedding.test.tsx +81 -0
  46. package/src/components/resource/__tests__/media-completeness.test.tsx +88 -0
  47. package/src/components/resource/browse-renderers.tsx +85 -0
  48. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  49. package/src/components/resource/panels/AssessmentPanel.tsx +12 -5
  50. package/src/components/resource/panels/AssistSection.tsx +4 -3
  51. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  52. package/src/components/resource/panels/CommentsPanel.tsx +12 -5
  53. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  54. package/src/components/resource/panels/HighlightPanel.tsx +12 -5
  55. package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
  56. package/src/components/resource/panels/ReferencesPanel.tsx +15 -6
  57. package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
  58. package/src/components/resource/panels/TagEntry.tsx +5 -3
  59. package/src/components/resource/panels/TaggingPanel.tsx +11 -4
  60. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +15 -2
  61. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
  62. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +39 -28
  63. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
  64. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +54 -24
  66. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
  67. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
  68. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
  70. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
  71. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +78 -59
  72. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
  73. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
  74. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +16 -7
  75. package/src/components/toolbar/Toolbar.css +14 -0
  76. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +7 -17
  77. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +43 -17
  78. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +2 -14
  79. package/src/styles/features/resource-viewer.css +19 -0
  80. package/dist/PdfAnnotationCanvas.client-P37SXBWQ.js.map +0 -1
  81. package/dist/chunk-O2MD7TGE.js.map +0 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * EMBEDDABLE-VIEWER-COMPLETION Phase 4 — media-completeness sweep.
3
+ *
4
+ * ONE mounted component (`ResourceViewer`, inline, bare session, no providers)
5
+ * renders every standard media type from the DEFAULTS — text, markdown, image,
6
+ * pdf — with the annotation-overlay container present and zero per-type wiring.
7
+ * (Formatted-markdown correctness is pinned by browse-renderers.test; visual
8
+ * overlay alignment is the plan's live smoke-test. This sweep pins the routing
9
+ * parity: every type reaches its renderer from one component.)
10
+ */
11
+ import { describe, it, expect, vi } from 'vitest';
12
+ import { render, screen } from '@testing-library/react';
13
+ import '@testing-library/jest-dom';
14
+ import type { SemiontSession } from '@semiont/sdk';
15
+ import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
16
+ import { ResourceViewer } from '../ResourceViewer';
17
+
18
+ vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div data-testid="md">{children}</div> }));
19
+ vi.mock('remark-gfm', () => ({ default: () => ({}) }));
20
+ // pdfjs can't load in jsdom — the sweep pins ROUTING to the pdf renderer, not pdf.js itself.
21
+ vi.mock('../../pdf-annotation/PdfAnnotationCanvas.client', () => ({
22
+ PdfAnnotationCanvas: ({ pdfUrl }: { pdfUrl: string }) => <div data-testid="pdf-canvas">{pdfUrl}</div>,
23
+ }));
24
+
25
+ const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
26
+
27
+ function fakeSession(): SemiontSession {
28
+ return {
29
+ client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
30
+ subscribe: () => () => {},
31
+ } as unknown as SemiontSession;
32
+ }
33
+
34
+ function makeResource(mediaType: string, content: string): SemiontResource & { content: string } {
35
+ return {
36
+ '@context': 'https://www.w3.org/ns/activitystreams',
37
+ '@id': 'res-1' as ResourceId,
38
+ name: 'Doc',
39
+ created: '2024-01-01T00:00:00Z',
40
+ entityTypes: [],
41
+ archived: false,
42
+ representations: [{ mediaType, byteSize: 10 }],
43
+ content,
44
+ };
45
+ }
46
+
47
+ function mount(mediaType: string, content: string) {
48
+ return render(
49
+ <ResourceViewer
50
+ resource={makeResource(mediaType, content)}
51
+ annotations={emptyAnnotations}
52
+ session={fakeSession()}
53
+ inline
54
+ />,
55
+ );
56
+ }
57
+
58
+ describe('media-completeness sweep (Phase 4) — one component, all types, inline, defaults only', () => {
59
+ it.each([
60
+ ['text/plain', 'plain body'],
61
+ ['text/markdown', '# md body'],
62
+ ])('%s renders via the text default with the overlay container', (mt, content) => {
63
+ const { container } = mount(mt, content);
64
+ expect(container.querySelector('.semiont-browse-view[data-mime-type="text"]')).toBeInTheDocument();
65
+ expect(screen.getByTestId('md')).toHaveTextContent(content.replace('# ', '# ').trim());
66
+ expect(container.querySelector('.semiont-browse-view__content')).toBeInTheDocument(); // overlay anchor
67
+ expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
68
+ });
69
+
70
+ it('image/png renders via the image default', () => {
71
+ const { container } = mount('image/png', 'https://media.test/img.png?token=t');
72
+ expect(container.querySelector('.semiont-browse-view[data-mime-type="image"]')).toBeInTheDocument();
73
+ expect(container.querySelector('img')).toBeInTheDocument();
74
+ expect(container.querySelector('.semiont-browse-view__content')).toBeInTheDocument();
75
+ });
76
+
77
+ it('application/pdf routes to the pdf default', async () => {
78
+ const { container } = mount('application/pdf', 'https://media.test/doc.pdf?token=t');
79
+ expect(container.querySelector('.semiont-browse-view[data-mime-type="pdf"]')).toBeInTheDocument();
80
+ expect(await screen.findByTestId('pdf-canvas')).toHaveTextContent('https://media.test/doc.pdf?token=t');
81
+ });
82
+
83
+ it('an unknown type degrades to metadata + download (no crash, no per-type wiring)', () => {
84
+ const { container } = mount('application/octet-stream', '');
85
+ expect(container.querySelector('[data-mime-type="unsupported"]')).toBeInTheDocument();
86
+ expect(screen.getByText(/Preview not available/)).toBeInTheDocument();
87
+ });
88
+ });
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import { lazy, Suspense, memo, type ComponentType } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import type { Annotation } from '@semiont/core';
7
+ import type { SemiontSession } from '@semiont/sdk';
8
+ import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
9
+
10
+ // Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
11
+ const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
12
+
13
+ /**
14
+ * Common props for a read-only ("browse") media renderer. `content` is decoded
15
+ * text for the text renderer, or a media URL for the image / PDF renderers.
16
+ * A consumer can implement this interface to swap in a custom renderer.
17
+ */
18
+ export interface MediaRendererProps {
19
+ content: string;
20
+ mimeType: string;
21
+ resourceUri: string;
22
+ annotations: Annotation[];
23
+ /**
24
+ * Session for interaction routing inside annotation-bearing renderers — the
25
+ * canvases emit clicks/hover via `session.client.browse/beckon`. Absent/null
26
+ * → the annotations still PAINT, but are inert (paint-only). Threaded by
27
+ * BrowseView's dispatch; custom renderers may ignore it.
28
+ */
29
+ session?: SemiontSession | null;
30
+ }
31
+
32
+ /** Read-only media dispatch, keyed by the registry render mode. */
33
+ export type BrowseMediaRenderers = Partial<Record<string, ComponentType<MediaRendererProps>>>;
34
+
35
+ const MemoizedMarkdown = memo(function MemoizedMarkdown({ content }: { content: string }) {
36
+ // No annotation plugins: annotations are applied as a DOM overlay after paint.
37
+ return <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>;
38
+ });
39
+
40
+ export function TextBrowseRenderer({ content }: MediaRendererProps) {
41
+ return <MemoizedMarkdown content={content} />;
42
+ }
43
+
44
+ export function ImageBrowseRenderer({ content, resourceUri, annotations, session }: MediaRendererProps) {
45
+ // The annotate-mode canvas, read-only (drawingMode=null): paints SvgSelector
46
+ // shapes over the image and routes click/hover via the session — the same
47
+ // shapes browse mode silently dropped when this was a bare ImageViewer
48
+ // (bugs/image-browse-renderer-drops-annotations.md).
49
+ return (
50
+ <SvgDrawingCanvas
51
+ imageUrl={content}
52
+ resourceUri={resourceUri}
53
+ existingAnnotations={annotations}
54
+ drawingMode={null}
55
+ selectedMotivation={null}
56
+ session={session}
57
+ />
58
+ );
59
+ }
60
+
61
+ export function PdfBrowseRenderer({ content, resourceUri, annotations, session }: MediaRendererProps) {
62
+ return (
63
+ <Suspense fallback={<div className="semiont-browse-view__loading">Loading PDF viewer...</div>}>
64
+ <PdfAnnotationCanvas
65
+ pdfUrl={content}
66
+ resourceUri={resourceUri}
67
+ existingAnnotations={annotations}
68
+ drawingMode={null}
69
+ selectedMotivation={null}
70
+ session={session}
71
+ />
72
+ </Suspense>
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Default read-only media renderers. `BrowseView` merges a caller's `renderers`
78
+ * override on top of these, so a host can swap one renderer (e.g. its own PDF
79
+ * viewer) or add a mode without forking the view.
80
+ */
81
+ export const defaultBrowseRenderers: Record<'text' | 'image' | 'pdf', ComponentType<MediaRendererProps>> = {
82
+ text: TextBrowseRenderer,
83
+ image: ImageBrowseRenderer,
84
+ pdf: PdfBrowseRenderer,
85
+ };
@@ -3,8 +3,7 @@
3
3
  import type { Ref } from 'react';
4
4
  import type { Annotation } from '@semiont/core';
5
5
  import { getAnnotationExactText } from '@semiont/core';
6
- import { useSemiont } from '../../../session/SemiontProvider';
7
- import { useObservable } from '../../../hooks/useObservable';
6
+ import type { SemiontSession } from '@semiont/sdk';
8
7
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
9
8
  import { renderAgentLabel } from './agent-label';
10
9
 
@@ -17,6 +16,8 @@ interface TextualBody {
17
16
  }
18
17
 
19
18
  interface AssessmentEntryProps {
19
+ /** Session for interaction routing (browse.click etc.); the panel threads it. */
20
+ session: SemiontSession | null;
20
21
  assessment: Annotation;
21
22
  isFocused: boolean;
22
23
  isHovered?: boolean;
@@ -69,13 +70,13 @@ function getAssessmentText(annotation: Annotation): string | null {
69
70
  }
70
71
 
71
72
  export function AssessmentEntry({
73
+ session,
72
74
  assessment,
73
75
  isFocused,
74
76
  isHovered = false,
75
77
  ref,
76
78
  }: AssessmentEntryProps) {
77
- const session = useObservable(useSemiont().activeSession$);
78
- const hoverProps = useHoverEmitter(assessment.id);
79
+ const hoverProps = useHoverEmitter(session, assessment.id);
79
80
 
80
81
  const selectedText = getAnnotationExactText(assessment);
81
82
  const assessmentText = getAssessmentText(assessment);
@@ -2,9 +2,8 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useSemiont } from '../../../session/SemiontProvider';
6
- import { useObservable } from '../../../hooks/useObservable';
7
- import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
5
+ import type { SemiontSession } from '@semiont/sdk';
6
+ import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
8
7
  import type { components, Selector } from '@semiont/core';
9
8
  import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
10
9
  import { AssessmentEntry } from './AssessmentEntry';
@@ -40,6 +39,10 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
40
39
  }
41
40
 
42
41
  interface AssessmentPanelProps {
42
+ /** Session carrying the client and event bus; null renders inert. */
43
+ session: SemiontSession | null;
44
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
45
+ resourceId: string;
43
46
  annotations: Annotation[];
44
47
  pendingAnnotation: PendingAnnotation | null;
45
48
  isAssisting?: boolean;
@@ -61,6 +64,8 @@ interface AssessmentPanelProps {
61
64
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
62
65
  */
63
66
  export function AssessmentPanel({
67
+ session,
68
+ resourceId,
64
69
  annotations,
65
70
  pendingAnnotation,
66
71
  isAssisting = false,
@@ -73,7 +78,6 @@ export function AssessmentPanel({
73
78
  hoveredAnnotationId,
74
79
  }: AssessmentPanelProps) {
75
80
  const t = useTranslations('AssessmentPanel');
76
- const session = useObservable(useSemiont().activeSession$);
77
81
  const [newAssessmentText, setNewAssessmentText] = useState('');
78
82
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
79
83
  const containerRef = useRef<HTMLDivElement>(null);
@@ -151,6 +155,7 @@ export function AssessmentPanel({
151
155
  : undefined;
152
156
 
153
157
  session?.client.mark.submit({
158
+ source: resourceId,
154
159
  motivation: 'assessing',
155
160
  selector: pendingAnnotation.selector,
156
161
  ...(body !== undefined ? { body } : {}),
@@ -181,7 +186,7 @@ export function AssessmentPanel({
181
186
  }, []);
182
187
 
183
188
  // Subscribe to click events - update focused state
184
- useEventSubscriptions({
189
+ useSessionEventSubscriptions(session, {
185
190
  'browse:click': handleAnnotationClick,
186
191
  });
187
192
 
@@ -243,6 +248,7 @@ export function AssessmentPanel({
243
248
  {/* Assist Section - only in Annotate mode and for text resources */}
244
249
  {annotateMode && (
245
250
  <AssistSection
251
+ session={session}
246
252
  annotationType="assessment"
247
253
  isAssisting={isAssisting}
248
254
  locale={locale}
@@ -260,6 +266,7 @@ export function AssessmentPanel({
260
266
  ) : (
261
267
  sortedAnnotations.map((assessment) => (
262
268
  <AssessmentEntry
269
+ session={session}
263
270
  key={assessment.id}
264
271
  assessment={assessment}
265
272
  isFocused={assessment.id === focusedAnnotationId}
@@ -2,14 +2,15 @@
2
2
 
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useSemiont } from '../../../session/SemiontProvider';
6
- import { useObservable } from '../../../hooks/useObservable';
5
+ import type { SemiontSession } from '@semiont/sdk';
7
6
  import type { Motivation, components } from '@semiont/core';
8
7
  import './AssistSection.css';
9
8
 
10
9
  type JobProgress = components['schemas']['JobProgress'];
11
10
 
12
11
  interface AssistSectionProps {
12
+ /** Session carrying the client and event bus; null renders inert. */
13
+ session: SemiontSession | null;
13
14
  annotationType: 'highlight' | 'assessment' | 'comment';
14
15
  isAssisting: boolean;
15
16
  /** User UI locale — written into the annotation body's `language` field for comment/assessment. */
@@ -34,6 +35,7 @@ interface AssistSectionProps {
34
35
  * @emits mark:progress-dismiss - Dismiss the annotation progress display
35
36
  */
36
37
  export function AssistSection({
38
+ session,
37
39
  annotationType,
38
40
  isAssisting,
39
41
  locale,
@@ -45,7 +47,6 @@ export function AssistSection({
45
47
  annotationType === 'assessment' ? 'AssessmentPanel' :
46
48
  'CommentsPanel';
47
49
  const t = useTranslations(panelName);
48
- const session = useObservable(useSemiont().activeSession$);
49
50
  const [instructions, setInstructions] = useState('');
50
51
  type ToneValue = 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive' | '';
51
52
  const [tone, setTone] = useState<ToneValue>('');
@@ -4,12 +4,13 @@ import { useState, useEffect, useRef, useImperativeHandle, type Ref } from 'reac
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
5
  import type { Annotation } from '@semiont/core';
6
6
  import { getAnnotationExactText, getCommentText } from '@semiont/core';
7
- import { useSemiont } from '../../../session/SemiontProvider';
8
- import { useObservable } from '../../../hooks/useObservable';
7
+ import type { SemiontSession } from '@semiont/sdk';
9
8
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
10
9
  import { renderAgentLabel } from './agent-label';
11
10
 
12
11
  interface CommentEntryProps {
12
+ /** Session for interaction routing (browse.click etc.); the panel threads it. */
13
+ session: SemiontSession | null;
13
14
  comment: Annotation;
14
15
  isFocused: boolean;
15
16
  isHovered?: boolean;
@@ -35,6 +36,7 @@ function formatRelativeTime(isoString: string): string {
35
36
  }
36
37
 
37
38
  export function CommentEntry({
39
+ session,
38
40
  comment,
39
41
  isFocused,
40
42
  isHovered = false,
@@ -42,8 +44,7 @@ export function CommentEntry({
42
44
  ref,
43
45
  }: CommentEntryProps) {
44
46
  const t = useTranslations('CommentsPanel');
45
- const session = useObservable(useSemiont().activeSession$);
46
- const hoverProps = useHoverEmitter(comment.id);
47
+ const hoverProps = useHoverEmitter(session, comment.id);
47
48
  const [isEditing, setIsEditing] = useState(false);
48
49
  const [editText, setEditText] = useState('');
49
50
  const internalRef = useRef<HTMLDivElement>(null);
@@ -2,9 +2,8 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useSemiont } from '../../../session/SemiontProvider';
6
- import { useObservable } from '../../../hooks/useObservable';
7
- import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
5
+ import type { SemiontSession } from '@semiont/sdk';
6
+ import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
8
7
  import type { components, Selector } from '@semiont/core';
9
8
  import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
10
9
  import { CommentEntry } from './CommentEntry';
@@ -40,6 +39,10 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
40
39
  }
41
40
 
42
41
  interface CommentsPanelProps {
42
+ /** Session carrying the client and event bus; null renders inert. */
43
+ session: SemiontSession | null;
44
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
45
+ resourceId: string;
43
46
  annotations: Annotation[];
44
47
  pendingAnnotation: PendingAnnotation | null;
45
48
  annotateMode?: boolean;
@@ -61,6 +64,8 @@ interface CommentsPanelProps {
61
64
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
62
65
  */
63
66
  export function CommentsPanel({
67
+ session,
68
+ resourceId,
64
69
  annotations,
65
70
  pendingAnnotation,
66
71
  annotateMode = true,
@@ -73,7 +78,6 @@ export function CommentsPanel({
73
78
  hoveredAnnotationId,
74
79
  }: CommentsPanelProps) {
75
80
  const t = useTranslations('CommentsPanel');
76
- const session = useObservable(useSemiont().activeSession$);
77
81
  const [newCommentText, setNewCommentText] = useState('');
78
82
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
79
83
  const containerRef = useRef<HTMLDivElement>(null);
@@ -164,13 +168,14 @@ export function CommentsPanel({
164
168
  setTimeout(() => setFocusedAnnotationId(null), 3000);
165
169
  }, []);
166
170
 
167
- useEventSubscriptions({
171
+ useSessionEventSubscriptions(session, {
168
172
  'browse:click': handleAnnotationClick,
169
173
  });
170
174
 
171
175
  const handleSaveNewComment = () => {
172
176
  if (newCommentText.trim() && pendingAnnotation) {
173
177
  session?.client.mark.submit({
178
+ source: resourceId,
174
179
  motivation: 'commenting',
175
180
  selector: pendingAnnotation.selector,
176
181
  body: [{ type: 'TextualBody', value: newCommentText, purpose: 'commenting' }],
@@ -253,6 +258,7 @@ export function CommentsPanel({
253
258
  {/* Assist Section - only in Annotate mode and for text resources */}
254
259
  {annotateMode && (
255
260
  <AssistSection
261
+ session={session}
256
262
  annotationType="comment"
257
263
  isAssisting={isAssisting}
258
264
  locale={locale}
@@ -270,6 +276,7 @@ export function CommentsPanel({
270
276
  ) : (
271
277
  sortedAnnotations.map((comment) => (
272
278
  <CommentEntry
279
+ session={session}
273
280
  key={comment.id}
274
281
  comment={comment}
275
282
  isFocused={comment.id === focusedAnnotationId}
@@ -3,12 +3,13 @@
3
3
  import type { Ref } from 'react';
4
4
  import type { Annotation } from '@semiont/core';
5
5
  import { getAnnotationExactText } from '@semiont/core';
6
- import { useSemiont } from '../../../session/SemiontProvider';
7
- import { useObservable } from '../../../hooks/useObservable';
6
+ import type { SemiontSession } from '@semiont/sdk';
8
7
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
9
8
  import { renderAgentLabel } from './agent-label';
10
9
 
11
10
  interface HighlightEntryProps {
11
+ /** Session for interaction routing (browse.click etc.); the panel threads it. */
12
+ session: SemiontSession | null;
12
13
  highlight: Annotation;
13
14
  isFocused: boolean;
14
15
  isHovered?: boolean;
@@ -33,13 +34,13 @@ function formatRelativeTime(isoString: string): string {
33
34
  }
34
35
 
35
36
  export function HighlightEntry({
37
+ session,
36
38
  highlight,
37
39
  isFocused,
38
40
  isHovered = false,
39
41
  ref,
40
42
  }: HighlightEntryProps) {
41
- const session = useObservable(useSemiont().activeSession$);
42
- const hoverProps = useHoverEmitter(highlight.id);
43
+ const hoverProps = useHoverEmitter(session, highlight.id);
43
44
 
44
45
  const selectedText = getAnnotationExactText(highlight);
45
46
 
@@ -2,9 +2,8 @@
2
2
 
3
3
  import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useSemiont } from '../../../session/SemiontProvider';
6
- import { useObservable } from '../../../hooks/useObservable';
7
- import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
5
+ import type { SemiontSession } from '@semiont/sdk';
6
+ import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
8
7
  import type { components, Selector } from '@semiont/core';
9
8
  import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
10
9
  import { HighlightEntry } from './HighlightEntry';
@@ -23,6 +22,10 @@ interface PendingAnnotation {
23
22
  }
24
23
 
25
24
  interface HighlightPanelProps {
25
+ /** Session carrying the client and event bus; null renders inert. */
26
+ session: SemiontSession | null;
27
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
28
+ resourceId: string;
26
29
  annotations: Annotation[];
27
30
  pendingAnnotation: PendingAnnotation | null;
28
31
  isAssisting?: boolean;
@@ -42,6 +45,8 @@ interface HighlightPanelProps {
42
45
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
43
46
  */
44
47
  export function HighlightPanel({
48
+ session,
49
+ resourceId,
45
50
  annotations,
46
51
  pendingAnnotation,
47
52
  isAssisting = false,
@@ -54,7 +59,6 @@ export function HighlightPanel({
54
59
  }: HighlightPanelProps) {
55
60
 
56
61
  const t = useTranslations('HighlightPanel');
57
- const session = useObservable(useSemiont().activeSession$);
58
62
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
59
63
  const containerRef = useRef<HTMLDivElement>(null);
60
64
 
@@ -125,7 +129,7 @@ export function HighlightPanel({
125
129
  setTimeout(() => setFocusedAnnotationId(null), 3000);
126
130
  }, []);
127
131
 
128
- useEventSubscriptions({
132
+ useSessionEventSubscriptions(session, {
129
133
  'browse:click': handleAnnotationClick,
130
134
  });
131
135
 
@@ -136,6 +140,7 @@ export function HighlightPanel({
136
140
  useEffect(() => {
137
141
  if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
138
142
  session?.client.mark.submit({
143
+ source: resourceId,
139
144
  motivation: 'highlighting',
140
145
  selector: pendingAnnotation.selector,
141
146
  });
@@ -151,6 +156,7 @@ export function HighlightPanel({
151
156
  {/* Assist Section - only in Annotate mode and for text resources */}
152
157
  {annotateMode && (
153
158
  <AssistSection
159
+ session={session}
154
160
  annotationType="highlight"
155
161
  isAssisting={isAssisting}
156
162
  progress={progress}
@@ -167,6 +173,7 @@ export function HighlightPanel({
167
173
  ) : (
168
174
  sortedAnnotations.map((highlight) => (
169
175
  <HighlightEntry
176
+ session={session}
170
177
  key={highlight.id}
171
178
  highlight={highlight}
172
179
  isFocused={highlight.id === focusedAnnotationId}
@@ -1,17 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import type { Ref } from 'react';
4
- import type { RouteBuilder } from '../../../contexts/RoutingContext';
5
4
  import { useTranslations } from '../../../contexts/TranslationContext';
6
5
  import type { Annotation } from '@semiont/core';
7
6
  import { resourceId } from '@semiont/core';
8
7
  import { getAnnotationExactText, isBodyResolved, getBodySource, getFragmentSelector, getSvgSelector, getTargetSelector } from '@semiont/core';
9
8
  import { getEntityTypes } from '@semiont/ontology';
10
9
  import { getResourceIcon } from '../../../lib/resource-utils';
11
- import { useSemiont } from '../../../session/SemiontProvider';
12
- import { useObservable } from '../../../hooks/useObservable';
10
+ import type { SemiontSession } from '@semiont/sdk';
13
11
  import { renderAgentLabel } from './agent-label';
14
- import { useObservableExternalNavigation } from '../../../hooks/useObservableBrowse';
15
12
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
16
13
 
17
14
  // Extended annotation type with runtime properties added by backend enrichment
@@ -21,29 +18,31 @@ interface EnrichedAnnotation extends Annotation {
21
18
  }
22
19
 
23
20
  interface ReferenceEntryProps {
21
+ /** Session for interaction routing (browse.click etc.); the panel threads it. */
22
+ session: SemiontSession | null;
24
23
  reference: Annotation;
25
24
  isFocused: boolean;
26
25
  isHovered?: boolean;
27
- routes: RouteBuilder;
26
+ /** Host-owned navigation: called with the resolved resource id when the entry's link opens. */
27
+ onOpenResource?: (resourceId: string) => void;
28
28
  annotateMode?: boolean;
29
29
  isGenerating?: boolean;
30
30
  ref?: Ref<HTMLDivElement>;
31
31
  }
32
32
 
33
33
  export function ReferenceEntry({
34
+ session,
34
35
  reference,
35
36
  isFocused,
36
37
  isHovered = false,
37
- routes,
38
+ onOpenResource,
38
39
  annotateMode = true,
39
40
  isGenerating = false,
40
41
  ref,
41
42
  }: ReferenceEntryProps) {
42
43
  const t = useTranslations('ReferencesPanel');
43
- const session = useObservable(useSemiont().activeSession$);
44
44
  const semiont = session?.client;
45
- const navigate = useObservableExternalNavigation();
46
- const hoverProps = useHoverEmitter(reference.id);
45
+ const hoverProps = useHoverEmitter(session, reference.id);
47
46
 
48
47
  const selectedText = getAnnotationExactText(reference) || '';
49
48
  const isResolved = isBodyResolved(reference.body);
@@ -65,7 +64,7 @@ export function ReferenceEntry({
65
64
  const handleOpen = () => {
66
65
  if (resolvedResourceUri) {
67
66
  // resolvedResourceUri is already a bare resource ID
68
- navigate(routes.resourceDetail(resolvedResourceUri), { resourceId: resolvedResourceUri });
67
+ onOpenResource?.(resolvedResourceUri);
69
68
  }
70
69
  };
71
70
 
@@ -2,9 +2,8 @@
2
2
 
3
3
  import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useSemiont } from '../../../session/SemiontProvider';
6
- import { useObservable } from '../../../hooks/useObservable';
7
- import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
5
+ import type { SemiontSession } from '@semiont/sdk';
6
+ import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
8
7
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
9
8
  import { AnnotateReferencesProgressWidget } from '../../AnnotateReferencesProgressWidget';
10
9
  import { ReferenceEntry } from './ReferenceEntry';
@@ -43,6 +42,12 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
43
42
  }
44
43
 
45
44
  interface Props {
45
+ /** Session carrying the client and event bus; null renders inert. */
46
+ session: SemiontSession | null;
47
+ /** Host-owned navigation: called with the resolved resource id when a reference entry opens. */
48
+ onOpenResource?: (resourceId: string) => void;
49
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
50
+ resourceId: string;
46
51
  // Generic panel props
47
52
  annotations?: Annotation[];
48
53
  isAssisting: boolean;
@@ -76,6 +81,9 @@ interface Props {
76
81
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
77
82
  */
78
83
  export function ReferencesPanel({
84
+ session,
85
+ onOpenResource,
86
+ resourceId,
79
87
  annotations = [],
80
88
  isAssisting,
81
89
  progress,
@@ -94,7 +102,6 @@ export function ReferencesPanel({
94
102
  sourceLanguage,
95
103
  }: Props) {
96
104
  const t = useTranslations('ReferencesPanel');
97
- const session = useObservable(useSemiont().activeSession$);
98
105
  const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
99
106
  const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
100
107
  const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
@@ -203,7 +210,7 @@ export function ReferencesPanel({
203
210
  setTimeout(() => setFocusedAnnotationId(null), 3000);
204
211
  }, []);
205
212
 
206
- useEventSubscriptions({
213
+ useSessionEventSubscriptions(session, {
207
214
  'browse:click': handleAnnotationClick,
208
215
  });
209
216
 
@@ -255,6 +262,7 @@ export function ReferencesPanel({
255
262
  if (pendingAnnotation) {
256
263
  const entityType = pendingEntityTypes.join(',') || undefined;
257
264
  session?.client.mark.submit({
265
+ source: resourceId,
258
266
  motivation: 'linking',
259
267
  selector: pendingAnnotation.selector,
260
268
  body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
@@ -488,11 +496,12 @@ export function ReferencesPanel({
488
496
  ) : (
489
497
  sortedAnnotations.map((reference) => (
490
498
  <ReferenceEntry
499
+ session={session}
491
500
  key={reference.id}
492
501
  reference={reference}
493
502
  isFocused={reference.id === focusedAnnotationId}
494
503
  isHovered={reference.id === hoveredAnnotationId}
495
- routes={routes}
504
+ onOpenResource={onOpenResource}
496
505
  annotateMode={annotateMode}
497
506
  isGenerating={reference.id === generatingReferenceId}
498
507
  ref={(el) => setEntryRef(reference.id, el)}