@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.80
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/dist/EventBusContext-7GvDyO0d.d.mts +414 -0
- package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
- package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
- package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-JZIO2A3B.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
- package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
- package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
- package/dist/id-7U7GGVWY.mjs.map +1 -0
- package/dist/index.css +123 -85
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +645 -471
- package/dist/index.mjs +3461 -3025
- package/dist/index.mjs.map +1 -1
- package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
- package/dist/sv-7ZK5EQEB.mjs.map +1 -0
- package/dist/test-utils.d.mts +18 -8
- package/dist/test-utils.mjs +36 -14
- package/dist/test-utils.mjs.map +1 -1
- package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +3 -1
- package/src/components/CodeMirrorRenderer.tsx +66 -93
- package/src/components/DetectionProgressWidget.tsx +16 -5
- package/src/components/LiveRegion.tsx +18 -18
- package/src/components/ResizeHandle.tsx +10 -4
- package/src/components/SessionTimer.tsx +2 -2
- package/src/components/Toolbar.tsx +18 -9
- package/src/components/__tests__/SessionTimer.test.tsx +9 -9
- package/src/components/annotation/AnnotateToolbar.tsx +17 -15
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
- package/src/components/annotation/annotation-entries.css +10 -0
- package/src/components/annotation-popups/JsonLdView.tsx +8 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
- package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
- package/src/components/modals/ResourceSearchModal.tsx +2 -2
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
- package/src/components/navigation/NavigationTabs.css +36 -24
- package/src/components/navigation/ObservableLink.tsx +91 -0
- package/src/components/navigation/SimpleNavigation.tsx +20 -16
- package/src/components/navigation/SortableResourceTab.tsx +11 -5
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
- package/src/components/resource/AnnotateView.tsx +64 -134
- package/src/components/resource/BrowseView.tsx +86 -166
- package/src/components/resource/HistoryEvent.tsx +13 -7
- package/src/components/resource/ResourceViewer.tsx +122 -264
- package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +121 -28
- package/src/components/resource/panels/DetectSection.css +36 -1
- package/src/components/resource/panels/DetectSection.tsx +38 -10
- package/src/components/resource/panels/HighlightEntry.tsx +25 -33
- package/src/components/resource/panels/HighlightPanel.tsx +100 -25
- package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
- package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +119 -30
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
- package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
- package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
- package/src/components/settings/SettingsPanel.tsx +15 -12
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
- package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +135 -88
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +308 -528
- package/translations/ar.json +6 -3
- package/translations/bn.json +6 -3
- package/translations/cs.json +6 -3
- package/translations/da.json +6 -3
- package/translations/de.json +6 -3
- package/translations/el.json +6 -3
- package/translations/en.json +6 -3
- package/translations/es.json +6 -3
- package/translations/fa.json +6 -3
- package/translations/fi.json +6 -3
- package/translations/fr.json +6 -3
- package/translations/he.json +6 -3
- package/translations/hi.json +6 -3
- package/translations/id.json +6 -3
- package/translations/it.json +6 -3
- package/translations/ja.json +6 -3
- package/translations/ko.json +6 -3
- package/translations/ms.json +6 -3
- package/translations/nl.json +6 -3
- package/translations/no.json +6 -3
- package/translations/pl.json +6 -3
- package/translations/pt.json +6 -3
- package/translations/ro.json +6 -3
- package/translations/sv.json +6 -3
- package/translations/th.json +6 -3
- package/translations/tr.json +6 -3
- package/translations/uk.json +6 -3
- package/translations/vi.json +6 -3
- package/translations/zh.json +6 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-EMHEHPCJ.mjs.map +0 -1
- package/dist/bn-OVCI4F6X.mjs.map +0 -1
- package/dist/chunk-JZIO2A3B.mjs.map +0 -1
- package/dist/chunk-LIHZTECW.mjs.map +0 -1
- package/dist/cs-FAN66Q2F.mjs.map +0 -1
- package/dist/da-YBBIHI2O.mjs.map +0 -1
- package/dist/de-MAYU33LB.mjs.map +0 -1
- package/dist/el-MKGSWN4O.mjs.map +0 -1
- package/dist/es-52LHUWJD.mjs.map +0 -1
- package/dist/fa-FJICRANB.mjs.map +0 -1
- package/dist/fi-O455XFCR.mjs.map +0 -1
- package/dist/fr-TXIXHOOE.mjs.map +0 -1
- package/dist/he-JBSOX5IN.mjs.map +0 -1
- package/dist/hi-KGHI3XVT.mjs.map +0 -1
- package/dist/id-5OCPPZLO.mjs.map +0 -1
- package/dist/it-PNBBZSM2.mjs.map +0 -1
- package/dist/ja-LDD7R3TJ.mjs.map +0 -1
- package/dist/ko-F47ZDEY3.mjs.map +0 -1
- package/dist/ms-Z7LMXJWL.mjs.map +0 -1
- package/dist/nl-6SJFBPJ3.mjs.map +0 -1
- package/dist/no-YXPBPSGF.mjs.map +0 -1
- package/dist/pl-P4AZ2QME.mjs.map +0 -1
- package/dist/pt-LHWUS6U6.mjs.map +0 -1
- package/dist/ro-EA5J2ZON.mjs.map +0 -1
- package/dist/sv-DATBS3UQ.mjs.map +0 -1
- package/dist/th-WTFJRWPT.mjs.map +0 -1
- package/dist/tr-IKO3RXOX.mjs.map +0 -1
- package/dist/uk-CF6CTTRK.mjs.map +0 -1
- package/dist/vi-AJLTXPZQ.mjs.map +0 -1
- package/dist/zh-U3ORHHYH.mjs.map +0 -1
- /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceViewer Mode Switching Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests ensure that switching between Browse and Annotate modes
|
|
5
|
+
* doesn't cause React Hook ordering violations.
|
|
6
|
+
*
|
|
7
|
+
* Bug: Previously had 3 separate useEventSubscriptions() calls causing
|
|
8
|
+
* "Rendered more hooks than during the previous render" error.
|
|
9
|
+
*
|
|
10
|
+
* Fix: Combined all event subscriptions into a single useEventSubscriptions() call.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
15
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
16
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
17
|
+
import { EventBusProvider } from '../../../contexts/EventBusContext';
|
|
18
|
+
import { CacheProvider } from '../../../contexts/CacheContext';
|
|
19
|
+
import { TranslationProvider } from '../../../contexts/TranslationContext';
|
|
20
|
+
import { ResourceAnnotationsProvider } from '../../../contexts/ResourceAnnotationsContext';
|
|
21
|
+
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
22
|
+
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
23
|
+
import type { components } from '@semiont/api-client';
|
|
24
|
+
|
|
25
|
+
type SemiontResource = components['schemas']['ResourceDescriptor'];
|
|
26
|
+
|
|
27
|
+
// Mock dependencies
|
|
28
|
+
vi.mock('../../../hooks/useObservableNavigation', () => ({
|
|
29
|
+
useObservableExternalNavigation: () => vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const mockResource: SemiontResource & { content: string } = {
|
|
33
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
34
|
+
'@id': 'http://example.com/resources/test-123',
|
|
35
|
+
name: 'Test Document',
|
|
36
|
+
created: '2024-01-01T00:00:00Z',
|
|
37
|
+
entityTypes: [],
|
|
38
|
+
archived: false,
|
|
39
|
+
representations: [
|
|
40
|
+
{
|
|
41
|
+
mediaType: 'text/plain',
|
|
42
|
+
byteSize: 100,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
content: 'This is test content for mode switching.',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const mockAnnotations = {
|
|
49
|
+
highlights: [],
|
|
50
|
+
references: [],
|
|
51
|
+
assessments: [],
|
|
52
|
+
comments: [],
|
|
53
|
+
tags: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const mockCacheManager = {
|
|
57
|
+
invalidateAnnotations: vi.fn(),
|
|
58
|
+
invalidate: vi.fn(),
|
|
59
|
+
invalidateEvents: vi.fn(),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const queryClient = new QueryClient({
|
|
63
|
+
defaultOptions: {
|
|
64
|
+
queries: { retry: false },
|
|
65
|
+
mutations: { retry: false },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const mockTranslationManager = {
|
|
70
|
+
t: (namespace: string, key: string, params?: Record<string, any>) => {
|
|
71
|
+
return `${namespace}.${key}`;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function TestWrapper({ children }: { children: React.ReactNode }) {
|
|
76
|
+
return (
|
|
77
|
+
<TranslationProvider translationManager={mockTranslationManager}>
|
|
78
|
+
<AuthTokenProvider token="test-token">
|
|
79
|
+
<ApiClientProvider baseUrl="http://localhost:4000">
|
|
80
|
+
<QueryClientProvider client={queryClient}>
|
|
81
|
+
<EventBusProvider>
|
|
82
|
+
<ResourceAnnotationsProvider>
|
|
83
|
+
<CacheProvider cacheManager={mockCacheManager}>
|
|
84
|
+
{children}
|
|
85
|
+
</CacheProvider>
|
|
86
|
+
</ResourceAnnotationsProvider>
|
|
87
|
+
</EventBusProvider>
|
|
88
|
+
</QueryClientProvider>
|
|
89
|
+
</ApiClientProvider>
|
|
90
|
+
</AuthTokenProvider>
|
|
91
|
+
</TranslationProvider>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('ResourceViewer - Mode Switching', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
// Clear localStorage before each test
|
|
99
|
+
if (typeof localStorage !== 'undefined') {
|
|
100
|
+
localStorage.clear();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should render without crashing in browse mode', () => {
|
|
105
|
+
render(
|
|
106
|
+
<TestWrapper>
|
|
107
|
+
<ResourceViewer
|
|
108
|
+
resource={mockResource}
|
|
109
|
+
annotations={mockAnnotations}
|
|
110
|
+
/>
|
|
111
|
+
</TestWrapper>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(screen.getByText('This is test content for mode switching.')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should switch to annotate mode without hook ordering errors', async () => {
|
|
118
|
+
const { rerender } = render(
|
|
119
|
+
<TestWrapper>
|
|
120
|
+
<ResourceViewer
|
|
121
|
+
resource={mockResource}
|
|
122
|
+
annotations={mockAnnotations}
|
|
123
|
+
/>
|
|
124
|
+
</TestWrapper>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Initial render in browse mode
|
|
128
|
+
expect(screen.getByText('This is test content for mode switching.')).toBeInTheDocument();
|
|
129
|
+
|
|
130
|
+
// Simulate mode toggle by setting localStorage and triggering re-render
|
|
131
|
+
localStorage.setItem('annotateMode', 'true');
|
|
132
|
+
|
|
133
|
+
// Re-render (simulating what would happen when the event fires)
|
|
134
|
+
rerender(
|
|
135
|
+
<TestWrapper>
|
|
136
|
+
<ResourceViewer
|
|
137
|
+
resource={mockResource}
|
|
138
|
+
annotations={mockAnnotations}
|
|
139
|
+
/>
|
|
140
|
+
</TestWrapper>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Should still render without errors
|
|
144
|
+
expect(screen.getByText('This is test content for mode switching.')).toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should toggle between browse and annotate modes multiple times without errors', async () => {
|
|
148
|
+
const { rerender } = render(
|
|
149
|
+
<TestWrapper>
|
|
150
|
+
<ResourceViewer
|
|
151
|
+
resource={mockResource}
|
|
152
|
+
annotations={mockAnnotations}
|
|
153
|
+
/>
|
|
154
|
+
</TestWrapper>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Toggle to annotate
|
|
158
|
+
localStorage.setItem('annotateMode', 'true');
|
|
159
|
+
rerender(
|
|
160
|
+
<TestWrapper>
|
|
161
|
+
<ResourceViewer
|
|
162
|
+
resource={mockResource}
|
|
163
|
+
annotations={mockAnnotations}
|
|
164
|
+
/>
|
|
165
|
+
</TestWrapper>
|
|
166
|
+
);
|
|
167
|
+
expect(screen.getByText('This is test content for mode switching.')).toBeInTheDocument();
|
|
168
|
+
|
|
169
|
+
// Toggle back to browse
|
|
170
|
+
localStorage.setItem('annotateMode', 'false');
|
|
171
|
+
rerender(
|
|
172
|
+
<TestWrapper>
|
|
173
|
+
<ResourceViewer
|
|
174
|
+
resource={mockResource}
|
|
175
|
+
annotations={mockAnnotations}
|
|
176
|
+
/>
|
|
177
|
+
</TestWrapper>
|
|
178
|
+
);
|
|
179
|
+
expect(screen.getByText('This is test content for mode switching.')).toBeInTheDocument();
|
|
180
|
+
|
|
181
|
+
// Toggle to annotate again
|
|
182
|
+
localStorage.setItem('annotateMode', 'true');
|
|
183
|
+
rerender(
|
|
184
|
+
<TestWrapper>
|
|
185
|
+
<ResourceViewer
|
|
186
|
+
resource={mockResource}
|
|
187
|
+
annotations={mockAnnotations}
|
|
188
|
+
/>
|
|
189
|
+
</TestWrapper>
|
|
190
|
+
);
|
|
191
|
+
expect(screen.getByText('This is test content for mode switching.')).toBeInTheDocument();
|
|
192
|
+
|
|
193
|
+
// No React Hook ordering errors should occur
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should maintain consistent hook calls across mode switches', async () => {
|
|
197
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
198
|
+
|
|
199
|
+
const { rerender } = render(
|
|
200
|
+
<TestWrapper>
|
|
201
|
+
<ResourceViewer
|
|
202
|
+
resource={mockResource}
|
|
203
|
+
annotations={mockAnnotations}
|
|
204
|
+
/>
|
|
205
|
+
</TestWrapper>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Multiple rapid mode switches
|
|
209
|
+
for (let i = 0; i < 5; i++) {
|
|
210
|
+
localStorage.setItem('annotateMode', i % 2 === 0 ? 'true' : 'false');
|
|
211
|
+
rerender(
|
|
212
|
+
<TestWrapper>
|
|
213
|
+
<ResourceViewer
|
|
214
|
+
resource={mockResource}
|
|
215
|
+
annotations={mockAnnotations}
|
|
216
|
+
/>
|
|
217
|
+
</TestWrapper>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Should not have any React Hook warnings
|
|
222
|
+
const hookErrors = consoleError.mock.calls.filter(call =>
|
|
223
|
+
call[0]?.toString().includes('Rendered more hooks') ||
|
|
224
|
+
call[0]?.toString().includes('Rendered fewer hooks')
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(hookErrors).toHaveLength(0);
|
|
228
|
+
|
|
229
|
+
consoleError.mockRestore();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
4
|
import type { components } from '@semiont/api-client';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/api-client';
|
|
6
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
6
7
|
|
|
7
8
|
type Annotation = components['schemas']['Annotation'];
|
|
8
9
|
|
|
@@ -17,9 +18,7 @@ interface TextualBody {
|
|
|
17
18
|
interface AssessmentEntryProps {
|
|
18
19
|
assessment: Annotation;
|
|
19
20
|
isFocused: boolean;
|
|
20
|
-
|
|
21
|
-
onAssessmentRef: (assessmentId: string, el: HTMLElement | null) => void;
|
|
22
|
-
onAssessmentHover?: (assessmentId: string | null) => void;
|
|
21
|
+
isHovered?: boolean;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
function formatRelativeTime(isoString: string): string {
|
|
@@ -67,42 +66,35 @@ function getAssessmentText(annotation: Annotation): string | null {
|
|
|
67
66
|
return null;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
onAssessmentRef(assessment.id, assessmentRef.current);
|
|
82
|
-
return () => {
|
|
83
|
-
onAssessmentRef(assessment.id, null);
|
|
84
|
-
};
|
|
85
|
-
}, [assessment.id, onAssessmentRef]);
|
|
86
|
-
|
|
87
|
-
// Scroll to assessment when focused
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (isFocused && assessmentRef.current) {
|
|
90
|
-
assessmentRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
91
|
-
}
|
|
92
|
-
}, [isFocused]);
|
|
69
|
+
export const AssessmentEntry = forwardRef<HTMLDivElement, AssessmentEntryProps>(
|
|
70
|
+
function AssessmentEntry(
|
|
71
|
+
{
|
|
72
|
+
assessment,
|
|
73
|
+
isFocused,
|
|
74
|
+
isHovered = false,
|
|
75
|
+
},
|
|
76
|
+
ref
|
|
77
|
+
) {
|
|
78
|
+
const eventBus = useEventBus();
|
|
93
79
|
|
|
94
80
|
const selectedText = getAnnotationExactText(assessment);
|
|
95
81
|
const assessmentText = getAssessmentText(assessment);
|
|
96
82
|
|
|
97
83
|
return (
|
|
98
84
|
<div
|
|
99
|
-
ref={
|
|
100
|
-
className=
|
|
85
|
+
ref={ref}
|
|
86
|
+
className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
|
|
101
87
|
data-type="assessment"
|
|
102
88
|
data-focused={isFocused ? 'true' : 'false'}
|
|
103
|
-
onClick={
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
onClick={() => {
|
|
90
|
+
eventBus.emit('annotation:click', { annotationId: assessment.id, motivation: assessment.motivation });
|
|
91
|
+
}}
|
|
92
|
+
onMouseEnter={() => {
|
|
93
|
+
eventBus.emit('annotation:hover', { annotationId: assessment.id });
|
|
94
|
+
}}
|
|
95
|
+
onMouseLeave={() => {
|
|
96
|
+
eventBus.emit('annotation:hover', { annotationId: null });
|
|
97
|
+
}}
|
|
106
98
|
>
|
|
107
99
|
{/* Selected text quote */}
|
|
108
100
|
{selectedText && (
|
|
@@ -124,4 +116,4 @@ export function AssessmentEntry({
|
|
|
124
116
|
</div>
|
|
125
117
|
</div>
|
|
126
118
|
);
|
|
127
|
-
}
|
|
119
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
5
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
6
|
+
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
6
7
|
import type { components, Selector } from '@semiont/api-client';
|
|
8
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
7
9
|
import { AssessmentEntry } from './AssessmentEntry';
|
|
8
|
-
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
9
10
|
import { DetectSection } from './DetectSection';
|
|
10
11
|
import { PanelHeader } from './PanelHeader';
|
|
11
12
|
import './AssessmentPanel.css';
|
|
@@ -38,13 +39,7 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
38
39
|
|
|
39
40
|
interface AssessmentPanelProps {
|
|
40
41
|
annotations: Annotation[];
|
|
41
|
-
onAnnotationClick: (annotation: Annotation) => void;
|
|
42
|
-
focusedAnnotationId: string | null;
|
|
43
|
-
hoveredAnnotationId?: string | null;
|
|
44
|
-
onAnnotationHover?: (annotationId: string | null) => void;
|
|
45
|
-
onCreate: (assessmentText: string) => void;
|
|
46
42
|
pendingAnnotation: PendingAnnotation | null;
|
|
47
|
-
onDetect?: (instructions?: string) => void | Promise<void>;
|
|
48
43
|
isDetecting?: boolean;
|
|
49
44
|
detectionProgress?: {
|
|
50
45
|
status: string;
|
|
@@ -52,31 +47,105 @@ interface AssessmentPanelProps {
|
|
|
52
47
|
message?: string;
|
|
53
48
|
} | null;
|
|
54
49
|
annotateMode?: boolean;
|
|
50
|
+
scrollToAnnotationId?: string | null;
|
|
51
|
+
onScrollCompleted?: () => void;
|
|
52
|
+
hoveredAnnotationId?: string | null;
|
|
55
53
|
}
|
|
56
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Panel for managing assessment annotations with text input
|
|
57
|
+
*
|
|
58
|
+
* @emits annotation:create - Create new assessment annotation. Payload: { motivation: 'assessing', selector: Selector | Selector[], body: Body[] }
|
|
59
|
+
* @emits annotation:cancel-pending - Cancel pending assessment annotation. Payload: undefined
|
|
60
|
+
* @subscribes annotation:click - Annotation clicked. Payload: { annotationId: string }
|
|
61
|
+
*/
|
|
57
62
|
export function AssessmentPanel({
|
|
58
63
|
annotations,
|
|
59
|
-
onAnnotationClick,
|
|
60
|
-
focusedAnnotationId,
|
|
61
|
-
hoveredAnnotationId,
|
|
62
|
-
onAnnotationHover,
|
|
63
|
-
onCreate,
|
|
64
64
|
pendingAnnotation,
|
|
65
|
-
onDetect,
|
|
66
65
|
isDetecting = false,
|
|
67
66
|
detectionProgress,
|
|
68
67
|
annotateMode = true,
|
|
68
|
+
scrollToAnnotationId,
|
|
69
|
+
onScrollCompleted,
|
|
70
|
+
hoveredAnnotationId,
|
|
69
71
|
}: AssessmentPanelProps) {
|
|
70
72
|
const t = useTranslations('AssessmentPanel');
|
|
71
|
-
const eventBus =
|
|
73
|
+
const eventBus = useEventBus();
|
|
72
74
|
const [newAssessmentText, setNewAssessmentText] = useState('');
|
|
75
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
76
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
|
|
78
|
+
// Direct ref management
|
|
79
|
+
const entryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
80
|
+
|
|
81
|
+
// Sort annotations by their position in the resource
|
|
82
|
+
const sortedAnnotations = useMemo(() => {
|
|
83
|
+
return [...annotations].sort((a, b) => {
|
|
84
|
+
const aSelector = getTextPositionSelector(getTargetSelector(a.target));
|
|
85
|
+
const bSelector = getTextPositionSelector(getTargetSelector(b.target));
|
|
86
|
+
if (!aSelector || !bSelector) return 0;
|
|
87
|
+
return aSelector.start - bSelector.start;
|
|
88
|
+
});
|
|
89
|
+
}, [annotations]);
|
|
90
|
+
|
|
91
|
+
// Ref callback for entry components
|
|
92
|
+
const setEntryRef = useCallback((id: string, element: HTMLDivElement | null) => {
|
|
93
|
+
if (element) {
|
|
94
|
+
entryRefs.current.set(id, element);
|
|
95
|
+
} else {
|
|
96
|
+
entryRefs.current.delete(id);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
// Handle scrollToAnnotationId (click scroll)
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!scrollToAnnotationId) return;
|
|
103
|
+
const element = entryRefs.current.get(scrollToAnnotationId);
|
|
104
|
+
if (element && containerRef.current) {
|
|
105
|
+
const elementTop = element.offsetTop;
|
|
106
|
+
const containerHeight = containerRef.current.clientHeight;
|
|
107
|
+
const elementHeight = element.offsetHeight;
|
|
108
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
109
|
+
containerRef.current.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
110
|
+
element.classList.remove('semiont-annotation-pulse');
|
|
111
|
+
void element.offsetWidth;
|
|
112
|
+
element.classList.add('semiont-annotation-pulse');
|
|
113
|
+
if (onScrollCompleted) onScrollCompleted();
|
|
114
|
+
}
|
|
115
|
+
}, [scrollToAnnotationId]);
|
|
73
116
|
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
// Handle hoveredAnnotationId (hover scroll only - pulse is handled by isHovered prop)
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!hoveredAnnotationId) return;
|
|
120
|
+
const element = entryRefs.current.get(hoveredAnnotationId);
|
|
121
|
+
if (!element || !containerRef.current) return;
|
|
122
|
+
|
|
123
|
+
const container = containerRef.current;
|
|
124
|
+
const elementRect = element.getBoundingClientRect();
|
|
125
|
+
const containerRect = container.getBoundingClientRect();
|
|
126
|
+
const isVisible = elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
|
|
127
|
+
if (!isVisible) {
|
|
128
|
+
const elementTop = element.offsetTop;
|
|
129
|
+
const containerHeight = container.clientHeight;
|
|
130
|
+
const elementHeight = element.offsetHeight;
|
|
131
|
+
const scrollTo = elementTop - (containerHeight / 2) + (elementHeight / 2);
|
|
132
|
+
container.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pulse effect is handled by isHovered prop on AssessmentEntry
|
|
136
|
+
}, [hoveredAnnotationId]);
|
|
76
137
|
|
|
77
138
|
const handleSaveNewAssessment = () => {
|
|
78
|
-
if (
|
|
79
|
-
|
|
139
|
+
if (pendingAnnotation) {
|
|
140
|
+
const body = newAssessmentText.trim()
|
|
141
|
+
? [{ type: 'TextualBody', value: newAssessmentText, purpose: 'assessing' }]
|
|
142
|
+
: [];
|
|
143
|
+
|
|
144
|
+
eventBus.emit('annotation:create', {
|
|
145
|
+
motivation: 'assessing',
|
|
146
|
+
selector: pendingAnnotation.selector,
|
|
147
|
+
body,
|
|
148
|
+
});
|
|
80
149
|
setNewAssessmentText('');
|
|
81
150
|
}
|
|
82
151
|
};
|
|
@@ -87,14 +156,25 @@ export function AssessmentPanel({
|
|
|
87
156
|
|
|
88
157
|
const handleEscape = (e: KeyboardEvent) => {
|
|
89
158
|
if (e.key === 'Escape') {
|
|
90
|
-
eventBus.emit('
|
|
159
|
+
eventBus.emit('annotation:cancel-pending', undefined);
|
|
91
160
|
setNewAssessmentText('');
|
|
92
161
|
}
|
|
93
162
|
};
|
|
94
163
|
|
|
95
164
|
document.addEventListener('keydown', handleEscape);
|
|
96
165
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
97
|
-
}, [pendingAnnotation
|
|
166
|
+
}, [pendingAnnotation]);
|
|
167
|
+
|
|
168
|
+
// Event handler for annotation clicks (extracted to avoid inline arrow function)
|
|
169
|
+
const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {
|
|
170
|
+
setFocusedAnnotationId(annotationId);
|
|
171
|
+
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
// Subscribe to click events - update focused state
|
|
175
|
+
useEventSubscriptions({
|
|
176
|
+
'annotation:click': handleAnnotationClick,
|
|
177
|
+
});
|
|
98
178
|
|
|
99
179
|
return (
|
|
100
180
|
<div className="semiont-panel">
|
|
@@ -129,7 +209,7 @@ export function AssessmentPanel({
|
|
|
129
209
|
<div className="semiont-annotation-prompt__actions">
|
|
130
210
|
<button
|
|
131
211
|
onClick={() => {
|
|
132
|
-
eventBus.emit('
|
|
212
|
+
eventBus.emit('annotation:cancel-pending', undefined);
|
|
133
213
|
setNewAssessmentText('');
|
|
134
214
|
}}
|
|
135
215
|
className="semiont-button semiont-button--secondary"
|
|
@@ -152,12 +232,11 @@ export function AssessmentPanel({
|
|
|
152
232
|
{/* Scrollable content area */}
|
|
153
233
|
<div ref={containerRef} className="semiont-panel__content">
|
|
154
234
|
{/* Detection Section - only in Annotate mode and for text resources */}
|
|
155
|
-
{annotateMode &&
|
|
235
|
+
{annotateMode && (
|
|
156
236
|
<DetectSection
|
|
157
237
|
annotationType="assessment"
|
|
158
238
|
isDetecting={isDetecting}
|
|
159
239
|
detectionProgress={detectionProgress}
|
|
160
|
-
onDetect={onDetect}
|
|
161
240
|
/>
|
|
162
241
|
)}
|
|
163
242
|
|
|
@@ -173,9 +252,8 @@ export function AssessmentPanel({
|
|
|
173
252
|
key={assessment.id}
|
|
174
253
|
assessment={assessment}
|
|
175
254
|
isFocused={assessment.id === focusedAnnotationId}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{...(onAnnotationHover && { onAssessmentHover: onAnnotationHover })}
|
|
255
|
+
isHovered={assessment.id === hoveredAnnotationId}
|
|
256
|
+
ref={(el) => setEntryRef(assessment.id, el)}
|
|
179
257
|
/>
|
|
180
258
|
))
|
|
181
259
|
)}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
5
|
import type { components } from '@semiont/api-client';
|
|
6
6
|
import { getAnnotationExactText, getCommentText } from '@semiont/api-client';
|
|
7
|
+
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
7
8
|
|
|
8
9
|
type Annotation = components['schemas']['Annotation'];
|
|
9
10
|
|
|
10
11
|
interface CommentEntryProps {
|
|
11
12
|
comment: Annotation;
|
|
12
13
|
isFocused: boolean;
|
|
13
|
-
|
|
14
|
-
onCommentRef: (commentId: string, el: HTMLElement | null) => void;
|
|
15
|
-
onCommentHover?: (commentId: string | null) => void;
|
|
14
|
+
isHovered?: boolean;
|
|
16
15
|
annotateMode?: boolean;
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -33,37 +32,38 @@ function formatRelativeTime(isoString: string): string {
|
|
|
33
32
|
return date.toLocaleDateString();
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
35
|
+
export const CommentEntry = forwardRef<HTMLDivElement, CommentEntryProps>(
|
|
36
|
+
function CommentEntry(
|
|
37
|
+
{
|
|
38
|
+
comment,
|
|
39
|
+
isFocused,
|
|
40
|
+
isHovered = false,
|
|
41
|
+
annotateMode = true,
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) {
|
|
44
45
|
const t = useTranslations('CommentsPanel');
|
|
46
|
+
const eventBus = useEventBus();
|
|
45
47
|
const [isEditing, setIsEditing] = useState(false);
|
|
46
48
|
const [editText, setEditText] = useState('');
|
|
47
|
-
const
|
|
49
|
+
const internalRef = useRef<HTMLDivElement>(null);
|
|
48
50
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
}, [comment.id, onCommentRef]);
|
|
51
|
+
// Combine external ref with internal ref
|
|
52
|
+
useImperativeHandle(ref, () => internalRef.current!);
|
|
53
|
+
|
|
54
|
+
const commentText = getCommentText(comment) || '';
|
|
55
|
+
const selectedText = getAnnotationExactText(comment);
|
|
56
56
|
|
|
57
|
-
// Scroll
|
|
57
|
+
// Scroll into view when focused
|
|
58
58
|
useEffect(() => {
|
|
59
|
-
if (isFocused &&
|
|
60
|
-
|
|
59
|
+
if (isFocused && internalRef.current) {
|
|
60
|
+
internalRef.current.scrollIntoView({
|
|
61
|
+
behavior: 'smooth',
|
|
62
|
+
block: 'center',
|
|
63
|
+
});
|
|
61
64
|
}
|
|
62
65
|
}, [isFocused]);
|
|
63
66
|
|
|
64
|
-
const commentText = getCommentText(comment) || '';
|
|
65
|
-
const selectedText = getAnnotationExactText(comment);
|
|
66
|
-
|
|
67
67
|
const handleEditClick = () => {
|
|
68
68
|
setEditText(commentText);
|
|
69
69
|
setIsEditing(true);
|
|
@@ -81,13 +81,19 @@ export function CommentEntry({
|
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<div
|
|
84
|
-
ref={
|
|
85
|
-
className=
|
|
84
|
+
ref={internalRef}
|
|
85
|
+
className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
|
|
86
86
|
data-type="comment"
|
|
87
87
|
data-focused={isFocused ? 'true' : 'false'}
|
|
88
|
-
onClick={
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
onClick={() => {
|
|
89
|
+
eventBus.emit('annotation:click', { annotationId: comment.id, motivation: comment.motivation });
|
|
90
|
+
}}
|
|
91
|
+
onMouseEnter={() => {
|
|
92
|
+
eventBus.emit('annotation:hover', { annotationId: comment.id });
|
|
93
|
+
}}
|
|
94
|
+
onMouseLeave={() => {
|
|
95
|
+
eventBus.emit('annotation:hover', { annotationId: null });
|
|
96
|
+
}}
|
|
91
97
|
>
|
|
92
98
|
{/* Selected text quote - only for text annotations */}
|
|
93
99
|
{selectedText && (
|
|
@@ -153,4 +159,4 @@ export function CommentEntry({
|
|
|
153
159
|
)}
|
|
154
160
|
</div>
|
|
155
161
|
);
|
|
156
|
-
}
|
|
162
|
+
});
|