@semiont/react-ui 0.5.22 → 0.5.24

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 (197) hide show
  1. package/dist/{ar-ZHKO4XDR.js → ar-JRTKCMTF.js} +11 -4
  2. package/dist/ar-JRTKCMTF.js.map +1 -0
  3. package/dist/{bn-3BUDKAK5.js → bn-LO6EDSCC.js} +11 -4
  4. package/dist/bn-LO6EDSCC.js.map +1 -0
  5. package/dist/{chunk-PFUXQPWJ.js → chunk-4QMTEPQ3.js} +92 -60
  6. package/dist/chunk-4QMTEPQ3.js.map +1 -0
  7. package/dist/{chunk-R52YAGHK.js → chunk-5Y5JDCKC.js} +11 -4
  8. package/dist/chunk-5Y5JDCKC.js.map +1 -0
  9. package/dist/{cs-2EFEUKTW.js → cs-LCHHOR4O.js} +11 -4
  10. package/dist/cs-LCHHOR4O.js.map +1 -0
  11. package/dist/{da-SUD3Q44Q.js → da-TARGPMJR.js} +11 -4
  12. package/dist/da-TARGPMJR.js.map +1 -0
  13. package/dist/{de-6IC5QRX7.js → de-SBH4QBPJ.js} +11 -4
  14. package/dist/de-SBH4QBPJ.js.map +1 -0
  15. package/dist/{el-Q4NAXFRC.js → el-2D2OLGWB.js} +11 -4
  16. package/dist/el-2D2OLGWB.js.map +1 -0
  17. package/dist/{en-P2L4X27N.js → en-QFIEG3CA.js} +2 -2
  18. package/dist/{es-YUKZBD4W.js → es-U6YK2LX6.js} +11 -4
  19. package/dist/es-U6YK2LX6.js.map +1 -0
  20. package/dist/{fa-OJ47LILE.js → fa-S7SZE37M.js} +11 -4
  21. package/dist/fa-S7SZE37M.js.map +1 -0
  22. package/dist/{fi-QFFL5CUB.js → fi-CBL3NIXN.js} +11 -4
  23. package/dist/fi-CBL3NIXN.js.map +1 -0
  24. package/dist/{fr-BOQZZWP4.js → fr-EABUHV6S.js} +11 -4
  25. package/dist/fr-EABUHV6S.js.map +1 -0
  26. package/dist/{he-LXDY3TFA.js → he-THWD244Y.js} +11 -4
  27. package/dist/he-THWD244Y.js.map +1 -0
  28. package/dist/{hi-HOJER77V.js → hi-GHGFZJ44.js} +11 -4
  29. package/dist/hi-GHGFZJ44.js.map +1 -0
  30. package/dist/{id-I5WQ6EWD.js → id-POZOUMDW.js} +11 -4
  31. package/dist/id-POZOUMDW.js.map +1 -0
  32. package/dist/index.d.ts +269 -69
  33. package/dist/index.js +2080 -1861
  34. package/dist/index.js.map +1 -1
  35. package/dist/{it-UYH642IJ.js → it-KDYJADSH.js} +11 -4
  36. package/dist/it-KDYJADSH.js.map +1 -0
  37. package/dist/{ja-JTVWAC4L.js → ja-Z5Z6ZF3P.js} +11 -4
  38. package/dist/ja-Z5Z6ZF3P.js.map +1 -0
  39. package/dist/{ko-IHXFMOIZ.js → ko-7X3XUXRN.js} +11 -4
  40. package/dist/ko-7X3XUXRN.js.map +1 -0
  41. package/dist/{ms-JH25VFLO.js → ms-BTWEIW43.js} +11 -4
  42. package/dist/ms-BTWEIW43.js.map +1 -0
  43. package/dist/{nl-SURXW7IQ.js → nl-IS2CGF5I.js} +11 -4
  44. package/dist/nl-IS2CGF5I.js.map +1 -0
  45. package/dist/{no-HXIKQFDZ.js → no-AAMEVA26.js} +11 -4
  46. package/dist/no-AAMEVA26.js.map +1 -0
  47. package/dist/{pl-7A4KU6XK.js → pl-PVXUQYFT.js} +11 -4
  48. package/dist/pl-PVXUQYFT.js.map +1 -0
  49. package/dist/{pt-6MGYP7Q5.js → pt-3ITQ76TL.js} +11 -4
  50. package/dist/pt-3ITQ76TL.js.map +1 -0
  51. package/dist/{ro-IROCHNIA.js → ro-FRB4IIVS.js} +11 -4
  52. package/dist/ro-FRB4IIVS.js.map +1 -0
  53. package/dist/{sv-WDJD75CV.js → sv-DS3RO24M.js} +11 -4
  54. package/dist/sv-DS3RO24M.js.map +1 -0
  55. package/dist/test-utils.js +9 -3
  56. package/dist/test-utils.js.map +1 -1
  57. package/dist/{th-RIO2E6B3.js → th-AX66MWWB.js} +11 -4
  58. package/dist/th-AX66MWWB.js.map +1 -0
  59. package/dist/{tr-TZ7ERCIN.js → tr-7KTB67TT.js} +11 -4
  60. package/dist/tr-7KTB67TT.js.map +1 -0
  61. package/dist/{uk-ZFWDKLUE.js → uk-SMQS2MAQ.js} +11 -4
  62. package/dist/uk-SMQS2MAQ.js.map +1 -0
  63. package/dist/{vi-UIOSP2JG.js → vi-D3JXL2EZ.js} +11 -4
  64. package/dist/vi-D3JXL2EZ.js.map +1 -0
  65. package/dist/{zh-6R6YUY46.js → zh-S754AWFS.js} +11 -4
  66. package/dist/zh-S754AWFS.js.map +1 -0
  67. package/package.json +4 -4
  68. package/src/components/annotation-popups/JsonLdView.tsx +1 -1
  69. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +8 -2
  70. package/src/components/modals/ReferenceWizardModal.tsx +2 -1
  71. package/src/components/modals/ResourceGenerateModal.tsx +10 -4
  72. package/src/components/modals/ResourceSearchModal.tsx +3 -1
  73. package/src/components/modals/SearchModal.tsx +3 -1
  74. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +5 -0
  75. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +2 -1
  76. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +2 -1
  77. package/src/components/resource/AnnotateView.tsx +70 -138
  78. package/src/components/resource/AnnotationHistory.tsx +53 -17
  79. package/src/components/resource/BrowseView.tsx +3 -2
  80. package/src/components/resource/ResourceViewer.tsx +39 -22
  81. package/src/components/resource/__tests__/AnnotateView.registry.test.tsx +114 -0
  82. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +63 -41
  83. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +2 -1
  84. package/src/components/resource/__tests__/ResourceViewer.multi-mount-invalidation.test.tsx +140 -0
  85. package/src/components/resource/__tests__/ResourceViewer.registry.test.tsx +89 -0
  86. package/src/components/resource/annotate-renderers.tsx +155 -0
  87. package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
  88. package/src/components/resource/panels/ReferencesPanel.tsx +38 -3
  89. package/src/components/resource/panels/TagEntry.tsx +5 -1
  90. package/src/components/resource/panels/TaggingPanel.tsx +6 -1
  91. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +41 -0
  92. package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +8 -2
  93. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +90 -0
  94. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +2 -2
  95. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +2 -2
  96. package/src/components/resource/panels/__tests__/UnifiedAnnotationsPanel.failure.test.tsx +78 -0
  97. package/src/components/settings/SettingsPanel.tsx +6 -2
  98. package/src/components/settings/__tests__/SettingsPanel.test.tsx +28 -3
  99. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +3 -11
  100. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -3
  101. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -1
  102. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -3
  103. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +1 -1
  104. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -8
  105. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -3
  106. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +10 -9
  107. package/src/features/admin-security/state/admin-security-state-unit.ts +3 -2
  108. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -1
  109. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -3
  110. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +7 -6
  111. package/src/features/admin-users/state/admin-users-state-unit.ts +3 -2
  112. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +36 -26
  113. package/src/features/auth-welcome/state/welcome-state-unit.ts +7 -2
  114. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -2
  115. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +0 -3
  116. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +37 -12
  117. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +9 -10
  118. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +3 -5
  119. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +0 -3
  120. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -5
  121. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +0 -3
  122. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -1
  123. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -3
  124. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +17 -7
  125. package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -3
  126. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +33 -12
  127. package/src/features/resource-compose/state/compose-page-state-unit.ts +9 -8
  128. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +3 -2
  129. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +25 -3
  130. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +90 -25
  131. package/src/features/resource-discovery/state/discover-state-unit.ts +30 -30
  132. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +9 -5
  133. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +70 -40
  134. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +111 -8
  135. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +167 -27
  136. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +74 -9
  137. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +29 -28
  138. package/translations/ar.json +10 -3
  139. package/translations/bn.json +10 -3
  140. package/translations/cs.json +10 -3
  141. package/translations/da.json +10 -3
  142. package/translations/de.json +10 -3
  143. package/translations/el.json +10 -3
  144. package/translations/en.json +10 -3
  145. package/translations/es.json +10 -3
  146. package/translations/fa.json +10 -3
  147. package/translations/fi.json +10 -3
  148. package/translations/fr.json +10 -3
  149. package/translations/he.json +10 -3
  150. package/translations/hi.json +10 -3
  151. package/translations/id.json +10 -3
  152. package/translations/it.json +10 -3
  153. package/translations/ja.json +10 -3
  154. package/translations/ko.json +10 -3
  155. package/translations/ms.json +10 -3
  156. package/translations/nl.json +10 -3
  157. package/translations/no.json +10 -3
  158. package/translations/pl.json +10 -3
  159. package/translations/pt.json +10 -3
  160. package/translations/ro.json +10 -3
  161. package/translations/sv.json +10 -3
  162. package/translations/th.json +10 -3
  163. package/translations/tr.json +10 -3
  164. package/translations/uk.json +10 -3
  165. package/translations/vi.json +10 -3
  166. package/translations/zh.json +10 -3
  167. package/dist/ar-ZHKO4XDR.js.map +0 -1
  168. package/dist/bn-3BUDKAK5.js.map +0 -1
  169. package/dist/chunk-PFUXQPWJ.js.map +0 -1
  170. package/dist/chunk-R52YAGHK.js.map +0 -1
  171. package/dist/cs-2EFEUKTW.js.map +0 -1
  172. package/dist/da-SUD3Q44Q.js.map +0 -1
  173. package/dist/de-6IC5QRX7.js.map +0 -1
  174. package/dist/el-Q4NAXFRC.js.map +0 -1
  175. package/dist/es-YUKZBD4W.js.map +0 -1
  176. package/dist/fa-OJ47LILE.js.map +0 -1
  177. package/dist/fi-QFFL5CUB.js.map +0 -1
  178. package/dist/fr-BOQZZWP4.js.map +0 -1
  179. package/dist/he-LXDY3TFA.js.map +0 -1
  180. package/dist/hi-HOJER77V.js.map +0 -1
  181. package/dist/id-I5WQ6EWD.js.map +0 -1
  182. package/dist/it-UYH642IJ.js.map +0 -1
  183. package/dist/ja-JTVWAC4L.js.map +0 -1
  184. package/dist/ko-IHXFMOIZ.js.map +0 -1
  185. package/dist/ms-JH25VFLO.js.map +0 -1
  186. package/dist/nl-SURXW7IQ.js.map +0 -1
  187. package/dist/no-HXIKQFDZ.js.map +0 -1
  188. package/dist/pl-7A4KU6XK.js.map +0 -1
  189. package/dist/pt-6MGYP7Q5.js.map +0 -1
  190. package/dist/ro-IROCHNIA.js.map +0 -1
  191. package/dist/sv-WDJD75CV.js.map +0 -1
  192. package/dist/th-RIO2E6B3.js.map +0 -1
  193. package/dist/tr-TZ7ERCIN.js.map +0 -1
  194. package/dist/uk-ZFWDKLUE.js.map +0 -1
  195. package/dist/vi-UIOSP2JG.js.map +0 -1
  196. package/dist/zh-6R6YUY46.js.map +0 -1
  197. /package/dist/{en-P2L4X27N.js.map → en-QFIEG3CA.js.map} +0 -0
@@ -1,11 +1,12 @@
1
1
  import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { asStates } from '../../../../__tests__/test-client';
2
3
  import { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';
3
4
  import { filter } from 'rxjs/operators';
4
5
  import { annotationId, resourceId as makeResourceId } from '@semiont/core';
5
6
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
- import { createResourceViewerPageStateUnit } from '../resource-viewer-page-state-unit';
7
- import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
8
- import { makeTestClient, type TestClient } from '../../../../__tests__/test-client';
7
+ import { createResourceViewerPageStateUnit, type ResourceViewerPageStateUnit } from '../resource-viewer-page-state-unit';
8
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing/axioms';
9
+ import { makeTestSession, type TestSession } from '../../../../__tests__/test-client';
9
10
 
10
11
  const RID = makeResourceId('res-1');
11
12
 
@@ -28,18 +29,18 @@ function clientWithNamespaces(overrides: {
28
29
  referencedBy$?: BehaviorSubject<unknown[] | undefined>;
29
30
  resourceRepresentation?: ReturnType<typeof vi.fn>;
30
31
  mediaToken?: ReturnType<typeof vi.fn>;
31
- } = {}): TestClient {
32
+ } = {}): TestSession {
32
33
  const annotations$ = overrides.annotations$ ?? new BehaviorSubject<unknown[] | undefined>([]);
33
34
  const entityTypes$ = overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person']);
34
35
  const events$ = overrides.events$ ?? new BehaviorSubject<unknown[] | undefined>([]);
35
36
  const referencedBy$ = overrides.referencedBy$ ?? new BehaviorSubject<unknown[] | undefined>([]);
36
37
 
37
- return makeTestClient({
38
+ return makeTestSession({
38
39
  browse: {
39
- annotations: () => annotations$.asObservable(),
40
- entityTypes: () => entityTypes$.asObservable(),
41
- events: () => events$.asObservable(),
42
- referencedBy: () => referencedBy$.asObservable(),
40
+ annotations: () => asStates(annotations$.asObservable()),
41
+ entityTypes: () => asStates(entityTypes$.asObservable()),
42
+ events: () => asStates(events$.asObservable()),
43
+ referencedBy: () => asStates(referencedBy$.asObservable()),
43
44
  resourceRepresentation: overrides.resourceRepresentation ?? vi.fn().mockResolvedValue({
44
45
  data: new TextEncoder().encode('hello').buffer,
45
46
  contentType: 'text/plain',
@@ -61,13 +62,13 @@ function clientWithNamespaces(overrides: {
61
62
  }
62
63
 
63
64
  describe('createResourceViewerPageStateUnit', () => {
64
- let tc: TestClient;
65
+ let tc: TestSession;
65
66
 
66
67
  afterEach(() => { tc?.bus.destroy(); });
67
68
 
68
69
  it('exposes flow VMs', () => {
69
70
  tc = clientWithNamespaces();
70
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
71
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
71
72
 
72
73
  expect(stateUnit.beckon).toBeDefined();
73
74
  expect(stateUnit.mark).toBeDefined();
@@ -83,9 +84,9 @@ describe('createResourceViewerPageStateUnit', () => {
83
84
  { id: 'a1', motivation: 'highlighting' },
84
85
  ]);
85
86
  tc = clientWithNamespaces({ annotations$ });
86
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
87
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
87
88
 
88
- const anns = await firstValueFrom(stateUnit.annotations$);
89
+ const anns = await firstValueFrom(stateUnit.annotations.value$);
89
90
  expect(anns).toHaveLength(1);
90
91
 
91
92
  stateUnit.dispose();
@@ -97,7 +98,7 @@ describe('createResourceViewerPageStateUnit', () => {
97
98
  { id: 'a2', motivation: 'commenting', body: [{ type: 'TextualBody', value: 'note' }], target: { selector: { type: 'TextQuoteSelector', exact: 'y' } } },
98
99
  ]);
99
100
  tc = clientWithNamespaces({ annotations$ });
100
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
101
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
101
102
 
102
103
  const groups = await firstValueFrom(stateUnit.annotationGroups$);
103
104
  expect(groups.highlights.length + groups.comments.length).toBeGreaterThanOrEqual(1);
@@ -107,9 +108,9 @@ describe('createResourceViewerPageStateUnit', () => {
107
108
 
108
109
  it('exposes entity types', async () => {
109
110
  tc = clientWithNamespaces();
110
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
111
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
111
112
 
112
- const types = await firstValueFrom(stateUnit.entityTypes$);
113
+ const types = await firstValueFrom(stateUnit.entityTypes.value$);
113
114
  expect(types).toEqual(['Person']);
114
115
 
115
116
  stateUnit.dispose();
@@ -118,9 +119,9 @@ describe('createResourceViewerPageStateUnit', () => {
118
119
  it('exposes events from browse namespace', async () => {
119
120
  const events$ = new BehaviorSubject<unknown[] | undefined>([{ id: 'e1', type: 'mark:added' }]);
120
121
  tc = clientWithNamespaces({ events$ });
121
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
122
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
122
123
 
123
- const events = await firstValueFrom(stateUnit.events$);
124
+ const events = await firstValueFrom(stateUnit.events.value$);
124
125
  expect(events).toEqual([{ id: 'e1', type: 'mark:added' }]);
125
126
 
126
127
  stateUnit.dispose();
@@ -129,9 +130,9 @@ describe('createResourceViewerPageStateUnit', () => {
129
130
  it('exposes referencedBy from browse namespace', async () => {
130
131
  const referencedBy$ = new BehaviorSubject<unknown[] | undefined>([{ resourceId: 'r2' }]);
131
132
  tc = clientWithNamespaces({ referencedBy$ });
132
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
133
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
133
134
 
134
- const refs = await firstValueFrom(stateUnit.referencedBy$);
135
+ const refs = await firstValueFrom(stateUnit.referencedBy.value$);
135
136
  expect(refs).toEqual([{ resourceId: 'r2' }]);
136
137
 
137
138
  stateUnit.dispose();
@@ -141,7 +142,7 @@ describe('createResourceViewerPageStateUnit', () => {
141
142
  const mediaToken = vi.fn().mockResolvedValue({ token: 'tok-456' });
142
143
  tc = clientWithNamespaces({ mediaToken });
143
144
  const stateUnit = createResourceViewerPageStateUnit(
144
- tc.client, RID, 'en', mockBrowse(),
145
+ tc.session, RID, 'en', mockBrowse(),
145
146
  { mediaType: 'image/png' },
146
147
  );
147
148
 
@@ -159,7 +160,7 @@ describe('createResourceViewerPageStateUnit', () => {
159
160
  const resourceRepresentation = vi.fn();
160
161
  tc = clientWithNamespaces({ mediaToken, resourceRepresentation });
161
162
  const stateUnit = createResourceViewerPageStateUnit(
162
- tc.client, RID, 'en', mockBrowse(),
163
+ tc.session, RID, 'en', mockBrowse(),
163
164
  { mediaType: 'application/zip' },
164
165
  );
165
166
 
@@ -178,7 +179,7 @@ describe('createResourceViewerPageStateUnit', () => {
178
179
  });
179
180
  tc = clientWithNamespaces({ mediaToken, resourceRepresentation });
180
181
  const stateUnit = createResourceViewerPageStateUnit(
181
- tc.client, RID, 'en', mockBrowse(),
182
+ tc.session, RID, 'en', mockBrowse(),
182
183
  { mediaType: 'text/x-custom' },
183
184
  );
184
185
 
@@ -190,7 +191,7 @@ describe('createResourceViewerPageStateUnit', () => {
190
191
 
191
192
  it('wizard initializes closed', async () => {
192
193
  tc = clientWithNamespaces();
193
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
194
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
194
195
 
195
196
  const wizard = await firstValueFrom(stateUnit.wizard$);
196
197
  expect(wizard.open).toBe(false);
@@ -200,7 +201,7 @@ describe('createResourceViewerPageStateUnit', () => {
200
201
 
201
202
  it('bind:initiate opens wizard and fires gather:requested', async () => {
202
203
  tc = clientWithNamespaces();
203
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
204
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
204
205
  const gatherEvents: unknown[] = [];
205
206
  tc.bus.get('gather:requested').subscribe((e) => gatherEvents.push(e));
206
207
 
@@ -220,7 +221,7 @@ describe('createResourceViewerPageStateUnit', () => {
220
221
 
221
222
  it('closeWizard resets wizard state', async () => {
222
223
  tc = clientWithNamespaces();
223
- const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
224
+ const stateUnit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
224
225
 
225
226
  tc.bus.get('bind:initiate').next({
226
227
  annotationId: annotationId('ann-1'),
@@ -239,6 +240,145 @@ describe('createResourceViewerPageStateUnit', () => {
239
240
  });
240
241
  });
241
242
 
243
+ describe('createResourceViewerPageStateUnit — list failure states', () => {
244
+ // A cache-backed list has three outcomes, not two. `browse.*()` delivers a
245
+ // terminal failure as an RxJS error (B15) once the B14 retry is exhausted
246
+ // with nothing stored — and a key that failed has no value either, so a
247
+ // (value | not-yet) model reports a dead request as an eternal spinner.
248
+ // Two panels do exactly that today: ReferencesPanel via
249
+ // `referencedByLoading = raw === undefined`, and AnnotationHistory via
250
+ // `loading = eventsData === undefined`.
251
+ // See .plans/PANEL-FAILURE-STATES.md
252
+
253
+ /**
254
+ * One case per cache-backed list. Each builds its OWN correctly-typed
255
+ * subject and returns the list off the unit, so the shared assertions below
256
+ * need no indexing by name and no cast.
257
+ */
258
+ /** The slice of `ListState<T>` these shared assertions read. */
259
+ interface ListProbe {
260
+ value$: Observable<unknown>;
261
+ loading$: Observable<boolean>;
262
+ error$: Observable<Error | null>;
263
+ }
264
+
265
+ const LIST_CASES: ReadonlyArray<{
266
+ name: string;
267
+ start: () => {
268
+ subject: { next: (s: never) => void };
269
+ tc: TestSession;
270
+ unit: ResourceViewerPageStateUnit;
271
+ list: ListProbe;
272
+ };
273
+ }> = [
274
+ {
275
+ name: 'annotations',
276
+ start: () => {
277
+ const subject = new BehaviorSubject<unknown[] | undefined>(undefined);
278
+ const tc = clientWithNamespaces({ annotations$: subject });
279
+ const unit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
280
+ return { subject, tc, unit, list: unit.annotations };
281
+ },
282
+ },
283
+ {
284
+ name: 'entityTypes',
285
+ start: () => {
286
+ const subject = new BehaviorSubject<string[] | undefined>(undefined);
287
+ const tc = clientWithNamespaces({ entityTypes$: subject });
288
+ const unit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
289
+ return { subject, tc, unit, list: unit.entityTypes };
290
+ },
291
+ },
292
+ {
293
+ name: 'events',
294
+ start: () => {
295
+ const subject = new BehaviorSubject<unknown[] | undefined>(undefined);
296
+ const tc = clientWithNamespaces({ events$: subject });
297
+ const unit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
298
+ return { subject, tc, unit, list: unit.events };
299
+ },
300
+ },
301
+ {
302
+ name: 'referencedBy',
303
+ start: () => {
304
+ const subject = new BehaviorSubject<unknown[] | undefined>(undefined);
305
+ const tc = clientWithNamespaces({ referencedBy$: subject });
306
+ const unit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
307
+ return { subject, tc, unit, list: unit.referencedBy };
308
+ },
309
+ },
310
+ ];
311
+
312
+ it.each(LIST_CASES)('$name: stops reporting loading and surfaces the reason on terminal failure', async ({ start }) => {
313
+ const { subject, tc, unit, list } = start();
314
+
315
+ expect(await firstValueFrom(list.loading$)).toBe(true);
316
+ expect(await firstValueFrom(list.error$)).toBeNull();
317
+
318
+ subject.next({ status: 'failed', error: new Error('Resource not found') } as never);
319
+
320
+ expect(await firstValueFrom(list.loading$)).toBe(false);
321
+ expect((await firstValueFrom(list.error$))?.message).toBe('Resource not found');
322
+
323
+ unit.dispose();
324
+ tc.bus.destroy();
325
+ });
326
+
327
+ it.each(LIST_CASES)('$name: keeps an empty value alongside the failure, so callers still render a list', async ({ start }) => {
328
+ const { subject, tc, unit, list } = start();
329
+
330
+ subject.next({ status: 'failed', error: new Error('boom') } as never);
331
+
332
+ expect(await firstValueFrom(list.value$)).toEqual([]);
333
+ unit.dispose();
334
+ tc.bus.destroy();
335
+ });
336
+
337
+ it('retry() clears the error, re-enters loading, and re-subscribes so a fresh attempt can succeed', async () => {
338
+ // D1/D3: failure is an EMISSION and recovery is a fresh SUBSCRIPTION —
339
+ // retry() re-attaches, and the new subscription's chain delivers.
340
+ const attempts: Array<BehaviorSubject<unknown>> = [];
341
+ const tc = makeTestSession({
342
+ browse: {
343
+ annotations: () => new BehaviorSubject<unknown>({ status: 'ready', value: [] }).asObservable(),
344
+ entityTypes: () => new BehaviorSubject<unknown>({ status: 'ready', value: [] }).asObservable(),
345
+ events: () => new BehaviorSubject<unknown>({ status: 'ready', value: [] }).asObservable(),
346
+ referencedBy: () => {
347
+ const s = new BehaviorSubject<unknown>({ status: 'pending' });
348
+ attempts.push(s);
349
+ return s.asObservable();
350
+ },
351
+ resourceRepresentation: vi.fn().mockResolvedValue({
352
+ data: new TextEncoder().encode('hello').buffer,
353
+ contentType: 'text/plain',
354
+ }),
355
+ },
356
+ auth: { mediaToken: vi.fn().mockResolvedValue({ token: 'tok' }) },
357
+ mark: { annotation: vi.fn(), delete: vi.fn(), assist: vi.fn(() => new Observable(() => {})) },
358
+ gather: { annotation: vi.fn(() => new Observable(() => {})) },
359
+ match: { search: vi.fn(() => new Observable(() => {})) },
360
+ yield: { fromAnnotation: vi.fn(() => new Observable(() => {})) },
361
+ bind: { body: vi.fn().mockResolvedValue(undefined) },
362
+ });
363
+ const unit = createResourceViewerPageStateUnit(tc.session, RID, 'en', mockBrowse());
364
+
365
+ attempts[0]!.next({ status: 'failed', error: new Error('nope') });
366
+ expect(await firstValueFrom(unit.referencedBy.error$)).not.toBeNull();
367
+
368
+ unit.referencedBy.retry();
369
+ expect(attempts.length).toBeGreaterThan(1);
370
+ expect(await firstValueFrom(unit.referencedBy.error$)).toBeNull();
371
+ expect(await firstValueFrom(unit.referencedBy.loading$)).toBe(true);
372
+
373
+ attempts[attempts.length - 1]!.next({ status: 'ready', value: [{ id: 'ref-1' }] });
374
+ expect(await firstValueFrom(unit.referencedBy.loading$)).toBe(false);
375
+ expect(await firstValueFrom(unit.referencedBy.value$)).toHaveLength(1);
376
+
377
+ unit.dispose();
378
+ tc.bus.destroy();
379
+ });
380
+ });
381
+
242
382
  describe('ResourceViewerPageStateUnit — StateUnit axioms', () => {
243
383
  it('satisfies the StateUnit axioms (A7-passed: browse survives; A7-owned: children disposed)', () => {
244
384
  const RID = makeResourceId('res-ax');
@@ -247,7 +387,7 @@ describe('ResourceViewerPageStateUnit — StateUnit axioms', () => {
247
387
  const browse = disposeProbe();
248
388
  const tc = clientWithNamespaces();
249
389
  return {
250
- unit: createResourceViewerPageStateUnit(tc.client, RID, 'en', browse as unknown as ShellStateUnit),
390
+ unit: createResourceViewerPageStateUnit(tc.session, RID, 'en', browse as unknown as ShellStateUnit),
251
391
  passedIn: [browse],
252
392
  teardown: () => tc.bus.destroy(),
253
393
  };
@@ -1,26 +1,91 @@
1
- import { type Observable, map } from 'rxjs';
1
+ import { readyValue } from '@semiont/sdk';
2
+ import {
3
+ BehaviorSubject,
4
+ combineLatest,
5
+ distinctUntilChanged,
6
+ map,
7
+ type Observable,
8
+ type Subscription,
9
+ } from 'rxjs';
2
10
  import type { ResourceDescriptor, ResourceId } from '@semiont/core';
3
11
  import type { StateUnit } from '@semiont/core';
4
- import type { SemiontClient } from '@semiont/sdk';
12
+ import type { SemiontSession } from '@semiont/sdk';
5
13
 
6
14
  export interface ResourceLoaderStateUnit extends StateUnit {
7
15
  resource$: Observable<ResourceDescriptor | undefined>;
8
16
  isLoading$: Observable<boolean>;
17
+ /** Terminal load failure (B15), or null. Cleared by any value or `invalidate()`. */
18
+ error$: Observable<Error | null>;
9
19
  invalidate(): void;
10
20
  }
11
21
 
22
+ /**
23
+ * Load one resource, in three states: loading, loaded, failed.
24
+ *
25
+ * The third state is not optional. `browse.resource()` delivers a terminal
26
+ * failure as an RxJS *error* notification (B15) when the B14 retry is
27
+ * exhausted with nothing cached — and a key that failed has no value either,
28
+ * so a (value | no value) model reports a dead request as an eternal spinner
29
+ * and drops the reason entirely. This unit catches the notification here so
30
+ * that (a) consumers can render the failure and retry it, and (b) it never
31
+ * escapes to RxJS as an unhandled rethrow.
32
+ * See .plans/bugs/resource-page-frozen-on-disposed-client-after-kb-switch.md (D4)
33
+ */
12
34
  export function createResourceLoaderStateUnit(
13
- client: SemiontClient,
35
+ session: SemiontSession,
14
36
  resourceId: ResourceId,
15
37
  ): ResourceLoaderStateUnit {
16
- const raw$ = client.browse.resource(resourceId);
17
- const resource$ = raw$;
18
- const isLoading$: Observable<boolean> = raw$.pipe(map((r) => r === undefined));
38
+ const { client } = session;
39
+ const resource$ = new BehaviorSubject<ResourceDescriptor | undefined>(undefined);
40
+ const error$ = new BehaviorSubject<Error | null>(null);
41
+
42
+ let subscription: Subscription | null = null;
43
+ let disposed = false;
44
+
45
+ /**
46
+ * (Re)subscribe to this key. An errored observable is terminated, so
47
+ * recovery needs a fresh subscription — which is also what re-arms the
48
+ * cache: `observe()` clears the B15 failure marker and starts a new
49
+ * attempt chain for a key it is asked about again.
50
+ */
51
+ const attach = (): void => {
52
+ if (disposed) return;
53
+ subscription?.unsubscribe();
54
+ subscription = client.browse.resource(resourceId).subscribe((st) => {
55
+ if (st.status === 'failed') {
56
+ // D1: failure is an emission — the subscription stays alive.
57
+ error$.next(st.error);
58
+ return;
59
+ }
60
+ if (st.status === 'ready' && error$.getValue() !== null) error$.next(null);
61
+ resource$.next(readyValue(st));
62
+ });
63
+ };
64
+ attach();
65
+
66
+ const isLoading$ = combineLatest([resource$, error$]).pipe(
67
+ map(([resource, error]) => resource === undefined && error === null),
68
+ distinctUntilChanged(),
69
+ );
19
70
 
20
71
  return {
21
- resource$,
72
+ // X1: owned subjects are published read-only.
73
+ resource$: resource$.asObservable(),
22
74
  isLoading$,
23
- invalidate: () => client.browse.invalidateResourceDetail(resourceId),
24
- dispose: () => {},
75
+ error$: error$.asObservable(),
76
+ invalidate: () => {
77
+ if (disposed) return;
78
+ error$.next(null);
79
+ client.browse.invalidateResourceDetail(resourceId);
80
+ attach();
81
+ },
82
+ dispose: () => {
83
+ if (disposed) return;
84
+ disposed = true;
85
+ subscription?.unsubscribe();
86
+ subscription = null;
87
+ resource$.complete();
88
+ error$.complete();
89
+ },
25
90
  };
26
91
  }
@@ -3,13 +3,14 @@ import type { ResourceId, components } from '@semiont/core';
3
3
  import { createDisposer } from '@semiont/sdk';
4
4
  import type { StateUnit } from '@semiont/core';
5
5
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
6
+ import { trackList, type ListState } from '../../../state/list-state';
6
7
  import { createBeckonStateUnit, type BeckonStateUnit } from '@semiont/sdk';
7
8
  import { createMarkStateUnit, type MarkStateUnit } from '@semiont/sdk';
8
9
  import { createGatherStateUnit, type GatherStateUnit } from '@semiont/sdk';
9
10
  import { createMatchStateUnit } from '@semiont/sdk';
10
11
  import { createYieldStateUnit, type YieldStateUnit } from '@semiont/sdk';
11
- import type { SemiontClient } from '@semiont/sdk';
12
- import { decodeWithCharset, textExtractionOf } from '@semiont/core';
12
+ import type { SemiontSession } from '@semiont/sdk';
13
+ import { decodeWithCharset, textExtractionOf, uuidV4 } from '@semiont/core';
13
14
  import { groupAnnotations } from '../../../lib/annotation-groups';
14
15
  import type { ReferencedByEntry } from '@semiont/sdk';
15
16
 
@@ -43,11 +44,12 @@ export interface ResourceViewerPageStateUnit extends StateUnit {
43
44
  gather: GatherStateUnit;
44
45
  yield: YieldStateUnit;
45
46
 
46
- annotations$: Observable<Annotation[]>;
47
+ annotations: ListState<Annotation[]>;
48
+ entityTypes: ListState<string[]>;
49
+ events: ListState<StoredEventResponse[]>;
50
+ referencedBy: ListState<ReferencedByEntry[]>;
51
+ /** Derived from `annotations.value$`; failure/loading live on `annotations`. */
47
52
  annotationGroups$: Observable<AnnotationGroups>;
48
- entityTypes$: Observable<string[]>;
49
- events$: Observable<StoredEventResponse[]>;
50
- referencedBy$: Observable<ReferencedByEntry[]>;
51
53
  content$: Observable<string>;
52
54
  contentLoading$: Observable<boolean>;
53
55
  mediaToken$: Observable<string | null>;
@@ -56,13 +58,18 @@ export interface ResourceViewerPageStateUnit extends StateUnit {
56
58
  closeWizard(): void;
57
59
  }
58
60
 
61
+ // Session-typed (SESSION-TYPED-FACTORIES.md D1): the parameter is the
62
+ // lifetime this unit must not outlive. The internal flow units below keep
63
+ // the narrower client — their lifetime is THIS unit's disposer, which is now
64
+ // session-bound; that is layering, not a loophole.
59
65
  export function createResourceViewerPageStateUnit(
60
- client: SemiontClient,
66
+ session: SemiontSession,
61
67
  resourceId: ResourceId,
62
68
  locale: string,
63
69
  browse: ShellStateUnit,
64
70
  options?: { mediaType?: string },
65
71
  ): ResourceViewerPageStateUnit {
72
+ const { client } = session;
66
73
  const disposer = createDisposer();
67
74
 
68
75
  const beckon = createBeckonStateUnit(client);
@@ -81,23 +88,17 @@ export function createResourceViewerPageStateUnit(
81
88
  disposer.add(matchStateUnit);
82
89
  disposer.add(yieldStateUnit);
83
90
 
84
- const annotations$: Observable<Annotation[]> = client.browse.annotations(resourceId).pipe(
85
- map((a) => a ?? []),
86
- );
91
+ const annotations = trackList<Annotation[]>(() => client.browse.annotations(resourceId), []);
92
+ const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
93
+ const events = trackList<StoredEventResponse[]>(() => client.browse.events(resourceId), []);
94
+ const referencedBy = trackList<ReferencedByEntry[]>(() => client.browse.referencedBy(resourceId), []);
95
+ disposer.add(annotations.dispose);
96
+ disposer.add(entityTypes.dispose);
97
+ disposer.add(events.dispose);
98
+ disposer.add(referencedBy.dispose);
87
99
 
88
- const annotationGroups$: Observable<AnnotationGroups> = annotations$.pipe(map(groupAnnotations));
89
-
90
- const entityTypes$: Observable<string[]> = client.browse.entityTypes().pipe(
91
- map((e) => e ?? []),
92
- );
93
-
94
- const events$: Observable<StoredEventResponse[]> = client.browse.events(resourceId).pipe(
95
- map((e) => e ?? []),
96
- );
97
-
98
- const referencedBy$: Observable<ReferencedByEntry[]> = client.browse.referencedBy(resourceId).pipe(
99
- map((r) => r ?? []),
100
- );
100
+ const annotationGroups$: Observable<AnnotationGroups> =
101
+ annotations.state.value$.pipe(map(groupAnnotations));
101
102
 
102
103
  const content$ = new BehaviorSubject<string>('');
103
104
  const contentLoading$ = new BehaviorSubject<boolean>(false);
@@ -143,7 +144,7 @@ export function createResourceViewerPageStateUnit(
143
144
  entityTypes: event.entityTypes,
144
145
  });
145
146
  client.bus.get('gather:requested').next({
146
- correlationId: crypto.randomUUID(),
147
+ correlationId: uuidV4(),
147
148
  annotationId: event.annotationId,
148
149
  resourceId: event.resourceId,
149
150
  options: { contextWindow: 2000 },
@@ -157,11 +158,11 @@ export function createResourceViewerPageStateUnit(
157
158
  mark,
158
159
  gather,
159
160
  yield: yieldStateUnit,
160
- annotations$,
161
+ annotations: annotations.state,
162
+ entityTypes: entityTypes.state,
163
+ events: events.state,
164
+ referencedBy: referencedBy.state,
161
165
  annotationGroups$,
162
- entityTypes$,
163
- events$,
164
- referencedBy$,
165
166
  content$: content$.asObservable(),
166
167
  contentLoading$: contentLoading$.asObservable(),
167
168
  mediaToken$: mediaToken$.asObservable(),
@@ -77,7 +77,9 @@
77
77
  "justNow": "الآن",
78
78
  "minutesAgo": "منذ {{count}}د",
79
79
  "hoursAgo": "منذ {{count}}س",
80
- "daysAgo": "منذ {{count}}ي"
80
+ "daysAgo": "منذ {{count}}ي",
81
+ "failed": "تعذّر تحميل السجل.",
82
+ "retry": "إعادة المحاولة"
81
83
  },
82
84
  "CommentsPanel": {
83
85
  "title": "التعليقات",
@@ -171,7 +173,9 @@
171
173
  "reference": "المراجع",
172
174
  "assessment": "التقييمات",
173
175
  "comment": "التعليقات",
174
- "tag": "العلامات"
176
+ "tag": "العلامات",
177
+ "annotationsFailed": "تعذّر تحميل التعليقات التوضيحية.",
178
+ "retry": "إعادة المحاولة"
175
179
  },
176
180
  "StatisticsPanel": {
177
181
  "title": "الإحصائيات",
@@ -220,7 +224,10 @@
220
224
  "loading": "جارٍ التحميل...",
221
225
  "loadingEllipsis": "جارٍ التحميل...",
222
226
  "untitledResource": "مورد بدون عنوان",
223
- "noText": "لا يوجد نص"
227
+ "noText": "لا يوجد نص",
228
+ "incomingReferencesFailed": "تعذّر تحميل المراجع الواردة.",
229
+ "retry": "إعادة المحاولة",
230
+ "entityTypesFailed": "تعذّر تحميل أنواع الكيانات."
224
231
  },
225
232
  "KeyboardShortcuts": {
226
233
  "title": "اختصارات لوحة المفاتيح",
@@ -77,7 +77,9 @@
77
77
  "justNow": "এইমাত্র",
78
78
  "minutesAgo": "{{count}} মি. আগে",
79
79
  "hoursAgo": "{{count}} ঘ. আগে",
80
- "daysAgo": "{{count}} দি. আগে"
80
+ "daysAgo": "{{count}} দি. আগে",
81
+ "failed": "ইতিহাস লোড করা যায়নি।",
82
+ "retry": "আবার চেষ্টা করুন"
81
83
  },
82
84
  "CommentsPanel": {
83
85
  "title": "মন্তব্যসমূহ",
@@ -171,7 +173,9 @@
171
173
  "reference": "রেফারেন্সসমূহ",
172
174
  "assessment": "মূল্যায়নসমূহ",
173
175
  "comment": "মন্তব্যসমূহ",
174
- "tag": "ট্যাগসমূহ"
176
+ "tag": "ট্যাগসমূহ",
177
+ "annotationsFailed": "টীকা লোড করা যায়নি।",
178
+ "retry": "আবার চেষ্টা করুন"
175
179
  },
176
180
  "StatisticsPanel": {
177
181
  "title": "পরিসংখ্যান",
@@ -220,7 +224,10 @@
220
224
  "loading": "লোড হচ্ছে...",
221
225
  "loadingEllipsis": "লোড হচ্ছে...",
222
226
  "untitledResource": "শিরোনামবিহীন সম্পদ",
223
- "noText": "কোনো পাঠ্য নেই"
227
+ "noText": "কোনো পাঠ্য নেই",
228
+ "incomingReferencesFailed": "ইনকামিং রেফারেন্স লোড করা যায়নি।",
229
+ "retry": "আবার চেষ্টা করুন",
230
+ "entityTypesFailed": "সত্তার ধরন লোড করা যায়নি।"
224
231
  },
225
232
  "KeyboardShortcuts": {
226
233
  "title": "কীবোর্ড শর্টকাট",
@@ -77,7 +77,9 @@
77
77
  "justNow": "právě teď",
78
78
  "minutesAgo": "před {{count}} min",
79
79
  "hoursAgo": "před {{count}} hod",
80
- "daysAgo": "před {{count}} dny"
80
+ "daysAgo": "před {{count}} dny",
81
+ "failed": "Historii se nepodařilo načíst.",
82
+ "retry": "Zkusit znovu"
81
83
  },
82
84
  "CommentsPanel": {
83
85
  "title": "Komentáře",
@@ -171,7 +173,9 @@
171
173
  "reference": "Reference",
172
174
  "assessment": "Hodnocení",
173
175
  "comment": "Komentáře",
174
- "tag": "Štítky"
176
+ "tag": "Štítky",
177
+ "annotationsFailed": "Anotace se nepodařilo načíst.",
178
+ "retry": "Zkusit znovu"
175
179
  },
176
180
  "StatisticsPanel": {
177
181
  "title": "Statistiky",
@@ -220,7 +224,10 @@
220
224
  "loading": "načítání...",
221
225
  "loadingEllipsis": "Načítání...",
222
226
  "untitledResource": "Zdroj bez názvu",
223
- "noText": "Žádný text"
227
+ "noText": "Žádný text",
228
+ "incomingReferencesFailed": "Příchozí reference se nepodařilo načíst.",
229
+ "retry": "Zkusit znovu",
230
+ "entityTypesFailed": "Typy entit se nepodařilo načíst."
224
231
  },
225
232
  "KeyboardShortcuts": {
226
233
  "title": "Klávesové zkratky",
@@ -77,7 +77,9 @@
77
77
  "justNow": "lige nu",
78
78
  "minutesAgo": "{{count}}m siden",
79
79
  "hoursAgo": "{{count}}t siden",
80
- "daysAgo": "{{count}}d siden"
80
+ "daysAgo": "{{count}}d siden",
81
+ "failed": "Historikken kunne ikke indlæses.",
82
+ "retry": "Prøv igen"
81
83
  },
82
84
  "CommentsPanel": {
83
85
  "title": "Kommentarer",
@@ -171,7 +173,9 @@
171
173
  "reference": "Referencer",
172
174
  "assessment": "Vurderinger",
173
175
  "comment": "Kommentarer",
174
- "tag": "Tags"
176
+ "tag": "Tags",
177
+ "annotationsFailed": "Annoteringerne kunne ikke indlæses.",
178
+ "retry": "Prøv igen"
175
179
  },
176
180
  "StatisticsPanel": {
177
181
  "title": "Statistik",
@@ -220,7 +224,10 @@
220
224
  "loading": "indlæser...",
221
225
  "loadingEllipsis": "Indlæser...",
222
226
  "untitledResource": "Unavngivet ressource",
223
- "noText": "Ingen tekst"
227
+ "noText": "Ingen tekst",
228
+ "incomingReferencesFailed": "Indgående referencer kunne ikke indlæses.",
229
+ "retry": "Prøv igen",
230
+ "entityTypesFailed": "Enhedstyper kunne ikke indlæses."
224
231
  },
225
232
  "KeyboardShortcuts": {
226
233
  "title": "Tastaturgenveje",