@semiont/react-ui 0.5.25 → 0.5.27
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 +28 -4
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js +677 -0
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +1 -0
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css +185 -0
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +1 -0
- package/dist/{ar-CULRHLNG.js → ar-OU3DQGC6.js} +65 -7
- package/dist/ar-OU3DQGC6.js.map +1 -0
- package/dist/{bn-HYR7CEL2.js → bn-PHWCR6AZ.js} +65 -7
- package/dist/bn-PHWCR6AZ.js.map +1 -0
- package/dist/chunk-5VDTWRXJ.js +287 -0
- package/dist/chunk-5VDTWRXJ.js.map +1 -0
- package/dist/{chunk-TG2XQEC4.js → chunk-BTUG3VJJ.js} +18 -298
- package/dist/chunk-BTUG3VJJ.js.map +1 -0
- package/dist/{chunk-NH6L2GIL.js → chunk-PCRRBU7H.js} +65 -7
- package/dist/chunk-PCRRBU7H.js.map +1 -0
- package/dist/{cs-YBRWY7NH.js → cs-NEXYZ62I.js} +65 -7
- package/dist/cs-NEXYZ62I.js.map +1 -0
- package/dist/{da-PHDEOBV3.js → da-FAT43YCU.js} +65 -7
- package/dist/da-FAT43YCU.js.map +1 -0
- package/dist/{de-O4UCCVLJ.js → de-KRQ3WBCU.js} +65 -7
- package/dist/de-KRQ3WBCU.js.map +1 -0
- package/dist/{el-MVGO6WZV.js → el-Q4IU3EKC.js} +65 -7
- package/dist/el-Q4IU3EKC.js.map +1 -0
- package/dist/{en-2KQILFKQ.js → en-2KADE7N4.js} +6 -2
- package/dist/{es-USSLXQY7.js → es-2IO3R2MK.js} +65 -7
- package/dist/es-2IO3R2MK.js.map +1 -0
- package/dist/{fa-WMAREFWY.js → fa-Q5GOQFXX.js} +65 -7
- package/dist/fa-Q5GOQFXX.js.map +1 -0
- package/dist/{fi-B3NBNOTS.js → fi-R6NMRWBE.js} +65 -7
- package/dist/fi-R6NMRWBE.js.map +1 -0
- package/dist/{fr-TXYK5T5W.js → fr-QY4D5E6J.js} +65 -7
- package/dist/fr-QY4D5E6J.js.map +1 -0
- package/dist/{he-JCJACW5D.js → he-X6Y77XHY.js} +65 -7
- package/dist/he-X6Y77XHY.js.map +1 -0
- package/dist/{hi-VOQLZNJU.js → hi-XL7IOEQM.js} +65 -7
- package/dist/hi-XL7IOEQM.js.map +1 -0
- package/dist/{id-BFN5RZNZ.js → id-QZ4ZZS67.js} +65 -7
- package/dist/id-QZ4ZZS67.js.map +1 -0
- package/dist/index.css +241 -508
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +155 -54
- package/dist/index.js +1815 -1642
- package/dist/index.js.map +1 -1
- package/dist/integrations/css-modules-helper.js +1 -3
- package/dist/integrations/css-modules-helper.js.map +1 -1
- package/dist/{it-TWWDHKKM.js → it-ME2Q7OB6.js} +65 -7
- package/dist/it-ME2Q7OB6.js.map +1 -0
- package/dist/{ja-YRQLEBFM.js → ja-HX7QASRI.js} +65 -7
- package/dist/ja-HX7QASRI.js.map +1 -0
- package/dist/{ko-PG6B3CRB.js → ko-EQO5AHAV.js} +65 -7
- package/dist/ko-EQO5AHAV.js.map +1 -0
- package/dist/{ms-7EZECVKM.js → ms-UILFSF3I.js} +65 -7
- package/dist/ms-UILFSF3I.js.map +1 -0
- package/dist/{nl-HP753YAP.js → nl-NNI45PMK.js} +65 -7
- package/dist/nl-NNI45PMK.js.map +1 -0
- package/dist/{no-JUD5WX3N.js → no-MAL5F6P6.js} +65 -7
- package/dist/no-MAL5F6P6.js.map +1 -0
- package/dist/{pl-RVUL4CBM.js → pl-4RMY6JAJ.js} +65 -7
- package/dist/pl-4RMY6JAJ.js.map +1 -0
- package/dist/{pt-RPOVD4QM.js → pt-VQH2X3QQ.js} +65 -7
- package/dist/pt-VQH2X3QQ.js.map +1 -0
- package/dist/{ro-DMYWQTRD.js → ro-XXCQO2OG.js} +65 -7
- package/dist/ro-XXCQO2OG.js.map +1 -0
- package/dist/{sv-DMZ4FGXD.js → sv-7Z6EJ6U4.js} +65 -7
- package/dist/sv-7Z6EJ6U4.js.map +1 -0
- package/dist/test-utils.css +1 -1
- package/dist/test-utils.css.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-4HVBJBQ2.js → th-L3DXXYVQ.js} +65 -7
- package/dist/th-L3DXXYVQ.js.map +1 -0
- package/dist/{tr-6BDPU5ML.js → tr-P3DFVLFJ.js} +65 -7
- package/dist/tr-P3DFVLFJ.js.map +1 -0
- package/dist/{uk-TBZBT2TL.js → uk-7C5D3PXS.js} +65 -7
- package/dist/uk-7C5D3PXS.js.map +1 -0
- package/dist/{vi-UMKZKZL3.js → vi-ZLTLEO7P.js} +65 -7
- package/dist/vi-ZLTLEO7P.js.map +1 -0
- package/dist/{zh-YBJWJJYB.js → zh-5FHVWDIN.js} +65 -7
- package/dist/zh-5FHVWDIN.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AssistProgress.tsx +144 -134
- package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
- package/src/components/ProtectedErrorBoundary.css +3 -1
- package/src/components/StatusDisplay.css +35 -35
- package/src/components/Toast.css +3 -1
- package/src/components/__tests__/AssistProgress.test.tsx +216 -148
- package/src/components/annotation/annotation-entries.css +0 -4
- package/src/components/annotation/annotations.css +1 -1
- package/src/components/annotation/references.css +0 -4
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
- package/src/components/layout/Header.css +0 -4
- package/src/components/layout/LeftSidebar.css +0 -4
- package/src/components/modals/ConfigureGenerationStep.tsx +109 -48
- package/src/components/modals/ConfigureSearchStep.tsx +23 -40
- package/src/components/modals/ReferenceWizardModal.tsx +55 -16
- package/src/components/modals/ResourceGenerateModal.tsx +24 -8
- package/src/components/modals/ResourceSearchModal.tsx +1 -1
- package/src/components/modals/SearchModal.css +5 -17
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/modals/SearchResultsStep.tsx +2 -20
- package/src/components/modals/WizardFooter.tsx +63 -0
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +255 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +72 -0
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +256 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +1 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
- package/src/components/modals/__tests__/WizardFooter.test.tsx +235 -0
- package/src/components/modals/modals.css +16 -42
- package/src/components/navigation/NavigationTabs.css +0 -20
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +162 -6
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +623 -232
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +764 -7
- package/src/components/pdf-annotation/__tests__/estimate-slot-height.test.ts +53 -0
- package/src/components/pdf-annotation/estimate-slot-height.ts +31 -0
- package/src/components/resource/AnnotateView.tsx +16 -5
- package/src/components/resource/AnnotationHistory.tsx +2 -0
- package/src/components/resource/BrowseView.tsx +15 -4
- package/src/components/resource/ResourceViewer.tsx +23 -24
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
- package/src/components/resource/__tests__/BrowseView.test.tsx +29 -3
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +33 -0
- package/src/components/resource/annotate-renderers.tsx +10 -1
- package/src/components/resource/browse-renderers.tsx +10 -1
- package/src/components/resource/event-formatting.ts +0 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
- package/src/components/resource/panels/AssessmentPanel.css +5 -6
- package/src/components/resource/panels/AssistSection.css +68 -271
- package/src/components/resource/panels/AssistSection.tsx +4 -4
- package/src/components/resource/panels/AssistShell.tsx +12 -4
- package/src/components/resource/panels/CollaborationPanel.css +47 -22
- package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
- package/src/components/resource/panels/CommentEntry.tsx +1 -1
- package/src/components/resource/panels/CommentsPanel.css +0 -16
- package/src/components/resource/panels/HighlightEntry.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.css +0 -7
- package/src/components/resource/panels/JsonLdPanel.css +0 -4
- package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.css +0 -12
- package/src/components/resource/panels/ReferencesPanel.tsx +12 -20
- package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
- package/src/components/resource/panels/ResourceInfoPanel.tsx +1 -1
- package/src/components/resource/panels/StatisticsPanel.css +0 -20
- package/src/components/resource/panels/TagEntry.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.css +0 -4
- package/src/components/resource/panels/TaggingPanel.tsx +2 -10
- package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +34 -53
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +37 -1
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
- package/src/components/settings/SettingsPanel.css +0 -4
- package/src/features/auth/auth.css +9 -33
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +25 -1
- package/src/features/resource-compose/state/compose-page-state-unit.ts +11 -2
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
- package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +26 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +42 -21
- package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
- package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
- package/src/styles/core/buttons.css +3 -1
- package/src/styles/core/forms.css +0 -21
- package/src/styles/core/indicators.css +0 -4
- package/src/styles/core/inputs.css +1 -5
- package/src/styles/core/progress.css +2 -34
- package/src/styles/core/sliders.css +0 -4
- package/src/styles/core/toggles.css +0 -4
- package/src/styles/features/compose.css +1 -1
- package/src/styles/features/resource-discovery.css +13 -0
- package/src/styles/features/resource-viewer.css +1 -21
- package/src/styles/features/static-pages.css +11 -11
- package/src/styles/motivations/motivation-assessment.css +2 -2
- package/src/styles/motivations/motivation-comment.css +2 -2
- package/src/styles/motivations/motivation-reference.css +2 -2
- package/src/styles/panels/history-panel.css +0 -40
- package/src/styles/panels/user-panel.css +0 -3
- package/src/styles/patterns/panel-helpers.css +0 -27
- package/src/styles/patterns/panel-sections.css +0 -59
- package/src/styles/patterns/panels-base.css +4 -36
- package/src/styles/utilities/motion.css +15 -15
- package/src/styles/variables.css +29 -1
- package/translations/ar.json +59 -5
- package/translations/bn.json +59 -5
- package/translations/cs.json +59 -5
- package/translations/da.json +59 -5
- package/translations/de.json +59 -5
- package/translations/el.json +59 -5
- package/translations/en.json +59 -5
- package/translations/es.json +59 -5
- package/translations/fa.json +59 -5
- package/translations/fi.json +59 -5
- package/translations/fr.json +59 -5
- package/translations/he.json +59 -5
- package/translations/hi.json +59 -5
- package/translations/id.json +59 -5
- package/translations/it.json +59 -5
- package/translations/ja.json +59 -5
- package/translations/ko.json +59 -5
- package/translations/ms.json +59 -5
- package/translations/nl.json +59 -5
- package/translations/no.json +59 -5
- package/translations/pl.json +59 -5
- package/translations/pt.json +59 -5
- package/translations/ro.json +59 -5
- package/translations/sv.json +59 -5
- package/translations/th.json +59 -5
- package/translations/tr.json +59 -5
- package/translations/uk.json +59 -5
- package/translations/vi.json +59 -5
- package/translations/zh.json +59 -5
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js +0 -462
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css +0 -98
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css.map +0 -1
- package/dist/ar-CULRHLNG.js.map +0 -1
- package/dist/bn-HYR7CEL2.js.map +0 -1
- package/dist/chunk-NH6L2GIL.js.map +0 -1
- package/dist/chunk-TG2XQEC4.js.map +0 -1
- package/dist/chunk-XUDKYAVC.js +0 -21
- package/dist/chunk-XUDKYAVC.js.map +0 -1
- package/dist/cs-YBRWY7NH.js.map +0 -1
- package/dist/da-PHDEOBV3.js.map +0 -1
- package/dist/de-O4UCCVLJ.js.map +0 -1
- package/dist/el-MVGO6WZV.js.map +0 -1
- package/dist/es-USSLXQY7.js.map +0 -1
- package/dist/fa-WMAREFWY.js.map +0 -1
- package/dist/fi-B3NBNOTS.js.map +0 -1
- package/dist/fr-TXYK5T5W.js.map +0 -1
- package/dist/he-JCJACW5D.js.map +0 -1
- package/dist/hi-VOQLZNJU.js.map +0 -1
- package/dist/id-BFN5RZNZ.js.map +0 -1
- package/dist/it-TWWDHKKM.js.map +0 -1
- package/dist/ja-YRQLEBFM.js.map +0 -1
- package/dist/ko-PG6B3CRB.js.map +0 -1
- package/dist/ms-7EZECVKM.js.map +0 -1
- package/dist/nl-HP753YAP.js.map +0 -1
- package/dist/no-JUD5WX3N.js.map +0 -1
- package/dist/pl-RVUL4CBM.js.map +0 -1
- package/dist/pt-RPOVD4QM.js.map +0 -1
- package/dist/ro-DMYWQTRD.js.map +0 -1
- package/dist/sv-DMZ4FGXD.js.map +0 -1
- package/dist/th-4HVBJBQ2.js.map +0 -1
- package/dist/tr-6BDPU5ML.js.map +0 -1
- package/dist/uk-TBZBT2TL.js.map +0 -1
- package/dist/vi-UMKZKZL3.js.map +0 -1
- package/dist/zh-YBJWJJYB.js.map +0 -1
- package/src/integrations/tailwind-plugin.cjs +0 -224
- /package/dist/{en-2KQILFKQ.js.map → en-2KADE7N4.js.map} +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF-CONTINUOUS-SCROLL S1b.
|
|
3
|
+
*
|
|
4
|
+
* The scroll column reserves space for unmounted pages. If the reservation is
|
|
5
|
+
* wrong, every mount and unmount changes the column's height and the scrollbar
|
|
6
|
+
* jumps — which is what S1 shipped, by reserving the raster height rather than
|
|
7
|
+
* the displayed one.
|
|
8
|
+
*
|
|
9
|
+
* These pins hold the estimate to the CSS the image is actually under:
|
|
10
|
+
* `max-width: 100%; height: auto`.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { estimateSlotHeight } from '../estimate-slot-height';
|
|
14
|
+
|
|
15
|
+
// US Letter at scale 1.5: 918 × 1188 raster, aspect 792/612.
|
|
16
|
+
const ASPECT = 792 / 612;
|
|
17
|
+
const RASTER_W = 918;
|
|
18
|
+
|
|
19
|
+
describe('estimateSlotHeight', () => {
|
|
20
|
+
it('scales to the column when the column is narrower than the raster', () => {
|
|
21
|
+
// The common case: `max-width: 100%` shrinks the image to the column.
|
|
22
|
+
expect(estimateSlotHeight(600, RASTER_W, ASPECT)).toBe(Math.round(600 * ASPECT));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('never upscales past the raster — max-width shrinks only', () => {
|
|
26
|
+
// A column wider than the raster leaves the image at natural size, so the
|
|
27
|
+
// height stops growing. Reserving columnWidth * aspect here would over-
|
|
28
|
+
// reserve and leave a gap under every page.
|
|
29
|
+
expect(estimateSlotHeight(1600, RASTER_W, ASPECT)).toBe(Math.round(RASTER_W * ASPECT));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('is exactly the raster height when the column matches the raster', () => {
|
|
33
|
+
expect(estimateSlotHeight(RASTER_W, RASTER_W, ASPECT)).toBe(Math.round(RASTER_W * ASPECT));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does NOT use the raster height as a width-independent constant', () => {
|
|
37
|
+
// The S1 defect, stated as a pin: the raster is 1188px tall, but in a
|
|
38
|
+
// 600px column the page occupies ~776px. Reserving 1188 is what made the
|
|
39
|
+
// column's height lurch on every mount.
|
|
40
|
+
const rasterHeight = Math.round(RASTER_W * ASPECT); // 1188
|
|
41
|
+
expect(estimateSlotHeight(600, RASTER_W, ASPECT)).not.toBe(rasterHeight);
|
|
42
|
+
expect(estimateSlotHeight(600, RASTER_W, ASPECT)).toBeLessThan(rasterHeight);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('answers null until every measurement is in, rather than guessing', () => {
|
|
46
|
+
// A wrong reservation is worse than none: an unsized slot is stable, a
|
|
47
|
+
// mis-sized one moves when the page lands.
|
|
48
|
+
expect(estimateSlotHeight(null, RASTER_W, ASPECT)).toBeNull();
|
|
49
|
+
expect(estimateSlotHeight(600, null, ASPECT)).toBeNull();
|
|
50
|
+
expect(estimateSlotHeight(600, RASTER_W, null)).toBeNull();
|
|
51
|
+
expect(estimateSlotHeight(0, RASTER_W, ASPECT)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The height a page will occupy once rendered, computed BEFORE rendering it.
|
|
3
|
+
*
|
|
4
|
+
* The scrolling column reserves space for pages it has not mounted. If the
|
|
5
|
+
* reserved height differs from the height the page actually takes, every mount
|
|
6
|
+
* and unmount changes the column's total height and the scrollbar jumps under
|
|
7
|
+
* the reader's cursor — the defect S1 shipped by reserving the *raster* height
|
|
8
|
+
* (`getViewport({ scale }).height`) instead of the *displayed* one.
|
|
9
|
+
*
|
|
10
|
+
* This models the CSS the image is under exactly:
|
|
11
|
+
*
|
|
12
|
+
* .semiont-pdf-annotation-canvas__image { max-width: 100%; height: auto; }
|
|
13
|
+
*
|
|
14
|
+
* `max-width` shrinks but never upscales, so the displayed width is
|
|
15
|
+
* `min(columnWidth, rasterWidth)` and the height follows the page's aspect
|
|
16
|
+
* ratio. Aspect is scale-invariant, so it can come from any viewport.
|
|
17
|
+
*
|
|
18
|
+
* @param columnWidth Measured inner width of the scroll column, in CSS px.
|
|
19
|
+
* @param rasterWidth Width of the rendered raster, in px (`viewport(scale).width`).
|
|
20
|
+
* @param aspect Page height ÷ width, from any viewport scale.
|
|
21
|
+
* @returns Reserved height in CSS px, or null when a measurement is not in yet.
|
|
22
|
+
*/
|
|
23
|
+
export function estimateSlotHeight(
|
|
24
|
+
columnWidth: number | null,
|
|
25
|
+
rasterWidth: number | null,
|
|
26
|
+
aspect: number | null,
|
|
27
|
+
): number | null {
|
|
28
|
+
if (!columnWidth || !rasterWidth || !aspect) return null;
|
|
29
|
+
if (columnWidth <= 0 || rasterWidth <= 0 || aspect <= 0) return null;
|
|
30
|
+
return Math.round(Math.min(columnWidth, rasterWidth) * aspect);
|
|
31
|
+
}
|
|
@@ -53,7 +53,7 @@ interface Props {
|
|
|
53
53
|
*
|
|
54
54
|
* @emits mark:requested - User requested to create annotation. Payload: { selector: Selector | Selector[], motivation: SelectionMotivation }
|
|
55
55
|
* @subscribes beckon:hover - Annotation hovered. Payload: { annotationId: string | null }
|
|
56
|
-
* @subscribes beckon:focus - Scroll to and highlight annotation. Payload: { annotationId: string }
|
|
56
|
+
* @subscribes beckon:focus - Scroll to and highlight annotation, unless `resourceId` names a different resource (D7). Payload: { annotationId: string, resourceId?: string }
|
|
57
57
|
*/
|
|
58
58
|
export function AnnotateView({
|
|
59
59
|
content,
|
|
@@ -111,10 +111,21 @@ export function AnnotateView({
|
|
|
111
111
|
// serves, so the behaviour no longer depends on which view mode is active.
|
|
112
112
|
// The `scrollToAnnotationId` prop path (uiState → renderer) stays as the
|
|
113
113
|
// host-facing capability it is; this is the in-app producer's route.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
// `resourceId` is a GUARD, not navigation (GUIDED-TOUR D7): it names the
|
|
115
|
+
// resource this focus applies to, and a viewer showing a different one
|
|
116
|
+
// ignores it. Absent means unscoped — the in-app emitters (history panel,
|
|
117
|
+
// annotation list) omit it because they are already scoped to the open
|
|
118
|
+
// resource, so treating absence as "not mine" would silence all of them.
|
|
119
|
+
// Focus never navigates; driving the Browser to a resource is
|
|
120
|
+
// `browse:resource-open`'s job.
|
|
121
|
+
const handleAnnotationFocus = useCallback(
|
|
122
|
+
({ annotationId, resourceId }: { annotationId?: string | null; resourceId?: string }) => {
|
|
123
|
+
if (resourceId && resourceId !== resourceUri) return;
|
|
124
|
+
if (!containerRef.current) return;
|
|
125
|
+
scrollAnnotationIntoView(annotationId ?? null, containerRef.current, { pulse: true });
|
|
126
|
+
},
|
|
127
|
+
[resourceUri],
|
|
128
|
+
);
|
|
118
129
|
|
|
119
130
|
// Annotation hover (session-scoped). Toolbar preference changes flow through
|
|
120
131
|
// props/callbacks, not the bus (TOOLBAR-PREFS-AS-PROPS).
|
|
@@ -51,6 +51,8 @@ export function AnnotationHistory({
|
|
|
51
51
|
|
|
52
52
|
// Sort events by oldest first (most recent at bottom)
|
|
53
53
|
// Filter out job events - they're represented by mark:body-updated events instead
|
|
54
|
+
// `job:progress` is no longer a persisted event type, but logs written before it
|
|
55
|
+
// was retired still hold them; naming it here keeps those out of the history.
|
|
54
56
|
const events: StoredEventLike[] = eventsData
|
|
55
57
|
.filter((e) => {
|
|
56
58
|
return e.type !== 'job:started' && e.type !== 'job:progress' && e.type !== 'job:completed';
|
|
@@ -155,7 +155,7 @@ export const BrowseView = memo(function BrowseView({
|
|
|
155
155
|
if (annotation) {
|
|
156
156
|
// The emission site owns the geometry: the clicked span's viewport
|
|
157
157
|
// rect rides the event so hosts can anchor popovers (A1).
|
|
158
|
-
session.client.browse.click(annotation.id,
|
|
158
|
+
session.client.browse.click(annotation.id, annotationElement.getBoundingClientRect());
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
};
|
|
@@ -263,9 +263,20 @@ export const BrowseView = memo(function BrowseView({
|
|
|
263
263
|
scrollToAnnotation(annotationId);
|
|
264
264
|
}, [scrollToAnnotation]);
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
// `resourceId` is a GUARD, not navigation (GUIDED-TOUR D7): it names the
|
|
267
|
+
// resource this focus applies to, and a viewer showing a different one
|
|
268
|
+
// ignores it. Absent means unscoped — the in-app emitters (history panel,
|
|
269
|
+
// annotation list) omit it because they are already scoped to the open
|
|
270
|
+
// resource, so treating absence as "not mine" would silence all of them.
|
|
271
|
+
// Focus never navigates; driving the Browser to a resource is
|
|
272
|
+
// `browse:resource-open`'s job.
|
|
273
|
+
const handleAnnotationFocus = useCallback(
|
|
274
|
+
({ annotationId, resourceId }: { annotationId?: string | null; resourceId?: string }) => {
|
|
275
|
+
if (resourceId && resourceId !== resourceUri) return;
|
|
276
|
+
scrollToAnnotation(annotationId ?? null, true);
|
|
277
|
+
},
|
|
278
|
+
[scrollToAnnotation, resourceUri],
|
|
279
|
+
);
|
|
269
280
|
|
|
270
281
|
useSessionEventSubscriptions(session, {
|
|
271
282
|
'beckon:hover': handleAnnotationHover,
|
|
@@ -8,7 +8,7 @@ import { BrowseView, type ReferenceHover } from './BrowseView';
|
|
|
8
8
|
import type { BrowseMediaRenderers } from './browse-renderers';
|
|
9
9
|
import { PopupContainer } from '../annotation-popups/SharedPopupElements';
|
|
10
10
|
import { JsonLdView } from '../annotation-popups/JsonLdView';
|
|
11
|
-
import type { Annotation, AnnotationId, ResourceDescriptor as SemiontResource,
|
|
11
|
+
import type { Annotation, AnnotationId, ResourceDescriptor as SemiontResource, EventMap, AnchorRect } from '@semiont/core';
|
|
12
12
|
import { getExactText, getTargetSelector, getPrimaryMediaType, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/core';
|
|
13
13
|
import type { SemiontSession } from '@semiont/sdk';
|
|
14
14
|
import { useSessionEventSubscriptions } from '../../hooks/useSessionEventSubscriptions';
|
|
@@ -102,7 +102,10 @@ interface Props {
|
|
|
102
102
|
* panel opening invokes the host's `onOpenPanel` callback — the host owns
|
|
103
103
|
* the panel and any `panel:open` emission.
|
|
104
104
|
*
|
|
105
|
-
* @subscribes browse:click -
|
|
105
|
+
* @subscribes browse:click - An annotation was clicked — locally, or driven over
|
|
106
|
+
* the wire by another participant. Payload: { annotationId: string, anchorRect? };
|
|
107
|
+
* the motivation is derived from the annotation the id names, and an id this
|
|
108
|
+
* viewer has not loaded is a no-op.
|
|
106
109
|
*/
|
|
107
110
|
export function ResourceViewer({
|
|
108
111
|
resource,
|
|
@@ -270,38 +273,34 @@ export function ResourceViewer({
|
|
|
270
273
|
}, [annotateMode, selectedClick, onOpenResource]);
|
|
271
274
|
|
|
272
275
|
// Annotation click coordinator - handles panel opening and scrolling
|
|
273
|
-
const handleAnnotationClickEvent = useCallback(({ annotationId,
|
|
276
|
+
const handleAnnotationClickEvent = useCallback(({ annotationId, anchorRect }: {
|
|
274
277
|
annotationId: string;
|
|
275
|
-
motivation: components['schemas']['Motivation'];
|
|
276
278
|
anchorRect?: AnchorRect;
|
|
277
279
|
}) => {
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
280
|
+
// Resolve the annotation FIRST. The id is the whole address, and the
|
|
281
|
+
// motivation is derived from the annotation it names rather than carried
|
|
282
|
+
// beside it (TOUR-CLICK D2) — one fact, one encoding. Resolving first also
|
|
283
|
+
// means a click naming an annotation this viewer has not loaded (a remote
|
|
284
|
+
// drive that arrived while the participant was elsewhere) finds nothing
|
|
285
|
+
// and does nothing, which is why the channel needs no resourceId guard.
|
|
286
|
+
const allAnnotations = [...highlights, ...references, ...assessments, ...comments, ...tags];
|
|
287
|
+
const annotation = allAnnotations.find(a => a.id === annotationId);
|
|
288
|
+
if (!annotation) return;
|
|
289
|
+
|
|
290
|
+
// The real annotation, not a `{ motivation } as Annotation` stand-in.
|
|
291
|
+
const metadata = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(annotation));
|
|
290
292
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (annotation) {
|
|
296
|
-
handleAnnotationClick(annotation);
|
|
297
|
-
}
|
|
293
|
+
// No side panel, or a click mode other than detail: this is a plain
|
|
294
|
+
// annotation click, not a panel-opening one.
|
|
295
|
+
if (!metadata?.hasSidePanel || selectedClick !== 'detail') {
|
|
296
|
+
handleAnnotationClick(annotation);
|
|
298
297
|
return;
|
|
299
298
|
}
|
|
300
299
|
|
|
301
300
|
// All annotations open the unified annotations panel — the host owns the panel.
|
|
302
301
|
// The panel internally switches tabs based on the motivation → tab mapping in UnifiedAnnotationsPanel.
|
|
303
302
|
// View geometry passes through untouched: the emitter owned it, the host anchors with it.
|
|
304
|
-
onOpenPanel?.({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation, ...(anchorRect ? { anchorRect } : {}) });
|
|
303
|
+
onOpenPanel?.({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation: annotation.motivation, ...(anchorRect ? { anchorRect } : {}) });
|
|
305
304
|
}, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick, onOpenPanel]);
|
|
306
305
|
|
|
307
306
|
// Single subscription call per file (see scripts/compliance/audit-hooks-ordering.ts).
|
|
@@ -56,3 +56,53 @@ describe('AnnotateView — beckon:focus scrolls the content', () => {
|
|
|
56
56
|
expect(scrollSpy.mock.calls[0]?.[0]).toBe('ann-7');
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
// GUIDED-TOUR P6 (D7) — `resourceId` is a GUARD, not navigation.
|
|
62
|
+
//
|
|
63
|
+
// The schema now says so out loud: "it names the resource this focus applies
|
|
64
|
+
// to, and a viewer currently showing a different resource ignores the event".
|
|
65
|
+
// Without the comparison the field was decorative — a guide beckoning a
|
|
66
|
+
// reference in doc B scrolled every participant's doc A.
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
68
|
+
describe('AnnotateView — beckon:focus is guarded by resourceId (P6/D7)', () => {
|
|
69
|
+
beforeEach(() => { scrollSpy.mockClear(); });
|
|
70
|
+
|
|
71
|
+
const renderAt = (resourceUri: string) => {
|
|
72
|
+
const { session, client } = createTestSemiontWrapper();
|
|
73
|
+
render(
|
|
74
|
+
<AnnotateView
|
|
75
|
+
content="hello world"
|
|
76
|
+
mimeType="text/plain"
|
|
77
|
+
resourceUri={resourceUri}
|
|
78
|
+
annotations={emptyAnnotations}
|
|
79
|
+
uiState={uiState}
|
|
80
|
+
session={session}
|
|
81
|
+
annotateMode
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
return client;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
it('ignores a focus aimed at a DIFFERENT resource', () => {
|
|
88
|
+
const client = renderAt('res-1');
|
|
89
|
+
client.bus.get('beckon:focus').next({ annotationId: 'ann-7', resourceId: 'res-2' });
|
|
90
|
+
expect(scrollSpy).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('still scrolls when the resource matches', () => {
|
|
94
|
+
const client = renderAt('res-1');
|
|
95
|
+
client.bus.get('beckon:focus').next({ annotationId: 'ann-7', resourceId: 'res-1' });
|
|
96
|
+
expect(scrollSpy).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('still scrolls when the event names no resource at all', () => {
|
|
100
|
+
// `resourceId` is optional in the schema (`required: []`), and the in-app
|
|
101
|
+
// emitters — the history panel, the annotation list — omit it because they
|
|
102
|
+
// are already scoped to the open resource. A guard that treated absence as
|
|
103
|
+
// "not mine" would break every one of them.
|
|
104
|
+
const client = renderAt('res-1');
|
|
105
|
+
client.bus.get('beckon:focus').next({ annotationId: 'ann-7' });
|
|
106
|
+
expect(scrollSpy).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -173,6 +173,8 @@ describe('AnnotationHistory', () => {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
it('filters out job events', () => {
|
|
176
|
+
// e3 is `job:progress`, a retired event type. Logs written before it was
|
|
177
|
+
// retired still hold them, so the filter must keep excluding it.
|
|
176
178
|
const events = [
|
|
177
179
|
makeStoredEvent('e1', 'mark:added', 1),
|
|
178
180
|
makeStoredEvent('e2', 'job:started', 2),
|
|
@@ -427,7 +427,7 @@ describe('BrowseView Component', () => {
|
|
|
427
427
|
{ id: 'comment-1', type: 'comment', motivation: 'commenting' },
|
|
428
428
|
] as const;
|
|
429
429
|
|
|
430
|
-
for (const { id, type
|
|
430
|
+
for (const { id, type } of clickCases) {
|
|
431
431
|
const el = document.createElement('span');
|
|
432
432
|
el.setAttribute('data-annotation-id', id);
|
|
433
433
|
el.setAttribute('data-annotation-type', type);
|
|
@@ -439,8 +439,7 @@ describe('BrowseView Component', () => {
|
|
|
439
439
|
await waitFor(() => {
|
|
440
440
|
expect(tracker.events.some(e =>
|
|
441
441
|
e.event === 'browse:click' &&
|
|
442
|
-
e.payload?.annotationId === id
|
|
443
|
-
e.payload?.motivation === motivation
|
|
442
|
+
e.payload?.annotationId === id
|
|
444
443
|
)).toBe(true);
|
|
445
444
|
});
|
|
446
445
|
}
|
|
@@ -653,3 +652,30 @@ describe('BrowseView Component', () => {
|
|
|
653
652
|
});
|
|
654
653
|
});
|
|
655
654
|
});
|
|
655
|
+
|
|
656
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
657
|
+
// GUIDED-TOUR P6 (D7) — the same guard BrowseView's sibling carries.
|
|
658
|
+
// BrowseView subscribed to `beckon:focus` first, so it is the one that has
|
|
659
|
+
// been scrolling on other people's resources the longest.
|
|
660
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
661
|
+
describe('BrowseView — beckon:focus is guarded by resourceId (P6/D7)', () => {
|
|
662
|
+
it('ignores a focus aimed at a different resource, honours one that matches', () => {
|
|
663
|
+
const { session, client } = createTestSemiontWrapper();
|
|
664
|
+
const { container } = render(
|
|
665
|
+
<BrowseView
|
|
666
|
+
content="hello world"
|
|
667
|
+
mimeType="text/plain"
|
|
668
|
+
resourceUri="res-1"
|
|
669
|
+
annotations={{ highlights: [], references: [], assessments: [], comments: [], tags: [] }}
|
|
670
|
+
session={session}
|
|
671
|
+
annotateMode={false}
|
|
672
|
+
/>,
|
|
673
|
+
);
|
|
674
|
+
expect(container).toBeTruthy();
|
|
675
|
+
|
|
676
|
+
// Neither call may throw; the assertion that matters is the guard's
|
|
677
|
+
// existence, pinned in AnnotateView where the scroll helper is mockable.
|
|
678
|
+
client.bus.get('beckon:focus').next({ annotationId: 'ann-7', resourceId: 'res-2' });
|
|
679
|
+
client.bus.get('beckon:focus').next({ annotationId: 'ann-7', resourceId: 'res-1' });
|
|
680
|
+
});
|
|
681
|
+
});
|
|
@@ -15,7 +15,8 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
15
15
|
import { render, screen, act, waitFor } from '@testing-library/react';
|
|
16
16
|
import '@testing-library/jest-dom';
|
|
17
17
|
import type { SemiontSession } from '@semiont/sdk';
|
|
18
|
-
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
18
|
+
import type { Annotation, ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
19
|
+
import { annotationId } from '@semiont/core';
|
|
19
20
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
20
21
|
import { ResourceViewer } from '../ResourceViewer';
|
|
21
22
|
|
|
@@ -46,6 +47,23 @@ const resource: SemiontResource & { content: string } = {
|
|
|
46
47
|
|
|
47
48
|
const annotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A highlight the viewer has actually loaded. Required since TOUR-CLICK D2: the
|
|
52
|
+
* click handler resolves the annotation by id and derives the motivation from
|
|
53
|
+
* it, so a click for an id absent from this collection is a no-op. Before that,
|
|
54
|
+
* the wire carried the motivation and the viewer never looked — which is why
|
|
55
|
+
* the anchorRect test below used to pass against an empty collection.
|
|
56
|
+
*/
|
|
57
|
+
const loadedHighlight: Annotation = {
|
|
58
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
59
|
+
type: 'Annotation',
|
|
60
|
+
id: annotationId('ann-1'),
|
|
61
|
+
motivation: 'highlighting',
|
|
62
|
+
target: { source: 'res-1', selector: { type: 'TextPositionSelector', start: 0, end: 10 } },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const annotationsWithHighlight = { ...annotations, highlights: [loadedHighlight] };
|
|
66
|
+
|
|
49
67
|
describe('ResourceViewer — embeddable (bring-your-own-session, no providers)', () => {
|
|
50
68
|
// GREEN: the whole browse-mode subtree (ResourceViewer → BrowseView →
|
|
51
69
|
// AnnotateToolbar) renders provider-free from a bare session.
|
|
@@ -73,7 +91,7 @@ describe('ResourceViewer — embeddable (bring-your-own-session, no providers)',
|
|
|
73
91
|
<ResourceViewer
|
|
74
92
|
session={session}
|
|
75
93
|
resource={resource}
|
|
76
|
-
annotations={
|
|
94
|
+
annotations={annotationsWithHighlight}
|
|
77
95
|
onOpenResource={vi.fn()}
|
|
78
96
|
onOpenPanel={onOpenPanel}
|
|
79
97
|
/>,
|
|
@@ -86,7 +104,6 @@ describe('ResourceViewer — embeddable (bring-your-own-session, no providers)',
|
|
|
86
104
|
act(() => {
|
|
87
105
|
eventBus.get('browse:click').next({
|
|
88
106
|
annotationId: 'ann-1',
|
|
89
|
-
motivation: 'highlighting',
|
|
90
107
|
anchorRect,
|
|
91
108
|
});
|
|
92
109
|
});
|
|
@@ -95,8 +112,35 @@ describe('ResourceViewer — embeddable (bring-your-own-session, no providers)',
|
|
|
95
112
|
expect(onOpenPanel).toHaveBeenCalledWith(expect.objectContaining({
|
|
96
113
|
panel: 'annotations',
|
|
97
114
|
scrollToAnnotationId: 'ann-1',
|
|
115
|
+
// Derived from the annotation the id names, never carried on the wire.
|
|
116
|
+
motivation: 'highlighting',
|
|
98
117
|
anchorRect: expect.objectContaining({ left: 5, width: 7 }),
|
|
99
118
|
}));
|
|
100
119
|
});
|
|
101
120
|
});
|
|
121
|
+
|
|
122
|
+
it('ignores a browse:click naming an annotation this viewer has not loaded', async () => {
|
|
123
|
+
// A remote drive can arrive while the participant is looking at something
|
|
124
|
+
// else. Resolving the annotation FIRST makes that a no-op by construction,
|
|
125
|
+
// which is why the channel carries no resourceId guard (TOUR-CLICK D3).
|
|
126
|
+
const { session, eventBus } = createTestSemiontWrapper();
|
|
127
|
+
const onOpenPanel = vi.fn();
|
|
128
|
+
|
|
129
|
+
render(
|
|
130
|
+
<ResourceViewer
|
|
131
|
+
session={session}
|
|
132
|
+
resource={resource}
|
|
133
|
+
annotations={annotationsWithHighlight}
|
|
134
|
+
onOpenResource={vi.fn()}
|
|
135
|
+
onOpenPanel={onOpenPanel}
|
|
136
|
+
/>,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
act(() => {
|
|
140
|
+
eventBus.get('browse:click').next({ annotationId: 'ann-not-here' });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
144
|
+
expect(onOpenPanel).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
102
146
|
});
|
|
@@ -109,5 +109,38 @@ describe('browse-renderers — annotation + session forwarding (dispatch contrac
|
|
|
109
109
|
expect(props.existingAnnotations).toEqual([shapeAnnotation()]); // pinned — worked before
|
|
110
110
|
expect(props.drawingMode).toBeNull();
|
|
111
111
|
expect(props.session).toBe(session); // NEW — was a session-less no-op
|
|
112
|
+
// PDF-CONTINUOUS-SCROLL S1: browse asks for the scrolling column.
|
|
113
|
+
expect(props.pageLayout).toBe('scroll');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// S2. Annotate is the mode people actually work in
|
|
117
|
+
// (.plans/PDF-CONTINUOUS-SCROLL.md D3, second correction), so it gets the
|
|
118
|
+
// column too — leaving it on Previous/Next made the feature invisible to
|
|
119
|
+
// its primary audience.
|
|
120
|
+
it('the annotate registry asks for the column as well, with the live tool', async () => {
|
|
121
|
+
const session = fakeSession();
|
|
122
|
+
const { AnnotateView } = await import('../AnnotateView');
|
|
123
|
+
const { container } = render(
|
|
124
|
+
<AnnotateView
|
|
125
|
+
content="blob:pdf-url"
|
|
126
|
+
mimeType="application/pdf"
|
|
127
|
+
resourceUri="res-1"
|
|
128
|
+
annotations={emptyAnnotations}
|
|
129
|
+
uiState={{
|
|
130
|
+
selectedMotivation: 'highlighting',
|
|
131
|
+
selectedClick: 'detail',
|
|
132
|
+
selectedShape: 'rectangle',
|
|
133
|
+
hoveredAnnotationId: null,
|
|
134
|
+
scrollToAnnotationId: null,
|
|
135
|
+
}}
|
|
136
|
+
annotateMode
|
|
137
|
+
session={session}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await within(container).findByText('pdf-canvas-mock');
|
|
142
|
+
const props = captured.pdf.at(-1)!;
|
|
143
|
+
expect(props.pageLayout).toBe('scroll');
|
|
144
|
+
expect(props.drawingMode).toBe('rectangle'); // the tool still reaches the pages
|
|
112
145
|
});
|
|
113
146
|
});
|
|
@@ -7,10 +7,18 @@ import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
|
7
7
|
import { CodeMirrorRenderer } from '../CodeMirrorRenderer';
|
|
8
8
|
import { segmentTextWithAnnotations } from '../../lib/text-segmentation';
|
|
9
9
|
import type { SelectionMotivation, ShapeType } from '../annotation/AnnotateToolbar';
|
|
10
|
+
import { useTranslations } from '../../contexts/TranslationContext';
|
|
10
11
|
|
|
11
12
|
// Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
|
|
12
13
|
// Kept lazy here deliberately: hoisting it to a static import would make it
|
|
13
14
|
// eager again and reintroduce the SSR problem this indirection exists for.
|
|
15
|
+
/** The lazy chunk's fallback needs its own component: a hook cannot run
|
|
16
|
+
* inside a `fallback` prop expression. */
|
|
17
|
+
function PdfViewerLoading() {
|
|
18
|
+
const t = useTranslations('PdfViewer');
|
|
19
|
+
return <>{t('viewerLoading')}</>;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const PdfAnnotationCanvas = lazy(() =>
|
|
15
23
|
import('../pdf-annotation/PdfAnnotationCanvas.client').then((mod) => ({ default: mod.PdfAnnotationCanvas })),
|
|
16
24
|
);
|
|
@@ -127,7 +135,7 @@ export function PdfAnnotateRenderer({
|
|
|
127
135
|
}: AnnotateMediaRendererProps) {
|
|
128
136
|
if (!content) return null;
|
|
129
137
|
return (
|
|
130
|
-
<Suspense fallback={<div className="semiont-annotate-view__loading"
|
|
138
|
+
<Suspense fallback={<div className="semiont-annotate-view__loading"><PdfViewerLoading /></div>}>
|
|
131
139
|
<PdfAnnotationCanvas
|
|
132
140
|
pdfUrl={content}
|
|
133
141
|
resourceUri={resourceUri}
|
|
@@ -137,6 +145,7 @@ export function PdfAnnotateRenderer({
|
|
|
137
145
|
session={session}
|
|
138
146
|
hoveredAnnotationId={hoveredAnnotationId || null}
|
|
139
147
|
hoverDelayMs={hoverDelayMs}
|
|
148
|
+
pageLayout="scroll"
|
|
140
149
|
/>
|
|
141
150
|
</Suspense>
|
|
142
151
|
);
|
|
@@ -6,8 +6,16 @@ import remarkGfm from 'remark-gfm';
|
|
|
6
6
|
import type { Annotation } from '@semiont/core';
|
|
7
7
|
import type { SemiontSession } from '@semiont/sdk';
|
|
8
8
|
import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
9
|
+
import { useTranslations } from '../../contexts/TranslationContext';
|
|
9
10
|
|
|
10
11
|
// Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
|
|
12
|
+
/** The lazy chunk's fallback needs its own component: a hook cannot run
|
|
13
|
+
* inside a `fallback` prop expression. */
|
|
14
|
+
function PdfViewerLoading() {
|
|
15
|
+
const t = useTranslations('PdfViewer');
|
|
16
|
+
return <>{t('viewerLoading')}</>;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -60,7 +68,7 @@ export function ImageBrowseRenderer({ content, resourceUri, annotations, session
|
|
|
60
68
|
|
|
61
69
|
export function PdfBrowseRenderer({ content, resourceUri, annotations, session }: MediaRendererProps) {
|
|
62
70
|
return (
|
|
63
|
-
<Suspense fallback={<div className="semiont-browse-view__loading"
|
|
71
|
+
<Suspense fallback={<div className="semiont-browse-view__loading"><PdfViewerLoading /></div>}>
|
|
64
72
|
<PdfAnnotationCanvas
|
|
65
73
|
pdfUrl={content}
|
|
66
74
|
resourceUri={resourceUri}
|
|
@@ -68,6 +76,7 @@ export function PdfBrowseRenderer({ content, resourceUri, annotations, session }
|
|
|
68
76
|
drawingMode={null}
|
|
69
77
|
selectedMotivation={null}
|
|
70
78
|
session={session}
|
|
79
|
+
pageLayout="scroll"
|
|
71
80
|
/>
|
|
72
81
|
</Suspense>
|
|
73
82
|
);
|
|
@@ -81,7 +81,6 @@ export function formatEventType(type: PersistedEventType, t: TranslateFn, payloa
|
|
|
81
81
|
|
|
82
82
|
case 'job:completed':
|
|
83
83
|
case 'job:started':
|
|
84
|
-
case 'job:progress':
|
|
85
84
|
case 'job:failed':
|
|
86
85
|
return t('jobEvent');
|
|
87
86
|
|
|
@@ -128,7 +127,6 @@ export function getEventEmoji(type: PersistedEventType, payload?: any): string {
|
|
|
128
127
|
case 'job:completed':
|
|
129
128
|
return '🔗'; // Link emoji for linked document creation
|
|
130
129
|
case 'job:started':
|
|
131
|
-
case 'job:progress':
|
|
132
130
|
return '⚙️'; // Gear for job processing
|
|
133
131
|
case 'job:failed':
|
|
134
132
|
return '❌'; // X mark for failed jobs
|
|
@@ -276,7 +274,6 @@ export function getEventDisplayContent(
|
|
|
276
274
|
}
|
|
277
275
|
|
|
278
276
|
case 'job:started':
|
|
279
|
-
case 'job:progress':
|
|
280
277
|
case 'job:failed':
|
|
281
278
|
case 'yield:representation-added':
|
|
282
279
|
case 'yield:representation-removed':
|
|
@@ -88,7 +88,7 @@ export function AssessmentEntry({
|
|
|
88
88
|
data-type="assessment"
|
|
89
89
|
data-focused={isFocused ? 'true' : 'false'}
|
|
90
90
|
onClick={() => {
|
|
91
|
-
session?.client.browse.click(assessment.id
|
|
91
|
+
session?.client.browse.click(assessment.id);
|
|
92
92
|
}}
|
|
93
93
|
{...hoverProps}
|
|
94
94
|
>
|
|
@@ -30,10 +30,6 @@
|
|
|
30
30
|
letter-spacing: 0.05em;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
[data-theme="dark"] .semiont-assessment-panel__label {
|
|
34
|
-
color: var(--semiont-text-secondary);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
.semiont-assessment-panel__rating {
|
|
38
34
|
display: flex;
|
|
39
35
|
gap: var(--semiont-panel-padding-xs);
|
|
@@ -46,8 +42,11 @@
|
|
|
46
42
|
transition: color 0.2s ease;
|
|
47
43
|
}
|
|
48
44
|
|
|
45
|
+
/* An UNRATED star must recede. gray-300 is near-white: on a dark panel the
|
|
46
|
+
empty stars outshone the filled ones, inverting the reading. Dark takes a
|
|
47
|
+
dim grey; the filled star keeps the warning hue. */
|
|
49
48
|
[data-theme="dark"] .semiont-assessment-panel__star {
|
|
50
|
-
color: var(--semiont-color-gray-
|
|
49
|
+
color: var(--semiont-color-gray-600);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
.semiont-assessment-panel__star:hover {
|
|
@@ -59,7 +58,7 @@
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
[data-theme="dark"] .semiont-assessment-panel__star--filled {
|
|
62
|
-
color: var(--semiont-color-warning);
|
|
61
|
+
color: var(--semiont-color-warning-light);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
.semiont-assessment-panel__textarea {
|