@semiont/react-ui 0.4.19 → 0.4.21

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 (108) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
  3. package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-KEDFYI6N.mjs +7788 -0
  6. package/dist/chunk-KEDFYI6N.mjs.map +1 -0
  7. package/dist/index.d.mts +171 -1140
  8. package/dist/index.mjs +3263 -13644
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +46 -21
  11. package/dist/test-utils.mjs +2499 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +1 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +9 -11
  16. package/src/components/StatusDisplay.tsx +42 -16
  17. package/src/components/Toolbar.tsx +4 -4
  18. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  19. package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
  20. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  21. package/src/components/annotation/AnnotateToolbar.tsx +8 -7
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  23. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
  26. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  27. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  28. package/src/components/modals/ResourceSearchModal.tsx +10 -6
  29. package/src/components/modals/SearchModal.tsx +10 -6
  30. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  31. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  32. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  33. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  34. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  35. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  36. package/src/components/navigation/ObservableLink.tsx +6 -6
  37. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  38. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  39. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  40. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
  41. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
  42. package/src/components/resource/AnnotateView.tsx +7 -6
  43. package/src/components/resource/AnnotationHistory.tsx +9 -12
  44. package/src/components/resource/BrowseView.tsx +8 -7
  45. package/src/components/resource/ResourceViewer.tsx +17 -25
  46. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  47. package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
  48. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
  49. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  50. package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
  51. package/src/components/resource/panels/AssistSection.tsx +11 -13
  52. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  53. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  54. package/src/components/resource/panels/CommentsPanel.tsx +9 -11
  55. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  56. package/src/components/resource/panels/HighlightPanel.tsx +10 -11
  57. package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
  58. package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
  59. package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
  60. package/src/components/resource/panels/TagEntry.tsx +5 -4
  61. package/src/components/resource/panels/TaggingPanel.tsx +10 -16
  62. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
  63. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
  64. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
  66. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
  67. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
  68. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  70. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
  71. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
  72. package/src/components/settings/SettingsPanel.tsx +8 -8
  73. package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
  74. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  75. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  76. package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
  77. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  78. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  79. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  80. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  81. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  82. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  83. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  84. package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
  85. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  86. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  87. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
  88. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
  89. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  90. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  91. package/dist/chunk-OZICDVH7.mjs +0 -62
  92. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  93. package/dist/chunk-R4CCMFJH.mjs +0 -877
  94. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  95. package/dist/chunk-VN5NY4SN.mjs +0 -200
  96. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  97. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  98. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  99. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  100. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  101. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  102. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  103. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  104. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  105. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  106. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  107. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  108. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -53,7 +53,7 @@ describe('PermissionDeniedModal', () => {
53
53
  describe('initial render', () => {
54
54
  it('does not render modal content when permissionDeniedAt is null', () => {
55
55
  renderWithProviders(<PermissionDeniedModal />, {
56
- knowledgeBaseSession: createMockKnowledgeBaseSession({
56
+ browser: createMockKnowledgeBaseSession({
57
57
  permissionDeniedAt: null,
58
58
  }),
59
59
  });
@@ -64,7 +64,7 @@ describe('PermissionDeniedModal', () => {
64
64
  describe('when permissionDeniedAt is set', () => {
65
65
  it('shows modal with default message when no message provided', () => {
66
66
  renderWithProviders(<PermissionDeniedModal />, {
67
- knowledgeBaseSession: createMockKnowledgeBaseSession({
67
+ browser: createMockKnowledgeBaseSession({
68
68
  permissionDeniedAt: Date.now(),
69
69
  }),
70
70
  });
@@ -75,7 +75,7 @@ describe('PermissionDeniedModal', () => {
75
75
 
76
76
  it('shows custom message from permissionDeniedMessage', () => {
77
77
  renderWithProviders(<PermissionDeniedModal />, {
78
- knowledgeBaseSession: createMockKnowledgeBaseSession({
78
+ browser: createMockKnowledgeBaseSession({
79
79
  permissionDeniedAt: Date.now(),
80
80
  permissionDeniedMessage: 'Admin access required for this resource',
81
81
  }),
@@ -86,7 +86,7 @@ describe('PermissionDeniedModal', () => {
86
86
 
87
87
  it('renders all three action buttons', () => {
88
88
  renderWithProviders(<PermissionDeniedModal />, {
89
- knowledgeBaseSession: createMockKnowledgeBaseSession({
89
+ browser: createMockKnowledgeBaseSession({
90
90
  permissionDeniedAt: Date.now(),
91
91
  }),
92
92
  });
@@ -101,7 +101,7 @@ describe('PermissionDeniedModal', () => {
101
101
  it('acknowledges and calls window.history.back on Go Back', () => {
102
102
  const ack = vi.fn();
103
103
  renderWithProviders(<PermissionDeniedModal />, {
104
- knowledgeBaseSession: createMockKnowledgeBaseSession({
104
+ browser: createMockKnowledgeBaseSession({
105
105
  permissionDeniedAt: Date.now(),
106
106
  permissionDeniedMessage: 'denied',
107
107
  acknowledgePermissionDenied: ack,
@@ -117,7 +117,7 @@ describe('PermissionDeniedModal', () => {
117
117
  it('acknowledges and navigates to / on Go to Home', () => {
118
118
  const ack = vi.fn();
119
119
  renderWithProviders(<PermissionDeniedModal />, {
120
- knowledgeBaseSession: createMockKnowledgeBaseSession({
120
+ browser: createMockKnowledgeBaseSession({
121
121
  permissionDeniedAt: Date.now(),
122
122
  permissionDeniedMessage: 'denied',
123
123
  acknowledgePermissionDenied: ack,
@@ -134,7 +134,7 @@ describe('PermissionDeniedModal', () => {
134
134
  const ack = vi.fn();
135
135
  mockLocation.pathname = '/admin/users';
136
136
  renderWithProviders(<PermissionDeniedModal />, {
137
- knowledgeBaseSession: createMockKnowledgeBaseSession({
137
+ browser: createMockKnowledgeBaseSession({
138
138
  permissionDeniedAt: Date.now(),
139
139
  permissionDeniedMessage: 'denied',
140
140
  acknowledgePermissionDenied: ack,
@@ -16,21 +16,23 @@ vi.mock('@headlessui/react', () => ({
16
16
  }));
17
17
 
18
18
  // Mock the api-client Observable surface.
19
- // Note: useApiClient is called on every render. The real ApiClientProvider
20
- // holds a single instance the mock must do the same, otherwise useMemo deps
21
- // invalidate on every render and RxJS pipelines restart from their initial
22
- // value on each keystroke.
19
+ // The session-based useSemiont path: useObservable(useSemiont().activeSession$)?.client
20
+ // We mock useSemiont to return a stable browser whose activeSession$ emits a
21
+ // session-shaped object that carries the mock client.
23
22
  const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
24
23
  const browseResourcesMock = vi.fn(() => browseResourcesSubject.asObservable());
25
24
  const stableMockClient = { browse: { resources: browseResourcesMock } };
25
+ const stableMockSession = { client: stableMockClient };
26
+ const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
27
+ const stableMockBrowser = { activeSession$: stableActiveSession$ };
26
28
 
27
- vi.mock('../../../contexts/ApiClientContext', async () => {
28
- const actual = await vi.importActual<typeof import('../../../contexts/ApiClientContext')>(
29
- '../../../contexts/ApiClientContext'
29
+ vi.mock('../../../session/SemiontProvider', async () => {
30
+ const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>(
31
+ '../../../session/SemiontProvider'
30
32
  );
31
33
  return {
32
34
  ...actual,
33
- useApiClient: () => stableMockClient,
35
+ useSemiont: () => stableMockBrowser,
34
36
  };
35
37
  });
36
38
 
@@ -36,19 +36,22 @@ vi.mock('@headlessui/react', () => ({
36
36
  const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
37
37
  const browseResourcesMock = vi.fn(() => browseResourcesSubject.asObservable());
38
38
 
39
- // Stable client reference — useApiClient is called on every render, so a
39
+ // Stable client reference — useSemiont is called on every render, so a
40
40
  // fresh object literal would invalidate useMemo deps and restart the RxJS
41
- // pipeline on every keystroke. The real ApiClientProvider holds a single
42
- // instance; the mock must do the same.
41
+ // pipeline on every keystroke. The real SemiontBrowser holds a single
42
+ // activeSession$ BehaviorSubject; the mock must do the same.
43
43
  const stableMockClient = { browse: { resources: browseResourcesMock } };
44
+ const stableMockSession = { client: stableMockClient };
45
+ const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
46
+ const stableMockBrowser = { activeSession$: stableActiveSession$ };
44
47
 
45
- vi.mock('../../../contexts/ApiClientContext', async () => {
46
- const actual = await vi.importActual<typeof import('../../../contexts/ApiClientContext')>(
47
- '../../../contexts/ApiClientContext'
48
+ vi.mock('../../../session/SemiontProvider', async () => {
49
+ const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>(
50
+ '../../../session/SemiontProvider'
48
51
  );
49
52
  return {
50
53
  ...actual,
51
- useApiClient: () => stableMockClient,
54
+ useSemiont: () => stableMockBrowser,
52
55
  };
53
56
  });
54
57
 
@@ -48,7 +48,7 @@ describe('SessionExpiredModal', () => {
48
48
  describe('initial render', () => {
49
49
  it('does not render modal content when sessionExpiredAt is null', () => {
50
50
  renderWithProviders(<SessionExpiredModal />, {
51
- knowledgeBaseSession: createMockKnowledgeBaseSession({
51
+ browser: createMockKnowledgeBaseSession({
52
52
  sessionExpiredAt: null,
53
53
  }),
54
54
  });
@@ -59,7 +59,7 @@ describe('SessionExpiredModal', () => {
59
59
  describe('when sessionExpiredAt is set', () => {
60
60
  it('renders the modal with default message', () => {
61
61
  renderWithProviders(<SessionExpiredModal />, {
62
- knowledgeBaseSession: createMockKnowledgeBaseSession({
62
+ browser: createMockKnowledgeBaseSession({
63
63
  sessionExpiredAt: Date.now(),
64
64
  }),
65
65
  });
@@ -71,7 +71,7 @@ describe('SessionExpiredModal', () => {
71
71
 
72
72
  it('renders the custom message from sessionExpiredMessage', () => {
73
73
  renderWithProviders(<SessionExpiredModal />, {
74
- knowledgeBaseSession: createMockKnowledgeBaseSession({
74
+ browser: createMockKnowledgeBaseSession({
75
75
  sessionExpiredAt: Date.now(),
76
76
  sessionExpiredMessage: 'Your token expired at 5pm',
77
77
  }),
@@ -85,7 +85,7 @@ describe('SessionExpiredModal', () => {
85
85
  const ack = vi.fn();
86
86
  mockLocation.pathname = '/know/discover';
87
87
  renderWithProviders(<SessionExpiredModal />, {
88
- knowledgeBaseSession: createMockKnowledgeBaseSession({
88
+ browser: createMockKnowledgeBaseSession({
89
89
  sessionExpiredAt: Date.now(),
90
90
  acknowledgeSessionExpired: ack,
91
91
  }),
@@ -100,7 +100,7 @@ describe('SessionExpiredModal', () => {
100
100
  it('calls acknowledgeSessionExpired and navigates to / on Go to Home', () => {
101
101
  const ack = vi.fn();
102
102
  renderWithProviders(<SessionExpiredModal />, {
103
- knowledgeBaseSession: createMockKnowledgeBaseSession({
103
+ browser: createMockKnowledgeBaseSession({
104
104
  sessionExpiredAt: Date.now(),
105
105
  acknowledgeSessionExpired: ack,
106
106
  }),
@@ -19,7 +19,7 @@ import {
19
19
  import { SortableResourceTab } from './SortableResourceTab';
20
20
  import { useDragAnnouncements } from '../../hooks/useDragAnnouncements';
21
21
  import { useTranslations } from '../../contexts/TranslationContext';
22
- import { useEventBus } from '../../contexts/EventBusContext';
22
+ import { useSemiont } from '../../session/SemiontProvider';
23
23
  import type { CollapsibleResourceNavigationProps } from '../../types/collapsible-navigation';
24
24
  import './CollapsibleResourceNavigation.css';
25
25
 
@@ -28,9 +28,9 @@ import './CollapsibleResourceNavigation.css';
28
28
  * Supports drag and drop for resource reordering when expanded.
29
29
  * Platform-agnostic design for use across different React environments.
30
30
  *
31
- * @emits browse:resource-reorder - Resource tab reordered. Payload: { oldIndex: number, newIndex: number }
32
- * @emits browse:resource-close - Resource tab closed. Payload: { resourceId: string }
33
- * @emits browse:sidebar-toggle - Toggle sidebar collapsed/expanded state. Payload: undefined
31
+ * @emits tabs:reorder - Resource tab reordered. Payload: { oldIndex: number, newIndex: number }
32
+ * @emits tabs:close - Resource tab closed. Payload: { resourceId: string }
33
+ * @emits shell:sidebar-toggle - Toggle sidebar collapsed/expanded state. Payload: undefined
34
34
  */
35
35
  export function CollapsibleResourceNavigation({
36
36
  fixedItems,
@@ -53,7 +53,7 @@ export function CollapsibleResourceNavigation({
53
53
 
54
54
  const { announcePickup, announceDrop, announceKeyboardReorder, announceCannotMove } = useDragAnnouncements();
55
55
  const t = useTranslations('CollapsibleResourceNavigation');
56
- const eventBus = useEventBus();
56
+ const semiont = useSemiont();
57
57
 
58
58
  // Use translations from context, with fallback to props for backward compatibility
59
59
  const mergedTranslations = {
@@ -110,12 +110,12 @@ export function CollapsibleResourceNavigation({
110
110
  }
111
111
 
112
112
  // Emit event
113
- eventBus.get('browse:resource-reorder').next({ oldIndex: currentIndex, newIndex });
113
+ semiont.emit('tabs:reorder', { oldIndex: currentIndex, newIndex });
114
114
 
115
115
  // Announce the change
116
116
  const resource = resources[currentIndex];
117
117
  announceKeyboardReorder(resource.name, direction, newIndex + 1, resources.length);
118
- }, [resources]);
118
+ }, [resources, semiont]);
119
119
 
120
120
  // Handle resource close
121
121
  const handleResourceClose = (resourceId: string, e: React.MouseEvent) => {
@@ -123,7 +123,7 @@ export function CollapsibleResourceNavigation({
123
123
  e.stopPropagation();
124
124
 
125
125
  // Emit event
126
- eventBus.get('browse:resource-close').next({ resourceId });
126
+ semiont.emit('tabs:close', { resourceId });
127
127
 
128
128
  // If we're closing the currently viewed resource, navigate to first fixed item or trigger callback
129
129
  const resourceHref = getResourceHref(resourceId);
@@ -151,7 +151,7 @@ export function CollapsibleResourceNavigation({
151
151
  const newIndex = resources.findIndex((resource) => resource.id === over.id);
152
152
  if (oldIndex !== -1 && newIndex !== -1) {
153
153
  // Emit event
154
- eventBus.get('browse:resource-reorder').next({ oldIndex, newIndex });
154
+ semiont.emit('tabs:reorder', { oldIndex, newIndex });
155
155
  const resource = resources[oldIndex];
156
156
  announceDrop(resource.name, newIndex + 1, resources.length);
157
157
  }
@@ -189,7 +189,7 @@ export function CollapsibleResourceNavigation({
189
189
  <span className="semiont-nav-section__header-text">{mergedTranslations.title}</span>
190
190
  )}
191
191
  <button
192
- onClick={() => eventBus.get('browse:sidebar-toggle').next(undefined)}
192
+ onClick={() => semiont.emit('shell:sidebar-toggle', undefined)}
193
193
  className="semiont-nav-section__header-icon"
194
194
  title={isCollapsed ? mergedTranslations.expandSidebar : mergedTranslations.collapseSidebar}
195
195
  aria-label={isCollapsed ? mergedTranslations.expandSidebar : mergedTranslations.collapseSidebar}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useCallback, useRef, useEffect } from 'react';
4
- import { useEventBus } from '../../contexts/EventBusContext';
4
+ import { useSemiont } from '../../session/SemiontProvider';
5
5
 
6
6
  /**
7
7
  * Props for ObservableLink component
@@ -27,7 +27,7 @@ export interface ObservableLinkProps extends React.AnchorHTMLAttributes<HTMLAnch
27
27
  * - State coordination before navigation
28
28
  * - Logging navigation flows
29
29
  *
30
- * The component emits 'browse:link-clicked' event before allowing
30
+ * The component emits 'nav:link-clicked' event before allowing
31
31
  * the browser to follow the link.
32
32
  *
33
33
  * @example
@@ -51,7 +51,7 @@ export interface ObservableLinkProps extends React.AnchorHTMLAttributes<HTMLAnch
51
51
  * </Link>
52
52
  * ```
53
53
  *
54
- * @emits browse:link-clicked - Link clicked by user. Payload: { href: string, label?: string }
54
+ * @emits nav:link-clicked - Link clicked by user. Payload: { href: string, label?: string }
55
55
  */
56
56
  export function ObservableLink({
57
57
  href,
@@ -60,7 +60,7 @@ export function ObservableLink({
60
60
  children,
61
61
  ...anchorProps
62
62
  }: ObservableLinkProps) {
63
- const eventBus = useEventBus();
63
+ const semiont = useSemiont();
64
64
 
65
65
  // Store callback in ref to avoid including in dependency arrays
66
66
  const onClickRef = useRef(onClick);
@@ -70,14 +70,14 @@ export function ObservableLink({
70
70
 
71
71
  const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
72
72
  // Emit event for observability
73
- eventBus.get('browse:link-clicked').next({
73
+ semiont.emit('nav:link-clicked', {
74
74
  href,
75
75
  label
76
76
  });
77
77
 
78
78
  // Call original onClick if provided
79
79
  onClickRef.current?.(e);
80
- }, [href, label]); // eventBus is global singleton - never in deps
80
+ }, [href, label, semiont]);
81
81
 
82
82
  return (
83
83
  <a
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useRef, useEffect } from 'react';
4
- import { useEventBus } from '../../contexts/EventBusContext';
4
+ import { useSemiont } from '../../session/SemiontProvider';
5
5
 
6
6
  export interface SimpleNavigationItem {
7
7
  name: string;
@@ -29,7 +29,7 @@ export interface SimpleNavigationProps {
29
29
  * Simple navigation component for Admin and Moderation modes.
30
30
  * Renders a section header with optional dropdown and static navigation tabs.
31
31
  *
32
- * @emits browse:sidebar-toggle - Toggle sidebar collapsed/expanded state. Payload: undefined
32
+ * @emits shell:sidebar-toggle - Toggle sidebar collapsed/expanded state. Payload: undefined
33
33
  */
34
34
  export function SimpleNavigation({
35
35
  title,
@@ -47,7 +47,7 @@ export function SimpleNavigation({
47
47
 
48
48
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
49
49
  const dropdownRef = useRef<HTMLDivElement>(null);
50
- const eventBus = useEventBus();
50
+ const semiont = useSemiont();
51
51
 
52
52
  const toggleDropdown = () => setIsDropdownOpen(!isDropdownOpen);
53
53
  const closeDropdown = () => setIsDropdownOpen(false);
@@ -86,7 +86,7 @@ export function SimpleNavigation({
86
86
  !isCollapsed && <span className="semiont-nav-section__header-text">{title}</span>
87
87
  )}
88
88
  <button
89
- onClick={() => eventBus.get('browse:sidebar-toggle').next(undefined)}
89
+ onClick={() => semiont.emit('shell:sidebar-toggle', undefined)}
90
90
  className="semiont-nav-section__header-icon"
91
91
  title={isCollapsed ? expandSidebarLabel : collapseSidebarLabel}
92
92
  aria-label={isCollapsed ? expandSidebarLabel : collapseSidebarLabel}
@@ -26,17 +26,17 @@ describe('ObservableLink', () => {
26
26
  expect(screen.getByText('Click me')).toBeInTheDocument();
27
27
  });
28
28
 
29
- it('emits browse:link-clicked with href and label on click', () => {
29
+ it('emits nav:link-clicked with href and label on click', () => {
30
30
  const handler = vi.fn();
31
31
 
32
- const { eventBus } = renderWithProviders(
32
+ const { shellBus } = renderWithProviders(
33
33
  <ObservableLink href="/discover" label="Discover">
34
34
  Discover Resources
35
35
  </ObservableLink>,
36
- { returnEventBus: true }
36
+ { returnShellBus: true }
37
37
  );
38
38
 
39
- const subscription = eventBus!.get('browse:link-clicked').subscribe(handler);
39
+ const subscription = shellBus!.get('nav:link-clicked').subscribe(handler);
40
40
 
41
41
  const link = screen.getByRole('link');
42
42
  fireEvent.click(link);
@@ -93,15 +93,15 @@ describe('SimpleNavigation', () => {
93
93
  });
94
94
 
95
95
  describe('sidebar toggle', () => {
96
- it('emits browse:sidebar-toggle on collapse button click', () => {
96
+ it('emits shell:sidebar-toggle on collapse button click', () => {
97
97
  const handler = vi.fn();
98
98
 
99
- const { eventBus } = renderWithProviders(
99
+ const { shellBus } = renderWithProviders(
100
100
  <SimpleNavigation {...defaultProps} />,
101
- { returnEventBus: true }
101
+ { returnShellBus: true }
102
102
  );
103
103
 
104
- const subscription = eventBus!.get('browse:sidebar-toggle').subscribe(handler);
104
+ const subscription = shellBus!.get('shell:sidebar-toggle').subscribe(handler);
105
105
 
106
106
  const collapseButton = screen.getByLabelText('Collapse sidebar');
107
107
  fireEvent.click(collapseButton);
@@ -1,11 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useRef, useState, useCallback, useEffect, useMemo } from 'react';
4
- import { createHoverHandlers } from '../../hooks/useBeckonFlow';
5
4
  import type { components } from '@semiont/core';
6
- import { getTargetSelector } from '@semiont/api-client';
5
+ import { createHoverHandlers, getTargetSelector, type SemiontSession } from '@semiont/api-client';
7
6
  import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
8
- import type { EventBus } from "@semiont/core"
9
7
  import {
10
8
  canvasToPdfCoordinates,
11
9
  pdfToCanvasCoordinates,
@@ -52,7 +50,7 @@ interface PdfAnnotationCanvasProps {
52
50
  existingAnnotations?: Annotation[];
53
51
  drawingMode: DrawingMode;
54
52
  selectedMotivation?: SelectionMotivation | null;
55
- eventBus?: EventBus;
53
+ session?: SemiontSession | null | undefined;
56
54
  hoveredAnnotationId?: string | null;
57
55
  selectedAnnotationId?: string | null;
58
56
  hoverDelayMs?: number;
@@ -70,7 +68,7 @@ export function PdfAnnotationCanvas({
70
68
  existingAnnotations = [],
71
69
  drawingMode,
72
70
  selectedMotivation,
73
- eventBus,
71
+ session,
74
72
  hoveredAnnotationId,
75
73
  selectedAnnotationId,
76
74
  hoverDelayMs = 150
@@ -235,7 +233,7 @@ export function PdfAnnotationCanvas({
235
233
  }, [isDrawing, selection]);
236
234
 
237
235
  const handleMouseUp = useCallback(() => {
238
- if (!isDrawing || !selection || !pageDimensions || !displayDimensions || !eventBus) {
236
+ if (!isDrawing || !selection || !pageDimensions || !displayDimensions || !session) {
239
237
  setIsDrawing(false);
240
238
  setSelection(null);
241
239
  return;
@@ -280,7 +278,7 @@ export function PdfAnnotationCanvas({
280
278
  });
281
279
 
282
280
  if (clickedAnnotation) {
283
- eventBus?.get('browse:click').next({ annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
281
+ session?.client.emit('browse:click', { annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });
284
282
  setIsDrawing(false);
285
283
  setSelection(null);
286
284
  return;
@@ -319,7 +317,7 @@ export function PdfAnnotationCanvas({
319
317
 
320
318
  // Emit annotation:requested event with FragmentSelector
321
319
  if (selectedMotivation) {
322
- eventBus.get('mark:requested').next({
320
+ session.client.emit('mark:requested', {
323
321
  selector: {
324
322
  type: 'FragmentSelector',
325
323
  conformsTo: 'http://tools.ietf.org/rfc/rfc3778',
@@ -357,8 +355,8 @@ export function PdfAnnotationCanvas({
357
355
 
358
356
  // Hover handlers with currentHover guard and dwell delay
359
357
  const { handleMouseEnter, handleMouseLeave } = useMemo(
360
- () => createHoverHandlers((annotationId) => eventBus?.get('beckon:hover').next({ annotationId }), hoverDelayMs),
361
- [eventBus, hoverDelayMs]
358
+ () => createHoverHandlers((annotationId) => session?.client.emit('beckon:hover', { annotationId }), hoverDelayMs),
359
+ [session, hoverDelayMs]
362
360
  );
363
361
 
364
362
  // Calculate motivation color
@@ -457,7 +455,7 @@ export function PdfAnnotationCanvas({
457
455
  cursor: 'pointer',
458
456
  opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7
459
457
  }}
460
- onClick={() => eventBus?.get('browse:click').next({ annotationId: ann.id, motivation: ann.motivation })}
458
+ onClick={() => session?.client.emit('browse:click', { annotationId: ann.id, motivation: ann.motivation })}
461
459
  onMouseEnter={() => handleMouseEnter(ann.id)}
462
460
  onMouseLeave={handleMouseLeave}
463
461
  />
@@ -130,7 +130,6 @@ describe('PdfAnnotationCanvas', () => {
130
130
  '@context': 'http://www.w3.org/ns/anno.jsonld',
131
131
  type: 'Annotation',
132
132
  id: 'ann-1',
133
- body: [],
134
133
  target: {
135
134
  source: mockResourceId,
136
135
  selector: {
@@ -14,7 +14,8 @@ const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCa
14
14
 
15
15
  import { CodeMirrorRenderer } from '../CodeMirrorRenderer';
16
16
  import type { EditorView } from '@codemirror/view';
17
- import { useEventBus } from '../../contexts/EventBusContext';
17
+ import { useSemiont } from '../../session/SemiontProvider';
18
+ import { useObservable } from '../../hooks/useObservable';
18
19
  import { useEventSubscriptions } from '../../contexts/useEventSubscription';
19
20
 
20
21
  // Type augmentation for custom DOM properties
@@ -68,7 +69,7 @@ export function AnnotateView({
68
69
  }: Props) {
69
70
  const { newAnnotationIds } = useResourceAnnotations();
70
71
  const containerRef = useRef<HTMLDivElement>(null);
71
- const eventBus = useEventBus();
72
+ const session = useObservable(useSemiont().activeSession$);
72
73
 
73
74
  const category = getMimeCategory(mimeType);
74
75
 
@@ -175,7 +176,7 @@ export function AnnotateView({
175
176
  const selectors = buildTextSelectors(content, text, start, end);
176
177
  if (!selectors) return;
177
178
 
178
- eventBus.get('mark:requested').next({
179
+ session?.client.emit('mark:requested', {
179
180
  selector: selectors,
180
181
  motivation: selectedMotivation
181
182
  });
@@ -218,7 +219,7 @@ export function AnnotateView({
218
219
  showLineNumbers={showLineNumbers}
219
220
  hoverDelayMs={hoverDelayMs}
220
221
  enableWidgets={enableWidgets}
221
- eventBus={eventBus}
222
+ session={session}
222
223
  {...(getTargetResourceName && { getTargetResourceName })}
223
224
  {...(generatingReferenceId !== undefined && { generatingReferenceId })}
224
225
  />
@@ -250,7 +251,7 @@ export function AnnotateView({
250
251
  existingAnnotations={allAnnotations}
251
252
  drawingMode={selectedMotivation ? selectedShape : null}
252
253
  selectedMotivation={selectedMotivation}
253
- eventBus={eventBus}
254
+ session={session}
254
255
  hoveredAnnotationId={hoveredAnnotationId || null}
255
256
  hoverDelayMs={hoverDelayMs}
256
257
  />
@@ -280,7 +281,7 @@ export function AnnotateView({
280
281
  existingAnnotations={allAnnotations}
281
282
  drawingMode={selectedMotivation ? selectedShape : null}
282
283
  selectedMotivation={selectedMotivation}
283
- eventBus={eventBus}
284
+ session={session}
284
285
  hoveredAnnotationId={hoveredAnnotationId || null}
285
286
  hoverDelayMs={hoverDelayMs}
286
287
  />
@@ -3,7 +3,8 @@
3
3
  import React, { useEffect, useRef } from 'react';
4
4
  import { useTranslations } from '../../contexts/TranslationContext';
5
5
  import type { RouteBuilder, LinkComponentProps } from '../../contexts/RoutingContext';
6
- import { useResources } from '../../lib/api-hooks';
6
+ import { useSemiont } from '../../session/SemiontProvider';
7
+ import { useObservable } from '../../hooks/useObservable';
7
8
  import type { ResourceId } from '@semiont/core';
8
9
  import { getAnnotationUriFromEvent, type StoredEventLike } from '@semiont/core';
9
10
  import { HistoryEvent } from './HistoryEvent';
@@ -19,17 +20,13 @@ interface Props {
19
20
 
20
21
  export function AnnotationHistory({ rUri, hoveredAnnotationId, onEventHover, onEventClick, Link, routes }: Props) {
21
22
  const t = useTranslations('AnnotationHistory');
23
+ const semiont = useObservable(useSemiont().activeSession$)?.client;
22
24
 
23
- // API hooks
24
- const resources = useResources();
25
-
26
- // Load events using React Query
27
- // React Query will automatically refetch when the query is invalidated by the parent
28
- const { data: eventsData, isLoading: loading, isError: error } = resources.events.useQuery(rUri);
29
-
30
- // Load annotations to look up text for removed/resolved events (single request)
31
- const { data: annotationsData } = resources.annotations.useQuery(rUri);
32
- const annotations = annotationsData?.annotations || [];
25
+ const eventsData = useObservable(semiont?.browse.events(rUri));
26
+ const annotationsData = useObservable(semiont?.browse.annotations(rUri));
27
+ const loading = eventsData === undefined;
28
+ const error = false;
29
+ const annotations = annotationsData ?? [];
33
30
 
34
31
  // Refs to track event elements for scrolling
35
32
  const eventRefs = useRef<Map<string, HTMLElement>>(new Map());
@@ -37,7 +34,7 @@ export function AnnotationHistory({ rUri, hoveredAnnotationId, onEventHover, onE
37
34
 
38
35
  // Sort events by oldest first (most recent at bottom)
39
36
  // Filter out job events - they're represented by mark:body-updated events instead
40
- const events: StoredEventLike[] = !eventsData?.events ? [] : (eventsData.events as StoredEventLike[])
37
+ const events: StoredEventLike[] = !eventsData ? [] : (eventsData as StoredEventLike[])
41
38
  .filter((e) => {
42
39
  return e.type !== 'job:started' && e.type !== 'job:progress' && e.type !== 'job:completed';
43
40
  })
@@ -3,9 +3,8 @@
3
3
  import { useEffect, useRef, useCallback, useMemo, memo, lazy, Suspense } from 'react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
- import { getMimeCategory, isPdfMimeType } from '@semiont/api-client';
6
+ import { getMimeCategory, isPdfMimeType, createHoverHandlers } from '@semiont/api-client';
7
7
  import { ANNOTATORS } from '../../lib/annotation-registry';
8
- import { createHoverHandlers } from '../../hooks/useBeckonFlow';
9
8
  import { scrollAnnotationIntoView } from '../../lib/scroll-utils';
10
9
  import { ImageViewer } from '../viewers';
11
10
  import { AnnotateToolbar, type ClickAction } from '../annotation/AnnotateToolbar';
@@ -23,7 +22,8 @@ import {
23
22
  const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
24
23
 
25
24
  import { useResourceAnnotations } from '../../contexts/ResourceAnnotationsContext';
26
- import { useEventBus } from '../../contexts/EventBusContext';
25
+ import { useSemiont } from '../../session/SemiontProvider';
26
+ import { useObservable } from '../../hooks/useObservable';
27
27
  import { useEventSubscriptions } from '../../contexts/useEventSubscription';
28
28
 
29
29
  interface Props {
@@ -78,7 +78,7 @@ export const BrowseView = memo(function BrowseView({
78
78
  hoverDelayMs = 150
79
79
  }: Props) {
80
80
  const { newAnnotationIds } = useResourceAnnotations();
81
- const eventBus = useEventBus();
81
+ const session = useObservable(useSemiont().activeSession$);
82
82
  const containerRef = useRef<HTMLDivElement>(null);
83
83
 
84
84
  const category = getMimeCategory(mimeType);
@@ -119,6 +119,7 @@ export const BrowseView = memo(function BrowseView({
119
119
  // Attach click handler, hover handler, and animations after render
120
120
  useEffect(() => {
121
121
  if (!containerRef.current) return;
122
+ if (!session) return;
122
123
 
123
124
  const container = containerRef.current;
124
125
 
@@ -134,13 +135,13 @@ export const BrowseView = memo(function BrowseView({
134
135
  if (annotationId && annotationType === 'reference') {
135
136
  const annotation = allAnnotations.find(a => a.id === annotationId);
136
137
  if (annotation) {
137
- eventBus.get('browse:click').next({ annotationId, motivation: annotation.motivation });
138
+ session.client.emit('browse:click', { annotationId, motivation: annotation.motivation });
138
139
  }
139
140
  }
140
141
  };
141
142
 
142
143
  const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
143
- (annotationId) => eventBus.get('beckon:hover').next({ annotationId }),
144
+ (annotationId) => session.client.emit('beckon:hover', { annotationId }),
144
145
  hoverDelayMs
145
146
  );
146
147
 
@@ -180,7 +181,7 @@ export const BrowseView = memo(function BrowseView({
180
181
  container.removeEventListener('mouseout', handleMouseOut);
181
182
  cleanupHover();
182
183
  };
183
- }, [content, allAnnotations, newAnnotationIds, hoverDelayMs]);
184
+ }, [content, allAnnotations, newAnnotationIds, hoverDelayMs, session]);
184
185
 
185
186
  // Helper to scroll annotation into view with pulse effect
186
187
  const scrollToAnnotation = useCallback((annotationId: string | null, removePulse = false) => {