@semiont/react-ui 0.5.5 → 0.5.7

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 (218) hide show
  1. package/README.md +59 -55
  2. package/dist/{PdfAnnotationCanvas.client-CN3C3S55.js → PdfAnnotationCanvas.client-NIMALXNZ.js} +7 -27
  3. package/dist/PdfAnnotationCanvas.client-NIMALXNZ.js.map +1 -0
  4. package/dist/{ar-U2EXWUMQ.js → ar-SONK6MON.js} +3 -7
  5. package/dist/ar-SONK6MON.js.map +1 -0
  6. package/dist/{bn-DRJGV772.js → bn-ZKPRITNG.js} +3 -7
  7. package/dist/bn-ZKPRITNG.js.map +1 -0
  8. package/dist/{chunk-3Q3TUKWP.js → chunk-Y2EEAOMZ.js} +29 -29
  9. package/dist/{cs-PTWDM23V.js → cs-LPXQ7NHQ.js} +3 -7
  10. package/dist/cs-LPXQ7NHQ.js.map +1 -0
  11. package/dist/{da-KSNIKYSS.js → da-6TKY7MCY.js} +6 -10
  12. package/dist/da-6TKY7MCY.js.map +1 -0
  13. package/dist/{de-F2XBEWFY.js → de-C3GNII74.js} +3 -7
  14. package/dist/de-C3GNII74.js.map +1 -0
  15. package/dist/{el-DLD2GWAP.js → el-UBCXQDJ7.js} +3 -7
  16. package/dist/el-UBCXQDJ7.js.map +1 -0
  17. package/dist/{es-WLPYWGB5.js → es-BQ23TRI7.js} +11 -15
  18. package/dist/es-BQ23TRI7.js.map +1 -0
  19. package/dist/{fa-BAXHSDZG.js → fa-AFTBZB77.js} +3 -7
  20. package/dist/fa-AFTBZB77.js.map +1 -0
  21. package/dist/{fi-FCHSYVOT.js → fi-WOYNLZC2.js} +3 -7
  22. package/dist/fi-WOYNLZC2.js.map +1 -0
  23. package/dist/{fr-3UERBSL6.js → fr-NDSMIFJM.js} +3 -7
  24. package/dist/fr-NDSMIFJM.js.map +1 -0
  25. package/dist/{he-F6F3FV2K.js → he-VJXVRDOY.js} +3 -7
  26. package/dist/he-VJXVRDOY.js.map +1 -0
  27. package/dist/{hi-4BK6IK7Q.js → hi-BF6PHIE2.js} +3 -7
  28. package/dist/hi-BF6PHIE2.js.map +1 -0
  29. package/dist/{id-7ECCWP3J.js → id-GXG5QCZY.js} +3 -7
  30. package/dist/id-GXG5QCZY.js.map +1 -0
  31. package/dist/index.css +103 -0
  32. package/dist/index.css.map +1 -1
  33. package/dist/index.d.ts +271 -120
  34. package/dist/index.js +877 -698
  35. package/dist/index.js.map +1 -1
  36. package/dist/{it-234Z6XK6.js → it-XKHHCBAF.js} +3 -7
  37. package/dist/it-XKHHCBAF.js.map +1 -0
  38. package/dist/{ja-PJWQI4OQ.js → ja-TX7VM4XD.js} +3 -7
  39. package/dist/ja-TX7VM4XD.js.map +1 -0
  40. package/dist/{ko-APUEW2RS.js → ko-DNC7EQ7J.js} +3 -7
  41. package/dist/ko-DNC7EQ7J.js.map +1 -0
  42. package/dist/{ms-PJBZWZWD.js → ms-POZGBKPH.js} +3 -7
  43. package/dist/ms-POZGBKPH.js.map +1 -0
  44. package/dist/{nl-L4C3ZBCU.js → nl-IRMTKI7Z.js} +4 -11
  45. package/dist/nl-IRMTKI7Z.js.map +1 -0
  46. package/dist/{no-QE5N5KNG.js → no-ZUDJA4S6.js} +20 -24
  47. package/dist/no-ZUDJA4S6.js.map +1 -0
  48. package/dist/{pl-5Q2D23PD.js → pl-2NGAXL5U.js} +3 -7
  49. package/dist/pl-2NGAXL5U.js.map +1 -0
  50. package/dist/{pt-AIGUOIOC.js → pt-ABMCXZUM.js} +118 -122
  51. package/dist/pt-ABMCXZUM.js.map +1 -0
  52. package/dist/{ro-T56CSHTY.js → ro-VOJP6O5X.js} +3 -7
  53. package/dist/ro-VOJP6O5X.js.map +1 -0
  54. package/dist/{sv-L4TJQ2UH.js → sv-4HVFIIE5.js} +43 -47
  55. package/dist/sv-4HVFIIE5.js.map +1 -0
  56. package/dist/test-utils.js +2 -2
  57. package/dist/test-utils.js.map +1 -1
  58. package/dist/{th-6O7Y6O2Q.js → th-IFPZP3HQ.js} +3 -7
  59. package/dist/th-IFPZP3HQ.js.map +1 -0
  60. package/dist/{tr-D4CQCSNO.js → tr-2GYEAMJ4.js} +3 -7
  61. package/dist/tr-2GYEAMJ4.js.map +1 -0
  62. package/dist/{uk-2HMQG6ND.js → uk-XCJBVLLD.js} +3 -7
  63. package/dist/uk-XCJBVLLD.js.map +1 -0
  64. package/dist/{vi-XVJ4RUEJ.js → vi-4FR7CB2F.js} +3 -7
  65. package/dist/vi-4FR7CB2F.js.map +1 -0
  66. package/dist/{zh-K2KDPGHK.js → zh-NSKFOINB.js} +3 -7
  67. package/dist/zh-NSKFOINB.js.map +1 -0
  68. package/package.json +17 -13
  69. package/src/components/Button/__tests__/Button.test.tsx +0 -2
  70. package/src/components/CodeMirrorRenderer.tsx +2 -0
  71. package/src/components/ErrorBoundary.tsx +0 -9
  72. package/src/components/ProtectedErrorBoundary.css +119 -0
  73. package/src/components/ProtectedErrorBoundary.tsx +24 -15
  74. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +0 -1
  75. package/src/components/__tests__/ErrorBoundary.test.tsx +20 -13
  76. package/src/components/__tests__/LiveRegion.hooks.test.tsx +1 -1
  77. package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +2 -1
  78. package/src/components/__tests__/ResizeHandle.test.tsx +0 -1
  79. package/src/components/__tests__/SessionExpiryBanner.test.tsx +0 -1
  80. package/src/components/__tests__/StatusDisplay.test.tsx +0 -1
  81. package/src/components/__tests__/Toast.test.tsx +2 -3
  82. package/src/components/__tests__/Toolbar.test.tsx +0 -1
  83. package/src/components/annotation/annotations.css +14 -0
  84. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +3 -5
  85. package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +0 -1
  86. package/src/components/branding/__tests__/SemiontBranding.test.tsx +1 -2
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +5 -6
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +1 -3
  89. package/src/components/layout/__tests__/SkipLinks.a11y.test.tsx +8 -8
  90. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +12 -1
  91. package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +0 -1
  92. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +3 -4
  93. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +1 -2
  94. package/src/components/modals/__tests__/SearchModal.basic.test.tsx +1 -1
  95. package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +0 -5
  96. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +1 -2
  97. package/src/components/modals/__tests__/SearchModal.visual.test.tsx +2 -2
  98. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +0 -1
  99. package/src/components/navigation/NavigationMenu.tsx +1 -1
  100. package/src/components/navigation/__tests__/Footer.a11y.test.tsx +4 -0
  101. package/src/components/navigation/__tests__/Footer.test.tsx +3 -6
  102. package/src/components/navigation/__tests__/NavigationMenu.a11y.test.tsx +1 -1
  103. package/src/components/navigation/__tests__/NavigationMenu.test.tsx +7 -9
  104. package/src/components/navigation/__tests__/ObservableLink.test.tsx +0 -1
  105. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +1 -2
  106. package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +0 -1
  107. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -4
  108. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +10 -19
  109. package/src/components/resource/AnnotateView.tsx +35 -37
  110. package/src/components/resource/BrowseView.tsx +31 -31
  111. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +0 -1
  112. package/src/components/resource/__tests__/BrowseView.test.tsx +12 -14
  113. package/src/components/resource/__tests__/HistoryEvent.test.tsx +0 -5
  114. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +4 -6
  115. package/src/components/resource/__tests__/event-formatting.test.ts +1 -1
  116. package/src/components/resource/panels/CollaborationPanel.tsx +1 -1
  117. package/src/components/resource/panels/JsonLdPanel.tsx +33 -16
  118. package/src/components/resource/panels/ReferencesPanel.tsx +1 -1
  119. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -5
  120. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +8 -7
  121. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +14 -10
  122. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +0 -1
  123. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +31 -18
  124. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +7 -6
  125. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +5 -6
  126. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +19 -13
  127. package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +95 -426
  128. package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +0 -1
  129. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +5 -5
  130. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +40 -7
  131. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +4 -4
  132. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +30 -32
  133. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +6 -6
  134. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +7 -6
  135. package/src/components/settings/__tests__/SettingsPanel.test.tsx +0 -1
  136. package/src/components/viewers/__tests__/ImageViewer.test.tsx +0 -1
  137. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +7 -10
  138. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +38 -27
  139. package/src/features/admin-exchange/components/ImportProgress.tsx +28 -34
  140. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +2 -0
  141. package/src/features/auth/__tests__/SignUpForm.a11y.test.tsx +11 -12
  142. package/src/features/auth/__tests__/SignUpForm.test.tsx +3 -3
  143. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -0
  144. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +11 -9
  145. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +2 -1
  146. package/src/features/resource-compose/components/ResourceComposePage.tsx +36 -9
  147. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -8
  148. package/src/features/resource-discovery/__tests__/ResourceCard.test.tsx +0 -1
  149. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +33 -35
  150. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +12 -11
  151. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +204 -11
  152. package/src/features/resource-discovery/state/discover-state-unit.ts +70 -11
  153. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
  154. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +10 -7
  155. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +37 -1
  156. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +14 -7
  157. package/src/integrations/__tests__/css-modules-helper.test.tsx +2 -3
  158. package/src/integrations/__tests__/styled-components-theme.test.ts +1 -3
  159. package/src/styles/features/exchange.css +0 -30
  160. package/src/styles/index.css +1 -0
  161. package/translations/ar.json +1 -3
  162. package/translations/bn.json +1 -3
  163. package/translations/cs.json +1 -3
  164. package/translations/da.json +4 -6
  165. package/translations/de.json +1 -3
  166. package/translations/el.json +1 -3
  167. package/translations/es.json +9 -11
  168. package/translations/fa.json +1 -3
  169. package/translations/fi.json +1 -3
  170. package/translations/fr.json +1 -3
  171. package/translations/he.json +1 -3
  172. package/translations/hi.json +1 -3
  173. package/translations/id.json +1 -3
  174. package/translations/it.json +1 -3
  175. package/translations/ja.json +1 -3
  176. package/translations/ko.json +1 -3
  177. package/translations/ms.json +1 -3
  178. package/translations/nl.json +2 -7
  179. package/translations/no.json +18 -20
  180. package/translations/pl.json +1 -3
  181. package/translations/pt.json +116 -118
  182. package/translations/ro.json +1 -3
  183. package/translations/sv.json +41 -43
  184. package/translations/th.json +1 -3
  185. package/translations/tr.json +1 -3
  186. package/translations/uk.json +1 -3
  187. package/translations/vi.json +1 -3
  188. package/translations/zh.json +1 -3
  189. package/dist/PdfAnnotationCanvas.client-CN3C3S55.js.map +0 -1
  190. package/dist/ar-U2EXWUMQ.js.map +0 -1
  191. package/dist/bn-DRJGV772.js.map +0 -1
  192. package/dist/cs-PTWDM23V.js.map +0 -1
  193. package/dist/da-KSNIKYSS.js.map +0 -1
  194. package/dist/de-F2XBEWFY.js.map +0 -1
  195. package/dist/el-DLD2GWAP.js.map +0 -1
  196. package/dist/es-WLPYWGB5.js.map +0 -1
  197. package/dist/fa-BAXHSDZG.js.map +0 -1
  198. package/dist/fi-FCHSYVOT.js.map +0 -1
  199. package/dist/fr-3UERBSL6.js.map +0 -1
  200. package/dist/he-F6F3FV2K.js.map +0 -1
  201. package/dist/hi-4BK6IK7Q.js.map +0 -1
  202. package/dist/id-7ECCWP3J.js.map +0 -1
  203. package/dist/it-234Z6XK6.js.map +0 -1
  204. package/dist/ja-PJWQI4OQ.js.map +0 -1
  205. package/dist/ko-APUEW2RS.js.map +0 -1
  206. package/dist/ms-PJBZWZWD.js.map +0 -1
  207. package/dist/nl-L4C3ZBCU.js.map +0 -1
  208. package/dist/no-QE5N5KNG.js.map +0 -1
  209. package/dist/pl-5Q2D23PD.js.map +0 -1
  210. package/dist/pt-AIGUOIOC.js.map +0 -1
  211. package/dist/ro-T56CSHTY.js.map +0 -1
  212. package/dist/sv-L4TJQ2UH.js.map +0 -1
  213. package/dist/th-6O7Y6O2Q.js.map +0 -1
  214. package/dist/tr-D4CQCSNO.js.map +0 -1
  215. package/dist/uk-2HMQG6ND.js.map +0 -1
  216. package/dist/vi-XVJ4RUEJ.js.map +0 -1
  217. package/dist/zh-K2KDPGHK.js.map +0 -1
  218. /package/dist/{chunk-3Q3TUKWP.js.map → chunk-Y2EEAOMZ.js.map} +0 -0
@@ -6,16 +6,12 @@
6
6
 
7
7
  export interface ImportProgressTranslations {
8
8
  phaseStarted: string;
9
- phaseEntityTypes: string;
10
- phaseResources: string;
11
- phaseAnnotations: string;
12
9
  phaseComplete: string;
13
10
  phaseError: string;
14
- hashChainValid: string;
15
- hashChainInvalid: string;
16
- streams: string;
17
- events: string;
18
- blobs: string;
11
+ statsEventsReplayed: string;
12
+ statsResourcesCreated: string;
13
+ statsAnnotationsCreated: string;
14
+ statsEntityTypesAdded: string;
19
15
  }
20
16
 
21
17
  export interface ImportProgressProps {
@@ -27,13 +23,17 @@ export interface ImportProgressProps {
27
23
 
28
24
  const PHASE_LABELS: Record<string, keyof ImportProgressTranslations> = {
29
25
  started: 'phaseStarted',
30
- 'entity-types': 'phaseEntityTypes',
31
- resources: 'phaseResources',
32
- annotations: 'phaseAnnotations',
33
26
  complete: 'phaseComplete',
34
27
  error: 'phaseError',
35
28
  };
36
29
 
30
+ const STAT_LABELS: Record<string, keyof ImportProgressTranslations> = {
31
+ eventsReplayed: 'statsEventsReplayed',
32
+ resourcesCreated: 'statsResourcesCreated',
33
+ annotationsCreated: 'statsAnnotationsCreated',
34
+ entityTypesAdded: 'statsEntityTypesAdded',
35
+ };
36
+
37
37
  export function ImportProgress({ phase, message, result, translations: t }: ImportProgressProps) {
38
38
  const labelKey = PHASE_LABELS[phase];
39
39
  const phaseLabel = labelKey ? t[labelKey] : phase;
@@ -41,6 +41,13 @@ export function ImportProgress({ phase, message, result, translations: t }: Impo
41
41
  const isComplete = phase === 'complete';
42
42
  const isError = phase === 'error';
43
43
 
44
+ const stats = result?.stats != null && typeof result.stats === 'object'
45
+ ? (result.stats as Record<string, unknown>)
46
+ : undefined;
47
+ const statEntries = stats
48
+ ? Object.entries(stats).filter((entry): entry is [string, number] => typeof entry[1] === 'number')
49
+ : [];
50
+
44
51
  return (
45
52
  <div className="semiont-exchange__progress">
46
53
  <div className={`semiont-exchange__phase-label${isError ? ' semiont-exchange__phase-label--error' : isComplete ? ' semiont-exchange__phase-label--complete' : ''}`}>
@@ -51,30 +58,17 @@ export function ImportProgress({ phase, message, result, translations: t }: Impo
51
58
  <p className="semiont-exchange__progress-message">{message}</p>
52
59
  )}
53
60
 
54
- {isComplete && result && (
61
+ {isComplete && statEntries.length > 0 && (
55
62
  <div className="semiont-exchange__result">
56
- {result.stats != null && typeof result.stats === 'object' && (
57
- <>
58
- {Object.entries(result.stats as Record<string, number>).map(([key, value]) => {
59
- const label = key === 'streams' ? t.streams
60
- : key === 'events' ? t.events
61
- : key === 'blobs' ? t.blobs
62
- : key;
63
- return (
64
- <div key={key} className="semiont-exchange__result-stat">
65
- <span className="semiont-exchange__result-value">{value}</span>
66
- <span className="semiont-exchange__result-label">{label}</span>
67
- </div>
68
- );
69
- })}
70
- </>
71
- )}
72
-
73
- {result.hashChainValid !== undefined && (
74
- <div className={`semiont-exchange__hash-badge${result.hashChainValid ? ' semiont-exchange__hash-badge--valid' : ' semiont-exchange__hash-badge--invalid'}`}>
75
- {result.hashChainValid ? t.hashChainValid : t.hashChainInvalid}
76
- </div>
77
- )}
63
+ {statEntries.map(([key, value]) => {
64
+ const statKey = STAT_LABELS[key];
65
+ return (
66
+ <div key={key} className="semiont-exchange__result-stat">
67
+ <span className="semiont-exchange__result-value">{value}</span>
68
+ <span className="semiont-exchange__result-label">{statKey ? t[statKey] : key}</span>
69
+ </div>
70
+ );
71
+ })}
78
72
  </div>
79
73
  )}
80
74
 
@@ -45,6 +45,8 @@ const mockTranslations = {
45
45
  errorEmailRequired: 'Email is required',
46
46
  errorPasswordRequired: 'Password is required',
47
47
  errorBackendUrlRequired: 'Backend URL is required',
48
+ errorBackendUrlInvalid: 'Backend URL is not valid',
49
+ errorBackendUrlUnreachable: 'Backend URL is unreachable',
48
50
  tagline: 'make meaning',
49
51
  };
50
52
 
@@ -54,7 +54,7 @@ describe('SignUpForm - Accessibility', () => {
54
54
  });
55
55
 
56
56
  it('should have no accessibility violations during loading state', async () => {
57
- const onSignUp = vi.fn(() => new Promise(() => {})); // Never resolves
57
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {})); // Never resolves
58
58
 
59
59
  const { container } = render(
60
60
  <SignUpForm
@@ -138,7 +138,7 @@ describe('SignUpForm - Accessibility', () => {
138
138
  });
139
139
 
140
140
  it('should disable button during loading state', async () => {
141
- const onSignUp = vi.fn(() => new Promise(() => {}));
141
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
142
142
 
143
143
  render(
144
144
  <SignUpForm
@@ -197,13 +197,12 @@ describe('SignUpForm - Accessibility', () => {
197
197
  it('should have Google icon inside button', () => {
198
198
  const onSignUp = vi.fn();
199
199
 
200
- const { container } = render(
201
- <SignUpForm
202
- onSignUp={onSignUp}
203
- Link={MockLink}
204
- translations={mockTranslations}
205
- />
206
- );
200
+ render(
201
+ <SignUpForm
202
+ onSignUp={onSignUp}
203
+ Link={MockLink}
204
+ translations={mockTranslations} />
205
+ );
207
206
 
208
207
  const button = screen.getByRole('button');
209
208
  const svg = button.querySelector('svg');
@@ -214,7 +213,7 @@ describe('SignUpForm - Accessibility', () => {
214
213
  });
215
214
 
216
215
  it('should show loading spinner during sign-up', async () => {
217
- const onSignUp = vi.fn(() => new Promise(() => {}));
216
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
218
217
 
219
218
  const { container } = render(
220
219
  <SignUpForm
@@ -349,7 +348,7 @@ describe('SignUpForm - Accessibility', () => {
349
348
 
350
349
  describe('Loading State Accessibility', () => {
351
350
  it('should announce loading state to screen readers', async () => {
352
- const onSignUp = vi.fn(() => new Promise(() => {}));
351
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
353
352
 
354
353
  render(
355
354
  <SignUpForm
@@ -368,7 +367,7 @@ describe('SignUpForm - Accessibility', () => {
368
367
  });
369
368
 
370
369
  it('should maintain button focus during loading', async () => {
371
- const onSignUp = vi.fn(() => new Promise(() => {}));
370
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>(() => {}));
372
371
 
373
372
  render(
374
373
  <SignUpForm
@@ -53,7 +53,7 @@ describe('SignUpForm', () => {
53
53
 
54
54
  describe('Sign-Up Interaction', () => {
55
55
  it('calls onSignUp when button is clicked', async () => {
56
- const onSignUp = vi.fn<[], Promise<void>>().mockResolvedValue(undefined);
56
+ const onSignUp = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
57
57
  render(<SignUpForm onSignUp={onSignUp} Link={MockLink} translations={mockTranslations} />);
58
58
 
59
59
  const button = screen.getByRole('button', { name: /Continue with Google/i });
@@ -63,7 +63,7 @@ describe('SignUpForm', () => {
63
63
  });
64
64
 
65
65
  it('shows loading state while signing up', async () => {
66
- const onSignUp = vi.fn<[], Promise<void>>(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
66
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
67
67
  render(<SignUpForm onSignUp={onSignUp} Link={MockLink} translations={mockTranslations} />);
68
68
 
69
69
  const button = screen.getByRole('button');
@@ -83,7 +83,7 @@ describe('SignUpForm', () => {
83
83
  });
84
84
 
85
85
  it('disables button during loading', async () => {
86
- const onSignUp = vi.fn<[], Promise<void>>(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
86
+ const onSignUp = vi.fn<() => Promise<void>>(() => new Promise<void>((resolve) => setTimeout(resolve, 100)));
87
87
  render(<SignUpForm onSignUp={onSignUp} Link={MockLink} translations={mockTranslations} />);
88
88
 
89
89
  const button = screen.getByRole('button');
@@ -13,6 +13,7 @@ import {
13
13
  } from '@heroicons/react/24/outline';
14
14
  import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
15
15
  import type { TagSchema } from '@semiont/sdk';
16
+ export type { TagSchema };
16
17
 
17
18
  export interface TagSchemasPageProps {
18
19
  // Data props
@@ -45,16 +45,12 @@ const createProps = (overrides?: Partial<LinkedDataPageProps>): LinkedDataPagePr
45
45
  },
46
46
  progress: {
47
47
  phaseStarted: 'Starting…',
48
- phaseEntityTypes: 'Entity types…',
49
- phaseResources: 'Resources…',
50
- phaseAnnotations: 'Annotations…',
51
48
  phaseComplete: 'Complete',
52
49
  phaseError: 'Failed',
53
- hashChainValid: 'Verified',
54
- hashChainInvalid: 'Verification failed',
55
- streams: 'Resources',
56
- events: 'Annotations',
57
- blobs: 'Entity types',
50
+ statsEventsReplayed: 'Events replayed',
51
+ statsResourcesCreated: 'Resources created',
52
+ statsAnnotationsCreated: 'Annotations created',
53
+ statsEntityTypesAdded: 'Entity types added',
58
54
  },
59
55
  },
60
56
  ToolbarPanels: () => <div data-testid="toolbar-panels" />,
@@ -100,9 +96,15 @@ describe('LinkedDataPage', () => {
100
96
  it('renders ImportProgress with result on completion', () => {
101
97
  render(<LinkedDataPage {...createProps({
102
98
  importPhase: 'complete',
103
- importResult: { resourcesCreated: 5, annotationsCreated: 12, entityTypesAdded: 3 },
99
+ importResult: { stats: { resourcesCreated: 5, annotationsCreated: 12, entityTypesAdded: 3 } },
104
100
  })} />);
105
101
  expect(screen.getByText('Complete')).toBeInTheDocument();
102
+ expect(screen.getByText('5')).toBeInTheDocument();
103
+ expect(screen.getByText('Resources created')).toBeInTheDocument();
104
+ expect(screen.getByText('12')).toBeInTheDocument();
105
+ expect(screen.getByText('Annotations created')).toBeInTheDocument();
106
+ expect(screen.getByText('3')).toBeInTheDocument();
107
+ expect(screen.getByText('Entity types added')).toBeInTheDocument();
106
108
  });
107
109
 
108
110
  it('applies panel-open class when common panel is active', () => {
@@ -8,7 +8,7 @@
8
8
  import { describe, it, expect, vi } from 'vitest';
9
9
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10
10
  import { ResourceComposePage } from '../components/ResourceComposePage';
11
- import type { ResourceComposePageProps, SaveResourceParams } from '../components/ResourceComposePage';
11
+ import type { ResourceComposePageProps } from '../components/ResourceComposePage';
12
12
  import { createTestSemiontWrapper } from '../../../test-utils';
13
13
 
14
14
  // Mock CodeMirrorRenderer to avoid CodeMirror dependencies
@@ -63,6 +63,7 @@ const createMockProps = (overrides?: Partial<ResourceComposePageProps>): Resourc
63
63
  initialLocale: 'en',
64
64
  theme: 'light',
65
65
  showLineNumbers: false,
66
+ hoverDelayMs: 0,
66
67
  activePanel: null,
67
68
  onSaveResource: vi.fn().mockResolvedValue(undefined),
68
69
  onCancel: vi.fn(),
@@ -8,7 +8,7 @@
8
8
 
9
9
  import React, { useState, useEffect } from 'react';
10
10
  import type { GatheredContext } from '@semiont/core';
11
- import { isImageMimeType, isPdfMimeType, LOCALES } from '@semiont/core';
11
+ import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
12
12
  import type { UploadProgress } from '@semiont/sdk';
13
13
  import { type CloneData, type ReferenceData } from '../state/compose-page-state-unit';
14
14
  import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
@@ -17,6 +17,29 @@ import { CodeMirrorRenderer } from '../../../components/CodeMirrorRenderer';
17
17
  import { useFormAnnouncements } from '../../../components/LiveRegion';
18
18
  import { UploadProgressBar } from './UploadProgressBar';
19
19
 
20
+ /**
21
+ * Big tent: every registry type is uploadable. The `accept` hint lists all
22
+ * known extensions — it's only a hint; `handleFileUpload` never rejects.
23
+ */
24
+ const UPLOAD_ACCEPT = Object.values(MEDIA_TYPES).map((c) => c.extension).join(',');
25
+
26
+ /**
27
+ * Detection chain for uploaded files (same spirit as the CLI):
28
+ * 1. the browser's `file.type`, if it names a registry member;
29
+ * 2. otherwise the filename extension (`file.type` is routinely empty for
30
+ * `.md` and friends);
31
+ * 3. otherwise `application/octet-stream` — itself a registry member, so
32
+ * under the big tent every file is uploadable.
33
+ */
34
+ function detectUploadMediaType(file: File): string {
35
+ if (file.type && isSupportedMediaType(file.type)) return file.type;
36
+ if (file.name.includes('.')) {
37
+ const byExt = mediaTypeForExtension(file.name.slice(file.name.lastIndexOf('.')));
38
+ if (byExt) return byExt;
39
+ }
40
+ return 'application/octet-stream';
41
+ }
42
+
20
43
  export interface ResourceComposePageProps {
21
44
  mode: 'new' | 'clone' | 'reference';
22
45
  cloneData?: CloneData | null;
@@ -170,8 +193,9 @@ export function ResourceComposePage({
170
193
  const file = e.target.files?.[0];
171
194
  if (!file) return;
172
195
 
196
+ const detectedMediaType = detectUploadMediaType(file);
173
197
  setUploadedFile(file);
174
- setFileMimeType(file.type);
198
+ setFileMimeType(detectedMediaType);
175
199
  setInputMethod('upload');
176
200
 
177
201
  // Set file name as default resource name if empty
@@ -180,8 +204,9 @@ export function ResourceComposePage({
180
204
  setNewResourceName(nameWithoutExt);
181
205
  }
182
206
 
183
- // For images and PDFs, create preview URL
184
- if (isImageMimeType(file.type) || isPdfMimeType(file.type)) {
207
+ // Images and PDFs get an object-URL preview; everything else reads as text.
208
+ const detectedRender = capabilitiesOf(detectedMediaType)?.render;
209
+ if (detectedRender === 'image' || detectedRender === 'pdf') {
185
210
  const previewUrl = URL.createObjectURL(file);
186
211
  setFilePreviewUrl(previewUrl);
187
212
  } else {
@@ -521,7 +546,7 @@ export function ResourceComposePage({
521
546
  <div className="semiont-form__upload-area">
522
547
  <input
523
548
  type="file"
524
- accept="text/plain,text/markdown,image/png,image/jpeg,application/pdf"
549
+ accept={UPLOAD_ACCEPT}
525
550
  onChange={handleFileUpload}
526
551
  className="semiont-form__upload-input"
527
552
  disabled={isCreating}
@@ -563,7 +588,7 @@ export function ResourceComposePage({
563
588
  )}
564
589
 
565
590
  {/* Image Preview */}
566
- {uploadedFile && filePreviewUrl && isImageMimeType(fileMimeType) && (
591
+ {uploadedFile && filePreviewUrl && capabilitiesOf(fileMimeType)?.render === 'image' && (
567
592
  <div className="semiont-form__image-preview">
568
593
  <p className="semiont-form__image-preview-label">Preview:</p>
569
594
  <div className="semiont-form__image-preview-container">
@@ -594,9 +619,11 @@ export function ResourceComposePage({
594
619
  disabled={isCreating}
595
620
  className="semiont-select"
596
621
  >
597
- <option value="text/markdown">Markdown (text/markdown)</option>
598
- <option value="text/plain">Plain Text (text/plain)</option>
599
- <option value="text/html">HTML (text/html)</option>
622
+ {AUTHORABLE_MEDIA_TYPES.map((mt) => (
623
+ <option key={mt} value={mt}>
624
+ {capabilitiesOf(mt)?.label ?? mt} ({mt})
625
+ </option>
626
+ ))}
600
627
  </select>
601
628
  </div>
602
629
  )}
@@ -1,11 +1,11 @@
1
1
  import { BehaviorSubject, type Observable, map } from 'rxjs';
2
- import type { GatheredContext, AnnotationId, ContentFormat, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
2
+ import type { GatheredContext, AnnotationId, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
3
3
  import { resourceId as makeResourceId, annotationId as makeAnnotationId } from '@semiont/core';
4
4
  import { createDisposer } from '@semiont/sdk';
5
5
  import type { StateUnit } from '@semiont/sdk';
6
6
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
7
7
  import type { SemiontClient } from '@semiont/sdk';
8
- import { getPrimaryMediaType, decodeWithCharset } from '@semiont/core';
8
+ import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
9
9
  import type { UploadProgress } from '@semiont/sdk';
10
10
 
11
11
  export type ComposeMode = 'new' | 'clone' | 'reference';
@@ -108,11 +108,8 @@ export function createComposePageStateUnit(
108
108
  const tokenResult = await client.yield.fromToken(params.token!);
109
109
  if (tokenResult && auth) {
110
110
  const rId = makeResourceId(tokenResult['@id']);
111
- const mediaType = getPrimaryMediaType(tokenResult) || 'text/plain';
112
- const { data } = await client.browse.resourceRepresentation(rId, {
113
- accept: mediaType as ContentFormat,
114
- });
115
- const content = decodeWithCharset(data, mediaType);
111
+ const { data, contentType } = await client.browse.resourceRepresentation(rId);
112
+ const content = decodeWithCharset(data, contentType);
116
113
  cloneData$.next({ sourceResource: tokenResult, sourceContent: content });
117
114
  }
118
115
  } catch {
@@ -143,7 +140,7 @@ export function createComposePageStateUnit(
143
140
  mimeType = saveParams.format ?? 'application/octet-stream';
144
141
  } else {
145
142
  const blob = new Blob([saveParams.content || ''], { type: saveParams.format ?? 'application/octet-stream' });
146
- const extension = saveParams.format === 'text/plain' ? '.txt' : saveParams.format === 'text/html' ? '.html' : '.md';
143
+ const extension = extensionForMediaType(saveParams.format ?? 'application/octet-stream');
147
144
  fileToUpload = new File([blob], saveParams.name + extension, { type: saveParams.format ?? 'application/octet-stream' });
148
145
  mimeType = saveParams.format ?? 'application/octet-stream';
149
146
  }
@@ -7,7 +7,6 @@
7
7
  import { describe, it, expect, vi } from 'vitest';
8
8
  import { render, screen, fireEvent } from '@testing-library/react';
9
9
  import { ResourceCard } from '../components/ResourceCard';
10
- import type { ResourceCardProps } from '../components/ResourceCard';
11
10
 
12
11
  const createMockResource = (overrides?: any) => ({
13
12
  '@context': 'https://www.w3.org/ns/anno.jsonld',
@@ -10,10 +10,12 @@ import { render, screen, fireEvent } from '@testing-library/react';
10
10
  import { ResourceDiscoveryPage } from '../components/ResourceDiscoveryPage';
11
11
  import type { ResourceDiscoveryPageProps } from '../components/ResourceDiscoveryPage';
12
12
  import { createTestSemiontWrapper } from '../../../test-utils';
13
+ import { resourceId } from '@semiont/core';
14
+ import type { ResourceDescriptor } from '@semiont/core';
13
15
 
14
- const createMockResource = (id: string, name: string, entityTypes: string[] = []) => ({
16
+ const createMockResource = (id: string, name: string, entityTypes: string[] = []): ResourceDescriptor => ({
15
17
  '@context': 'https://www.w3.org/ns/anno.jsonld',
16
- '@id': id,
18
+ '@id': resourceId(id),
17
19
  '@type': 'schema:DigitalDocument',
18
20
  name,
19
21
  description: `Description for ${name}`,
@@ -31,6 +33,8 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
31
33
  isSearching: false,
32
34
  searchQuery: '',
33
35
  onSearchQueryChange: vi.fn(),
36
+ selectedEntityType: '',
37
+ onSelectedEntityTypeChange: vi.fn(),
34
38
  theme: 'light',
35
39
  showLineNumbers: false,
36
40
  activePanel: null,
@@ -254,65 +258,59 @@ describe('ResourceDiscoveryPage', () => {
254
258
  expect(screen.getByRole('button', { name: 'Report' })).toBeInTheDocument();
255
259
  });
256
260
 
257
- it('filters documents by entity type', () => {
261
+ it('calls onSelectedEntityTypeChange when a filter chip is clicked', () => {
262
+ const onSelectedEntityTypeChange = vi.fn();
258
263
  const props = createMockProps({
259
- recentDocuments: [
260
- createMockResource('1', 'Doc 1', ['Document']),
261
- createMockResource('2', 'Doc 2', ['Article']),
262
- createMockResource('3', 'Doc 3', ['Document']),
263
- ],
264
264
  entityTypes: ['Document', 'Article'],
265
+ onSelectedEntityTypeChange,
265
266
  });
266
267
  renderWithProviders(<ResourceDiscoveryPage {...props} />);
267
268
 
268
- // Initially all documents shown
269
- expect(screen.getByText('Doc 1')).toBeInTheDocument();
270
- expect(screen.getByText('Doc 2')).toBeInTheDocument();
271
- expect(screen.getByText('Doc 3')).toBeInTheDocument();
272
-
273
- // Filter by Document
274
- const documentButton = screen.getByRole('button', { name: 'Document' });
275
- fireEvent.click(documentButton);
269
+ fireEvent.click(screen.getByRole('button', { name: 'Document' }));
270
+ expect(onSelectedEntityTypeChange).toHaveBeenCalledWith('Document');
276
271
 
277
- expect(screen.getByText('Doc 1')).toBeInTheDocument();
278
- expect(screen.queryByText('Doc 2')).not.toBeInTheDocument();
279
- expect(screen.getByText('Doc 3')).toBeInTheDocument();
272
+ fireEvent.click(screen.getByRole('button', { name: 'Article' }));
273
+ expect(onSelectedEntityTypeChange).toHaveBeenCalledWith('Article');
280
274
  });
281
275
 
282
- it('shows filtered heading when entity type selected', () => {
276
+ it('shows filtered heading when selectedEntityType prop is set', () => {
283
277
  const props = createMockProps({
284
278
  recentDocuments: [createMockResource('1', 'Doc 1', ['Document'])],
285
279
  entityTypes: ['Document'],
280
+ selectedEntityType: 'Document',
286
281
  });
287
282
  renderWithProviders(<ResourceDiscoveryPage {...props} />);
288
283
 
289
- const documentButton = screen.getByRole('button', { name: 'Document' });
290
- fireEvent.click(documentButton);
291
-
292
284
  expect(screen.getByText('Documents tagged with Document')).toBeInTheDocument();
293
285
  });
294
286
 
295
- it('resets filter when "All" button clicked', () => {
287
+ it('calls onSelectedEntityTypeChange with empty string when "All" is clicked', () => {
288
+ const onSelectedEntityTypeChange = vi.fn();
289
+ const props = createMockProps({
290
+ entityTypes: ['Document', 'Article'],
291
+ selectedEntityType: 'Document',
292
+ onSelectedEntityTypeChange,
293
+ });
294
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
295
+
296
+ fireEvent.click(screen.getByRole('button', { name: 'All' }));
297
+ expect(onSelectedEntityTypeChange).toHaveBeenCalledWith('');
298
+ });
299
+
300
+ it('renders the recentDocuments prop as-is without applying any post-filter', () => {
301
+ // The component is now controlled — backend filtering means
302
+ // `recentDocuments` already contains only the resources matching the
303
+ // active `selectedEntityType`. The component must not re-filter.
296
304
  const props = createMockProps({
297
305
  recentDocuments: [
298
306
  createMockResource('1', 'Doc 1', ['Document']),
299
307
  createMockResource('2', 'Doc 2', ['Article']),
300
308
  ],
301
309
  entityTypes: ['Document', 'Article'],
310
+ selectedEntityType: 'Document',
302
311
  });
303
312
  renderWithProviders(<ResourceDiscoveryPage {...props} />);
304
313
 
305
- // Filter by Document
306
- const documentButton = screen.getByRole('button', { name: 'Document' });
307
- fireEvent.click(documentButton);
308
-
309
- expect(screen.getByText('Doc 1')).toBeInTheDocument();
310
- expect(screen.queryByText('Doc 2')).not.toBeInTheDocument();
311
-
312
- // Click All
313
- const allButton = screen.getByRole('button', { name: 'All' });
314
- fireEvent.click(allButton);
315
-
316
314
  expect(screen.getByText('Doc 1')).toBeInTheDocument();
317
315
  expect(screen.getByText('Doc 2')).toBeInTheDocument();
318
316
  });
@@ -5,7 +5,7 @@
5
5
  * All dependencies passed as props - no Next.js hooks!
6
6
  */
7
7
 
8
- import React, { useState, useCallback, useRef } from 'react';
8
+ import React, { useCallback, useRef } from 'react';
9
9
  import { getResourceId } from '@semiont/core';
10
10
  import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
11
11
  import { useRovingTabIndex } from '../../../hooks/useRovingTabIndex';
@@ -26,6 +26,11 @@ export interface ResourceDiscoveryPageProps {
26
26
  searchQuery: string;
27
27
  onSearchQueryChange: (query: string) => void;
28
28
 
29
+ // Controlled entity-type filter — owned by the state unit so filtering
30
+ // pushes to the backend rather than running as a post-fetch array filter.
31
+ selectedEntityType: string;
32
+ onSelectedEntityTypeChange: (entityType: string) => void;
33
+
29
34
  // UI state props
30
35
  theme: 'light' | 'dark';
31
36
  showLineNumbers: boolean;
@@ -67,6 +72,8 @@ export function ResourceDiscoveryPage({
67
72
  isSearching,
68
73
  searchQuery,
69
74
  onSearchQueryChange,
75
+ selectedEntityType,
76
+ onSelectedEntityTypeChange,
70
77
  theme,
71
78
  showLineNumbers,
72
79
  activePanel,
@@ -75,17 +82,11 @@ export function ResourceDiscoveryPage({
75
82
  translations: t,
76
83
  ToolbarPanels,
77
84
  }: ResourceDiscoveryPageProps) {
78
- const [selectedEntityType, setSelectedEntityType] = useState<string>('');
79
-
80
85
  const hasSearchQuery = searchQuery.trim() !== '';
81
86
 
82
87
  // When searching, render search results; otherwise render recent.
83
- const baseDocuments = hasSearchQuery ? searchDocuments : recentDocuments;
84
- const filteredResources = !selectedEntityType
85
- ? baseDocuments
86
- : baseDocuments.filter((resource: ResourceDescriptor) =>
87
- resource.entityTypes && resource.entityTypes.includes(selectedEntityType)
88
- );
88
+ // Both already arrive entity-type-filtered from the backend — no post-filter here.
89
+ const filteredResources = hasSearchQuery ? searchDocuments : recentDocuments;
89
90
 
90
91
  // Roving tabindex for entity type filters
91
92
  const entityFilterRoving = useRovingTabIndex<HTMLDivElement>(
@@ -105,8 +106,8 @@ export function ResourceDiscoveryPage({
105
106
 
106
107
  // Memoized callbacks
107
108
  const handleEntityTypeFilter = useCallback((entityType: string) => {
108
- setSelectedEntityType(entityType);
109
- }, []);
109
+ onSelectedEntityTypeChange(entityType);
110
+ }, [onSelectedEntityTypeChange]);
110
111
 
111
112
  const openResource = useCallback((resource: ResourceDescriptor) => {
112
113
  const resourceId = getResourceId(resource);