@semiont/react-ui 0.5.26 → 0.5.28
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-6ZSF6ZTJ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +33 -6
- package/dist/{PdfAnnotationCanvas.client-JCJMXWWJ.css → PdfAnnotationCanvas.client-TGDPPGXJ.css} +6 -1
- package/dist/{ar-YTUUGK2O.js → ar-FABPTJVB.js} +88 -26
- package/dist/{bn-ASGNGPMD.js → bn-LKP6BDMH.js} +88 -26
- package/dist/{chunk-L3WKRWP3.js → chunk-2GOHTTHW.js} +88 -26
- package/dist/{chunk-OBQOZCJE.js → chunk-2YHEE63S.js} +31 -31
- package/dist/{cs-WMYVAOZY.js → cs-YQYNWHRG.js} +88 -26
- package/dist/{da-FSHHN4D6.js → da-QHZOYKRY.js} +88 -26
- package/dist/{de-FJFO3TPT.js → de-GKA7RD7J.js} +88 -26
- package/dist/{el-XOH4QIAG.js → el-45T257AL.js} +88 -26
- package/dist/{en-3FCBW3YD.js → en-RVW5EQ66.js} +4 -2
- package/dist/{es-HDX4QI7E.js → es-AVER7RU4.js} +88 -26
- package/dist/{fa-DJ6WTFTJ.js → fa-K3VPK3O2.js} +88 -26
- package/dist/{fi-4S7H7W7U.js → fi-5NEKF7EB.js} +88 -26
- package/dist/{fr-6UJSYHXC.js → fr-FUMNDLBZ.js} +88 -26
- package/dist/{he-ZZJOHJX2.js → he-3JGNA3PR.js} +88 -26
- package/dist/{hi-HZJTAW7U.js → hi-IJY375DF.js} +88 -26
- package/dist/{id-E3I6ILEM.js → id-LWHKJ7ZD.js} +88 -26
- package/dist/index.css +380 -524
- package/dist/index.d.ts +318 -192
- package/dist/index.js +2960 -2421
- package/dist/integrations/css-modules-helper.js +1 -3
- package/dist/{it-ULAPAD7U.js → it-EBVOV4RW.js} +88 -26
- package/dist/{ja-SAHZQ4CZ.js → ja-KWRIX2NF.js} +88 -26
- package/dist/{ko-DFU2ADQ4.js → ko-L5CJMFDS.js} +88 -26
- package/dist/{ms-NWL76XNT.js → ms-LSNMJDR2.js} +88 -26
- package/dist/{nl-ATCM76WN.js → nl-ZB32MB7U.js} +88 -26
- package/dist/{no-UIAQ5RJF.js → no-4GTNY7M5.js} +88 -26
- package/dist/{pl-NOECQDXD.js → pl-LUHBLTHB.js} +88 -26
- package/dist/{pt-SVWIIUPC.js → pt-C4H3UNQ6.js} +88 -26
- package/dist/{ro-NR2DRTZG.js → ro-GGITS5V4.js} +88 -26
- package/dist/{sv-STVJ37JP.js → sv-T56RTLBP.js} +88 -26
- package/dist/test-utils.css +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +2 -2
- package/dist/{th-UWQR4K54.js → th-22X2IWCB.js} +88 -26
- package/dist/{tr-WNXZSD27.js → tr-77TFG62H.js} +88 -26
- package/dist/{uk-SJRMB47Y.js → uk-MIIL6NXT.js} +88 -26
- package/dist/{vi-TMRQFES2.js → vi-7JYOIGNV.js} +88 -26
- package/dist/{zh-QOL3LYEM.js → zh-ID6DRYU4.js} +88 -26
- package/package.json +11 -10
- package/src/components/AssistProgress.tsx +178 -134
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
- package/src/components/ProtectedErrorBoundary.css +3 -1
- package/src/components/StatusDisplay.css +1 -1
- package/src/components/Toast.css +3 -1
- package/src/components/__tests__/AssistProgress.test.tsx +268 -141
- package/src/components/annotation/annotation-entries.css +0 -4
- 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/ComposeStep.tsx +218 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +131 -54
- package/src/components/modals/ConfigureSearchStep.tsx +42 -40
- package/src/components/modals/ContextSummary.tsx +185 -72
- package/src/components/modals/CorpusPane.tsx +75 -0
- package/src/components/modals/DiscardPrompt.tsx +38 -0
- package/src/components/modals/GatherContextStep.tsx +237 -107
- package/src/components/modals/ReferenceWizardModal.tsx +287 -60
- package/src/components/modals/ResourceGenerateModal.tsx +233 -109
- package/src/components/modals/SearchModal.css +335 -111
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/modals/SearchResultsStep.tsx +63 -88
- package/src/components/modals/WizardFooter.tsx +69 -0
- package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
- package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +290 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +95 -0
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +635 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +277 -85
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +344 -0
- package/src/components/modals/modals.css +16 -42
- package/src/components/navigation/NavigationTabs.css +0 -20
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +21 -2
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +36 -3
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +41 -6
- package/src/components/resource/AnnotateView.tsx +19 -8
- package/src/components/resource/AnnotationHistory.tsx +2 -0
- package/src/components/resource/BrowseView.tsx +20 -9
- package/src/components/resource/ResourceViewer.tsx +27 -28
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
- 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.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +38 -12
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
- package/src/components/resource/annotate-renderers.tsx +3 -3
- 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 +95 -263
- 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 +10 -3
- package/src/components/resource/panels/ReferencesPanel.css +0 -12
- package/src/components/resource/panels/ReferencesPanel.tsx +16 -20
- package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
- package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
- 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/UnifiedAnnotationsPanel.tsx +7 -2
- 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 +41 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +67 -54
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +185 -7
- 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 +2 -26
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -76
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -48
- 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 +71 -15
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +166 -71
- 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 +35 -29
- package/src/styles/core/indicators.css +0 -4
- package/src/styles/core/inputs.css +0 -4
- 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/resource-discovery.css +13 -0
- package/src/styles/features/resource-viewer.css +0 -42
- package/src/styles/features/resource.css +0 -4
- package/src/styles/motivations/motivation-assessment.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 +0 -32
- package/translations/ar.json +84 -24
- package/translations/bn.json +84 -24
- package/translations/cs.json +84 -24
- package/translations/da.json +84 -24
- package/translations/de.json +84 -24
- package/translations/el.json +84 -24
- package/translations/en.json +84 -24
- package/translations/es.json +84 -24
- package/translations/fa.json +84 -24
- package/translations/fi.json +84 -24
- package/translations/fr.json +84 -24
- package/translations/he.json +84 -24
- package/translations/hi.json +84 -24
- package/translations/id.json +84 -24
- package/translations/it.json +84 -24
- package/translations/ja.json +84 -24
- package/translations/ko.json +84 -24
- package/translations/ms.json +84 -24
- package/translations/nl.json +84 -24
- package/translations/no.json +84 -24
- package/translations/pl.json +84 -24
- package/translations/pt.json +84 -24
- package/translations/ro.json +84 -24
- package/translations/sv.json +84 -24
- package/translations/th.json +84 -24
- package/translations/tr.json +84 -24
- package/translations/uk.json +84 -24
- package/translations/vi.json +84 -24
- package/translations/zh.json +84 -24
- package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +0 -1
- package/dist/ar-YTUUGK2O.js.map +0 -1
- package/dist/bn-ASGNGPMD.js.map +0 -1
- package/dist/chunk-BTUG3VJJ.js.map +0 -1
- package/dist/chunk-L3WKRWP3.js.map +0 -1
- package/dist/chunk-O23TLLXW.js.map +0 -1
- package/dist/chunk-OBQOZCJE.js.map +0 -1
- package/dist/chunk-QT7LYM72.js.map +0 -1
- package/dist/chunk-VXASXU4K.js.map +0 -1
- package/dist/chunk-XUDKYAVC.js +0 -21
- package/dist/chunk-XUDKYAVC.js.map +0 -1
- package/dist/cs-WMYVAOZY.js.map +0 -1
- package/dist/da-FSHHN4D6.js.map +0 -1
- package/dist/de-FJFO3TPT.js.map +0 -1
- package/dist/el-XOH4QIAG.js.map +0 -1
- package/dist/en-3FCBW3YD.js.map +0 -1
- package/dist/es-HDX4QI7E.js.map +0 -1
- package/dist/fa-DJ6WTFTJ.js.map +0 -1
- package/dist/fi-4S7H7W7U.js.map +0 -1
- package/dist/fr-6UJSYHXC.js.map +0 -1
- package/dist/he-ZZJOHJX2.js.map +0 -1
- package/dist/hi-HZJTAW7U.js.map +0 -1
- package/dist/id-E3I6ILEM.js.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/css-modules-helper.js.map +0 -1
- package/dist/integrations/styled-components-theme.js.map +0 -1
- package/dist/it-ULAPAD7U.js.map +0 -1
- package/dist/ja-SAHZQ4CZ.js.map +0 -1
- package/dist/ko-DFU2ADQ4.js.map +0 -1
- package/dist/ms-NWL76XNT.js.map +0 -1
- package/dist/nl-ATCM76WN.js.map +0 -1
- package/dist/no-UIAQ5RJF.js.map +0 -1
- package/dist/pl-NOECQDXD.js.map +0 -1
- package/dist/pt-SVWIIUPC.js.map +0 -1
- package/dist/ro-NR2DRTZG.js.map +0 -1
- package/dist/sv-STVJ37JP.js.map +0 -1
- package/dist/test-utils.css.map +0 -1
- package/dist/test-utils.js.map +0 -1
- package/dist/th-UWQR4K54.js.map +0 -1
- package/dist/tr-WNXZSD27.js.map +0 -1
- package/dist/uk-SJRMB47Y.js.map +0 -1
- package/dist/vi-TMRQFES2.js.map +0 -1
- package/dist/zh-QOL3LYEM.js.map +0 -1
- package/src/components/modals/ResourceSearchModal.tsx +0 -224
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -159
- package/src/integrations/tailwind-plugin.cjs +0 -224
|
@@ -275,7 +275,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
275
275
|
});
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
-
test('passes the annotation rect as browse.click
|
|
278
|
+
test('passes the annotation rect as browse.click second argument (A1 anchor)', async () => {
|
|
279
279
|
const click = vi.fn();
|
|
280
280
|
const session = {
|
|
281
281
|
client: { browse: { click }, beckon: { hover: vi.fn() } },
|
|
@@ -328,8 +328,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
328
328
|
|
|
329
329
|
expect(click).toHaveBeenCalledTimes(1);
|
|
330
330
|
expect(click.mock.calls[0]?.[0]).toBe('ann-1');
|
|
331
|
-
|
|
332
|
-
const anchorRect = click.mock.calls[0]?.[2];
|
|
331
|
+
const anchorRect = click.mock.calls[0]?.[1];
|
|
333
332
|
expect(anchorRect).toBeDefined();
|
|
334
333
|
expect(typeof anchorRect.width).toBe('number');
|
|
335
334
|
});
|
|
@@ -401,8 +400,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
401
400
|
|
|
402
401
|
expect(click).toHaveBeenCalledTimes(1);
|
|
403
402
|
expect(click.mock.calls[0]?.[0]).toBe('ann-hit-1');
|
|
404
|
-
expect(click.mock.calls[0]?.[1]).
|
|
405
|
-
expect(click.mock.calls[0]?.[2]).toMatchObject({
|
|
403
|
+
expect(click.mock.calls[0]?.[1]).toMatchObject({
|
|
406
404
|
left: displayRect.x,
|
|
407
405
|
top: displayRect.y,
|
|
408
406
|
width: displayRect.width,
|
|
@@ -510,6 +508,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
510
508
|
beckon: { hover: vi.fn() },
|
|
511
509
|
browse: {
|
|
512
510
|
resourceAnchoredText: vi.fn().mockResolvedValue({
|
|
511
|
+
kind: 'extracted',
|
|
513
512
|
text: 'Hello world again',
|
|
514
513
|
items: [
|
|
515
514
|
{ start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 },
|
|
@@ -562,6 +561,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
562
561
|
} as unknown as Awaited<ReturnType<typeof loadPdfDocument>>);
|
|
563
562
|
|
|
564
563
|
const resourceAnchoredText = vi.fn().mockResolvedValue({
|
|
564
|
+
kind: 'extracted',
|
|
565
565
|
text: 'Hello world again',
|
|
566
566
|
items: [
|
|
567
567
|
{ start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 },
|
|
@@ -729,6 +729,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
729
729
|
scannedDoc();
|
|
730
730
|
|
|
731
731
|
const resourceAnchoredText = vi.fn().mockResolvedValue({
|
|
732
|
+
kind: 'extracted',
|
|
732
733
|
text: 'Hello world again',
|
|
733
734
|
items: [{ start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 }],
|
|
734
735
|
});
|
|
@@ -891,6 +892,40 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
891
892
|
});
|
|
892
893
|
});
|
|
893
894
|
|
|
895
|
+
test('the strip sizes its scrollport to the panel that clips it — not the window', async () => {
|
|
896
|
+
// The strip is sticky with a CSS ceiling of 100vh — right when the
|
|
897
|
+
// nearest scroller is the window, a lie inside an inner-scrolled panel:
|
|
898
|
+
// there the strip's scrollport extends below the panel's clip, and
|
|
899
|
+
// `scrollIntoView({block:'nearest'})` considers a tick in that hidden
|
|
900
|
+
// band already in view — the active page vanished when it crossed the
|
|
901
|
+
// strip's bottom (never the top, whose edges coincide). Measured live:
|
|
902
|
+
// strip scrollport 126→846 vs panel 126→639. The fix sizes the
|
|
903
|
+
// scrollport to the scroller that actually clips it.
|
|
904
|
+
const io = stubIntersectionObserver();
|
|
905
|
+
scannedDoc();
|
|
906
|
+
|
|
907
|
+
const scroller = document.createElement('div');
|
|
908
|
+
scroller.style.overflowY = 'auto';
|
|
909
|
+
Object.defineProperty(scroller, 'clientHeight', { value: 513, configurable: true });
|
|
910
|
+
document.body.appendChild(scroller);
|
|
911
|
+
|
|
912
|
+
render(
|
|
913
|
+
<PdfAnnotationCanvas resourceUri="res-1"
|
|
914
|
+
pdfUrl={mockPdfUrl}
|
|
915
|
+
drawingMode={null}
|
|
916
|
+
pageLayout="scroll"
|
|
917
|
+
/>,
|
|
918
|
+
{ container: scroller },
|
|
919
|
+
);
|
|
920
|
+
await waitFor(() => {
|
|
921
|
+
expect(document.querySelectorAll('.semiont-pdf-annotation-canvas__strip-page')).toHaveLength(5);
|
|
922
|
+
});
|
|
923
|
+
io.fire([1]);
|
|
924
|
+
|
|
925
|
+
const strip = document.querySelector('.semiont-pdf-annotation-canvas__strip') as HTMLElement;
|
|
926
|
+
await waitFor(() => expect(strip.style.maxHeight).toBe('513px'));
|
|
927
|
+
});
|
|
928
|
+
|
|
894
929
|
test('clicking a strip rectangle jumps to that page', async () => {
|
|
895
930
|
const io = stubIntersectionObserver();
|
|
896
931
|
scannedDoc();
|
|
@@ -1246,7 +1281,7 @@ describe('PdfAnnotationCanvas', () => {
|
|
|
1246
1281
|
|
|
1247
1282
|
const resourceAnchoredText = vi.fn()
|
|
1248
1283
|
.mockRejectedValueOnce(new Error('transport down'))
|
|
1249
|
-
.mockResolvedValue({ text: 'later', items: [] });
|
|
1284
|
+
.mockResolvedValue({ kind: 'extracted', text: 'later', items: [] });
|
|
1250
1285
|
const session = {
|
|
1251
1286
|
client: { beckon: { hover: vi.fn() }, browse: { resourceAnchoredText } },
|
|
1252
1287
|
} as unknown as import('@semiont/sdk').SemiontSession;
|
|
@@ -39,7 +39,7 @@ interface Props {
|
|
|
39
39
|
/** Session for the shown resource — its client emits mark:* / mark.request; its bus feeds toolbar + beckon events. */
|
|
40
40
|
session: SemiontSession | null;
|
|
41
41
|
/** Recently-created annotation ids to sparkle (host-provided; was ResourceAnnotationsContext). */
|
|
42
|
-
|
|
42
|
+
sparkleAnnotationIds?: Set<string>;
|
|
43
43
|
/** The bar's Mode control reports the chosen mode here (the owner applies it). */
|
|
44
44
|
onModeChange?: (mode: boolean) => void;
|
|
45
45
|
/** Render the built-in bar (default). false → no bar; selection capture and drawing stay live. */
|
|
@@ -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,
|
|
@@ -69,7 +69,7 @@ export function AnnotateView({
|
|
|
69
69
|
hoverDelayMs = 150,
|
|
70
70
|
annotateMode,
|
|
71
71
|
session,
|
|
72
|
-
|
|
72
|
+
sparkleAnnotationIds,
|
|
73
73
|
onModeChange,
|
|
74
74
|
showToolbar = true,
|
|
75
75
|
renderers,
|
|
@@ -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).
|
|
@@ -256,7 +267,7 @@ export function AnnotateView({
|
|
|
256
267
|
selectedMotivation={selectedMotivation}
|
|
257
268
|
hoveredAnnotationId={hoveredAnnotationId}
|
|
258
269
|
hoverDelayMs={hoverDelayMs}
|
|
259
|
-
|
|
270
|
+
sparkleAnnotationIds={sparkleAnnotationIds}
|
|
260
271
|
scrollToAnnotationId={scrollToAnnotationId}
|
|
261
272
|
showLineNumbers={showLineNumbers}
|
|
262
273
|
enableWidgets={enableWidgets}
|
|
@@ -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';
|
|
@@ -35,7 +35,7 @@ interface Props {
|
|
|
35
35
|
/** Session for the shown resource — emits browse:click / beckon:hover; its bus feeds beckon events. */
|
|
36
36
|
session: SemiontSession | null;
|
|
37
37
|
/** Recently-created annotation ids to sparkle (host-provided; was ResourceAnnotationsContext). */
|
|
38
|
-
|
|
38
|
+
sparkleAnnotationIds?: Set<string>;
|
|
39
39
|
/** Override the read-only media renderers (render mode → renderer); merged over the defaults. */
|
|
40
40
|
renderers?: BrowseMediaRenderers;
|
|
41
41
|
/** A content link (`<a href>` in the rendered content) was clicked. The viewer preventDefaults and
|
|
@@ -87,7 +87,7 @@ export const BrowseView = memo(function BrowseView({
|
|
|
87
87
|
annotateMode,
|
|
88
88
|
hoverDelayMs = 150,
|
|
89
89
|
session,
|
|
90
|
-
|
|
90
|
+
sparkleAnnotationIds,
|
|
91
91
|
renderers,
|
|
92
92
|
onLinkClick,
|
|
93
93
|
onReferenceHover,
|
|
@@ -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
|
};
|
|
@@ -227,11 +227,11 @@ export const BrowseView = memo(function BrowseView({
|
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
// Apply animation classes to new annotations
|
|
230
|
-
if (
|
|
230
|
+
if (sparkleAnnotationIds) {
|
|
231
231
|
const annotationSpans = container.querySelectorAll('[data-annotation-id]');
|
|
232
232
|
annotationSpans.forEach((span) => {
|
|
233
233
|
const annotationId = span.getAttribute('data-annotation-id');
|
|
234
|
-
if (annotationId &&
|
|
234
|
+
if (annotationId && sparkleAnnotationIds.has(annotationId)) {
|
|
235
235
|
span.classList.add('annotation-sparkle');
|
|
236
236
|
}
|
|
237
237
|
});
|
|
@@ -248,7 +248,7 @@ export const BrowseView = memo(function BrowseView({
|
|
|
248
248
|
cleanupHover();
|
|
249
249
|
referentSub?.unsubscribe(); // in-flight referent load dies with the effect
|
|
250
250
|
};
|
|
251
|
-
}, [content, allAnnotations,
|
|
251
|
+
}, [content, allAnnotations, sparkleAnnotationIds, hoverDelayMs, session, onReferenceHover]);
|
|
252
252
|
|
|
253
253
|
// Helper to scroll annotation into view with pulse effect
|
|
254
254
|
const scrollToAnnotation = useCallback((annotationId: string | null, removePulse = false) => {
|
|
@@ -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';
|
|
@@ -50,7 +50,7 @@ interface Props {
|
|
|
50
50
|
/** Inline display variant: auto-height, no inner scroll, no pane chrome (browse path). Default: fill-the-pane. */
|
|
51
51
|
inline?: boolean;
|
|
52
52
|
/** Recently-created annotation ids to sparkle (threaded to the browse/annotate subtree). */
|
|
53
|
-
|
|
53
|
+
sparkleAnnotationIds?: Set<string>;
|
|
54
54
|
generatingReferenceId?: string | null;
|
|
55
55
|
showLineNumbers?: boolean;
|
|
56
56
|
hoverDelayMs?: number;
|
|
@@ -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,
|
|
@@ -113,7 +116,7 @@ export function ResourceViewer({
|
|
|
113
116
|
onLinkClick,
|
|
114
117
|
onReferenceHover,
|
|
115
118
|
inline = false,
|
|
116
|
-
|
|
119
|
+
sparkleAnnotationIds,
|
|
117
120
|
generatingReferenceId,
|
|
118
121
|
showLineNumbers = false,
|
|
119
122
|
hoverDelayMs,
|
|
@@ -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).
|
|
@@ -357,7 +356,7 @@ export function ResourceViewer({
|
|
|
357
356
|
onModeChange={changeAnnotateMode}
|
|
358
357
|
showToolbar={showToolbar}
|
|
359
358
|
session={session}
|
|
360
|
-
|
|
359
|
+
sparkleAnnotationIds={sparkleAnnotationIds}
|
|
361
360
|
{...(annotateRenderers && { renderers: annotateRenderers })}
|
|
362
361
|
/>
|
|
363
362
|
) : (
|
|
@@ -373,7 +372,7 @@ export function ResourceViewer({
|
|
|
373
372
|
onClickActionChange={changeClickAction}
|
|
374
373
|
showToolbar={showToolbar}
|
|
375
374
|
session={session}
|
|
376
|
-
|
|
375
|
+
sparkleAnnotationIds={sparkleAnnotationIds}
|
|
377
376
|
onLinkClick={onLinkClick}
|
|
378
377
|
onReferenceHover={onReferenceHover}
|
|
379
378
|
inline={inline}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EMBEDDABLE-RESOURCE-VIEWER step 1b — AnnotateView + AnnotateToolbar provider-free.
|
|
3
3
|
*
|
|
4
|
-
* AnnotateView takes `session` + `
|
|
4
|
+
* AnnotateView takes `session` + `sparkleAnnotationIds` as props; the REAL
|
|
5
5
|
* AnnotateToolbar (not mocked — its decoupling is the crux of 1b) takes `session`
|
|
6
6
|
* as a prop. CodeMirrorRenderer is mocked (heavy; already prop-based).
|
|
7
7
|
*
|
|
@@ -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),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EMBEDDABLE-RESOURCE-VIEWER step 1a — BrowseView renders provider-free.
|
|
3
3
|
*
|
|
4
|
-
* BrowseView takes its `session` + `
|
|
4
|
+
* BrowseView takes its `session` + `sparkleAnnotationIds` as props (not
|
|
5
5
|
* `useSemiont()` / `useResourceAnnotations()`), and subscribes to session-scoped
|
|
6
6
|
* beckon events via `session.subscribe`. `AnnotateToolbar` is mocked here — its
|
|
7
7
|
* own provider decoupling is step 1b — so this spec isolates BrowseView's body.
|
|
@@ -8,7 +8,7 @@ import { createTestSemiontWrapper } from '../../../test-utils';
|
|
|
8
8
|
|
|
9
9
|
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
10
10
|
|
|
11
|
-
// BrowseView takes its `session` + `
|
|
11
|
+
// BrowseView takes its `session` + `sparkleAnnotationIds` as props now (step 1a) — no
|
|
12
12
|
// ResourceAnnotationsContext / SemiontProvider reach-in. `makeSession` (below)
|
|
13
13
|
// wraps the fake client so browse:click / beckon:hover land on the bus the
|
|
14
14
|
// trackers listen on, and session.subscribe registers beckon:* on that bus.
|
|
@@ -125,14 +125,14 @@ function makeSession(client: SemiontClient): SemiontSession {
|
|
|
125
125
|
|
|
126
126
|
const renderWithProviders = (
|
|
127
127
|
component: React.ReactElement<React.ComponentProps<typeof BrowseView>>,
|
|
128
|
-
options: {
|
|
128
|
+
options: { sparkleAnnotationIds?: Set<string> } = {}
|
|
129
129
|
) => {
|
|
130
130
|
const { SemiontWrapper, client } = createTestSemiontWrapper();
|
|
131
131
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
132
132
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
133
133
|
);
|
|
134
134
|
return render(
|
|
135
|
-
React.cloneElement(component, { session: makeSession(client),
|
|
135
|
+
React.cloneElement(component, { session: makeSession(client), sparkleAnnotationIds: options.sparkleAnnotationIds }),
|
|
136
136
|
{ wrapper: Wrapper },
|
|
137
137
|
);
|
|
138
138
|
};
|
|
@@ -140,7 +140,7 @@ const renderWithProviders = (
|
|
|
140
140
|
const renderWithEventTracking = (
|
|
141
141
|
component: React.ReactElement<React.ComponentProps<typeof BrowseView>>,
|
|
142
142
|
tracker: ReturnType<typeof createEventTracker>,
|
|
143
|
-
options: {
|
|
143
|
+
options: { sparkleAnnotationIds?: Set<string> } = {}
|
|
144
144
|
) => {
|
|
145
145
|
const { SemiontWrapper, eventBus, client } = createTestSemiontWrapper();
|
|
146
146
|
tracker._attach(eventBus);
|
|
@@ -148,7 +148,7 @@ const renderWithEventTracking = (
|
|
|
148
148
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
149
149
|
);
|
|
150
150
|
return render(
|
|
151
|
-
React.cloneElement(component, { session: makeSession(client),
|
|
151
|
+
React.cloneElement(component, { session: makeSession(client), sparkleAnnotationIds: options.sparkleAnnotationIds }),
|
|
152
152
|
{ wrapper: Wrapper },
|
|
153
153
|
);
|
|
154
154
|
};
|
|
@@ -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
|
}
|
|
@@ -546,7 +545,7 @@ describe('BrowseView Component', () => {
|
|
|
546
545
|
|
|
547
546
|
describe('Annotation Animation Classes', () => {
|
|
548
547
|
it('should apply sparkle class to new annotations', () => {
|
|
549
|
-
const
|
|
548
|
+
const sparkleAnnotationIds = new Set(['new-annotation-1']);
|
|
550
549
|
|
|
551
550
|
const annotations = {
|
|
552
551
|
...defaultProps.annotations,
|
|
@@ -554,12 +553,12 @@ describe('BrowseView Component', () => {
|
|
|
554
553
|
};
|
|
555
554
|
|
|
556
555
|
renderWithProviders(<BrowseView {...defaultProps} annotations={annotations} />, {
|
|
557
|
-
|
|
556
|
+
sparkleAnnotationIds
|
|
558
557
|
});
|
|
559
558
|
|
|
560
|
-
// Verify the
|
|
559
|
+
// Verify the sparkleAnnotationIds set contains the expected annotation
|
|
561
560
|
// In the actual component, this triggers the sparkle class application
|
|
562
|
-
expect(
|
|
561
|
+
expect(sparkleAnnotationIds.has('new-annotation-1')).toBe(true);
|
|
563
562
|
});
|
|
564
563
|
});
|
|
565
564
|
|
|
@@ -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
|
});
|