@semiont/react-ui 0.2.46 → 0.3.1

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 (215) hide show
  1. package/dist/{PdfAnnotationCanvas.client-COQREPXU.mjs → PdfAnnotationCanvas.client-PVTVPDBQ.mjs} +3 -4
  2. package/dist/PdfAnnotationCanvas.client-PVTVPDBQ.mjs.map +1 -0
  3. package/dist/{ar-7SUXNE34.mjs → ar-5REA6P4J.mjs} +48 -6
  4. package/dist/ar-5REA6P4J.mjs.map +1 -0
  5. package/dist/{bn-XOET3DOI.mjs → bn-YHRYHHPD.mjs} +48 -6
  6. package/dist/bn-YHRYHHPD.mjs.map +1 -0
  7. package/dist/{chunk-3JTO27MH.mjs → chunk-D4GAAQMM.mjs} +2 -9
  8. package/dist/{chunk-Q2KV6Y2J.mjs → chunk-PFQYNPQJ.mjs} +32 -32
  9. package/dist/{chunk-JH7BXE2P.mjs → chunk-VVCCMJS7.mjs} +47 -5
  10. package/dist/chunk-VVCCMJS7.mjs.map +1 -0
  11. package/dist/{cs-X63DXX7L.mjs → cs-JTJXTX2T.mjs} +48 -6
  12. package/dist/cs-JTJXTX2T.mjs.map +1 -0
  13. package/dist/{da-OWTCV57A.mjs → da-MK37SJB6.mjs} +48 -6
  14. package/dist/da-MK37SJB6.mjs.map +1 -0
  15. package/dist/{de-77BMFDVF.mjs → de-LGBCWERA.mjs} +48 -6
  16. package/dist/de-LGBCWERA.mjs.map +1 -0
  17. package/dist/dist-YLEIY3JJ.mjs +547 -0
  18. package/dist/dist-YLEIY3JJ.mjs.map +1 -0
  19. package/dist/{el-FIBNLH2V.mjs → el-FKJMFCWY.mjs} +48 -6
  20. package/dist/el-FKJMFCWY.mjs.map +1 -0
  21. package/dist/{en-XWEPVTB4.mjs → en-AOSMPC2M.mjs} +5 -3
  22. package/dist/{es-726NTS53.mjs → es-LVDPIXWU.mjs} +48 -6
  23. package/dist/es-LVDPIXWU.mjs.map +1 -0
  24. package/dist/{fa-3N4CIWE6.mjs → fa-3VA2PIUD.mjs} +48 -6
  25. package/dist/fa-3VA2PIUD.mjs.map +1 -0
  26. package/dist/{fi-JOM3M7Z4.mjs → fi-3WM75ZLR.mjs} +48 -6
  27. package/dist/fi-3WM75ZLR.mjs.map +1 -0
  28. package/dist/{fr-56QSXS7E.mjs → fr-NK4A72WA.mjs} +48 -6
  29. package/dist/fr-NK4A72WA.mjs.map +1 -0
  30. package/dist/{he-SNAXPJEK.mjs → he-IACZDZMB.mjs} +48 -6
  31. package/dist/he-IACZDZMB.mjs.map +1 -0
  32. package/dist/{hi-CRBRD5TB.mjs → hi-JZ7MGMMS.mjs} +48 -6
  33. package/dist/hi-JZ7MGMMS.mjs.map +1 -0
  34. package/dist/{id-BRCVLICF.mjs → id-P3KDQGNK.mjs} +48 -6
  35. package/dist/id-P3KDQGNK.mjs.map +1 -0
  36. package/dist/index.css +123 -12
  37. package/dist/index.css.map +1 -1
  38. package/dist/index.d.mts +353 -107
  39. package/dist/index.mjs +3139 -1811
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/{it-M2Z27BNB.mjs → it-LQS33SUY.mjs} +48 -6
  42. package/dist/it-LQS33SUY.mjs.map +1 -0
  43. package/dist/{ja-TZUKW7HD.mjs → ja-G4FKZPWD.mjs} +48 -6
  44. package/dist/ja-G4FKZPWD.mjs.map +1 -0
  45. package/dist/{ko-NKBGGOL6.mjs → ko-2XWKQ7BA.mjs} +48 -6
  46. package/dist/ko-2XWKQ7BA.mjs.map +1 -0
  47. package/dist/{magic-string.es-7FJ3LUGB.mjs → magic-string.es-K77I4ZQN.mjs} +2 -2
  48. package/dist/{ms-XFXPN6RX.mjs → ms-2SNONIUD.mjs} +48 -6
  49. package/dist/ms-2SNONIUD.mjs.map +1 -0
  50. package/dist/{nl-MVYXAS5C.mjs → nl-BMZUAJ7J.mjs} +48 -6
  51. package/dist/nl-BMZUAJ7J.mjs.map +1 -0
  52. package/dist/{no-XOLO4JPV.mjs → no-6J3WIZ6L.mjs} +48 -6
  53. package/dist/no-6J3WIZ6L.mjs.map +1 -0
  54. package/dist/{pl-TRWLMMC4.mjs → pl-QQ7DAUVK.mjs} +48 -6
  55. package/dist/pl-QQ7DAUVK.mjs.map +1 -0
  56. package/dist/{pt-M3TE24UI.mjs → pt-MU3GN7MW.mjs} +48 -6
  57. package/dist/pt-MU3GN7MW.mjs.map +1 -0
  58. package/dist/{ro-QBFG2T64.mjs → ro-6GBE72QK.mjs} +48 -6
  59. package/dist/ro-6GBE72QK.mjs.map +1 -0
  60. package/dist/{sv-IUECBXWX.mjs → sv-NQIL7PEM.mjs} +48 -6
  61. package/dist/sv-NQIL7PEM.mjs.map +1 -0
  62. package/dist/test-utils.mjs +16994 -22140
  63. package/dist/test-utils.mjs.map +1 -1
  64. package/dist/{th-US7KIN5Q.mjs → th-6OCNZQBE.mjs} +48 -6
  65. package/dist/th-6OCNZQBE.mjs.map +1 -0
  66. package/dist/{tr-DWJ2FFUK.mjs → tr-XWJ5P3SC.mjs} +48 -6
  67. package/dist/tr-XWJ5P3SC.mjs.map +1 -0
  68. package/dist/{uk-M4ZE4DPZ.mjs → uk-AKSN6DGW.mjs} +48 -6
  69. package/dist/uk-AKSN6DGW.mjs.map +1 -0
  70. package/dist/{vi-FERZNPSH.mjs → vi-23GHQ45M.mjs} +48 -6
  71. package/dist/vi-23GHQ45M.mjs.map +1 -0
  72. package/dist/{zh-3J2I3WYK.mjs → zh-ITT4QBSN.mjs} +48 -6
  73. package/dist/zh-ITT4QBSN.mjs.map +1 -0
  74. package/package.json +18 -14
  75. package/src/components/Button/Button.tsx +23 -25
  76. package/src/components/annotation/AnnotateToolbar.tsx +1 -1
  77. package/src/components/annotation-popups/SharedPopupElements.tsx +5 -7
  78. package/src/components/image-annotation/SvgDrawingCanvas.tsx +3 -4
  79. package/src/components/modals/ConfigureGenerationStep.tsx +190 -0
  80. package/src/components/modals/ConfigureSearchStep.tsx +105 -0
  81. package/src/components/modals/ContextSummary.tsx +202 -0
  82. package/src/components/modals/GatherContextStep.tsx +93 -0
  83. package/src/components/modals/KeyboardShortcutsHelpModal.tsx +4 -6
  84. package/src/components/modals/ProposeEntitiesModal.tsx +4 -6
  85. package/src/components/modals/ReferenceWizardModal.tsx +338 -0
  86. package/src/components/modals/ResourceSearchModal.tsx +4 -6
  87. package/src/components/modals/SearchModal.css +43 -0
  88. package/src/components/modals/SearchModal.tsx +4 -6
  89. package/src/components/modals/SearchResultsStep.tsx +126 -0
  90. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +3 -4
  91. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +36 -14
  92. package/src/components/resource/AnnotateView.tsx +4 -4
  93. package/src/components/resource/AnnotationHistory.tsx +2 -2
  94. package/src/components/resource/BrowseView.tsx +4 -4
  95. package/src/components/resource/ResourceViewer.tsx +4 -7
  96. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +16 -16
  97. package/src/components/resource/__tests__/BrowseView.test.tsx +2 -2
  98. package/src/components/resource/__tests__/HistoryEvent.test.tsx +1 -1
  99. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +1 -1
  100. package/src/components/resource/panels/AssessmentEntry.tsx +9 -11
  101. package/src/components/resource/panels/CommentEntry.tsx +10 -12
  102. package/src/components/resource/panels/HighlightEntry.tsx +9 -11
  103. package/src/components/resource/panels/ReferenceEntry.tsx +57 -104
  104. package/src/components/resource/panels/ReferencesPanel.css +94 -13
  105. package/src/components/resource/panels/ReferencesPanel.tsx +1 -2
  106. package/src/components/resource/panels/TagEntry.tsx +9 -11
  107. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +7 -7
  108. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +2 -2
  109. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +64 -101
  110. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +1 -1
  111. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +3 -3
  112. package/src/components/viewers/ImageViewer.tsx +3 -6
  113. package/src/components/viewers/__tests__/ImageViewer.test.tsx +3 -3
  114. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +5 -5
  115. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +141 -0
  116. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +41 -0
  117. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +148 -0
  118. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +106 -0
  119. package/src/features/admin-exchange/components/AdminExchangePage.tsx +120 -0
  120. package/src/features/admin-exchange/components/ExportCard.tsx +35 -0
  121. package/src/features/admin-exchange/components/ImportCard.tsx +188 -0
  122. package/src/features/admin-exchange/components/ImportProgress.tsx +86 -0
  123. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +3 -3
  124. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +2 -2
  125. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +4 -4
  126. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -3
  127. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +117 -0
  128. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +121 -0
  129. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -5
  130. package/src/features/resource-compose/components/ResourceComposePage.tsx +56 -1
  131. package/src/features/resource-discovery/__tests__/ResourceCard.test.tsx +1 -1
  132. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +2 -2
  133. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +3 -3
  134. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +11 -10
  135. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +2 -2
  136. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +22 -115
  137. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +3 -3
  138. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +20 -20
  139. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +7 -7
  140. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +5 -21
  141. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +2 -2
  142. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +45 -82
  143. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +4 -4
  144. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +146 -72
  145. package/src/integrations/tailwind-plugin.js +3 -3
  146. package/src/styles/core/buttons.css +31 -0
  147. package/src/styles/features/exchange.css +404 -0
  148. package/src/styles/index.css +1 -0
  149. package/translations/ar.json +44 -4
  150. package/translations/bn.json +44 -4
  151. package/translations/cs.json +44 -4
  152. package/translations/da.json +130 -90
  153. package/translations/de.json +124 -84
  154. package/translations/el.json +44 -4
  155. package/translations/en.json +44 -4
  156. package/translations/es.json +44 -4
  157. package/translations/fa.json +44 -4
  158. package/translations/fi.json +70 -30
  159. package/translations/fr.json +44 -4
  160. package/translations/he.json +44 -4
  161. package/translations/hi.json +44 -4
  162. package/translations/id.json +45 -5
  163. package/translations/it.json +64 -24
  164. package/translations/ja.json +45 -5
  165. package/translations/ko.json +44 -4
  166. package/translations/ms.json +45 -5
  167. package/translations/nl.json +43 -3
  168. package/translations/no.json +106 -66
  169. package/translations/pl.json +44 -4
  170. package/translations/pt.json +45 -5
  171. package/translations/ro.json +44 -4
  172. package/translations/sv.json +44 -4
  173. package/translations/th.json +44 -4
  174. package/translations/tr.json +44 -4
  175. package/translations/uk.json +44 -4
  176. package/translations/vi.json +44 -4
  177. package/translations/zh.json +44 -4
  178. package/dist/PdfAnnotationCanvas.client-COQREPXU.mjs.map +0 -1
  179. package/dist/ar-7SUXNE34.mjs.map +0 -1
  180. package/dist/bn-XOET3DOI.mjs.map +0 -1
  181. package/dist/chunk-JH7BXE2P.mjs.map +0 -1
  182. package/dist/cs-X63DXX7L.mjs.map +0 -1
  183. package/dist/da-OWTCV57A.mjs.map +0 -1
  184. package/dist/de-77BMFDVF.mjs.map +0 -1
  185. package/dist/el-FIBNLH2V.mjs.map +0 -1
  186. package/dist/es-726NTS53.mjs.map +0 -1
  187. package/dist/fa-3N4CIWE6.mjs.map +0 -1
  188. package/dist/fi-JOM3M7Z4.mjs.map +0 -1
  189. package/dist/fr-56QSXS7E.mjs.map +0 -1
  190. package/dist/he-SNAXPJEK.mjs.map +0 -1
  191. package/dist/hi-CRBRD5TB.mjs.map +0 -1
  192. package/dist/id-BRCVLICF.mjs.map +0 -1
  193. package/dist/it-M2Z27BNB.mjs.map +0 -1
  194. package/dist/ja-TZUKW7HD.mjs.map +0 -1
  195. package/dist/ko-NKBGGOL6.mjs.map +0 -1
  196. package/dist/ms-XFXPN6RX.mjs.map +0 -1
  197. package/dist/nl-MVYXAS5C.mjs.map +0 -1
  198. package/dist/no-XOLO4JPV.mjs.map +0 -1
  199. package/dist/pl-TRWLMMC4.mjs.map +0 -1
  200. package/dist/pt-M3TE24UI.mjs.map +0 -1
  201. package/dist/ro-QBFG2T64.mjs.map +0 -1
  202. package/dist/sv-IUECBXWX.mjs.map +0 -1
  203. package/dist/th-US7KIN5Q.mjs.map +0 -1
  204. package/dist/tr-DWJ2FFUK.mjs.map +0 -1
  205. package/dist/uk-M4ZE4DPZ.mjs.map +0 -1
  206. package/dist/vi-FERZNPSH.mjs.map +0 -1
  207. package/dist/zh-3J2I3WYK.mjs.map +0 -1
  208. package/src/examples/ButtonUsageExample.tsx +0 -242
  209. package/src/examples/button-css-modules.module.css +0 -164
  210. package/src/examples/button-styled-components.tsx +0 -215
  211. package/src/examples/button-tailwind.css +0 -51
  212. /package/dist/{chunk-3JTO27MH.mjs.map → chunk-D4GAAQMM.mjs.map} +0 -0
  213. /package/dist/{chunk-Q2KV6Y2J.mjs.map → chunk-PFQYNPQJ.mjs.map} +0 -0
  214. /package/dist/{en-XWEPVTB4.mjs.map → en-AOSMPC2M.mjs.map} +0 -0
  215. /package/dist/{magic-string.es-7FJ3LUGB.mjs.map → magic-string.es-K77I4ZQN.mjs.map} +0 -0
@@ -53,20 +53,21 @@ import { ReferenceEntry } from '../ReferenceEntry';
53
53
 
54
54
  const createMockReference = (overrides?: Partial<Annotation>): Annotation => ({
55
55
  '@context': 'http://www.w3.org/ns/anno.jsonld',
56
- id: 'http://example.com/annotations/ref-1',
56
+ id: 'ref-1',
57
57
  type: 'Annotation',
58
58
  motivation: 'linking',
59
59
  created: '2024-06-15T12:00:00Z',
60
60
  modified: '2024-06-15T12:00:00Z',
61
61
  target: {
62
- source: 'http://example.com/resources/resource-1',
62
+ source: 'resource-1',
63
63
  selector: {
64
64
  type: 'TextQuoteSelector',
65
65
  exact: 'referenced text',
66
66
  },
67
67
  },
68
68
  body: {
69
- source: 'http://example.com/resources/linked-doc',
69
+ type: 'SpecificResource',
70
+ source: 'linked-doc',
70
71
  },
71
72
  ...overrides,
72
73
  });
@@ -124,7 +125,7 @@ describe('ReferenceEntry', () => {
124
125
 
125
126
  it('should show link icon when reference is resolved', () => {
126
127
  mockIsBodyResolved.mockReturnValue(true);
127
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
128
+ mockGetBodySource.mockReturnValue('linked-doc');
128
129
 
129
130
  const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
130
131
 
@@ -161,7 +162,7 @@ describe('ReferenceEntry', () => {
161
162
 
162
163
  it('should render resolved document name when enriched', () => {
163
164
  mockIsBodyResolved.mockReturnValue(true);
164
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
165
+ mockGetBodySource.mockReturnValue('linked-doc');
165
166
 
166
167
  const enrichedRef = {
167
168
  ...createMockReference(),
@@ -247,7 +248,7 @@ describe('ReferenceEntry', () => {
247
248
  await userEvent.click(entry);
248
249
 
249
250
  expect(clickHandler).toHaveBeenCalledWith({
250
- annotationId: 'http://example.com/annotations/ref-1',
251
+ annotationId: 'ref-1',
251
252
  motivation: 'linking',
252
253
  });
253
254
 
@@ -255,57 +256,57 @@ describe('ReferenceEntry', () => {
255
256
  });
256
257
  });
257
258
 
258
- describe('Resolved reference actions', () => {
259
- it('should show open button when resolved', () => {
259
+ describe('Status icon — resolved reference', () => {
260
+ it('should navigate on 🔗 icon click', async () => {
260
261
  mockIsBodyResolved.mockReturnValue(true);
261
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
262
+ mockGetBodySource.mockReturnValue('linked-doc');
262
263
 
263
264
  const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
264
265
 
265
- const openButton = container.querySelector('button[title="ReferencesPanel.open"]');
266
- expect(openButton).toBeInTheDocument();
266
+ const icon = container.querySelector('.semiont-reference-icon')!;
267
+ await userEvent.click(icon);
268
+
269
+ expect(mockRoutes.resourceDetail).toHaveBeenCalledWith('linked-doc');
270
+ expect(mockNavigate).toHaveBeenCalledWith('/resources/linked-doc', { resourceId: 'linked-doc' });
271
+ });
272
+
273
+ it('should have clickable class when resolved', () => {
274
+ mockIsBodyResolved.mockReturnValue(true);
275
+ mockGetBodySource.mockReturnValue('linked-doc');
276
+
277
+ const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
278
+
279
+ const icon = container.querySelector('.semiont-reference-icon');
280
+ expect(icon).toHaveClass('semiont-reference-icon--clickable');
267
281
  });
268
282
 
269
- it('should show unlink button when resolved and in annotate mode', () => {
283
+ it('should show hover-reveal unlink button in annotate mode', () => {
270
284
  mockIsBodyResolved.mockReturnValue(true);
271
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
285
+ mockGetBodySource.mockReturnValue('linked-doc');
272
286
 
273
287
  const { container } = renderWithProviders(
274
288
  <ReferenceEntry {...defaultProps} annotateMode={true} />
275
289
  );
276
290
 
277
- const unlinkButton = container.querySelector('button[title="ReferencesPanel.unlink"]');
291
+ const unlinkButton = container.querySelector('.semiont-reference-unlink');
278
292
  expect(unlinkButton).toBeInTheDocument();
279
293
  });
280
294
 
281
295
  it('should not show unlink button when not in annotate mode', () => {
282
296
  mockIsBodyResolved.mockReturnValue(true);
283
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
297
+ mockGetBodySource.mockReturnValue('linked-doc');
284
298
 
285
299
  const { container } = renderWithProviders(
286
300
  <ReferenceEntry {...defaultProps} annotateMode={false} />
287
301
  );
288
302
 
289
- const unlinkButton = container.querySelector('button[title="ReferencesPanel.unlink"]');
303
+ const unlinkButton = container.querySelector('.semiont-reference-unlink');
290
304
  expect(unlinkButton).not.toBeInTheDocument();
291
305
  });
292
306
 
293
- it('should navigate on open click', async () => {
294
- mockIsBodyResolved.mockReturnValue(true);
295
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
296
-
297
- const { container } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
298
-
299
- const openButton = container.querySelector('button[title="ReferencesPanel.open"]')!;
300
- await userEvent.click(openButton);
301
-
302
- expect(mockRoutes.resourceDetail).toHaveBeenCalledWith('linked-doc');
303
- expect(mockNavigate).toHaveBeenCalledWith('/resources/linked-doc', { resourceId: 'linked-doc' });
304
- });
305
-
306
307
  it('should emit bind:update-body on unlink click', async () => {
307
308
  mockIsBodyResolved.mockReturnValue(true);
308
- mockGetBodySource.mockReturnValue('http://example.com/resources/linked-doc');
309
+ mockGetBodySource.mockReturnValue('linked-doc');
309
310
  const unlinkHandler = vi.fn();
310
311
 
311
312
  const { container, eventBus } = renderWithProviders(
@@ -315,11 +316,11 @@ describe('ReferenceEntry', () => {
315
316
 
316
317
  const subscription = eventBus!.get('bind:update-body').subscribe(unlinkHandler);
317
318
 
318
- const unlinkButton = container.querySelector('button[title="ReferencesPanel.unlink"]')!;
319
+ const unlinkButton = container.querySelector('.semiont-reference-unlink')!;
319
320
  await userEvent.click(unlinkButton);
320
321
 
321
322
  expect(unlinkHandler).toHaveBeenCalledWith({
322
- annotationUri: 'http://example.com/annotations/ref-1',
323
+ annotationId: 'ref-1',
323
324
  resourceId: 'resource-1',
324
325
  operations: [{ op: 'remove' }],
325
326
  });
@@ -328,110 +329,72 @@ describe('ReferenceEntry', () => {
328
329
  });
329
330
  });
330
331
 
331
- describe('Stub reference actions', () => {
332
- it('should show generate, search, and create buttons in annotate mode', () => {
333
- mockIsBodyResolved.mockReturnValue(false);
334
-
335
- const { container } = renderWithProviders(
336
- <ReferenceEntry {...defaultProps} annotateMode={true} />
337
- );
338
-
339
- expect(container.querySelector('button[title="ReferencesPanel.generate"]')).toBeInTheDocument();
340
- expect(container.querySelector('button[title="ReferencesPanel.find"]')).toBeInTheDocument();
341
- expect(container.querySelector('button[title="ReferencesPanel.create"]')).toBeInTheDocument();
342
- });
343
-
344
- it('should not show stub actions when not in annotate mode', () => {
332
+ describe('Status icon — stub reference', () => {
333
+ it('should emit bind:initiate on icon click in annotate mode', async () => {
345
334
  mockIsBodyResolved.mockReturnValue(false);
346
-
347
- const { container } = renderWithProviders(
348
- <ReferenceEntry {...defaultProps} annotateMode={false} />
349
- );
350
-
351
- expect(container.querySelector('button[title="ReferencesPanel.generate"]')).not.toBeInTheDocument();
352
- expect(container.querySelector('button[title="ReferencesPanel.find"]')).not.toBeInTheDocument();
353
- expect(container.querySelector('button[title="ReferencesPanel.create"]')).not.toBeInTheDocument();
354
- });
355
-
356
- it('should emit yield:modal-open on generate click', async () => {
357
- mockIsBodyResolved.mockReturnValue(false);
358
- const generateHandler = vi.fn();
335
+ mockGetEntityTypes.mockReturnValue(['Person']);
336
+ const initiateHandler = vi.fn();
359
337
 
360
338
  const { container, eventBus } = renderWithProviders(
361
339
  <ReferenceEntry {...defaultProps} annotateMode={true} />,
362
340
  { returnEventBus: true }
363
341
  );
364
342
 
365
- const subscription = eventBus!.get('yield:modal-open').subscribe(generateHandler);
343
+ const subscription = eventBus!.get('bind:initiate').subscribe(initiateHandler);
366
344
 
367
- const generateButton = container.querySelector('button[title="ReferencesPanel.generate"]')!;
368
- await userEvent.click(generateButton);
345
+ const icon = container.querySelector('.semiont-reference-icon')!;
346
+ await userEvent.click(icon);
369
347
 
370
- expect(generateHandler).toHaveBeenCalledWith({
371
- annotationUri: 'http://example.com/annotations/ref-1',
372
- resourceUri: 'http://example.com/resources/resource-1',
348
+ expect(initiateHandler).toHaveBeenCalledWith({
349
+ annotationId: 'ref-1',
350
+ resourceId: 'resource-1',
373
351
  defaultTitle: 'referenced text',
352
+ entityTypes: ['Person'],
374
353
  });
375
354
 
376
355
  subscription.unsubscribe();
377
356
  });
378
357
 
379
- it('should emit bind:link on search click', async () => {
358
+ it('should have clickable class in annotate mode', () => {
380
359
  mockIsBodyResolved.mockReturnValue(false);
381
- const searchHandler = vi.fn();
382
360
 
383
- const { container, eventBus } = renderWithProviders(
384
- <ReferenceEntry {...defaultProps} annotateMode={true} />,
385
- { returnEventBus: true }
361
+ const { container } = renderWithProviders(
362
+ <ReferenceEntry {...defaultProps} annotateMode={true} />
386
363
  );
387
364
 
388
- const subscription = eventBus!.get('bind:link').subscribe(searchHandler);
365
+ const icon = container.querySelector('.semiont-reference-icon');
366
+ expect(icon).toHaveClass('semiont-reference-icon--clickable');
367
+ });
389
368
 
390
- const searchButton = container.querySelector('button[title="ReferencesPanel.find"]')!;
391
- await userEvent.click(searchButton);
369
+ it('should not be clickable in browse mode', () => {
370
+ mockIsBodyResolved.mockReturnValue(false);
392
371
 
393
- expect(searchHandler).toHaveBeenCalledWith({
394
- annotationUri: 'http://example.com/annotations/ref-1',
395
- searchTerm: 'referenced text',
396
- });
372
+ const { container } = renderWithProviders(
373
+ <ReferenceEntry {...defaultProps} annotateMode={false} />
374
+ );
397
375
 
398
- subscription.unsubscribe();
376
+ const icon = container.querySelector('.semiont-reference-icon');
377
+ expect(icon).not.toHaveClass('semiont-reference-icon--clickable');
399
378
  });
400
379
 
401
- it('should emit bind:create-manual on create click', async () => {
380
+ it('should not emit bind:initiate on icon click in browse mode', async () => {
402
381
  mockIsBodyResolved.mockReturnValue(false);
403
- mockGetEntityTypes.mockReturnValue(['Person']);
404
- const createHandler = vi.fn();
382
+ const initiateHandler = vi.fn();
405
383
 
406
384
  const { container, eventBus } = renderWithProviders(
407
- <ReferenceEntry {...defaultProps} annotateMode={true} />,
385
+ <ReferenceEntry {...defaultProps} annotateMode={false} />,
408
386
  { returnEventBus: true }
409
387
  );
410
388
 
411
- const subscription = eventBus!.get('bind:create-manual').subscribe(createHandler);
389
+ const subscription = eventBus!.get('bind:initiate').subscribe(initiateHandler);
412
390
 
413
- const createButton = container.querySelector('button[title="ReferencesPanel.create"]')!;
414
- await userEvent.click(createButton);
391
+ const icon = container.querySelector('.semiont-reference-icon')!;
392
+ await userEvent.click(icon);
415
393
 
416
- expect(createHandler).toHaveBeenCalledWith({
417
- annotationUri: 'http://example.com/annotations/ref-1',
418
- title: 'referenced text',
419
- entityTypes: ['Person'],
420
- });
394
+ expect(initiateHandler).not.toHaveBeenCalled();
421
395
 
422
396
  subscription.unsubscribe();
423
397
  });
424
-
425
- it('should set data-generating on generate button', () => {
426
- mockIsBodyResolved.mockReturnValue(false);
427
-
428
- const { container } = renderWithProviders(
429
- <ReferenceEntry {...defaultProps} annotateMode={true} isGenerating={true} />
430
- );
431
-
432
- const generateButton = container.querySelector('button[title="ReferencesPanel.generate"]');
433
- expect(generateButton).toHaveAttribute('data-generating', 'true');
434
- });
435
398
  });
436
399
 
437
400
  describe('data-type attribute', () => {
@@ -33,7 +33,7 @@ const createMockAnnotation = (overrides?: Partial<Annotation>): Annotation => ({
33
33
  created: '2024-06-15T12:00:00Z',
34
34
  modified: '2024-06-15T12:00:00Z',
35
35
  target: {
36
- source: 'http://example.com/resources/1',
36
+ source: '1',
37
37
  selector: {
38
38
  type: 'TextQuoteSelector',
39
39
  exact: 'some text',
@@ -542,7 +542,7 @@ describe('TaggingPanel Component', () => {
542
542
  />
543
543
  );
544
544
 
545
- const annotateButton = screen.getByRole('button', { name: /✨ Annotate/i });
545
+ const annotateButton = screen.getByRole('button', { name: /✨\s*Annotate/i });
546
546
  expect(annotateButton).toBeDisabled();
547
547
  });
548
548
 
@@ -557,7 +557,7 @@ describe('TaggingPanel Component', () => {
557
557
  const issueCheckbox = screen.getByLabelText(/Issue/);
558
558
  await userEvent.click(issueCheckbox);
559
559
 
560
- const annotateButton = screen.getByRole('button', { name: /✨ Annotate/i });
560
+ const annotateButton = screen.getByRole('button', { name: /✨\s*Annotate/i });
561
561
  expect(annotateButton).not.toBeDisabled();
562
562
  });
563
563
 
@@ -577,7 +577,7 @@ describe('TaggingPanel Component', () => {
577
577
  await userEvent.click(issueCheckbox);
578
578
  await userEvent.click(ruleCheckbox);
579
579
 
580
- const annotateButton = screen.getByRole('button', { name: /✨ Annotate/i });
580
+ const annotateButton = screen.getByRole('button', { name: /✨\s*Annotate/i });
581
581
  await userEvent.click(annotateButton);
582
582
 
583
583
  await waitFor(() => {
@@ -1,18 +1,15 @@
1
- import type { ResourceUri } from '@semiont/core';
1
+ import type { ResourceId } from '@semiont/core';
2
2
 
3
3
  interface ImageViewerProps {
4
- resourceUri: ResourceUri;
4
+ resourceUri: ResourceId;
5
5
  mimeType: string;
6
6
  alt?: string;
7
7
  }
8
8
 
9
9
  export function ImageViewer({ resourceUri, alt = 'Resource image' }: ImageViewerProps) {
10
- // Extract resource ID from W3C canonical URI (last segment of path)
11
- const resourceId = resourceUri.split('/').pop();
12
-
13
10
  // Use Next.js API route proxy instead of direct backend call
14
11
  // This allows us to add authentication headers which <img> tags can't send
15
- const imageUrl = `/api/resources/${resourceId}`;
12
+ const imageUrl = `/api/resources/${resourceUri}`;
16
13
 
17
14
  return (
18
15
  <div className="semiont-image-viewer">
@@ -4,11 +4,11 @@ import { screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../../test-utils';
6
6
  import { ImageViewer } from '../ImageViewer';
7
- import type { ResourceUri } from '@semiont/core';
7
+ import type { ResourceId } from '@semiont/core';
8
8
 
9
9
  describe('ImageViewer', () => {
10
10
  const defaultProps = {
11
- resourceUri: 'http://example.com/resources/abc-123' as ResourceUri,
11
+ resourceUri: 'abc-123' as ResourceId,
12
12
  mimeType: 'image/png',
13
13
  };
14
14
 
@@ -38,7 +38,7 @@ describe('ImageViewer', () => {
38
38
  it('should extract the last segment of the URI as resource ID', () => {
39
39
  renderWithProviders(
40
40
  <ImageViewer
41
- resourceUri={'http://example.com/deep/path/to/resource-xyz' as ResourceUri}
41
+ resourceUri={'resource-xyz' as ResourceId}
42
42
  mimeType="image/jpeg"
43
43
  />
44
44
  );
@@ -285,7 +285,7 @@ describe('AdminDevOpsPage', () => {
285
285
 
286
286
  expect(ToolbarPanels).toHaveBeenCalledWith(
287
287
  expect.objectContaining({ theme: 'dark' }),
288
- expect.anything()
288
+ undefined,
289
289
  );
290
290
  });
291
291
 
@@ -296,7 +296,7 @@ describe('AdminDevOpsPage', () => {
296
296
 
297
297
  expect(ToolbarPanels).toHaveBeenCalledWith(
298
298
  expect.objectContaining({ showLineNumbers: true }),
299
- expect.anything()
299
+ undefined,
300
300
  );
301
301
  });
302
302
 
@@ -307,7 +307,7 @@ describe('AdminDevOpsPage', () => {
307
307
 
308
308
  expect(ToolbarPanels).toHaveBeenCalledWith(
309
309
  expect.objectContaining({ activePanel: 'settings' }),
310
- expect.anything()
310
+ undefined,
311
311
  );
312
312
  });
313
313
 
@@ -318,7 +318,7 @@ describe('AdminDevOpsPage', () => {
318
318
 
319
319
  expect(Toolbar).toHaveBeenCalledWith(
320
320
  expect.objectContaining({ context: 'simple' }),
321
- expect.anything()
321
+ undefined,
322
322
  );
323
323
  });
324
324
 
@@ -329,7 +329,7 @@ describe('AdminDevOpsPage', () => {
329
329
 
330
330
  expect(Toolbar).toHaveBeenCalledWith(
331
331
  expect.objectContaining({ activePanel: 'help' }),
332
- expect.anything()
332
+ undefined,
333
333
  );
334
334
  });
335
335
 
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Tests for AdminExchangePage component
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { render, screen } from '@testing-library/react';
7
+ import { AdminExchangePage } from '../components/AdminExchangePage';
8
+ import type { AdminExchangePageProps } from '../components/AdminExchangePage';
9
+
10
+ const createProps = (overrides?: Partial<AdminExchangePageProps>): AdminExchangePageProps => ({
11
+ onExport: vi.fn(),
12
+ isExporting: false,
13
+ onFileSelected: vi.fn(),
14
+ onImport: vi.fn(),
15
+ onCancelImport: vi.fn(),
16
+ selectedFile: null,
17
+ preview: null,
18
+ isImporting: false,
19
+ importPhase: null,
20
+ theme: 'light',
21
+ showLineNumbers: false,
22
+ activePanel: null,
23
+ translations: {
24
+ title: 'Backup & Restore',
25
+ subtitle: 'Back up and restore your knowledge base',
26
+ export: {
27
+ title: 'Backup',
28
+ description: 'Create a lossless backup',
29
+ exportButton: 'Create Backup',
30
+ exporting: 'Creating backup…',
31
+ },
32
+ import: {
33
+ title: 'Restore',
34
+ description: 'Restore from a backup',
35
+ dropzoneLabel: 'Drop a file here',
36
+ dropzoneActive: 'Drop to upload',
37
+ detectedFormat: 'Format',
38
+ statsPreview: 'Preview',
39
+ importButton: 'Restore',
40
+ importing: 'Restoring…',
41
+ importConfirmTitle: 'Confirm',
42
+ importConfirmMessage: 'Cannot undo.',
43
+ confirmImport: 'Proceed',
44
+ cancelImport: 'Cancel',
45
+ },
46
+ progress: {
47
+ phaseStarted: 'Starting…',
48
+ phaseEntityTypes: 'Entity types…',
49
+ phaseResources: 'Resources…',
50
+ phaseAnnotations: 'Annotations…',
51
+ phaseComplete: 'Complete',
52
+ phaseError: 'Failed',
53
+ hashChainValid: 'Hash valid',
54
+ hashChainInvalid: 'Hash invalid',
55
+ streams: 'Streams',
56
+ events: 'Events',
57
+ blobs: 'Blobs',
58
+ },
59
+ },
60
+ ToolbarPanels: () => <div data-testid="toolbar-panels" />,
61
+ Toolbar: () => <div data-testid="toolbar" />,
62
+ ...overrides,
63
+ });
64
+
65
+ describe('AdminExchangePage', () => {
66
+ it('renders page title and subtitle', () => {
67
+ render(<AdminExchangePage {...createProps()} />);
68
+ expect(screen.getByText('Backup & Restore')).toBeInTheDocument();
69
+ expect(screen.getByText('Back up and restore your knowledge base')).toBeInTheDocument();
70
+ });
71
+
72
+ it('renders ExportCard', () => {
73
+ render(<AdminExchangePage {...createProps()} />);
74
+ expect(screen.getByRole('heading', { name: 'Backup' })).toBeInTheDocument();
75
+ expect(screen.getByText('Create a lossless backup')).toBeInTheDocument();
76
+ });
77
+
78
+ it('renders ImportCard', () => {
79
+ render(<AdminExchangePage {...createProps()} />);
80
+ expect(screen.getByRole('heading', { name: 'Restore' })).toBeInTheDocument();
81
+ expect(screen.getByText('Restore from a backup')).toBeInTheDocument();
82
+ });
83
+
84
+ it('renders toolbar components', () => {
85
+ render(<AdminExchangePage {...createProps()} />);
86
+ expect(screen.getByTestId('toolbar-panels')).toBeInTheDocument();
87
+ expect(screen.getByTestId('toolbar')).toBeInTheDocument();
88
+ });
89
+
90
+ it('does not render ImportProgress when importPhase is null', () => {
91
+ const { container } = render(<AdminExchangePage {...createProps()} />);
92
+ expect(container.querySelector('.semiont-exchange__progress')).not.toBeInTheDocument();
93
+ });
94
+
95
+ it('renders ImportProgress when importPhase is set', () => {
96
+ render(<AdminExchangePage {...createProps({ importPhase: 'started' })} />);
97
+ expect(screen.getByText('Starting…')).toBeInTheDocument();
98
+ });
99
+
100
+ it('renders ImportProgress with backup result on completion', () => {
101
+ render(<AdminExchangePage {...createProps({
102
+ importPhase: 'complete',
103
+ importResult: { stats: { streams: 5, events: 42, blobs: 3 }, hashChainValid: true },
104
+ })} />);
105
+ expect(screen.getByText('Complete')).toBeInTheDocument();
106
+ expect(screen.getByText('5')).toBeInTheDocument();
107
+ });
108
+
109
+ it('applies panel-open class when common panel is active', () => {
110
+ const { container } = render(<AdminExchangePage {...createProps({ activePanel: 'user' })} />);
111
+ expect(container.querySelector('.semiont-page--panel-open')).toBeInTheDocument();
112
+ });
113
+
114
+ it('does not apply panel-open class when no panel is active', () => {
115
+ const { container } = render(<AdminExchangePage {...createProps({ activePanel: null })} />);
116
+ expect(container.querySelector('.semiont-page--panel-open')).not.toBeInTheDocument();
117
+ });
118
+
119
+ it('passes theme to ToolbarPanels', () => {
120
+ const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
121
+ render(<AdminExchangePage {...createProps({ theme: 'dark', ToolbarPanels })} />);
122
+ expect(ToolbarPanels).toHaveBeenCalledWith(
123
+ expect.objectContaining({ theme: 'dark' }),
124
+ undefined,
125
+ );
126
+ });
127
+
128
+ it('passes context to Toolbar', () => {
129
+ const Toolbar = vi.fn(() => <div data-testid="toolbar" />);
130
+ render(<AdminExchangePage {...createProps({ Toolbar })} />);
131
+ expect(Toolbar).toHaveBeenCalledWith(
132
+ expect.objectContaining({ context: 'simple' }),
133
+ undefined,
134
+ );
135
+ });
136
+
137
+ it('renders cards in grid layout', () => {
138
+ const { container } = render(<AdminExchangePage {...createProps()} />);
139
+ expect(container.querySelector('.semiont-exchange__cards')).toBeInTheDocument();
140
+ });
141
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tests for ExportCard component
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { render, screen, fireEvent } from '@testing-library/react';
7
+ import { ExportCard } from '../components/ExportCard';
8
+ import type { ExportCardProps } from '../components/ExportCard';
9
+
10
+ const createProps = (overrides?: Partial<ExportCardProps>): ExportCardProps => ({
11
+ onExport: vi.fn(),
12
+ isExporting: false,
13
+ translations: {
14
+ title: 'Backup',
15
+ description: 'Create a lossless backup',
16
+ exportButton: 'Create Backup',
17
+ exporting: 'Creating backup…',
18
+ },
19
+ ...overrides,
20
+ });
21
+
22
+ describe('ExportCard', () => {
23
+ it('renders title and description', () => {
24
+ render(<ExportCard {...createProps()} />);
25
+ expect(screen.getByRole('heading', { name: 'Backup' })).toBeInTheDocument();
26
+ expect(screen.getByText('Create a lossless backup')).toBeInTheDocument();
27
+ });
28
+
29
+ it('calls onExport when button is clicked', () => {
30
+ const onExport = vi.fn();
31
+ render(<ExportCard {...createProps({ onExport })} />);
32
+ fireEvent.click(screen.getByRole('button', { name: 'Create Backup' }));
33
+ expect(onExport).toHaveBeenCalled();
34
+ });
35
+
36
+ it('disables button and shows exporting text when isExporting', () => {
37
+ render(<ExportCard {...createProps({ isExporting: true })} />);
38
+ const button = screen.getByRole('button', { name: 'Creating backup…' });
39
+ expect(button).toBeDisabled();
40
+ });
41
+ });