@semiont/react-ui 0.4.20 → 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.
- package/README.md +8 -5
- package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
- package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-KEDFYI6N.mjs +7788 -0
- package/dist/chunk-KEDFYI6N.mjs.map +1 -0
- package/dist/index.d.mts +171 -1140
- package/dist/index.mjs +3263 -13644
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +46 -21
- package/dist/test-utils.mjs +2499 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +1 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +9 -11
- package/src/components/StatusDisplay.tsx +42 -16
- package/src/components/Toolbar.tsx +4 -4
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
- package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -7
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
- package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +10 -6
- package/src/components/modals/SearchModal.tsx +10 -6
- package/src/components/modals/SessionExpiredModal.tsx +11 -11
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
- package/src/components/navigation/ObservableLink.tsx +6 -6
- package/src/components/navigation/SimpleNavigation.tsx +4 -4
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
- package/src/components/resource/AnnotateView.tsx +7 -6
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +8 -7
- package/src/components/resource/ResourceViewer.tsx +17 -25
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
- package/src/components/resource/panels/AssistSection.tsx +11 -13
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +9 -11
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +10 -11
- package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
- package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
- package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
- package/src/components/resource/panels/TagEntry.tsx +5 -4
- package/src/components/resource/panels/TaggingPanel.tsx +10 -16
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
- package/src/components/settings/SettingsPanel.tsx +8 -8
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
- package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
- package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
- package/dist/chunk-OZICDVH7.mjs +0 -62
- package/dist/chunk-OZICDVH7.mjs.map +0 -1
- package/dist/chunk-R4CCMFJH.mjs +0 -877
- package/dist/chunk-R4CCMFJH.mjs.map +0 -1
- package/dist/chunk-VN5NY4SN.mjs +0 -200
- package/dist/chunk-VN5NY4SN.mjs.map +0 -1
- package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
- package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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('../../../
|
|
28
|
-
const actual = await vi.importActual<typeof import('../../../
|
|
29
|
-
'../../../
|
|
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
|
-
|
|
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 —
|
|
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
|
|
42
|
-
//
|
|
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('../../../
|
|
46
|
-
const actual = await vi.importActual<typeof import('../../../
|
|
47
|
-
'../../../
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
32
|
-
* @emits
|
|
33
|
-
* @emits
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={() =>
|
|
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 {
|
|
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 '
|
|
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
|
|
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
|
|
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
|
-
|
|
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]);
|
|
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 {
|
|
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
|
|
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
|
|
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={() =>
|
|
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
|
|
29
|
+
it('emits nav:link-clicked with href and label on click', () => {
|
|
30
30
|
const handler = vi.fn();
|
|
31
31
|
|
|
32
|
-
const {
|
|
32
|
+
const { shellBus } = renderWithProviders(
|
|
33
33
|
<ObservableLink href="/discover" label="Discover">
|
|
34
34
|
Discover Resources
|
|
35
35
|
</ObservableLink>,
|
|
36
|
-
{
|
|
36
|
+
{ returnShellBus: true }
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
const subscription =
|
|
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
|
|
96
|
+
it('emits shell:sidebar-toggle on collapse button click', () => {
|
|
97
97
|
const handler = vi.fn();
|
|
98
98
|
|
|
99
|
-
const {
|
|
99
|
+
const { shellBus } = renderWithProviders(
|
|
100
100
|
<SimpleNavigation {...defaultProps} />,
|
|
101
|
-
{
|
|
101
|
+
{ returnShellBus: true }
|
|
102
102
|
);
|
|
103
103
|
|
|
104
|
-
const subscription =
|
|
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
|
-
|
|
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
|
-
|
|
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 || !
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
361
|
-
[
|
|
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={() =>
|
|
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
|
/>
|
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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) => {
|