@semiont/react-ui 0.4.20 → 0.4.22

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 (125) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-5QESNO5H.mjs} +13 -16
  3. package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-4NOUO3W6.mjs +7788 -0
  6. package/dist/chunk-4NOUO3W6.mjs.map +1 -0
  7. package/dist/index.d.mts +212 -1206
  8. package/dist/index.mjs +3332 -13712
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +48 -21
  11. package/dist/test-utils.mjs +2505 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +2 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +12 -12
  16. package/src/components/LiveRegion.tsx +1 -2
  17. package/src/components/StatusDisplay.tsx +42 -16
  18. package/src/components/Toolbar.tsx +4 -4
  19. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  20. package/src/components/__tests__/StatusDisplay.test.tsx +50 -65
  21. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  22. package/src/components/annotation/AnnotateToolbar.tsx +8 -9
  23. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  24. package/src/components/annotation-popups/JsonLdView.tsx +1 -2
  25. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
  26. package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
  27. package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
  28. package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
  29. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  30. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  31. package/src/components/modals/ResourceSearchModal.tsx +12 -8
  32. package/src/components/modals/SearchModal.tsx +11 -6
  33. package/src/components/modals/SearchResultsStep.tsx +1 -3
  34. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  35. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  36. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  37. package/src/components/modals/__tests__/SearchModal.accessibility.test.tsx +6 -2
  38. package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
  39. package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
  40. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  41. package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
  42. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  43. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  44. package/src/components/navigation/ObservableLink.tsx +6 -6
  45. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  46. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  47. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  48. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +15 -18
  49. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
  50. package/src/components/resource/AnnotateView.tsx +8 -10
  51. package/src/components/resource/AnnotationHistory.tsx +9 -12
  52. package/src/components/resource/BrowseView.tsx +11 -8
  53. package/src/components/resource/ResourceViewer.tsx +22 -34
  54. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  55. package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
  56. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
  57. package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
  58. package/src/components/resource/event-formatting.ts +2 -3
  59. package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
  60. package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
  61. package/src/components/resource/panels/AssistSection.tsx +15 -21
  62. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  63. package/src/components/resource/panels/CommentEntry.tsx +7 -8
  64. package/src/components/resource/panels/CommentsPanel.tsx +11 -13
  65. package/src/components/resource/panels/HighlightEntry.tsx +7 -8
  66. package/src/components/resource/panels/HighlightPanel.tsx +12 -13
  67. package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
  68. package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
  69. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
  70. package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
  71. package/src/components/resource/panels/TagEntry.tsx +7 -8
  72. package/src/components/resource/panels/TaggingPanel.tsx +14 -23
  73. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
  74. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
  75. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
  76. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  77. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
  78. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
  79. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
  80. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
  81. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
  82. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  83. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  84. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +28 -53
  85. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
  86. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
  87. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
  88. package/src/components/settings/SettingsPanel.tsx +9 -9
  89. package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
  90. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
  91. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  92. package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
  93. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
  94. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  95. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
  96. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
  97. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  98. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  99. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  100. package/src/features/resource-compose/components/ResourceComposePage.tsx +6 -22
  101. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  102. package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
  103. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
  104. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
  105. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
  106. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  107. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  108. package/dist/chunk-OZICDVH7.mjs +0 -62
  109. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  110. package/dist/chunk-R4CCMFJH.mjs +0 -877
  111. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  112. package/dist/chunk-VN5NY4SN.mjs +0 -200
  113. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  114. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  115. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  116. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  117. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  118. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  119. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  120. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  121. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  122. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  123. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  124. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  125. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -1,24 +1,24 @@
1
1
  'use client';
2
2
 
3
3
  import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
4
- import { useKnowledgeBaseSession } from '../../contexts/KnowledgeBaseSessionContext';
4
+ import { useSemiont } from '../../session/SemiontProvider';
5
+ import { useObservable } from '../../hooks/useObservable';
5
6
 
6
7
  /**
7
8
  * Modal that surfaces when a 403 forbidden error is reported via
8
9
  * `notifyPermissionDenied` (called from QueryCache.onError).
9
10
  *
10
- * Reads `permissionDeniedAt` and `permissionDeniedMessage` from
11
- * KnowledgeBaseSessionContext. The provider clears the flag when the user
12
- * dismisses the modal.
13
- *
14
- * Must be mounted inside KnowledgeBaseSessionProvider.
11
+ * Reads `permissionDeniedAt$` and `permissionDeniedMessage$` from the
12
+ * active `FrontendSessionSignals`. The signals instance clears the
13
+ * flag when the user dismisses the modal. Modal state lives on
14
+ * signals (not the session itself) so headless sessions
15
+ * (workers/CLIs) don't carry dead observables.
15
16
  */
16
17
  export function PermissionDeniedModal() {
17
- const {
18
- permissionDeniedAt,
19
- permissionDeniedMessage,
20
- acknowledgePermissionDenied,
21
- } = useKnowledgeBaseSession();
18
+ const signals = useObservable(useSemiont().activeSignals$);
19
+ const permissionDeniedAt = useObservable(signals?.permissionDeniedAt$) ?? null;
20
+ const permissionDeniedMessage = useObservable(signals?.permissionDeniedMessage$) ?? null;
21
+ const acknowledgePermissionDenied = () => signals?.acknowledgePermissionDenied();
22
22
  const showModal = permissionDeniedAt !== null;
23
23
  const message = permissionDeniedMessage ?? 'You do not have permission to perform this action.';
24
24
 
@@ -2,7 +2,10 @@
2
2
 
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
5
- import type { GatheredContext, EventBus } from '@semiont/core';
5
+ import type { GatheredContext } from '@semiont/core';
6
+ import { useSemiont } from '../../session/SemiontProvider';
7
+ import { useObservable } from '../../hooks/useObservable';
8
+ import { useEventSubscription } from '../../contexts/useEventSubscription';
6
9
  import { GatherContextStep } from './GatherContextStep';
7
10
  import { ConfigureGenerationStep } from './ConfigureGenerationStep';
8
11
  import type { GenerationConfig } from './ConfigureGenerationStep';
@@ -34,8 +37,6 @@ export interface ReferenceWizardModalProps {
34
37
  context: GatheredContext | null;
35
38
  contextLoading: boolean;
36
39
  contextError: Error | null;
37
- /** Event bus for emitting downstream events */
38
- eventBus: EventBus;
39
40
  /** Callbacks */
40
41
  onGenerateSubmit: (referenceId: string, config: GenerationConfig) => void;
41
42
  onLinkResource: (referenceId: string, targetResourceId: string) => void;
@@ -91,12 +92,12 @@ export function ReferenceWizardModal({
91
92
  context,
92
93
  contextLoading,
93
94
  contextError,
94
- eventBus,
95
95
  onGenerateSubmit,
96
96
  onLinkResource,
97
97
  onComposeNavigate,
98
98
  translations: t,
99
99
  }: ReferenceWizardModalProps) {
100
+ const session = useObservable(useSemiont().activeSession$);
100
101
  const [wizardStep, setWizardStep] = useState<WizardStep>({ step: 'gather' });
101
102
  const [isSearching, setIsSearching] = useState(false);
102
103
  const [userHint, setUserHint] = useState('');
@@ -110,19 +111,14 @@ export function ReferenceWizardModal({
110
111
  }
111
112
  }, [isOpen]);
112
113
 
113
- // Subscribe to search results
114
- useEffect(() => {
114
+ // Subscribe to search results (only react while open and for the current annotation)
115
+ useEventSubscription('match:search-results', (event) => {
115
116
  if (!isOpen) return;
116
-
117
- const subscription = eventBus.get('match:search-results').subscribe((event) => {
118
- if (annotationId && event.referenceId === annotationId) {
119
- setIsSearching(false);
120
- setWizardStep({ step: 'search-results', results: event.response as ScoredResult[] });
121
- }
122
- });
123
-
124
- return () => subscription.unsubscribe();
125
- }, [isOpen, eventBus, annotationId]);
117
+ if (annotationId && event.referenceId === annotationId) {
118
+ setIsSearching(false);
119
+ setWizardStep({ step: 'search-results', results: event.response as ScoredResult[] });
120
+ }
121
+ });
126
122
 
127
123
  const handleBind = useCallback(() => {
128
124
  setWizardStep({ step: 'configure-search' });
@@ -147,7 +143,7 @@ export function ReferenceWizardModal({
147
143
  if (!annotationId || !context || !resourceId) return;
148
144
  setIsSearching(true);
149
145
  const contextWithHint = userHint ? { ...context, userHint } : context;
150
- eventBus.get('match:search-requested').next({
146
+ session?.client.match.requestSearch({
151
147
  correlationId: crypto.randomUUID(),
152
148
  resourceId,
153
149
  referenceId: annotationId,
@@ -156,7 +152,7 @@ export function ReferenceWizardModal({
156
152
  useSemanticScoring: config.useSemanticScoring,
157
153
  });
158
154
  // Stay on configure-search until results arrive (subscription above handles transition)
159
- }, [annotationId, resourceId, context, eventBus, userHint]);
155
+ }, [annotationId, resourceId, context, session, userHint]);
160
156
 
161
157
  const handleGenerateSubmit = useCallback((config: GenerationConfig) => {
162
158
  if (!annotationId) return;
@@ -1,16 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
5
5
  import { map } from 'rxjs/operators';
6
- import type { components } from '@semiont/core';
7
- import { getResourceId, getPrimaryRepresentation } from '@semiont/api-client';
8
- import { useApiClient } from '../../contexts/ApiClientContext';
6
+ import { getResourceId, getPrimaryRepresentation } from '@semiont/core';
7
+ import { createSearchPipeline } from '@semiont/sdk';
8
+ import { useSemiont } from '../../session/SemiontProvider';
9
9
  import { useObservable } from '../../hooks/useObservable';
10
10
  import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
11
- import { createSearchPipeline } from '../../lib/search-pipeline';
12
11
 
13
- type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
12
+ import type { ResourceDescriptor } from '@semiont/core';
14
13
 
15
14
  type SearchResult = {
16
15
  id: string;
@@ -56,7 +55,12 @@ export function ResourceSearchModal({
56
55
  translations = {}
57
56
  }: ResourceSearchModalProps) {
58
57
  const { announceSearchResults, announceSearching, announceNavigation } = useSearchAnnouncements();
59
- const semiont = useApiClient();
58
+ const semiont = useObservable(useSemiont().activeSession$)?.client;
59
+ // Pipeline factory captures `semiont` once via useState; if semiont is
60
+ // still loading at first render the captured value would be undefined.
61
+ // Route through a ref so the fetch closure reads the latest client.
62
+ const semiontRef = useRef(semiont);
63
+ semiontRef.current = semiont;
60
64
 
61
65
  const t = {
62
66
  title: translations.title || 'Search Resources',
@@ -70,7 +74,7 @@ export function ResourceSearchModal({
70
74
  const [pipeline] = useState(() =>
71
75
  createSearchPipeline<SearchResult>(
72
76
  (q) =>
73
- semiont.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
77
+ semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
74
78
  map((resources) => {
75
79
  if (resources === undefined) return undefined;
76
80
  return resources
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useEffect } from 'react';
3
+ import React, { useState, useEffect, useRef } from 'react';
4
4
  import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
5
5
  import { map } from 'rxjs/operators';
6
- import { getResourceId } from '@semiont/api-client';
6
+ import { getResourceId } from '@semiont/core';
7
+ import { createSearchPipeline } from '@semiont/sdk';
7
8
  import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
8
- import { useApiClient } from '../../contexts/ApiClientContext';
9
+ import { useSemiont } from '../../session/SemiontProvider';
9
10
  import { useObservable } from '../../hooks/useObservable';
10
- import { createSearchPipeline } from '../../lib/search-pipeline';
11
11
  import './SearchModal.css';
12
12
 
13
13
  const SEARCH_DEBOUNCE_MS = 300;
@@ -45,7 +45,12 @@ export function SearchModal({
45
45
  translations = {}
46
46
  }: SearchModalProps) {
47
47
  const { announceSearchResults, announceSearching } = useSearchAnnouncements();
48
- const semiont = useApiClient();
48
+ const semiont = useObservable(useSemiont().activeSession$)?.client;
49
+ // Pipeline factory captures `semiont` once via useState; if semiont is
50
+ // still loading at first render the captured value would be undefined.
51
+ // Route through a ref so the fetch closure reads the latest client.
52
+ const semiontRef = useRef(semiont);
53
+ semiontRef.current = semiont;
49
54
  const [selectedIndex, setSelectedIndex] = useState(0);
50
55
 
51
56
  const t = {
@@ -66,7 +71,7 @@ export function SearchModal({
66
71
  const [pipeline] = useState(() =>
67
72
  createSearchPipeline<SearchResult>(
68
73
  (q) =>
69
- semiont.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
74
+ semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
70
75
  map((resources) => {
71
76
  if (resources === undefined) return undefined;
72
77
  return resources
@@ -1,11 +1,9 @@
1
1
  'use client';
2
2
 
3
- import type { components, GatheredContext } from '@semiont/core';
3
+ import type { GatheredContext, ResourceDescriptor } from '@semiont/core';
4
4
  import { ContextSummary } from './ContextSummary';
5
5
  import type { ContextSummaryTranslations } from './ContextSummary';
6
6
 
7
- type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
8
-
9
7
  export type ScoredResult = ResourceDescriptor & {
10
8
  score?: number;
11
9
  matchReason?: string;
@@ -1,24 +1,24 @@
1
1
  'use client';
2
2
 
3
3
  import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
4
- import { useKnowledgeBaseSession } from '../../contexts/KnowledgeBaseSessionContext';
4
+ import { useSemiont } from '../../session/SemiontProvider';
5
+ import { useObservable } from '../../hooks/useObservable';
5
6
 
6
7
  /**
7
8
  * Modal that surfaces when the active KB's session expires (a 401 from
8
- * either the provider's own JWT validation or from any React Query call
9
+ * either the session's own JWT validation or from any React Query call
9
10
  * via the QueryCache.onError handler).
10
11
  *
11
- * Reads `sessionExpiredAt` from KnowledgeBaseSessionContext. When the user
12
- * dismisses the modal, the provider clears the flag.
13
- *
14
- * Must be mounted inside KnowledgeBaseSessionProvider.
12
+ * Reads `sessionExpiredAt$` from the active `FrontendSessionSignals`.
13
+ * When the user dismisses the modal, the signals instance clears the
14
+ * flag. Modal state lives on signals (not the session itself) so
15
+ * headless sessions (workers/CLIs) don't carry dead observables.
15
16
  */
16
17
  export function SessionExpiredModal() {
17
- const {
18
- sessionExpiredAt,
19
- sessionExpiredMessage,
20
- acknowledgeSessionExpired,
21
- } = useKnowledgeBaseSession();
18
+ const signals = useObservable(useSemiont().activeSignals$);
19
+ const sessionExpiredAt = useObservable(signals?.sessionExpiredAt$) ?? null;
20
+ const sessionExpiredMessage = useObservable(signals?.sessionExpiredMessage$) ?? null;
21
+ const acknowledgeSessionExpired = () => signals?.acknowledgeSessionExpired();
22
22
  const showModal = sessionExpiredAt !== null;
23
23
 
24
24
  const handleSignIn = () => {
@@ -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
 
@@ -15,9 +15,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
15
15
  }));
16
16
 
17
17
  // Mock getResourceId
18
- vi.mock('@semiont/api-client', () => ({
18
+ vi.mock('@semiont/core', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('@semiont/core')>();
20
+ return {
21
+ ...actual,
19
22
  getResourceId: vi.fn((resource: any) => resource?.id)
20
- }));
23
+ };
24
+ });
21
25
 
22
26
  describe.skip('SearchModal Component - Accessibility', () => {
23
27
  // TODO: All SearchModal tests skipped due to HeadlessUI Dialog + jsdom memory issues
@@ -16,9 +16,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
16
16
  }));
17
17
 
18
18
  // Mock getResourceId
19
- vi.mock('@semiont/api-client', () => ({
19
+ vi.mock('@semiont/core', async (importOriginal) => {
20
+ const actual = await importOriginal<typeof import('@semiont/core')>();
21
+ return {
22
+ ...actual,
20
23
  getResourceId: vi.fn((resource: any) => resource?.id)
21
- }));
24
+ };
25
+ });
22
26
 
23
27
  describe('SearchModal Component - Basic Rendering', () => {
24
28
  const defaultProps = {
@@ -15,9 +15,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
15
15
  }));
16
16
 
17
17
  // Mock getResourceId
18
- vi.mock('@semiont/api-client', () => ({
18
+ vi.mock('@semiont/core', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('@semiont/core')>();
20
+ return {
21
+ ...actual,
19
22
  getResourceId: vi.fn((resource: any) => resource?.id)
20
- }));
23
+ };
24
+ });
21
25
 
22
26
  describe.skip('SearchModal Component - Keyboard Navigation', () => {
23
27
  // TODO: All SearchModal tests skipped due to HeadlessUI Dialog + jsdom memory issues
@@ -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
 
@@ -15,9 +15,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
15
15
  }));
16
16
 
17
17
  // Mock getResourceId
18
- vi.mock('@semiont/api-client', () => ({
18
+ vi.mock('@semiont/core', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('@semiont/core')>();
20
+ return {
21
+ ...actual,
19
22
  getResourceId: vi.fn((resource: any) => resource?.id)
20
- }));
23
+ };
24
+ });
21
25
 
22
26
  describe.skip('SearchModal Component - Visual States', () => {
23
27
  // TODO: All SearchModal tests skipped due to HeadlessUI Dialog + jsdom memory issues
@@ -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