@semiont/react-ui 0.5.10 → 0.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{PdfAnnotationCanvas.client-P37SXBWQ.js → PdfAnnotationCanvas.client-75GY2EDR.js} +15 -6
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
- package/dist/{chunk-5LFFAVDI.js → chunk-PFUXQPWJ.js} +96 -96
- package/dist/{chunk-5LFFAVDI.js.map → chunk-PFUXQPWJ.js.map} +1 -1
- package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
- package/dist/chunk-VXASXU4K.js.map +1 -0
- package/dist/index.css +150 -112
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +334 -48
- package/dist/index.js +2494 -2201
- package/dist/index.js.map +1 -1
- package/dist/test-utils.d.ts +5 -1
- package/dist/test-utils.js +10 -4
- package/dist/test-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/components/ResourceTagsInline.tsx +94 -11
- package/src/components/StatusDisplay.css +36 -0
- package/src/components/__tests__/ResourceTagsInline.test.tsx +101 -3
- package/src/components/annotation/AnnotateToolbar.tsx +53 -28
- package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +76 -0
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +124 -360
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +17 -5
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +17 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +143 -7
- package/src/components/resource/AnnotateView.tsx +59 -38
- package/src/components/resource/BrowseView.tsx +172 -138
- package/src/components/resource/ResourceViewer.tsx +123 -130
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +55 -0
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +49 -0
- package/src/components/resource/__tests__/BrowseView.links.test.tsx +60 -0
- package/src/components/resource/__tests__/BrowseView.overlay-async.test.tsx +113 -0
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +161 -0
- package/src/components/resource/__tests__/BrowseView.registry.test.tsx +43 -0
- package/src/components/resource/__tests__/BrowseView.test.tsx +133 -53
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +102 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +12 -7
- package/src/components/resource/__tests__/ResourceViewer.toolbar-opt-out.test.tsx +135 -0
- package/src/components/resource/__tests__/ResourceViewer.toolbar-prefs.test.tsx +200 -0
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
- package/src/components/resource/__tests__/browse-renderers.test.tsx +34 -0
- package/src/components/resource/__tests__/inline-embedding.test.tsx +81 -0
- package/src/components/resource/__tests__/media-completeness.test.tsx +88 -0
- package/src/components/resource/browse-renderers.tsx +85 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +12 -5
- package/src/components/resource/panels/AssistSection.tsx +4 -3
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +12 -5
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +12 -5
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
- package/src/components/resource/panels/ReferencesPanel.tsx +15 -6
- package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -3
- package/src/components/resource/panels/TaggingPanel.tsx +11 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +15 -2
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +39 -28
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +54 -24
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +78 -59
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +16 -7
- package/src/components/toolbar/Toolbar.css +14 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +7 -17
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +43 -17
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +2 -14
- package/src/styles/features/resource-viewer.css +19 -0
- package/dist/PdfAnnotationCanvas.client-P37SXBWQ.js.map +0 -1
- package/dist/chunk-O2MD7TGE.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { cloneElement, type ReactElement } from 'react';
|
|
2
3
|
import { render, screen } from '@testing-library/react';
|
|
3
4
|
import '@testing-library/jest-dom';
|
|
4
5
|
import { of } from 'rxjs';
|
|
5
|
-
import { CacheObservable } from '@semiont/sdk';
|
|
6
|
+
import { CacheObservable, type SemiontSession } from '@semiont/sdk';
|
|
6
7
|
import { renderWithProviders, createTestSemiontWrapper } from '../../../../test-utils';
|
|
7
8
|
import userEvent from '@testing-library/user-event';
|
|
8
9
|
import type { TagSchema } from '@semiont/core';
|
|
@@ -65,6 +66,17 @@ describe('TagEntry', () => {
|
|
|
65
66
|
const defaultProps = {
|
|
66
67
|
tag: createMockTag(),
|
|
67
68
|
isFocused: false,
|
|
69
|
+
// Satisfies JSX at element construction only; render helpers/tests pass
|
|
70
|
+
// the real per-test factory session explicitly. Never put a live session
|
|
71
|
+
// here — module-scope clients get disposed after the first test.
|
|
72
|
+
session: null,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// The component is provider-free: hand it a fresh per-test fake session as
|
|
76
|
+
// the `session` prop. renderWithProviders still supplies translations/toasts.
|
|
77
|
+
const renderTagEntry = (element: ReactElement<{ session: SemiontSession | null }>) => {
|
|
78
|
+
const { session } = createTestSemiontWrapper();
|
|
79
|
+
return renderWithProviders(cloneElement(element, { session }));
|
|
68
80
|
};
|
|
69
81
|
|
|
70
82
|
beforeEach(() => {
|
|
@@ -76,13 +88,13 @@ describe('TagEntry', () => {
|
|
|
76
88
|
|
|
77
89
|
describe('Rendering', () => {
|
|
78
90
|
it('should render the category badge', () => {
|
|
79
|
-
|
|
91
|
+
renderTagEntry(<TagEntry {...defaultProps} />);
|
|
80
92
|
|
|
81
93
|
expect(screen.getByText('Entity')).toBeInTheDocument();
|
|
82
94
|
});
|
|
83
95
|
|
|
84
96
|
it('should render the selected text in quotes', () => {
|
|
85
|
-
|
|
97
|
+
renderTagEntry(<TagEntry {...defaultProps} />);
|
|
86
98
|
|
|
87
99
|
expect(screen.getByText(/Tagged text content/)).toBeInTheDocument();
|
|
88
100
|
});
|
|
@@ -91,7 +103,7 @@ describe('TagEntry', () => {
|
|
|
91
103
|
const longText = 'C'.repeat(200);
|
|
92
104
|
mockGetAnnotationExactText.mockReturnValue(longText);
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
renderTagEntry(<TagEntry {...defaultProps} />);
|
|
95
107
|
|
|
96
108
|
expect(screen.getByText(new RegExp(`"${'C'.repeat(150)}`))).toBeInTheDocument();
|
|
97
109
|
expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
|
|
@@ -101,7 +113,7 @@ describe('TagEntry', () => {
|
|
|
101
113
|
const exactText = 'D'.repeat(150);
|
|
102
114
|
mockGetAnnotationExactText.mockReturnValue(exactText);
|
|
103
115
|
|
|
104
|
-
const { container } =
|
|
116
|
+
const { container } = renderTagEntry(<TagEntry {...defaultProps} />);
|
|
105
117
|
|
|
106
118
|
const quote = container.querySelector('.semiont-annotation-entry__quote');
|
|
107
119
|
expect(quote).toBeInTheDocument();
|
|
@@ -121,11 +133,11 @@ describe('TagEntry', () => {
|
|
|
121
133
|
// Stub the cache to resolve immediately with the test schema —
|
|
122
134
|
// exercises the rendering path without round-tripping through the
|
|
123
135
|
// transport's HTTP plumbing.
|
|
124
|
-
const { SemiontWrapper, client } = createTestSemiontWrapper();
|
|
136
|
+
const { SemiontWrapper, client, session } = createTestSemiontWrapper();
|
|
125
137
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
126
138
|
CacheObservable.from(of([NER_SCHEMA]))
|
|
127
139
|
);
|
|
128
|
-
render(<TagEntry {...defaultProps} />, { wrapper: SemiontWrapper });
|
|
140
|
+
render(<TagEntry {...defaultProps} session={session} />, { wrapper: SemiontWrapper });
|
|
129
141
|
|
|
130
142
|
expect(screen.getByText('Named Entity Recognition')).toBeInTheDocument();
|
|
131
143
|
});
|
|
@@ -135,17 +147,17 @@ describe('TagEntry', () => {
|
|
|
135
147
|
|
|
136
148
|
// Stub the cache to resolve to an empty list — the schema lookup
|
|
137
149
|
// misses, the schema-name `<span>` is not rendered.
|
|
138
|
-
const { SemiontWrapper, client } = createTestSemiontWrapper();
|
|
150
|
+
const { SemiontWrapper, client, session } = createTestSemiontWrapper();
|
|
139
151
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
140
152
|
CacheObservable.from(of([]))
|
|
141
153
|
);
|
|
142
|
-
const { container } = render(<TagEntry {...defaultProps} />, { wrapper: SemiontWrapper });
|
|
154
|
+
const { container } = render(<TagEntry {...defaultProps} session={session} />, { wrapper: SemiontWrapper });
|
|
143
155
|
|
|
144
156
|
expect(container.querySelector('.semiont-annotation-entry__meta')).not.toBeInTheDocument();
|
|
145
157
|
});
|
|
146
158
|
|
|
147
159
|
it('should render category badge with correct data-variant', () => {
|
|
148
|
-
const { container } =
|
|
160
|
+
const { container } = renderTagEntry(<TagEntry {...defaultProps} />);
|
|
149
161
|
|
|
150
162
|
const badge = container.querySelector('.semiont-tag-badge');
|
|
151
163
|
expect(badge).toBeInTheDocument();
|
|
@@ -157,12 +169,14 @@ describe('TagEntry', () => {
|
|
|
157
169
|
it('should emit browse:click on click', async () => {
|
|
158
170
|
const clickHandler = vi.fn();
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
172
|
+
// The session prop is the only session the provider-free component
|
|
173
|
+
// sees — subscribe on the SAME factory's bus that backs it.
|
|
174
|
+
const { session, eventBus } = createTestSemiontWrapper();
|
|
175
|
+
const subscription = eventBus.get('browse:click').subscribe(clickHandler);
|
|
164
176
|
|
|
165
|
-
const
|
|
177
|
+
const { container } = renderWithProviders(
|
|
178
|
+
<TagEntry {...defaultProps} session={session} />
|
|
179
|
+
);
|
|
166
180
|
|
|
167
181
|
const entry = container.firstChild as HTMLElement;
|
|
168
182
|
await userEvent.click(entry);
|
|
@@ -178,7 +192,7 @@ describe('TagEntry', () => {
|
|
|
178
192
|
|
|
179
193
|
describe('Hover state', () => {
|
|
180
194
|
it('should apply pulse class when isHovered is true', () => {
|
|
181
|
-
const { container } =
|
|
195
|
+
const { container } = renderTagEntry(
|
|
182
196
|
<TagEntry {...defaultProps} isHovered={true} />
|
|
183
197
|
);
|
|
184
198
|
|
|
@@ -187,7 +201,7 @@ describe('TagEntry', () => {
|
|
|
187
201
|
});
|
|
188
202
|
|
|
189
203
|
it('should not apply pulse class when isHovered is false', () => {
|
|
190
|
-
const { container } =
|
|
204
|
+
const { container } = renderTagEntry(
|
|
191
205
|
<TagEntry {...defaultProps} isHovered={false} />
|
|
192
206
|
);
|
|
193
207
|
|
|
@@ -198,7 +212,7 @@ describe('TagEntry', () => {
|
|
|
198
212
|
|
|
199
213
|
describe('Focus state', () => {
|
|
200
214
|
it('should set data-focused to true when focused', () => {
|
|
201
|
-
const { container } =
|
|
215
|
+
const { container } = renderTagEntry(
|
|
202
216
|
<TagEntry {...defaultProps} isFocused={true} />
|
|
203
217
|
);
|
|
204
218
|
|
|
@@ -207,7 +221,7 @@ describe('TagEntry', () => {
|
|
|
207
221
|
});
|
|
208
222
|
|
|
209
223
|
it('should set data-type to tag', () => {
|
|
210
|
-
const { container } =
|
|
224
|
+
const { container } = renderTagEntry(<TagEntry {...defaultProps} />);
|
|
211
225
|
|
|
212
226
|
const entry = container.firstChild as HTMLElement;
|
|
213
227
|
expect(entry).toHaveAttribute('data-type', 'tag');
|
|
@@ -5,7 +5,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import '@testing-library/jest-dom';
|
|
7
7
|
import { of } from 'rxjs';
|
|
8
|
-
import { CacheObservable } from '@semiont/sdk';
|
|
8
|
+
import { CacheObservable, type SemiontSession } from '@semiont/sdk';
|
|
9
9
|
import { TaggingPanel } from '../TaggingPanel';
|
|
10
10
|
import type { EventBus, TagSchema } from '@semiont/core';
|
|
11
11
|
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
@@ -53,8 +53,8 @@ const TEST_TAG_SCHEMAS: TagSchema[] = [
|
|
|
53
53
|
},
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
-
const renderWithEventBus = (component: React.ReactElement
|
|
57
|
-
const { SemiontWrapper, eventBus, client } = createTestSemiontWrapper();
|
|
56
|
+
const renderWithEventBus = (component: React.ReactElement<{ session: SemiontSession | null }>, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
57
|
+
const { SemiontWrapper, eventBus, client, session } = createTestSemiontWrapper();
|
|
58
58
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
59
59
|
CacheObservable.from(of(TEST_TAG_SCHEMAS))
|
|
60
60
|
);
|
|
@@ -62,21 +62,24 @@ const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<
|
|
|
62
62
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
63
63
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
64
64
|
);
|
|
65
|
-
|
|
65
|
+
// The component is provider-free: the `session` prop is the only session it
|
|
66
|
+
// sees. Inject the factory session here so it is the SAME session whose
|
|
67
|
+
// client backs `eventBus` (trackers subscribe there).
|
|
68
|
+
return render(React.cloneElement(component, { session }), { wrapper: Wrapper });
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
// Variant for the empty-registry case: the cache resolves to `[]`
|
|
69
72
|
// (post-bootstrap, no schemas registered). Distinct from the still-
|
|
70
73
|
// loading case where the observable yields `undefined`.
|
|
71
|
-
const renderWithEmptyRegistry = (component: React.ReactElement) => {
|
|
72
|
-
const { SemiontWrapper, client } = createTestSemiontWrapper();
|
|
74
|
+
const renderWithEmptyRegistry = (component: React.ReactElement<{ session: SemiontSession | null }>) => {
|
|
75
|
+
const { SemiontWrapper, client, session } = createTestSemiontWrapper();
|
|
73
76
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
74
77
|
CacheObservable.from(of([]))
|
|
75
78
|
);
|
|
76
79
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
77
80
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
78
81
|
);
|
|
79
|
-
return render(component, { wrapper: Wrapper });
|
|
82
|
+
return render(React.cloneElement(component, { session }), { wrapper: Wrapper });
|
|
80
83
|
};
|
|
81
84
|
|
|
82
85
|
// Mock TranslationContext. The component now uses `schema.name` /
|
|
@@ -185,8 +188,14 @@ const createPendingAnnotation = (exact: string) => ({
|
|
|
185
188
|
|
|
186
189
|
describe('TaggingPanel Component', () => {
|
|
187
190
|
const defaultProps = {
|
|
191
|
+
resourceId: 'res-1',
|
|
188
192
|
annotations: mockTags.empty,
|
|
189
193
|
pendingAnnotation: null,
|
|
194
|
+
// Satisfies JSX at element construction only; the render helpers always
|
|
195
|
+
// replace it (via cloneElement) with the per-test factory session. Never
|
|
196
|
+
// put a live session here — module-scope clients get disposed after the
|
|
197
|
+
// first test.
|
|
198
|
+
session: null,
|
|
190
199
|
};
|
|
191
200
|
|
|
192
201
|
beforeEach(() => {
|
|
@@ -219,3 +219,17 @@
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/* Popup Components */
|
|
222
|
+
|
|
223
|
+
/* ── Compact display form ────────────────────────────────────────────────────
|
|
224
|
+
* Display-only variant for inline embeds: icon-only, tight, chromeless. The
|
|
225
|
+
* bar's functionality is identical — only its form changes. */
|
|
226
|
+
.semiont-annotate-toolbar--compact {
|
|
227
|
+
gap: 0.375rem;
|
|
228
|
+
padding: 0.25rem 0;
|
|
229
|
+
background: transparent; /* the host container owns the background */
|
|
230
|
+
border-bottom: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.semiont-annotate-toolbar--compact .semiont-dropdown-label {
|
|
234
|
+
display: none; /* icon-only collapsed triggers; expanded menus keep labels */
|
|
235
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
-
import { render, screen
|
|
9
|
+
import { render, screen } from '@testing-library/react';
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { ResourceViewerPage } from '../components/ResourceViewerPage';
|
|
12
12
|
import type { ResourceViewerPageProps } from '../components/ResourceViewerPage';
|
|
@@ -276,8 +276,11 @@ describe('ResourceViewerPage', () => {
|
|
|
276
276
|
expect(screen.queryByText('📦 Archived')).not.toBeInTheDocument();
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
it('shows archived badge
|
|
280
|
-
|
|
279
|
+
it('shows archived badge when the toolbar prefs hold annotate mode', () => {
|
|
280
|
+
// Mode is a toolbar PREF now (TOOLBAR-PREFS-AS-PROPS): the page's
|
|
281
|
+
// useToolbarPrefs() policy layer initializes from the persisted key and
|
|
282
|
+
// feeds the viewer controlled props — no mark:mode-toggled bus event.
|
|
283
|
+
localStorage.setItem('annotateMode', 'true');
|
|
281
284
|
localStorage.setItem('activeToolbarPanel', 'annotations');
|
|
282
285
|
|
|
283
286
|
const props = createMockProps({
|
|
@@ -289,20 +292,7 @@ describe('ResourceViewerPage', () => {
|
|
|
289
292
|
|
|
290
293
|
renderWithProviders(<ResourceViewerPage {...props} />);
|
|
291
294
|
|
|
292
|
-
//
|
|
293
|
-
expect(screen.queryByText('📦 Archived')).not.toBeInTheDocument();
|
|
294
|
-
|
|
295
|
-
// Get the handler map that ResourceViewerPage passed to useEventSubscriptions
|
|
296
|
-
const handlerMap = mockUseEventSubscriptions.mock.calls[mockUseEventSubscriptions.mock.calls.length - 1]?.[0] as Record<string, () => void>;
|
|
297
|
-
expect(handlerMap).toBeDefined();
|
|
298
|
-
expect(handlerMap['mark:mode-toggled']).toBeDefined();
|
|
299
|
-
|
|
300
|
-
// Fire the mode toggle — this is what the toolbar emits
|
|
301
|
-
act(() => {
|
|
302
|
-
handlerMap['mark:mode-toggled']();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// After toggle: annotateMode is true, so archived badge should appear
|
|
295
|
+
// annotateMode true (from the policy layer) → archived badge visible
|
|
306
296
|
expect(screen.getByText('📦 Archived')).toBeInTheDocument();
|
|
307
297
|
|
|
308
298
|
localStorage.clear();
|
|
@@ -28,6 +28,9 @@ import { useTheme } from '../../../contexts/ThemeContext';
|
|
|
28
28
|
import { useLineNumbers } from '../../../hooks/useLineNumbers';
|
|
29
29
|
import { useHoverDelay } from '../../../hooks/useHoverDelay';
|
|
30
30
|
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
31
|
+
import { useObservableExternalNavigation } from '../../../hooks/useObservableBrowse';
|
|
32
|
+
import { useToolbarPrefs } from '../../../hooks/useToolbarPrefs';
|
|
33
|
+
import { getSelectorType } from '../../../lib/media-shapes';
|
|
31
34
|
import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
|
|
32
35
|
import { useSemiont } from '../../../session/SemiontProvider';
|
|
33
36
|
import { createResourceViewerPageStateUnit } from '../state/resource-viewer-page-state-unit';
|
|
@@ -135,26 +138,48 @@ export function ResourceViewerPage({
|
|
|
135
138
|
const browser = useSemiont();
|
|
136
139
|
const session = useObservable(browser.activeSession$);
|
|
137
140
|
const semiont = session?.client;
|
|
141
|
+
const navigateExternal = useObservableExternalNavigation();
|
|
142
|
+
|
|
143
|
+
// ResourceViewer is bring-your-own-session: feed it the active session plus
|
|
144
|
+
// host-owned navigation (reference follow) and panel control (app-scoped bus).
|
|
145
|
+
const handleViewerOpenResource = useCallback((id: string) => {
|
|
146
|
+
navigateExternal(`/know/resource/${id}`, { resourceId: id });
|
|
147
|
+
}, [navigateExternal]);
|
|
148
|
+
|
|
149
|
+
const handleViewerOpenPanel = useCallback((event: EventMap['panel:open']) => {
|
|
150
|
+
browser.emit('panel:open', event);
|
|
151
|
+
}, [browser]);
|
|
138
152
|
|
|
139
153
|
// UI state hooks
|
|
140
154
|
const { showError, showSuccess, showInfo } = useToast();
|
|
141
155
|
const { theme, setTheme } = useTheme();
|
|
142
156
|
const { showLineNumbers, toggleLineNumbers } = useLineNumbers();
|
|
143
157
|
const { hoverDelayMs } = useHoverDelay();
|
|
144
|
-
const { triggerSparkleAnimation, clearNewAnnotationId } = useResourceAnnotations();
|
|
158
|
+
const { triggerSparkleAnimation, clearNewAnnotationId, newAnnotationIds } = useResourceAnnotations();
|
|
145
159
|
|
|
146
160
|
// Render mode chooses the content path: 'text' decodes inline; 'image'
|
|
147
161
|
// and 'pdf' go through the media-token (binary) path. 'none'/registry-miss
|
|
148
162
|
// fall to the text path harmlessly — the viewer shows metadata + download.
|
|
149
163
|
const resourceMediaType = getPrimaryMediaType(resource) || 'text/plain';
|
|
164
|
+
|
|
165
|
+
// Toolbar prefs: the POLICY layer (TOOLBAR-PREFS-AS-PROPS). The page owns the
|
|
166
|
+
// Browser's global-toolbar UX — shared values, localStorage persistence — and
|
|
167
|
+
// feeds the viewer its controlled props; the components hold no pref state policy.
|
|
168
|
+
const toolbarPrefs = useToolbarPrefs(getSelectorType(resourceMediaType));
|
|
169
|
+
const annotateMode = toolbarPrefs.annotateMode;
|
|
150
170
|
const renderMode = capabilitiesOf(resourceMediaType)?.render;
|
|
151
171
|
const isBinary = renderMode === 'image' || renderMode === 'pdf';
|
|
152
172
|
|
|
153
173
|
// Text path: fetch and decode representation (disabled for binary — mediaToken path handles those)
|
|
154
|
-
|
|
174
|
+
// Headless hook returns the error; the page (chrome tier) owns the toast.
|
|
175
|
+
const { content: textContent, loading: textLoading, error: contentError } = useResourceContent(semiont ?? null, rUri, resource, !isBinary);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (contentError) showError('Failed to load resource representation');
|
|
179
|
+
}, [contentError, showError]);
|
|
155
180
|
|
|
156
181
|
// Binary path: fetch short-lived media token, construct URL
|
|
157
|
-
const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(rUri);
|
|
182
|
+
const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(semiont ?? null, rUri);
|
|
158
183
|
const binaryContent = (isBinary && mediaToken && semiont)
|
|
159
184
|
? `${semiont.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
|
|
160
185
|
: '';
|
|
@@ -387,13 +412,8 @@ export function ResourceViewerPage({
|
|
|
387
412
|
}
|
|
388
413
|
}, [routes.know, session]);
|
|
389
414
|
|
|
390
|
-
const handleModeToggled = useCallback(() => {
|
|
391
|
-
setAnnotateMode(prev => !prev);
|
|
392
|
-
}, []);
|
|
393
|
-
|
|
394
415
|
// Event bus subscriptions (combined into single useEventSubscriptions call to prevent hook ordering issues)
|
|
395
416
|
useEventSubscriptions({
|
|
396
|
-
'mark:mode-toggled': handleModeToggled,
|
|
397
417
|
'mark:archive': handleResourceArchive,
|
|
398
418
|
'mark:unarchive': handleResourceUnarchive,
|
|
399
419
|
'yield:clone': handleResourceClone,
|
|
@@ -435,15 +455,6 @@ export function ResourceViewerPage({
|
|
|
435
455
|
const primaryMediaType = primaryRep?.mediaType;
|
|
436
456
|
const primaryByteSize = primaryRep?.byteSize;
|
|
437
457
|
|
|
438
|
-
// Annotate mode state - synced via mark:mode-toggled event from AnnotateToolbar
|
|
439
|
-
const [annotateMode, setAnnotateMode] = useState(() => {
|
|
440
|
-
if (typeof window !== 'undefined') {
|
|
441
|
-
return localStorage.getItem('annotateMode') === 'true';
|
|
442
|
-
}
|
|
443
|
-
return false;
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
|
|
447
458
|
// Combine resource with content
|
|
448
459
|
const resourceWithContent = { ...resource, content };
|
|
449
460
|
|
|
@@ -516,6 +527,18 @@ export function ResourceViewerPage({
|
|
|
516
527
|
<ResourceViewer
|
|
517
528
|
resource={resourceWithContent}
|
|
518
529
|
annotations={groups ?? { highlights: [], comments: [], assessments: [], references: [], tags: [] }}
|
|
530
|
+
session={session ?? null}
|
|
531
|
+
onOpenResource={handleViewerOpenResource}
|
|
532
|
+
onOpenPanel={handleViewerOpenPanel}
|
|
533
|
+
annotateMode={toolbarPrefs.annotateMode}
|
|
534
|
+
onAnnotateModeChange={toolbarPrefs.setAnnotateMode}
|
|
535
|
+
clickAction={toolbarPrefs.clickAction}
|
|
536
|
+
onClickActionChange={toolbarPrefs.setClickAction}
|
|
537
|
+
selectionMotivation={toolbarPrefs.selectionMotivation}
|
|
538
|
+
onSelectionMotivationChange={toolbarPrefs.setSelectionMotivation}
|
|
539
|
+
shape={toolbarPrefs.shape}
|
|
540
|
+
onShapeChange={toolbarPrefs.setShape}
|
|
541
|
+
newAnnotationIds={newAnnotationIds}
|
|
519
542
|
generatingReferenceId={generationProgress?.annotationId ?? null}
|
|
520
543
|
showLineNumbers={showLineNumbers}
|
|
521
544
|
hoverDelayMs={hoverDelayMs}
|
|
@@ -551,6 +574,8 @@ export function ResourceViewerPage({
|
|
|
551
574
|
{/* Unified Annotations Panel */}
|
|
552
575
|
{activePanel === 'annotations' && !resource.archived && (
|
|
553
576
|
<UnifiedAnnotationsPanel
|
|
577
|
+
session={session ?? null}
|
|
578
|
+
onOpenResource={handleViewerOpenResource}
|
|
554
579
|
annotations={annotations}
|
|
555
580
|
annotators={ANNOTATORS}
|
|
556
581
|
annotateMode={annotateMode}
|
|
@@ -589,6 +614,7 @@ export function ResourceViewerPage({
|
|
|
589
614
|
{/* Document Info Panel */}
|
|
590
615
|
{activePanel === 'info' && (
|
|
591
616
|
<ResourceInfoPanel
|
|
617
|
+
session={session ?? null}
|
|
592
618
|
resourceId={rUri}
|
|
593
619
|
documentEntityTypes={documentEntityTypes}
|
|
594
620
|
documentLocale={getLanguage(resource)}
|
|
@@ -10,7 +10,7 @@ import { createMatchStateUnit } from '@semiont/sdk';
|
|
|
10
10
|
import { createYieldStateUnit, type YieldStateUnit } from '@semiont/sdk';
|
|
11
11
|
import type { SemiontClient } from '@semiont/sdk';
|
|
12
12
|
import { decodeWithCharset, textExtractionOf } from '@semiont/core';
|
|
13
|
-
import {
|
|
13
|
+
import { groupAnnotations } from '../../../lib/annotation-groups';
|
|
14
14
|
import type { ReferencedByEntry } from '@semiont/sdk';
|
|
15
15
|
|
|
16
16
|
import type { Annotation } from '@semiont/core';
|
|
@@ -85,19 +85,7 @@ export function createResourceViewerPageStateUnit(
|
|
|
85
85
|
map((a) => a ?? []),
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
-
const annotationGroups$: Observable<AnnotationGroups> = annotations$.pipe(
|
|
89
|
-
map((anns) => {
|
|
90
|
-
const groups: AnnotationGroups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
|
|
91
|
-
for (const ann of anns) {
|
|
92
|
-
if (isHighlight(ann)) groups.highlights.push(ann);
|
|
93
|
-
else if (isComment(ann)) groups.comments.push(ann);
|
|
94
|
-
else if (isAssessment(ann)) groups.assessments.push(ann);
|
|
95
|
-
else if (isReference(ann)) groups.references.push(ann);
|
|
96
|
-
else if (isTag(ann)) groups.tags.push(ann);
|
|
97
|
-
}
|
|
98
|
-
return groups;
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
88
|
+
const annotationGroups$: Observable<AnnotationGroups> = annotations$.pipe(map(groupAnnotations));
|
|
101
89
|
|
|
102
90
|
const entityTypes$: Observable<string[]> = client.browse.entityTypes().pipe(
|
|
103
91
|
map((e) => e ?? []),
|
|
@@ -111,6 +111,25 @@
|
|
|
111
111
|
padding: 0 1.5rem; /* Move padding from parent to scrollable content */
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/* ── Inline embed variant ────────────────────────────────────────────────────
|
|
115
|
+
* Auto-height to content, no inner scroll container, no pane chrome — the
|
|
116
|
+
* viewer drops into a chat bubble / card / list item and the HOST page
|
|
117
|
+
* scrolls. Default (no modifier) keeps the fill-the-pane behavior above. */
|
|
118
|
+
.semiont-resource-viewer--inline {
|
|
119
|
+
height: auto;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.semiont-browse-view--inline {
|
|
123
|
+
height: auto;
|
|
124
|
+
background-color: transparent; /* the host container owns the background */
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.semiont-browse-view--inline .semiont-browse-view__content {
|
|
128
|
+
flex: none; /* size to content, not to a pane */
|
|
129
|
+
overflow: visible; /* no inner scrollbar — the host page is the scroll context */
|
|
130
|
+
padding: 0; /* no pane gutter — the host owns spacing */
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
/* ── Markdown prose typography ──────────────────────────────────────────────
|
|
115
134
|
* The global CSS reset zeroes margins/padding on all block elements and
|
|
116
135
|
* removes list styles. Re-apply semantic typographic defaults scoped to the
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/pdf-annotation/PdfAnnotationCanvas.tsx","../src/lib/pdf-coordinates.ts"],"sourcesContent":["'use client';\n\nimport React, { useRef, useState, useCallback, useEffect, useMemo } from 'react';\nimport type { Annotation } from '@semiont/core';\nimport {\n getTargetSelector,\n createFragmentSelector,\n parseFragmentSelector,\n getPageFromFragment,\n} from '@semiont/core';\nimport { createHoverHandlers, type SemiontSession } from '@semiont/sdk';\nimport type { SelectionMotivation } from '../annotation/AnnotateToolbar';\nimport {\n canvasToPdfCoordinates,\n pdfToCanvasCoordinates,\n type CanvasRectangle\n} from '../../lib/pdf-coordinates';\nimport {\n loadPdfDocument,\n renderPdfPageToDataUrl,\n type PDFDocumentProxy\n} from '../../lib/browser-pdfjs';\nimport './PdfAnnotationCanvas.css';\n\nexport type DrawingMode = 'rectangle' | 'circle' | 'polygon' | null;\n\n/**\n * Get color for annotation based on motivation\n */\nfunction getMotivationColor(motivation: SelectionMotivation | null): { stroke: string; fill: string } {\n if (!motivation) {\n return { stroke: 'rgb(156, 163, 175)', fill: 'rgba(156, 163, 175, 0.2)' };\n }\n\n switch (motivation) {\n case 'highlighting':\n return { stroke: 'rgb(250, 204, 21)', fill: 'rgba(250, 204, 21, 0.3)' };\n case 'linking':\n return { stroke: 'rgb(59, 130, 246)', fill: 'rgba(59, 130, 246, 0.2)' };\n case 'assessing':\n return { stroke: 'rgb(239, 68, 68)', fill: 'rgba(239, 68, 68, 0.2)' };\n case 'commenting':\n return { stroke: 'rgb(255, 255, 255)', fill: 'rgba(255, 255, 255, 0.2)' };\n default:\n return { stroke: 'rgb(156, 163, 175)', fill: 'rgba(156, 163, 175, 0.2)' };\n }\n}\n\ninterface PdfAnnotationCanvasProps {\n pdfUrl: string;\n existingAnnotations?: Annotation[];\n drawingMode: DrawingMode;\n selectedMotivation?: SelectionMotivation | null;\n session?: SemiontSession | null | undefined;\n hoveredAnnotationId?: string | null;\n selectedAnnotationId?: string | null;\n hoverDelayMs?: number;\n}\n\n/**\n * PDF annotation canvas with page navigation and rectangle drawing\n *\n * @emits browse:click - Annotation clicked on PDF. Payload: { annotationId: string, motivation: Motivation }\n * @emits mark:requested - New annotation drawn on PDF. Payload: { selector: FragmentSelector, motivation: SelectionMotivation }\n * @emits beckon:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }\n */\nexport function PdfAnnotationCanvas({\n pdfUrl,\n existingAnnotations = [],\n drawingMode,\n selectedMotivation,\n session,\n hoveredAnnotationId,\n selectedAnnotationId,\n hoverDelayMs = 150\n}: PdfAnnotationCanvasProps) {\n // PDF state\n const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);\n const [numPages, setNumPages] = useState<number>(0);\n const [pageNumber, setPageNumber] = useState(1);\n const [pageImageUrl, setPageImageUrl] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [pageDimensions, setPageDimensions] = useState<{ width: number; height: number } | null>(null);\n const [displayDimensions, setDisplayDimensions] = useState<{ width: number; height: number } | null>(null);\n const [scale] = useState(1.5); // Fixed scale for better quality\n\n // Drawing state\n const [isDrawing, setIsDrawing] = useState(false);\n const [selection, setSelection] = useState<CanvasRectangle | null>(null);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const imageRef = useRef<HTMLImageElement>(null);\n\n // Load PDF document on mount\n useEffect(() => {\n let cancelled = false;\n\n async function loadPdf() {\n try {\n setIsLoading(true);\n setError(null);\n\n const doc = await loadPdfDocument(pdfUrl);\n\n if (cancelled) return;\n\n setPdfDoc(doc);\n setNumPages(doc.numPages);\n setIsLoading(false);\n } catch (err) {\n if (cancelled) return;\n\n console.error('Error loading PDF:', err);\n setError('Failed to load PDF');\n setIsLoading(false);\n }\n }\n\n loadPdf();\n\n return () => {\n cancelled = true;\n };\n }, [pdfUrl]);\n\n // Load current page when page number changes\n useEffect(() => {\n if (!pdfDoc) return;\n\n let cancelled = false;\n const doc = pdfDoc;\n\n async function loadPage() {\n try {\n const page = await doc.getPage(pageNumber);\n\n if (cancelled) return;\n\n // Get page dimensions (at scale 1.0)\n const viewport = page.getViewport({ scale: 1.0 });\n setPageDimensions({\n width: viewport.width,\n height: viewport.height\n });\n\n // Render page to image\n const { dataUrl } = await renderPdfPageToDataUrl(page, scale);\n\n if (cancelled) return;\n\n setPageImageUrl(dataUrl);\n } catch (err) {\n if (cancelled) return;\n\n console.error('Error loading page:', err);\n setError('Failed to load page');\n }\n }\n\n loadPage();\n\n return () => {\n cancelled = true;\n };\n }, [pdfDoc, pageNumber, scale]);\n\n // Update display dimensions on resize\n useEffect(() => {\n const updateDisplayDimensions = () => {\n if (imageRef.current) {\n setDisplayDimensions({\n width: imageRef.current.clientWidth,\n height: imageRef.current.clientHeight\n });\n }\n };\n\n updateDisplayDimensions();\n\n // Use ResizeObserver to detect image element size changes\n // This catches: sidebar open/close, window resize, font size changes, etc.\n let resizeObserver: ResizeObserver | null = null;\n\n try {\n resizeObserver = new ResizeObserver(updateDisplayDimensions);\n if (imageRef.current) {\n resizeObserver.observe(imageRef.current);\n }\n } catch (error) {\n // Fallback for browsers without ResizeObserver support\n console.warn('ResizeObserver not supported, falling back to window resize listener');\n window.addEventListener('resize', updateDisplayDimensions);\n }\n\n return () => {\n if (resizeObserver) {\n resizeObserver.disconnect();\n } else {\n window.removeEventListener('resize', updateDisplayDimensions);\n }\n };\n }, [pageImageUrl]);\n\n // Mouse event handlers for drawing\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n if (!drawingMode) return;\n if (!imageRef.current) return;\n\n const rect = imageRef.current.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Clear any previous selection when starting new drawing\n setIsDrawing(true);\n setSelection({\n startX: x,\n startY: y,\n endX: x,\n endY: y\n });\n }, [drawingMode]);\n\n const handleMouseMove = useCallback((e: React.MouseEvent) => {\n if (!isDrawing || !selection || !imageRef.current) return;\n\n const rect = imageRef.current.getBoundingClientRect();\n\n setSelection({\n ...selection,\n endX: e.clientX - rect.left,\n endY: e.clientY - rect.top\n });\n }, [isDrawing, selection]);\n\n const handleMouseUp = useCallback(() => {\n if (!isDrawing || !selection || !pageDimensions || !displayDimensions || !session) {\n setIsDrawing(false);\n setSelection(null);\n return;\n }\n\n // Calculate drag distance\n const dragDistance = Math.sqrt(\n Math.pow(selection.endX - selection.startX, 2) +\n Math.pow(selection.endY - selection.startY, 2)\n );\n\n // Minimum drag threshold in pixels (10px)\n const MIN_DRAG_DISTANCE = 10;\n\n if (dragDistance < MIN_DRAG_DISTANCE) {\n // This was a click, not a drag - check if we clicked an existing annotation\n if (existingAnnotations.length > 0) {\n const clickedAnnotation = pageAnnotations.find(ann => {\n const fragmentSel = getFragmentSelector(ann.target);\n if (!fragmentSel) return false;\n\n const pdfCoord = parseFragmentSelector(fragmentSel.value);\n if (!pdfCoord) return false;\n\n const rect = pdfToCanvasCoordinates(pdfCoord, pageDimensions.height, 1.0);\n\n // Scale to display coordinates\n const scaleX = displayDimensions.width / pageDimensions.width;\n const scaleY = displayDimensions.height / pageDimensions.height;\n\n const displayX = rect.x * scaleX;\n const displayY = rect.y * scaleY;\n const displayWidth = rect.width * scaleX;\n const displayHeight = rect.height * scaleY;\n\n return (\n selection.endX >= displayX &&\n selection.endX <= displayX + displayWidth &&\n selection.endY >= displayY &&\n selection.endY <= displayY + displayHeight\n );\n });\n\n if (clickedAnnotation) {\n session?.client.browse.click(clickedAnnotation.id, clickedAnnotation.motivation);\n setIsDrawing(false);\n setSelection(null);\n return;\n }\n }\n\n // Click on empty space - do nothing\n setIsDrawing(false);\n setSelection(null);\n return;\n }\n\n // This was a drag - create new annotation\n // Scale selection from display coordinates to native page coordinates\n const scaleX = pageDimensions.width / displayDimensions.width;\n const scaleY = pageDimensions.height / displayDimensions.height;\n\n const nativeSelection: CanvasRectangle = {\n startX: selection.startX * scaleX,\n startY: selection.startY * scaleY,\n endX: selection.endX * scaleX,\n endY: selection.endY * scaleY\n };\n\n // Convert canvas coordinates to PDF coordinates\n const pdfCoord = canvasToPdfCoordinates(\n nativeSelection,\n pageNumber,\n pageDimensions.width,\n pageDimensions.height,\n 1.0 // Use scale 1.0 since we already scaled to native coords\n );\n\n // Create FragmentSelector\n const fragmentSelector = createFragmentSelector(pdfCoord);\n\n // Emit annotation:requested event with FragmentSelector\n if (selectedMotivation) {\n session.client.mark.request(\n {\n type: 'FragmentSelector',\n conformsTo: 'http://tools.ietf.org/rfc/rfc3778',\n value: fragmentSelector,\n },\n selectedMotivation,\n );\n }\n\n // Keep drawing state active to show preview until annotation is persisted\n // The parent component should clear this by changing drawingMode after save\n setIsDrawing(false);\n // Note: We keep selection so the preview remains visible\n // It will be cleared when drawingMode changes or user starts new selection\n }, [isDrawing, selection, pageNumber, pageDimensions, displayDimensions, selectedMotivation, existingAnnotations]);\n\n // Helper to get FragmentSelector from annotation target\n const getFragmentSelector = (target: Annotation['target']) => {\n const selector = getTargetSelector(target);\n if (!selector) return null;\n const selectors = Array.isArray(selector) ? selector : [selector];\n\n const found = selectors.find(s => s.type === 'FragmentSelector');\n if (!found || found.type !== 'FragmentSelector') return null;\n return found as { type: 'FragmentSelector'; value: string; conformsTo?: string };\n };\n\n // Filter annotations for current page\n const pageAnnotations = existingAnnotations.filter(ann => {\n const fragmentSel = getFragmentSelector(ann.target);\n if (!fragmentSel) return false;\n const page = getPageFromFragment(fragmentSel.value);\n return page === pageNumber;\n });\n\n // Hover handlers with currentHover guard and dwell delay\n const { handleMouseEnter, handleMouseLeave } = useMemo(\n () => createHoverHandlers((id) => session?.client.beckon.hover(id), hoverDelayMs),\n [session, hoverDelayMs]\n );\n\n // Calculate motivation color\n const { stroke, fill } = getMotivationColor(selectedMotivation ?? null);\n\n if (error) {\n return <div className=\"semiont-pdf-annotation-canvas__error\">{error}</div>;\n }\n\n return (\n <div className=\"semiont-pdf-annotation-canvas\">\n {isLoading && <div className=\"semiont-pdf-annotation-canvas__loading\">Loading PDF...</div>}\n\n <div\n ref={containerRef}\n className=\"semiont-pdf-annotation-canvas__container\"\n style={{ display: isLoading ? 'none' : undefined }}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseUp}\n onMouseLeave={() => {\n if (isDrawing) {\n setIsDrawing(false);\n setSelection(null);\n }\n }}\n data-drawing-mode={drawingMode || 'none'}\n >\n {/* PDF page rendered as image */}\n {pageImageUrl && (\n <img\n ref={imageRef}\n src={pageImageUrl}\n alt={`PDF page ${pageNumber}`}\n className=\"semiont-pdf-annotation-canvas__image\"\n draggable={false}\n style={{ pointerEvents: 'none' }}\n onLoad={() => {\n // Use double RAF to ensure layout is complete even in onLoad\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (imageRef.current) {\n setDisplayDimensions({\n width: imageRef.current.clientWidth,\n height: imageRef.current.clientHeight\n });\n }\n });\n });\n }}\n />\n )}\n\n {/* SVG overlay for annotations */}\n {displayDimensions && pageDimensions && (\n <div className=\"semiont-pdf-annotation-canvas__overlay-container\">\n <div className=\"semiont-pdf-annotation-canvas__overlay\">\n <svg\n className=\"semiont-pdf-annotation-canvas__svg\"\n width={displayDimensions.width}\n height={displayDimensions.height}\n >\n {/* Render existing annotations for this page */}\n {pageAnnotations.map(ann => {\n const fragmentSel = getFragmentSelector(ann.target);\n if (!fragmentSel) return null;\n\n const pdfCoord = parseFragmentSelector(fragmentSel.value);\n if (!pdfCoord) return null;\n\n const rect = pdfToCanvasCoordinates(pdfCoord, pageDimensions.height, 1.0);\n\n // Scale to display coordinates\n const scaleX = displayDimensions.width / pageDimensions.width;\n const scaleY = displayDimensions.height / pageDimensions.height;\n\n const isHovered = ann.id === hoveredAnnotationId;\n const isSelected = ann.id === selectedAnnotationId;\n\n // Get color for this annotation's motivation (not the selected motivation)\n const annMotivation = ann.motivation as SelectionMotivation | null;\n const { stroke: annStroke, fill: annFill } = getMotivationColor(annMotivation);\n\n return (\n <rect\n key={ann.id}\n x={rect.x * scaleX}\n y={rect.y * scaleY}\n width={rect.width * scaleX}\n height={rect.height * scaleY}\n stroke={annStroke}\n strokeWidth={isSelected ? 4 : isHovered ? 3 : 2}\n fill={annFill}\n style={{\n pointerEvents: 'auto',\n cursor: 'pointer',\n opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7\n }}\n onClick={() => session?.client.browse.click(ann.id, ann.motivation)}\n onMouseEnter={() => handleMouseEnter(ann.id)}\n onMouseLeave={handleMouseLeave}\n />\n );\n })}\n\n {/* Render current selection while drawing or awaiting save */}\n {selection && (() => {\n const rectX = Math.min(selection.startX, selection.endX);\n const rectY = Math.min(selection.startY, selection.endY);\n const rectWidth = Math.abs(selection.endX - selection.startX);\n const rectHeight = Math.abs(selection.endY - selection.startY);\n\n // PDF only supports rectangle shapes (FragmentSelector with viewrect)\n // Circle/polygon are disabled in the UI for PDF media types\n return (\n <rect\n x={rectX}\n y={rectY}\n width={rectWidth}\n height={rectHeight}\n stroke={stroke}\n strokeWidth={2}\n strokeDasharray=\"5,5\"\n fill={fill}\n pointerEvents=\"none\"\n />\n );\n })()}\n </svg>\n </div>\n </div>\n )}\n </div>\n\n {/* Page navigation controls */}\n {numPages > 0 && (\n <div className=\"semiont-pdf-annotation-canvas__controls\">\n <button\n disabled={pageNumber <= 1}\n onClick={() => setPageNumber(pageNumber - 1)}\n className=\"semiont-pdf-annotation-canvas__button\"\n >\n Previous\n </button>\n <span className=\"semiont-pdf-annotation-canvas__page-info\">\n Page {pageNumber} of {numPages}\n </span>\n <button\n disabled={pageNumber >= numPages}\n onClick={() => setPageNumber(pageNumber + 1)}\n className=\"semiont-pdf-annotation-canvas__button\"\n >\n Next\n </button>\n </div>\n )}\n </div>\n );\n}\n","/**\n * PDF Canvas Coordinate Transforms\n *\n * Converts between canvas space (pixels, top-left origin, Y increases downward)\n * and PDF space (points, bottom-left origin, Y increases upward) — the Y-flip and\n * scale. UI-only: the server has no canvas.\n *\n * `PdfCoordinate` and the viewrect FragmentSelector codec live in `@semiont/core`.\n *\n * Based on RFC 3778 PDF Fragment Identifiers:\n * https://tools.ietf.org/html/rfc3778\n */\n\nimport type { PdfCoordinate } from '@semiont/core';\n\nexport interface Rectangle {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface CanvasRectangle {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}\n\n/**\n * Convert canvas coordinates to PDF coordinates\n *\n * Canvas: Origin at top-left, Y increases downward\n * PDF: Origin at bottom-left, Y increases upward\n *\n * @param canvasRect - Rectangle in canvas pixel coordinates\n * @param page - PDF page number (1-indexed)\n * @param pageWidth - PDF page width in points\n * @param pageHeight - PDF page height in points\n * @param scale - Current canvas scale factor\n */\nexport function canvasToPdfCoordinates(\n canvasRect: CanvasRectangle,\n page: number,\n _pageWidth: number,\n pageHeight: number,\n scale: number = 1\n): PdfCoordinate {\n // Normalize rectangle (handle drag in any direction)\n const x1 = Math.min(canvasRect.startX, canvasRect.endX);\n const y1 = Math.min(canvasRect.startY, canvasRect.endY);\n const x2 = Math.max(canvasRect.startX, canvasRect.endX);\n const y2 = Math.max(canvasRect.startY, canvasRect.endY);\n\n // Convert from canvas pixels to PDF points\n const pdfX = x1 / scale;\n const pdfWidth = (x2 - x1) / scale;\n\n // Flip Y coordinate (canvas top-left to PDF bottom-left)\n const pdfY = pageHeight - (y2 / scale);\n const pdfHeight = (y2 - y1) / scale;\n\n return {\n page,\n x: Math.round(pdfX),\n y: Math.round(pdfY),\n width: Math.round(pdfWidth),\n height: Math.round(pdfHeight)\n };\n}\n\n/**\n * Convert PDF coordinates to canvas coordinates\n *\n * @param pdfCoord - Coordinates in PDF space\n * @param pageHeight - PDF page height in points\n * @param scale - Current canvas scale factor\n */\nexport function pdfToCanvasCoordinates(\n pdfCoord: PdfCoordinate,\n pageHeight: number,\n scale: number = 1\n): Rectangle {\n // Convert from PDF points to canvas pixels\n const canvasX = pdfCoord.x * scale;\n const canvasWidth = pdfCoord.width * scale;\n\n // Flip Y coordinate (PDF bottom-left to canvas top-left)\n const canvasY = (pageHeight - pdfCoord.y - pdfCoord.height) * scale;\n const canvasHeight = pdfCoord.height * scale;\n\n return {\n x: canvasX,\n y: canvasY,\n width: canvasWidth,\n height: canvasHeight\n };\n}\n"],"mappings":"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","names":["error","pdfCoord","scaleX","scaleY"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/browser-pdfjs.ts"],"sourcesContent":["/**\n * Browser PDF.js — bundled from the npm `pdfjs-dist` (v6) package, loaded\n * lazily.\n *\n * pdf.js (the ~300 kB display layer) is pulled in via a dynamic `import()` the\n * first time a PDF is actually opened, so it is code-split out of the main app\n * bundle — restoring the lazy-load behaviour the old CDN loader had. Only the\n * pdf.js *types* are imported statically (erased at build time, zero runtime\n * cost).\n *\n * The worker can't be resolved inside this tsup-built library (Vite's `?url`\n * lives in the app), so the host hands us the worker URL via `setPdfWorkerSrc`\n * once at startup; it is applied to `GlobalWorkerOptions` when pdf.js loads.\n * See `apps/frontend/src/main.tsx`.\n */\nimport type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';\n\nexport type { PDFDocumentProxy };\n\nlet workerSrc: string | undefined;\n\n/**\n * Supply the (Vite-resolved) pdf.js worker URL. Call once at app startup,\n * before any PDF is opened.\n */\nexport function setPdfWorkerSrc(src: string): void {\n workerSrc = src;\n}\n\nlet pdfjsPromise: Promise<typeof import('pdfjs-dist')> | undefined;\n\n/** Lazy-load pdf.js once, applying the worker URL on first load. */\nasync function getPdfjs(): Promise<typeof import('pdfjs-dist')> {\n if (!pdfjsPromise) {\n pdfjsPromise = import('pdfjs-dist').then((pdfjsLib) => {\n if (workerSrc) {\n pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;\n }\n return pdfjsLib;\n });\n }\n return pdfjsPromise;\n}\n\n/**\n * Load a PDF document from a URL. The URL must carry auth (e.g. `?token=…`);\n * pdf.js streams the document directly.\n */\nexport async function loadPdfDocument(url: string): Promise<PDFDocumentProxy> {\n const pdfjsLib = await getPdfjs();\n return pdfjsLib.getDocument({ url }).promise;\n}\n\n/**\n * Render a PDF page to a PNG data URL. The `page` is already loaded (its owning\n * document came from `loadPdfDocument`), so no pdf.js import is needed here.\n */\nexport async function renderPdfPageToDataUrl(\n page: PDFPageProxy,\n scale = 1.0,\n): Promise<{ dataUrl: string; width: number; height: number }> {\n const viewport = page.getViewport({ scale });\n\n const canvas = document.createElement('canvas');\n canvas.width = viewport.width;\n canvas.height = viewport.height;\n\n // pdf.js 6 requires the `canvas` parameter; `canvasContext` is deprecated.\n await page.render({ canvas, viewport }).promise;\n\n return {\n dataUrl: canvas.toDataURL('image/png'),\n width: viewport.width,\n height: viewport.height,\n };\n}\n"],"mappings":";;;AAmBA,IAAI;AAMG,SAAS,gBAAgB,KAAmB;AACjD,cAAY;AACd;AAEA,IAAI;AAGJ,eAAe,WAAiD;AAC9D,MAAI,CAAC,cAAc;AACjB,mBAAe,OAAO,YAAY,EAAE,KAAK,CAAC,aAAa;AACrD,UAAI,WAAW;AACb,iBAAS,oBAAoB,YAAY;AAAA,MAC3C;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAMA,eAAsB,gBAAgB,KAAwC;AAC5E,QAAM,WAAW,MAAM,SAAS;AAChC,SAAO,SAAS,YAAY,EAAE,IAAI,CAAC,EAAE;AACvC;AAMA,eAAsB,uBACpB,MACA,QAAQ,GACqD;AAC7D,QAAM,WAAW,KAAK,YAAY,EAAE,MAAM,CAAC;AAE3C,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ,SAAS;AACxB,SAAO,SAAS,SAAS;AAGzB,QAAM,KAAK,OAAO,EAAE,QAAQ,SAAS,CAAC,EAAE;AAExC,SAAO;AAAA,IACL,SAAS,OAAO,UAAU,WAAW;AAAA,IACrC,OAAO,SAAS;AAAA,IAChB,QAAQ,SAAS;AAAA,EACnB;AACF;","names":[]}
|