@semiont/react-ui 0.5.9 → 0.5.11

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.
Files changed (209) hide show
  1. package/README.md +1 -1
  2. package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js → PdfAnnotationCanvas.client-KWQ7XDWT.js} +5 -2
  3. package/dist/PdfAnnotationCanvas.client-KWQ7XDWT.js.map +1 -0
  4. package/dist/{ar-WA47UUWA.js → ar-ZHKO4XDR.js} +45 -4
  5. package/dist/ar-ZHKO4XDR.js.map +1 -0
  6. package/dist/{bn-5ANDRIU6.js → bn-3BUDKAK5.js} +45 -4
  7. package/dist/bn-3BUDKAK5.js.map +1 -0
  8. package/dist/{chunk-YBGN4ACY.js → chunk-O23TLLXW.js} +1 -6
  9. package/dist/{chunk-WHUGODTB.js → chunk-PFUXQPWJ.js} +127 -127
  10. package/dist/{chunk-WHUGODTB.js.map → chunk-PFUXQPWJ.js.map} +1 -1
  11. package/dist/{chunk-GERSK2DY.js → chunk-R52YAGHK.js} +42 -1
  12. package/dist/chunk-R52YAGHK.js.map +1 -0
  13. package/dist/{cs-3RU7F4JX.js → cs-2EFEUKTW.js} +45 -4
  14. package/dist/cs-2EFEUKTW.js.map +1 -0
  15. package/dist/{da-GSW5P5HG.js → da-SUD3Q44Q.js} +45 -4
  16. package/dist/da-SUD3Q44Q.js.map +1 -0
  17. package/dist/{de-JUAUYF4Z.js → de-6IC5QRX7.js} +45 -4
  18. package/dist/de-6IC5QRX7.js.map +1 -0
  19. package/dist/{el-JNLWCKEC.js → el-Q4NAXFRC.js} +45 -4
  20. package/dist/el-Q4NAXFRC.js.map +1 -0
  21. package/dist/{en-RBN2GUHF.js → en-P2L4X27N.js} +5 -3
  22. package/dist/{es-WPOX225H.js → es-YUKZBD4W.js} +45 -4
  23. package/dist/es-YUKZBD4W.js.map +1 -0
  24. package/dist/{fa-TAEP6N77.js → fa-OJ47LILE.js} +45 -4
  25. package/dist/fa-OJ47LILE.js.map +1 -0
  26. package/dist/{fi-ZVZHANSP.js → fi-QFFL5CUB.js} +45 -4
  27. package/dist/fi-QFFL5CUB.js.map +1 -0
  28. package/dist/{fr-VLZW7M4N.js → fr-BOQZZWP4.js} +45 -4
  29. package/dist/fr-BOQZZWP4.js.map +1 -0
  30. package/dist/{he-QFAFYA77.js → he-LXDY3TFA.js} +45 -4
  31. package/dist/he-LXDY3TFA.js.map +1 -0
  32. package/dist/{hi-AO3DQPCV.js → hi-HOJER77V.js} +45 -4
  33. package/dist/hi-HOJER77V.js.map +1 -0
  34. package/dist/{id-GTXF42WM.js → id-I5WQ6EWD.js} +45 -4
  35. package/dist/id-I5WQ6EWD.js.map +1 -0
  36. package/dist/index.css +150 -112
  37. package/dist/index.css.map +1 -1
  38. package/dist/index.d.ts +403 -45
  39. package/dist/index.js +4660 -18860
  40. package/dist/index.js.map +1 -1
  41. package/dist/integrations/css-modules-helper.js +1 -1
  42. package/dist/integrations/styled-components-theme.js +1 -1
  43. package/dist/{it-AS5GM232.js → it-UYH642IJ.js} +45 -4
  44. package/dist/it-UYH642IJ.js.map +1 -0
  45. package/dist/{ja-GZ4HLUOF.js → ja-JTVWAC4L.js} +45 -4
  46. package/dist/ja-JTVWAC4L.js.map +1 -0
  47. package/dist/{ko-A4XUXFGJ.js → ko-IHXFMOIZ.js} +45 -4
  48. package/dist/ko-IHXFMOIZ.js.map +1 -0
  49. package/dist/{ms-YBAO3S6K.js → ms-JH25VFLO.js} +45 -4
  50. package/dist/ms-JH25VFLO.js.map +1 -0
  51. package/dist/{nl-3TJGIIIU.js → nl-SURXW7IQ.js} +45 -4
  52. package/dist/nl-SURXW7IQ.js.map +1 -0
  53. package/dist/{no-4AXIQPRQ.js → no-HXIKQFDZ.js} +45 -4
  54. package/dist/no-HXIKQFDZ.js.map +1 -0
  55. package/dist/{pl-5GP6GKXO.js → pl-7A4KU6XK.js} +45 -4
  56. package/dist/pl-7A4KU6XK.js.map +1 -0
  57. package/dist/{pt-26Y6JFG5.js → pt-6MGYP7Q5.js} +45 -4
  58. package/dist/pt-6MGYP7Q5.js.map +1 -0
  59. package/dist/{ro-C7UXFRWJ.js → ro-IROCHNIA.js} +45 -4
  60. package/dist/ro-IROCHNIA.js.map +1 -0
  61. package/dist/{sv-44DVD76T.js → sv-WDJD75CV.js} +45 -4
  62. package/dist/sv-WDJD75CV.js.map +1 -0
  63. package/dist/test-utils.js +9 -4
  64. package/dist/test-utils.js.map +1 -1
  65. package/dist/{th-GIQRTBOY.js → th-RIO2E6B3.js} +45 -4
  66. package/dist/th-RIO2E6B3.js.map +1 -0
  67. package/dist/{tr-WMQWO4D6.js → tr-TZ7ERCIN.js} +45 -4
  68. package/dist/tr-TZ7ERCIN.js.map +1 -0
  69. package/dist/{uk-I7ML6RGG.js → uk-ZFWDKLUE.js} +45 -4
  70. package/dist/uk-ZFWDKLUE.js.map +1 -0
  71. package/dist/{vi-FGWECASQ.js → vi-UIOSP2JG.js} +45 -4
  72. package/dist/vi-UIOSP2JG.js.map +1 -0
  73. package/dist/{zh-L5FN73XC.js → zh-6R6YUY46.js} +45 -4
  74. package/dist/zh-6R6YUY46.js.map +1 -0
  75. package/package.json +18 -9
  76. package/src/components/AnnotateReferencesProgressWidget.tsx +35 -16
  77. package/src/components/ResourceTagsInline.tsx +94 -11
  78. package/src/components/StatusDisplay.css +36 -0
  79. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +41 -91
  80. package/src/components/__tests__/ResourceTagsInline.test.tsx +101 -3
  81. package/src/components/annotation/AnnotateToolbar.tsx +53 -28
  82. package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +76 -0
  83. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +124 -360
  84. package/src/components/image-annotation/SvgDrawingCanvas.tsx +5 -1
  85. package/src/components/modals/ConfigureGatherStep.tsx +102 -0
  86. package/src/components/modals/ContextSummary.tsx +15 -6
  87. package/src/components/modals/GatherContextStep.tsx +104 -59
  88. package/src/components/modals/ResourceGenerateModal.tsx +258 -0
  89. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +82 -0
  90. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +107 -0
  91. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +197 -0
  92. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +5 -0
  93. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +6 -6
  94. package/src/components/resource/AnnotateView.tsx +59 -38
  95. package/src/components/resource/BrowseView.tsx +159 -133
  96. package/src/components/resource/ResourceViewer.tsx +119 -128
  97. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +55 -0
  98. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +49 -0
  99. package/src/components/resource/__tests__/BrowseView.links.test.tsx +60 -0
  100. package/src/components/resource/__tests__/BrowseView.overlay-async.test.tsx +113 -0
  101. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +161 -0
  102. package/src/components/resource/__tests__/BrowseView.registry.test.tsx +43 -0
  103. package/src/components/resource/__tests__/BrowseView.test.tsx +35 -22
  104. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +63 -0
  105. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +12 -7
  106. package/src/components/resource/__tests__/ResourceViewer.toolbar-opt-out.test.tsx +135 -0
  107. package/src/components/resource/__tests__/ResourceViewer.toolbar-prefs.test.tsx +200 -0
  108. package/src/components/resource/__tests__/browse-renderers.test.tsx +34 -0
  109. package/src/components/resource/__tests__/inline-embedding.test.tsx +81 -0
  110. package/src/components/resource/__tests__/media-completeness.test.tsx +88 -0
  111. package/src/components/resource/browse-renderers.tsx +63 -0
  112. package/src/components/resource/panels/AssessmentPanel.tsx +4 -0
  113. package/src/components/resource/panels/CommentsPanel.tsx +4 -0
  114. package/src/components/resource/panels/HighlightPanel.tsx +4 -0
  115. package/src/components/resource/panels/ReferencesPanel.tsx +14 -0
  116. package/src/components/resource/panels/ResourceInfoPanel.tsx +22 -0
  117. package/src/components/resource/panels/TaggingPanel.tsx +4 -0
  118. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +5 -2
  119. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +1 -0
  120. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +1 -0
  121. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +16 -16
  122. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  123. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +1 -0
  124. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +26 -0
  125. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -0
  126. package/src/components/toolbar/Toolbar.css +14 -0
  127. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +14 -0
  128. package/src/features/admin-exchange/state/exchange-state-unit.ts +4 -2
  129. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +14 -0
  130. package/src/features/admin-security/state/admin-security-state-unit.ts +4 -2
  131. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +12 -5
  132. package/src/features/admin-users/state/admin-users-state-unit.ts +4 -2
  133. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +11 -0
  134. package/src/features/auth-welcome/state/welcome-state-unit.ts +1 -1
  135. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +14 -0
  136. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +4 -2
  137. package/src/features/resource-compose/components/ResourceComposePage.tsx +13 -7
  138. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +13 -0
  139. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -2
  140. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +15 -9
  141. package/src/features/resource-discovery/state/discover-state-unit.ts +5 -2
  142. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +7 -17
  143. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +119 -17
  144. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +11 -0
  145. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +30 -0
  146. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +1 -1
  147. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +7 -16
  148. package/src/styles/features/resource-viewer.css +19 -0
  149. package/translations/ar.json +41 -2
  150. package/translations/bn.json +41 -2
  151. package/translations/cs.json +41 -2
  152. package/translations/da.json +41 -2
  153. package/translations/de.json +41 -2
  154. package/translations/el.json +41 -2
  155. package/translations/en.json +39 -0
  156. package/translations/es.json +41 -2
  157. package/translations/fa.json +41 -2
  158. package/translations/fi.json +41 -2
  159. package/translations/fr.json +41 -2
  160. package/translations/he.json +41 -2
  161. package/translations/hi.json +41 -2
  162. package/translations/id.json +41 -2
  163. package/translations/it.json +41 -2
  164. package/translations/ja.json +41 -2
  165. package/translations/ko.json +41 -2
  166. package/translations/ms.json +41 -2
  167. package/translations/nl.json +41 -2
  168. package/translations/no.json +41 -2
  169. package/translations/pl.json +41 -2
  170. package/translations/pt.json +41 -2
  171. package/translations/ro.json +41 -2
  172. package/translations/sv.json +41 -2
  173. package/translations/th.json +41 -2
  174. package/translations/tr.json +41 -2
  175. package/translations/uk.json +41 -2
  176. package/translations/vi.json +41 -2
  177. package/translations/zh.json +41 -2
  178. package/dist/PdfAnnotationCanvas.client-U4EVBZEV.js.map +0 -1
  179. package/dist/ar-WA47UUWA.js.map +0 -1
  180. package/dist/bn-5ANDRIU6.js.map +0 -1
  181. package/dist/chunk-GERSK2DY.js.map +0 -1
  182. package/dist/cs-3RU7F4JX.js.map +0 -1
  183. package/dist/da-GSW5P5HG.js.map +0 -1
  184. package/dist/de-JUAUYF4Z.js.map +0 -1
  185. package/dist/el-JNLWCKEC.js.map +0 -1
  186. package/dist/es-WPOX225H.js.map +0 -1
  187. package/dist/fa-TAEP6N77.js.map +0 -1
  188. package/dist/fi-ZVZHANSP.js.map +0 -1
  189. package/dist/fr-VLZW7M4N.js.map +0 -1
  190. package/dist/he-QFAFYA77.js.map +0 -1
  191. package/dist/hi-AO3DQPCV.js.map +0 -1
  192. package/dist/id-GTXF42WM.js.map +0 -1
  193. package/dist/it-AS5GM232.js.map +0 -1
  194. package/dist/ja-GZ4HLUOF.js.map +0 -1
  195. package/dist/ko-A4XUXFGJ.js.map +0 -1
  196. package/dist/ms-YBAO3S6K.js.map +0 -1
  197. package/dist/nl-3TJGIIIU.js.map +0 -1
  198. package/dist/no-4AXIQPRQ.js.map +0 -1
  199. package/dist/pl-5GP6GKXO.js.map +0 -1
  200. package/dist/pt-26Y6JFG5.js.map +0 -1
  201. package/dist/ro-C7UXFRWJ.js.map +0 -1
  202. package/dist/sv-44DVD76T.js.map +0 -1
  203. package/dist/th-GIQRTBOY.js.map +0 -1
  204. package/dist/tr-WMQWO4D6.js.map +0 -1
  205. package/dist/uk-I7ML6RGG.js.map +0 -1
  206. package/dist/vi-FGWECASQ.js.map +0 -1
  207. package/dist/zh-L5FN73XC.js.map +0 -1
  208. /package/dist/{chunk-YBGN4ACY.js.map → chunk-O23TLLXW.js.map} +0 -0
  209. /package/dist/{en-RBN2GUHF.js.map → en-P2L4X27N.js.map} +0 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * TOOLBAR-PREFS-AS-PROPS — keystone + per-pref specs.
3
+ *
4
+ * Preferences are state, not events: each of the four toolbar prefs (mode, click
5
+ * action, selection motivation, shape) is a controlled/uncontrolled prop pair.
6
+ * Controlled instances render the given value and report intents via the callback —
7
+ * never self-mutate, never touch localStorage, never hear other instances.
8
+ * Uncontrolled instances hold a plain internal default (false / 'detail' /
9
+ * 'linking' / 'rectangle') — NOT the legacy localStorage+bus behavior (that lives
10
+ * in the useToolbarPrefs() policy layer).
11
+ *
12
+ * RED ledger: authored `it.fails` (observed: 5 expected fail), flipped to `it` at
13
+ * Phase 1 GREEN. Provider-free; real AnnotateToolbar (its controls are the subject).
14
+ */
15
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
16
+ import { render, fireEvent, within, waitFor } from '@testing-library/react';
17
+ import '@testing-library/jest-dom';
18
+ import type { SemiontSession } from '@semiont/sdk';
19
+ import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
20
+ import { ResourceViewer } from '../ResourceViewer';
21
+
22
+ vi.mock('../../CodeMirrorRenderer', () => ({ CodeMirrorRenderer: () => <div>cm-mock</div> }));
23
+ vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({ SvgDrawingCanvas: () => <div>svg-mock</div> }));
24
+ vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
25
+ vi.mock('remark-gfm', () => ({ default: () => ({}) }));
26
+
27
+ function makeResource(mediaType = 'text/plain'): SemiontResource & { content: string } {
28
+ return {
29
+ '@context': 'https://www.w3.org/ns/activitystreams',
30
+ '@id': 'res-1' as ResourceId,
31
+ name: 'Doc',
32
+ created: '2024-01-01T00:00:00Z',
33
+ entityTypes: [],
34
+ archived: false,
35
+ representations: [{ mediaType, byteSize: 10 }],
36
+ content: 'Prefs content.',
37
+ };
38
+ }
39
+
40
+ const annotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
41
+
42
+ /** Session double with a live subscribe registry so specs can fire legacy broadcasts. */
43
+ function liveSession() {
44
+ const handlers = new Map<string, Set<(p: unknown) => void>>();
45
+ const client = {
46
+ browse: { click: vi.fn(), invalidateAnnotationList: vi.fn() },
47
+ beckon: { hover: vi.fn() },
48
+ mark: {
49
+ toggleMode: vi.fn(), delete: vi.fn(), request: vi.fn(),
50
+ changeSelection: vi.fn(), changeClick: vi.fn(), changeShape: vi.fn(),
51
+ },
52
+ };
53
+ const session = {
54
+ client,
55
+ subscribe: (channel: string, handler: (p: unknown) => void) => {
56
+ if (!handlers.has(channel)) handlers.set(channel, new Set());
57
+ handlers.get(channel)!.add(handler);
58
+ return () => { handlers.get(channel)!.delete(handler); };
59
+ },
60
+ } as unknown as SemiontSession;
61
+ const fire = (channel: string, payload?: unknown) =>
62
+ handlers.get(channel)?.forEach((h) => h(payload));
63
+ return { session, client, fire };
64
+ }
65
+
66
+ const isAnnotate = (c: HTMLElement) => !!c.querySelector('.semiont-annotate-view');
67
+ const isBrowse = (c: HTMLElement) => !!c.querySelector('.semiont-browse-view');
68
+
69
+ /** Open a bar dropdown group by aria-label and click an option in its menu. */
70
+ async function pick(container: HTMLElement, group: string, option: string) {
71
+ const trigger = within(container).getByLabelText(group);
72
+ fireEvent.mouseEnter(trigger.parentElement!);
73
+ await waitFor(() => {
74
+ const menu = within(container).getByRole('menu');
75
+ fireEvent.click(within(menu).getByText(option));
76
+ });
77
+ }
78
+
79
+ const PREF_KEYS = ['annotateMode', 'semiont-toolbar-click', 'semiont-toolbar-selection'];
80
+ let getSpy: ReturnType<typeof vi.spyOn>;
81
+ let setSpy: ReturnType<typeof vi.spyOn>;
82
+ const prefKeyTouched = (spy: ReturnType<typeof vi.spyOn>) =>
83
+ spy.mock.calls.some((call: unknown[]) => {
84
+ const key = String(call[0]);
85
+ return PREF_KEYS.includes(key) || key.includes('shape');
86
+ });
87
+
88
+ describe('ResourceViewer — toolbar prefs as props', () => {
89
+ beforeEach(() => {
90
+ vi.clearAllMocks();
91
+ localStorage.clear();
92
+ getSpy = vi.spyOn(Storage.prototype, 'getItem');
93
+ setSpy = vi.spyOn(Storage.prototype, 'setItem');
94
+ });
95
+ afterEach(() => {
96
+ getSpy.mockRestore();
97
+ setSpy.mockRestore();
98
+ });
99
+
100
+ // ── Keystone: per-instance isolation across three viewers on one session ──
101
+ it('keystone: three viewers — controlled A/B independent, C uncontrolled; B\'s Mode click fires only B\'s callback', async () => {
102
+ const { session, client } = liveSession();
103
+ const onB = vi.fn();
104
+ const a = render(
105
+ <ResourceViewer session={session} resource={makeResource()} annotations={annotations}
106
+ annotateMode={true} clickAction="follow" onAnnotateModeChange={vi.fn()} onClickActionChange={vi.fn()} />,
107
+ );
108
+ const b = render(
109
+ <ResourceViewer session={session} resource={makeResource()} annotations={annotations}
110
+ annotateMode={false} onAnnotateModeChange={onB} />,
111
+ );
112
+ const c = render(
113
+ <ResourceViewer session={session} resource={makeResource()} annotations={annotations} />,
114
+ );
115
+
116
+ expect(isAnnotate(a.container)).toBe(true); // A renders its controlled mode
117
+ expect(isBrowse(b.container)).toBe(true);
118
+ expect(isBrowse(c.container)).toBe(true); // C: plain default (false)
119
+
120
+ await pick(b.container, 'Mode', 'Annotate');
121
+
122
+ expect(onB).toHaveBeenCalledWith(true); // B reports intent…
123
+ expect(isBrowse(b.container)).toBe(true); // …and does not self-flip
124
+ expect(isAnnotate(a.container)).toBe(true); // A unchanged
125
+ expect(isBrowse(c.container)).toBe(true); // C unchanged
126
+ expect(client.mark.toggleMode).not.toHaveBeenCalled(); // no global emit
127
+ expect(prefKeyTouched(getSpy)).toBe(false); // no localStorage traffic
128
+ expect(prefKeyTouched(setSpy)).toBe(false);
129
+ });
130
+
131
+ // ── Per-pref: mode ──
132
+ it('mode: uncontrolled uses the plain default and is inert to the legacy broadcast', () => {
133
+ localStorage.setItem('annotateMode', 'true'); // legacy key must be IGNORED
134
+ getSpy.mockClear(); setSpy.mockClear();
135
+ const { session, fire } = liveSession();
136
+ const u = render(
137
+ <ResourceViewer session={session} resource={makeResource()} annotations={annotations} />,
138
+ );
139
+ expect(isBrowse(u.container)).toBe(true); // plain default, not the stored 'true'
140
+ fire('mark:mode-toggled');
141
+ expect(isBrowse(u.container)).toBe(true); // inert: preference events are gone
142
+ expect(prefKeyTouched(getSpy)).toBe(false);
143
+ expect(prefKeyTouched(setSpy)).toBe(false);
144
+ });
145
+
146
+ // ── Per-pref: click action ──
147
+ it('clickAction: controlled renders the value; the bar control reports and does not mutate', async () => {
148
+ const { session, client } = liveSession();
149
+ const onChange = vi.fn();
150
+ const v = render(
151
+ <ResourceViewer session={session} resource={makeResource()} annotations={annotations}
152
+ clickAction="follow" onClickActionChange={onChange} />,
153
+ );
154
+ const bar = v.container.querySelector('.semiont-annotate-toolbar') as HTMLElement;
155
+ expect(within(bar).getByText('Follow')).toBeInTheDocument(); // renders the prop value
156
+
157
+ await pick(v.container, 'Click', 'Detail');
158
+ expect(onChange).toHaveBeenCalledWith('detail'); // reports intent
159
+ expect(within(bar).getByText('Follow')).toBeInTheDocument(); // does not self-mutate
160
+ expect(client.mark.changeClick).not.toHaveBeenCalled();
161
+ });
162
+
163
+ // ── Per-pref: selection motivation (annotate-mode bar) ──
164
+ it('selectionMotivation: controlled renders the value; picking reports; re-picking the current reports null', async () => {
165
+ const { session, client } = liveSession();
166
+ const onChange = vi.fn();
167
+ const v = render(
168
+ <ResourceViewer session={session} resource={makeResource()} annotations={annotations}
169
+ annotateMode={true} onAnnotateModeChange={vi.fn()}
170
+ selectionMotivation="highlighting" onSelectionMotivationChange={onChange} />,
171
+ );
172
+ const bar = v.container.querySelector('.semiont-annotate-toolbar') as HTMLElement;
173
+ expect(within(bar).getByText('Highlight')).toBeInTheDocument(); // renders the prop value
174
+
175
+ await pick(v.container, 'Motivation', 'Comment');
176
+ expect(onChange).toHaveBeenCalledWith('commenting');
177
+
178
+ await pick(v.container, 'Motivation', 'Highlight'); // current value → toggles off
179
+ expect(onChange).toHaveBeenCalledWith(null);
180
+ expect(client.mark.changeSelection).not.toHaveBeenCalled();
181
+ });
182
+
183
+ // ── Per-pref: shape (media-gated; image annotate bar) ──
184
+ it('shape: controlled renders the value; picking reports and does not mutate', async () => {
185
+ const { session, client } = liveSession();
186
+ const onChange = vi.fn();
187
+ const v = render(
188
+ <ResourceViewer session={session} resource={makeResource('image/png')} annotations={annotations}
189
+ annotateMode={true} onAnnotateModeChange={vi.fn()}
190
+ shape="circle" onShapeChange={onChange} />,
191
+ );
192
+ const bar = v.container.querySelector('.semiont-annotate-toolbar') as HTMLElement;
193
+ expect(within(bar).getByText('Circle')).toBeInTheDocument(); // renders the prop value
194
+
195
+ await pick(v.container, 'Shape', 'Polygon');
196
+ expect(onChange).toHaveBeenCalledWith('polygon');
197
+ expect(within(bar).getByText('Circle')).toBeInTheDocument(); // does not self-mutate
198
+ expect(client.mark.changeShape).not.toHaveBeenCalled();
199
+ });
200
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * EMBEDDABLE-VIEWER-COMPLETION Phase 0 — regression: the browse `text` default
3
+ * renders markdown as **formatted prose**, not raw source.
4
+ *
5
+ * The other BrowseView suites mock `react-markdown` away for simplicity, so
6
+ * nothing currently pins that the default text renderer actually *formats*
7
+ * markdown. This spec uses the real renderer and guards against a regression to
8
+ * raw/source rendering.
9
+ */
10
+ import { describe, it, expect } from 'vitest';
11
+ import { render } from '@testing-library/react';
12
+ import '@testing-library/jest-dom';
13
+ import { TextBrowseRenderer, defaultBrowseRenderers } from '../browse-renderers';
14
+
15
+ const base = { mimeType: 'text/markdown', resourceUri: 'res-1', annotations: [] };
16
+
17
+ describe('browse-renderers — markdown-as-prose (Phase 0 regression)', () => {
18
+ it('TextBrowseRenderer is the default `text` renderer', () => {
19
+ expect(defaultBrowseRenderers.text).toBe(TextBrowseRenderer);
20
+ });
21
+
22
+ it('renders markdown as formatted HTML, not raw source', () => {
23
+ const { container } = render(
24
+ <TextBrowseRenderer content={'# Big Heading\n\nsome **bold** and a [link](https://x.test).'} {...base} />,
25
+ );
26
+ // Formatted: heading / strong / anchor elements exist (react-markdown output).
27
+ expect(container.querySelector('h1')?.textContent).toBe('Big Heading');
28
+ expect(container.querySelector('strong')?.textContent).toBe('bold');
29
+ expect(container.querySelector('a')?.getAttribute('href')).toBe('https://x.test');
30
+ // NOT raw: the literal markdown syntax must not survive to the text content.
31
+ expect(container.textContent).not.toContain('# Big Heading');
32
+ expect(container.textContent).not.toContain('**bold**');
33
+ });
34
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * EMBEDDABLE-VIEWER-COMPLETION Phase 1 — inline embedding.
3
+ *
4
+ * `inline` is a display variant (default: today's pane behavior, frontend
5
+ * untouched): the viewer renders at content height in a bare container — no
6
+ * inner scroll container, no pane chrome. jsdom can't compute stylesheet
7
+ * layout, so this spec pins the two testable seams: (1) the components emit
8
+ * the `--inline` modifier classes; (2) the stylesheet contains the inline
9
+ * overrides (height:auto / overflow:visible / padding drop). The visual
10
+ * auto-height check is the plan's live smoke-test.
11
+ *
12
+ * Started RED (no `inline` prop) and GREEN once Phase 1 lands.
13
+ */
14
+ import { describe, it, expect, vi } from 'vitest';
15
+ import { render } from '@testing-library/react';
16
+ import '@testing-library/jest-dom';
17
+ import { readFileSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import type { SemiontSession } from '@semiont/sdk';
20
+ import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
21
+ import { BrowseView } from '../BrowseView';
22
+ import { ResourceViewer } from '../ResourceViewer';
23
+
24
+ vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
25
+ vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
26
+ vi.mock('remark-gfm', () => ({ default: () => ({}) }));
27
+
28
+ const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
29
+
30
+ function fakeSession(): SemiontSession {
31
+ return {
32
+ client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
33
+ subscribe: () => () => {},
34
+ } as unknown as SemiontSession;
35
+ }
36
+
37
+ const resource: SemiontResource & { content: string } = {
38
+ '@context': 'https://www.w3.org/ns/activitystreams',
39
+ '@id': 'res-1' as ResourceId,
40
+ name: 'Doc',
41
+ created: '2024-01-01T00:00:00Z',
42
+ entityTypes: [],
43
+ archived: false,
44
+ representations: [{ mediaType: 'text/plain', byteSize: 10 }],
45
+ content: 'Inline content.',
46
+ };
47
+
48
+ describe('inline embedding (Phase 1)', () => {
49
+ it('BrowseView: `inline` adds the modifier class; default does not', () => {
50
+ const props = {
51
+ content: 'x', mimeType: 'text/plain', resourceUri: 'res-1',
52
+ annotations: emptyAnnotations, annotateMode: false, session: fakeSession(),
53
+ };
54
+ const { container: pane } = render(<BrowseView {...props} />);
55
+ expect(pane.querySelector('.semiont-browse-view')).not.toHaveClass('semiont-browse-view--inline');
56
+
57
+ const { container } = render(<BrowseView {...props} inline />);
58
+ expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
59
+ });
60
+
61
+ it('ResourceViewer: `inline` marks its root and threads to BrowseView', () => {
62
+ const { container } = render(
63
+ <ResourceViewer
64
+ resource={resource} annotations={emptyAnnotations}
65
+ session={fakeSession()} inline
66
+ />,
67
+ );
68
+ expect(container.querySelector('.semiont-resource-viewer')).toHaveClass('semiont-resource-viewer--inline');
69
+ expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
70
+ });
71
+
72
+ it('stylesheet carries the inline overrides (static gate — layout itself is the live check)', () => {
73
+ // vitest runs with cwd = the package root (same in CI's per-workspace run).
74
+ const css = readFileSync(resolve(process.cwd(), 'src/styles/features/resource-viewer.css'), 'utf8');
75
+ // Auto-height roots, and the content area stops being a scroll container / drops the pane gutter.
76
+ expect(css).toMatch(/\.semiont-resource-viewer--inline\s*\{[^}]*height:\s*auto/);
77
+ expect(css).toMatch(/\.semiont-browse-view--inline\s*\{[^}]*height:\s*auto/);
78
+ expect(css).toMatch(/\.semiont-browse-view--inline\s+\.semiont-browse-view__content\s*\{[^}]*overflow:\s*visible/);
79
+ expect(css).toMatch(/\.semiont-browse-view--inline\s+\.semiont-browse-view__content\s*\{[^}]*padding:\s*0/);
80
+ });
81
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * EMBEDDABLE-VIEWER-COMPLETION Phase 4 — media-completeness sweep.
3
+ *
4
+ * ONE mounted component (`ResourceViewer`, inline, bare session, no providers)
5
+ * renders every standard media type from the DEFAULTS — text, markdown, image,
6
+ * pdf — with the annotation-overlay container present and zero per-type wiring.
7
+ * (Formatted-markdown correctness is pinned by browse-renderers.test; visual
8
+ * overlay alignment is the plan's live smoke-test. This sweep pins the routing
9
+ * parity: every type reaches its renderer from one component.)
10
+ */
11
+ import { describe, it, expect, vi } from 'vitest';
12
+ import { render, screen } from '@testing-library/react';
13
+ import '@testing-library/jest-dom';
14
+ import type { SemiontSession } from '@semiont/sdk';
15
+ import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
16
+ import { ResourceViewer } from '../ResourceViewer';
17
+
18
+ vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div data-testid="md">{children}</div> }));
19
+ vi.mock('remark-gfm', () => ({ default: () => ({}) }));
20
+ // pdfjs can't load in jsdom — the sweep pins ROUTING to the pdf renderer, not pdf.js itself.
21
+ vi.mock('../../pdf-annotation/PdfAnnotationCanvas.client', () => ({
22
+ PdfAnnotationCanvas: ({ pdfUrl }: { pdfUrl: string }) => <div data-testid="pdf-canvas">{pdfUrl}</div>,
23
+ }));
24
+
25
+ const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
26
+
27
+ function fakeSession(): SemiontSession {
28
+ return {
29
+ client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
30
+ subscribe: () => () => {},
31
+ } as unknown as SemiontSession;
32
+ }
33
+
34
+ function makeResource(mediaType: string, content: string): SemiontResource & { content: string } {
35
+ return {
36
+ '@context': 'https://www.w3.org/ns/activitystreams',
37
+ '@id': 'res-1' as ResourceId,
38
+ name: 'Doc',
39
+ created: '2024-01-01T00:00:00Z',
40
+ entityTypes: [],
41
+ archived: false,
42
+ representations: [{ mediaType, byteSize: 10 }],
43
+ content,
44
+ };
45
+ }
46
+
47
+ function mount(mediaType: string, content: string) {
48
+ return render(
49
+ <ResourceViewer
50
+ resource={makeResource(mediaType, content)}
51
+ annotations={emptyAnnotations}
52
+ session={fakeSession()}
53
+ inline
54
+ />,
55
+ );
56
+ }
57
+
58
+ describe('media-completeness sweep (Phase 4) — one component, all types, inline, defaults only', () => {
59
+ it.each([
60
+ ['text/plain', 'plain body'],
61
+ ['text/markdown', '# md body'],
62
+ ])('%s renders via the text default with the overlay container', (mt, content) => {
63
+ const { container } = mount(mt, content);
64
+ expect(container.querySelector('.semiont-browse-view[data-mime-type="text"]')).toBeInTheDocument();
65
+ expect(screen.getByTestId('md')).toHaveTextContent(content.replace('# ', '# ').trim());
66
+ expect(container.querySelector('.semiont-browse-view__content')).toBeInTheDocument(); // overlay anchor
67
+ expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
68
+ });
69
+
70
+ it('image/png renders via the image default', () => {
71
+ const { container } = mount('image/png', 'https://media.test/img.png?token=t');
72
+ expect(container.querySelector('.semiont-browse-view[data-mime-type="image"]')).toBeInTheDocument();
73
+ expect(container.querySelector('img')).toBeInTheDocument();
74
+ expect(container.querySelector('.semiont-browse-view__content')).toBeInTheDocument();
75
+ });
76
+
77
+ it('application/pdf routes to the pdf default', async () => {
78
+ const { container } = mount('application/pdf', 'https://media.test/doc.pdf?token=t');
79
+ expect(container.querySelector('.semiont-browse-view[data-mime-type="pdf"]')).toBeInTheDocument();
80
+ expect(await screen.findByTestId('pdf-canvas')).toHaveTextContent('https://media.test/doc.pdf?token=t');
81
+ });
82
+
83
+ it('an unknown type degrades to metadata + download (no crash, no per-type wiring)', () => {
84
+ const { container } = mount('application/octet-stream', '');
85
+ expect(container.querySelector('[data-mime-type="unsupported"]')).toBeInTheDocument();
86
+ expect(screen.getByText(/Preview not available/)).toBeInTheDocument();
87
+ });
88
+ });
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { lazy, Suspense, memo, type ComponentType } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import type { Annotation } from '@semiont/core';
7
+ import { ImageViewer } from '../viewers';
8
+
9
+ // Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
10
+ const PdfAnnotationCanvas = lazy(() => import('../pdf-annotation/PdfAnnotationCanvas.client').then(mod => ({ default: mod.PdfAnnotationCanvas })));
11
+
12
+ /**
13
+ * Common props for a read-only ("browse") media renderer. `content` is decoded
14
+ * text for the text renderer, or a media URL for the image / PDF renderers.
15
+ * A consumer can implement this interface to swap in a custom renderer.
16
+ */
17
+ export interface MediaRendererProps {
18
+ content: string;
19
+ mimeType: string;
20
+ resourceUri: string;
21
+ annotations: Annotation[];
22
+ }
23
+
24
+ /** Read-only media dispatch, keyed by the registry render mode. */
25
+ export type BrowseMediaRenderers = Partial<Record<string, ComponentType<MediaRendererProps>>>;
26
+
27
+ const MemoizedMarkdown = memo(function MemoizedMarkdown({ content }: { content: string }) {
28
+ // No annotation plugins: annotations are applied as a DOM overlay after paint.
29
+ return <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>;
30
+ });
31
+
32
+ export function TextBrowseRenderer({ content }: MediaRendererProps) {
33
+ return <MemoizedMarkdown content={content} />;
34
+ }
35
+
36
+ export function ImageBrowseRenderer({ content, mimeType }: MediaRendererProps) {
37
+ return <ImageViewer imageUrl={content} mimeType={mimeType} alt="Resource content" />;
38
+ }
39
+
40
+ export function PdfBrowseRenderer({ content, resourceUri, annotations }: MediaRendererProps) {
41
+ return (
42
+ <Suspense fallback={<div className="semiont-browse-view__loading">Loading PDF viewer...</div>}>
43
+ <PdfAnnotationCanvas
44
+ pdfUrl={content}
45
+ resourceUri={resourceUri}
46
+ existingAnnotations={annotations}
47
+ drawingMode={null}
48
+ selectedMotivation={null}
49
+ />
50
+ </Suspense>
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Default read-only media renderers. `BrowseView` merges a caller's `renderers`
56
+ * override on top of these, so a host can swap one renderer (e.g. its own PDF
57
+ * viewer) or add a mode without forking the view.
58
+ */
59
+ export const defaultBrowseRenderers: Record<'text' | 'image' | 'pdf', ComponentType<MediaRendererProps>> = {
60
+ text: TextBrowseRenderer,
61
+ image: ImageBrowseRenderer,
62
+ pdf: PdfBrowseRenderer,
63
+ };
@@ -40,6 +40,8 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
40
40
  }
41
41
 
42
42
  interface AssessmentPanelProps {
43
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
44
+ resourceId: string;
43
45
  annotations: Annotation[];
44
46
  pendingAnnotation: PendingAnnotation | null;
45
47
  isAssisting?: boolean;
@@ -61,6 +63,7 @@ interface AssessmentPanelProps {
61
63
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
62
64
  */
63
65
  export function AssessmentPanel({
66
+ resourceId,
64
67
  annotations,
65
68
  pendingAnnotation,
66
69
  isAssisting = false,
@@ -151,6 +154,7 @@ export function AssessmentPanel({
151
154
  : undefined;
152
155
 
153
156
  session?.client.mark.submit({
157
+ source: resourceId,
154
158
  motivation: 'assessing',
155
159
  selector: pendingAnnotation.selector,
156
160
  ...(body !== undefined ? { body } : {}),
@@ -40,6 +40,8 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
40
40
  }
41
41
 
42
42
  interface CommentsPanelProps {
43
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
44
+ resourceId: string;
43
45
  annotations: Annotation[];
44
46
  pendingAnnotation: PendingAnnotation | null;
45
47
  annotateMode?: boolean;
@@ -61,6 +63,7 @@ interface CommentsPanelProps {
61
63
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
62
64
  */
63
65
  export function CommentsPanel({
66
+ resourceId,
64
67
  annotations,
65
68
  pendingAnnotation,
66
69
  annotateMode = true,
@@ -171,6 +174,7 @@ export function CommentsPanel({
171
174
  const handleSaveNewComment = () => {
172
175
  if (newCommentText.trim() && pendingAnnotation) {
173
176
  session?.client.mark.submit({
177
+ source: resourceId,
174
178
  motivation: 'commenting',
175
179
  selector: pendingAnnotation.selector,
176
180
  body: [{ type: 'TextualBody', value: newCommentText, purpose: 'commenting' }],
@@ -23,6 +23,8 @@ interface PendingAnnotation {
23
23
  }
24
24
 
25
25
  interface HighlightPanelProps {
26
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
27
+ resourceId: string;
26
28
  annotations: Annotation[];
27
29
  pendingAnnotation: PendingAnnotation | null;
28
30
  isAssisting?: boolean;
@@ -42,6 +44,7 @@ interface HighlightPanelProps {
42
44
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
43
45
  */
44
46
  export function HighlightPanel({
47
+ resourceId,
45
48
  annotations,
46
49
  pendingAnnotation,
47
50
  isAssisting = false,
@@ -136,6 +139,7 @@ export function HighlightPanel({
136
139
  useEffect(() => {
137
140
  if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
138
141
  session?.client.mark.submit({
142
+ source: resourceId,
139
143
  motivation: 'highlighting',
140
144
  selector: pendingAnnotation.selector,
141
145
  });
@@ -43,6 +43,8 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
43
43
  }
44
44
 
45
45
  interface Props {
46
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
47
+ resourceId: string;
46
48
  // Generic panel props
47
49
  annotations?: Annotation[];
48
50
  isAssisting: boolean;
@@ -76,6 +78,7 @@ interface Props {
76
78
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
77
79
  */
78
80
  export function ReferencesPanel({
81
+ resourceId,
79
82
  annotations = [],
80
83
  isAssisting,
81
84
  progress,
@@ -255,6 +258,7 @@ export function ReferencesPanel({
255
258
  if (pendingAnnotation) {
256
259
  const entityType = pendingEntityTypes.join(',') || undefined;
257
260
  session?.client.mark.submit({
261
+ source: resourceId,
258
262
  motivation: 'linking',
259
263
  selector: pendingAnnotation.selector,
260
264
  body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
@@ -455,6 +459,16 @@ export function ReferencesPanel({
455
459
  <AnnotateReferencesProgressWidget
456
460
  progress={progress}
457
461
  annotationType="reference"
462
+ cancelJobType="annotation"
463
+ translations={{
464
+ title: t('annotationProgressTitle'),
465
+ cancel: t('cancelAnnotation'),
466
+ inProgress: t('annotating'),
467
+ complete: t('complete'),
468
+ failed: t('failed'),
469
+ found: (count) => t('found', { count }),
470
+ current: (entityType) => t('current', { entityType }),
471
+ }}
458
472
  />
459
473
  )}
460
474
  </>
@@ -23,6 +23,12 @@ interface Props {
23
23
  wasAttributedTo?: Agent | Agent[] | undefined;
24
24
  wasDerivedFrom?: string | string[] | undefined;
25
25
  generator?: Agent | Agent[] | undefined;
26
+ /**
27
+ * Open the resource-generate flow. UI-only — the SDK isn't involved in
28
+ * *opening* the display (if collaborative gather ever lands, this graduates
29
+ * to a verb). Omit to hide the Generate action.
30
+ */
31
+ onGenerate?: () => void;
26
32
  }
27
33
 
28
34
  /**
@@ -45,6 +51,7 @@ export function ResourceInfoPanel({
45
51
  wasAttributedTo,
46
52
  wasDerivedFrom,
47
53
  generator,
54
+ onGenerate,
48
55
  }: Props) {
49
56
  const t = useTranslations('ResourceInfoPanel');
50
57
  const session = useObservable(useSemiont().activeSession$);
@@ -177,6 +184,21 @@ export function ResourceInfoPanel({
177
184
  </div>
178
185
  )}
179
186
 
187
+ {/* Generate Action — opens the resource-generate flow (UI-only) */}
188
+ {onGenerate && (
189
+ <div className="semiont-resource-info-panel__action-section">
190
+ <button
191
+ onClick={onGenerate}
192
+ className="semiont-resource-button semiont-resource-button--secondary"
193
+ >
194
+ ✨ {t('generate')}
195
+ </button>
196
+ <p className="semiont-resource-info-panel__description">
197
+ {t('generateDescription')}
198
+ </p>
199
+ </div>
200
+ )}
201
+
180
202
  {/* Clone Action */}
181
203
  <div className="semiont-resource-info-panel__action-section">
182
204
  <button
@@ -39,6 +39,8 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
39
39
  }
40
40
 
41
41
  interface TaggingPanelProps {
42
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
43
+ resourceId: string;
42
44
  annotations: Annotation[];
43
45
  annotateMode?: boolean;
44
46
  isAssisting?: boolean;
@@ -62,6 +64,7 @@ interface TaggingPanelProps {
62
64
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
63
65
  */
64
66
  export function TaggingPanel({
67
+ resourceId,
65
68
  annotations,
66
69
  annotateMode = true,
67
70
  isAssisting = false,
@@ -309,6 +312,7 @@ export function TaggingPanel({
309
312
  onChange={(e) => {
310
313
  if (e.target.value && pendingAnnotation) {
311
314
  session?.client.mark.submit({
315
+ source: resourceId,
312
316
  motivation: 'tagging',
313
317
  selector: pendingAnnotation.selector,
314
318
  body: [