@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,10 +1,12 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
+ import { asStates } from '../../../../__tests__/test-client';
3
+ import { sessionOf } from '../../../../__tests__/test-client';
2
4
  import { BehaviorSubject, firstValueFrom } from 'rxjs';
3
5
  import { filter } from 'rxjs/operators';
4
6
  import type { SemiontClient } from '@semiont/sdk';
5
7
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
8
  import { createEntityTagsStateUnit } from '../entity-tags-state-unit';
7
- import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
9
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing/axioms';
8
10
 
9
11
  function mockBrowse(): ShellStateUnit {
10
12
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -17,7 +19,7 @@ function mockClient(overrides: {
17
19
  const entityTypes$ = overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person', 'Place']);
18
20
  return {
19
21
  browse: {
20
- entityTypes: () => entityTypes$.asObservable(),
22
+ entityTypes: () => asStates(entityTypes$.asObservable()),
21
23
  },
22
24
  frame: {
23
25
  addEntityType: overrides.addEntityType ?? vi.fn().mockResolvedValue(undefined),
@@ -27,9 +29,9 @@ function mockClient(overrides: {
27
29
 
28
30
  describe('createEntityTagsStateUnit', () => {
29
31
  it('exposes entity types from browse namespace', async () => {
30
- const stateUnit = createEntityTagsStateUnit(mockClient(), mockBrowse());
32
+ const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient()), mockBrowse());
31
33
 
32
- const types = await firstValueFrom(stateUnit.entityTypes$);
34
+ const types = await firstValueFrom(stateUnit.entityTypes.value$);
33
35
  expect(types).toEqual(['Person', 'Place']);
34
36
 
35
37
  stateUnit.dispose();
@@ -37,20 +39,20 @@ describe('createEntityTagsStateUnit', () => {
37
39
 
38
40
  it('reports loading when entity types are undefined', async () => {
39
41
  const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
40
- const stateUnit = createEntityTagsStateUnit(mockClient({ entityTypes$ }), mockBrowse());
42
+ const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ entityTypes$ })), mockBrowse());
41
43
 
42
- const loading = await firstValueFrom(stateUnit.isLoading$);
44
+ const loading = await firstValueFrom(stateUnit.entityTypes.loading$);
43
45
  expect(loading).toBe(true);
44
46
 
45
47
  entityTypes$.next(['Tag']);
46
- const loaded = await firstValueFrom(stateUnit.isLoading$.pipe(filter((l) => !l)));
48
+ const loaded = await firstValueFrom(stateUnit.entityTypes.loading$.pipe(filter((l) => !l)));
47
49
  expect(loaded).toBe(false);
48
50
 
49
51
  stateUnit.dispose();
50
52
  });
51
53
 
52
54
  it('setNewTag updates newTag$', async () => {
53
- const stateUnit = createEntityTagsStateUnit(mockClient(), mockBrowse());
55
+ const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient()), mockBrowse());
54
56
 
55
57
  stateUnit.setNewTag('Organization');
56
58
  const tag = await firstValueFrom(stateUnit.newTag$);
@@ -61,7 +63,7 @@ describe('createEntityTagsStateUnit', () => {
61
63
 
62
64
  it('addTag calls client and clears newTag$', async () => {
63
65
  const addEntityType = vi.fn().mockResolvedValue(undefined);
64
- const stateUnit = createEntityTagsStateUnit(mockClient({ addEntityType }), mockBrowse());
66
+ const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ addEntityType })), mockBrowse());
65
67
 
66
68
  stateUnit.setNewTag('Event');
67
69
  await stateUnit.addTag();
@@ -75,7 +77,7 @@ describe('createEntityTagsStateUnit', () => {
75
77
 
76
78
  it('addTag ignores empty/whitespace input', async () => {
77
79
  const addEntityType = vi.fn();
78
- const stateUnit = createEntityTagsStateUnit(mockClient({ addEntityType }), mockBrowse());
80
+ const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ addEntityType })), mockBrowse());
79
81
 
80
82
  stateUnit.setNewTag(' ');
81
83
  await stateUnit.addTag();
@@ -87,7 +89,7 @@ describe('createEntityTagsStateUnit', () => {
87
89
 
88
90
  it('addTag sets error on failure', async () => {
89
91
  const addEntityType = vi.fn().mockRejectedValue(new Error('duplicate'));
90
- const stateUnit = createEntityTagsStateUnit(mockClient({ addEntityType }), mockBrowse());
92
+ const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ addEntityType })), mockBrowse());
91
93
 
92
94
  stateUnit.setNewTag('Person');
93
95
  await stateUnit.addTag();
@@ -107,10 +109,33 @@ describe('EntityTagsStateUnit — StateUnit axioms', () => {
107
109
  assertStateUnitAxioms({
108
110
  setup: () => {
109
111
  const browse = disposeProbe();
110
- return { unit: createEntityTagsStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
112
+ return { unit: createEntityTagsStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit), passedIn: [browse] };
111
113
  },
112
114
  surfaces: (u) => [u.newTag$, u.error$, u.isAdding$],
113
115
  invocations: (u) => [() => u.setNewTag(''), () => u.addTag()],
114
116
  });
115
117
  });
116
118
  });
119
+
120
+ describe('createEntityTagsStateUnit — terminal load failure', () => {
121
+ // `isLoading$` was `raw$.pipe(map(e => e === undefined))` and
122
+ // moderate/entity-tags/page.tsx returns a full-page loading screen off it,
123
+ // so a failed load froze the route. The existing `error$` is the ADD-tag
124
+ // error and is deliberately left alone.
125
+ // See .plans/PANEL-FAILURE-STATES.md
126
+
127
+ it('stops loading and surfaces the load failure, distinct from the add-tag error', async () => {
128
+ const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
129
+ const unit = createEntityTagsStateUnit(sessionOf(mockClient({ entityTypes$ })), mockBrowse());
130
+
131
+ expect(await firstValueFrom(unit.entityTypes.loading$)).toBe(true);
132
+ entityTypes$.next({ status: 'failed', error: new Error('Resource not found') } as never);
133
+
134
+ expect(await firstValueFrom(unit.entityTypes.loading$)).toBe(false);
135
+ expect(await firstValueFrom(unit.entityTypes.error$)).not.toBeNull();
136
+ // The add-tag error channel is untouched by a load failure.
137
+ expect(await firstValueFrom(unit.error$)).toBe('');
138
+
139
+ unit.dispose();
140
+ });
141
+ });
@@ -1,13 +1,13 @@
1
- import { BehaviorSubject, type Observable, map } from 'rxjs';
1
+ import { BehaviorSubject, type Observable } from 'rxjs';
2
2
  import { createDisposer } from '@semiont/sdk';
3
3
  import type { StateUnit } from '@semiont/core';
4
4
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
5
- import type { SemiontClient } from '@semiont/sdk';
5
+ import { trackList, type ListState } from '../../../state/list-state';
6
+ import type { SemiontSession } from '@semiont/sdk';
6
7
 
7
8
  export interface EntityTagsStateUnit extends StateUnit {
8
9
  browse: ShellStateUnit;
9
- entityTypes$: Observable<string[]>;
10
- isLoading$: Observable<boolean>;
10
+ entityTypes: ListState<string[]>;
11
11
  newTag$: Observable<string>;
12
12
  error$: Observable<string>;
13
13
  isAdding$: Observable<boolean>;
@@ -16,9 +16,10 @@ export interface EntityTagsStateUnit extends StateUnit {
16
16
  }
17
17
 
18
18
  export function createEntityTagsStateUnit(
19
- client: SemiontClient,
19
+ session: SemiontSession,
20
20
  browse: ShellStateUnit,
21
21
  ): EntityTagsStateUnit {
22
+ const { client } = session;
22
23
  const disposer = createDisposer();
23
24
  // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
24
25
  // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
@@ -28,9 +29,8 @@ export function createEntityTagsStateUnit(
28
29
  const error$ = new BehaviorSubject<string>('');
29
30
  const isAdding$ = new BehaviorSubject<boolean>(false);
30
31
 
31
- const raw$ = client.browse.entityTypes();
32
- const entityTypes$: Observable<string[]> = raw$.pipe(map((e) => e ?? []));
33
- const isLoading$: Observable<boolean> = raw$.pipe(map((e) => e === undefined));
32
+ const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
33
+ disposer.add(entityTypes.dispose);
34
34
 
35
35
  const addTag = async (): Promise<void> => {
36
36
  const tag = newTag$.getValue().trim();
@@ -49,8 +49,7 @@ export function createEntityTagsStateUnit(
49
49
 
50
50
  return {
51
51
  browse,
52
- entityTypes$,
53
- isLoading$,
52
+ entityTypes: entityTypes.state,
54
53
  newTag$: newTag$.asObservable(),
55
54
  error$: error$.asObservable(),
56
55
  isAdding$: isAdding$.asObservable(),
@@ -14,7 +14,6 @@ const createMockProps = (overrides?: Partial<RecentDocumentsPageProps>): RecentD
14
14
  hasDocuments: false,
15
15
  isLoading: false,
16
16
  theme: 'light',
17
- showLineNumbers: false,
18
17
  activePanel: null,
19
18
  translations: {
20
19
  pageTitle: 'Recent Documents',
@@ -205,13 +204,13 @@ describe('RecentDocumentsPage', () => {
205
204
  );
206
205
  });
207
206
 
208
- it('passes showLineNumbers to toolbar panels', () => {
207
+ it('does not thread showLineNumbers to toolbar panels — the setting is context-backed', () => {
209
208
  const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
210
- const props = createMockProps({ showLineNumbers: true, ToolbarPanels });
209
+ const props = createMockProps({ToolbarPanels });
211
210
  render(<RecentDocumentsPage {...props} />);
212
211
 
213
212
  expect(ToolbarPanels).toHaveBeenCalledWith(
214
- expect.objectContaining({ showLineNumbers: true }),
213
+ expect.not.objectContaining({ showLineNumbers: expect.anything() }),
215
214
  undefined,
216
215
  );
217
216
  });
@@ -342,7 +341,6 @@ describe('RecentDocumentsPage', () => {
342
341
  const props = createMockProps({
343
342
  hasDocuments: true,
344
343
  theme: 'dark',
345
- showLineNumbers: true,
346
344
  activePanel: 'settings',
347
345
  });
348
346
  render(<RecentDocumentsPage {...props} />);
@@ -15,7 +15,6 @@ export interface RecentDocumentsPageProps {
15
15
 
16
16
  // UI state
17
17
  theme: 'light' | 'dark' | 'system';
18
- showLineNumbers: boolean;
19
18
  activePanel: string | null;
20
19
 
21
20
  // Translations
@@ -37,7 +36,6 @@ export interface RecentDocumentsPageProps {
37
36
  export function RecentDocumentsPage({
38
37
  isLoading,
39
38
  theme,
40
- showLineNumbers,
41
39
  activePanel,
42
40
  translations: t,
43
41
  ToolbarPanels,
@@ -95,7 +93,6 @@ export function RecentDocumentsPage({
95
93
  <ToolbarPanels
96
94
  activePanel={activePanel}
97
95
  theme={theme}
98
- showLineNumbers={showLineNumbers}
99
96
  />
100
97
 
101
98
  <Toolbar
@@ -61,7 +61,6 @@ const createMockProps = (overrides?: Partial<TagSchemasPageProps>): TagSchemasPa
61
61
  schemas: mockSchemas,
62
62
  isLoading: false,
63
63
  theme: 'light',
64
- showLineNumbers: false,
65
64
  activePanel: null,
66
65
  translations: {
67
66
  pageTitle: 'Tag Schemas',
@@ -276,13 +275,13 @@ describe('TagSchemasPage', () => {
276
275
  );
277
276
  });
278
277
 
279
- it('passes showLineNumbers to toolbar panels', () => {
278
+ it('does not thread showLineNumbers to toolbar panels — the setting is context-backed', () => {
280
279
  const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
281
- const props = createMockProps({ showLineNumbers: true, ToolbarPanels });
280
+ const props = createMockProps({ToolbarPanels });
282
281
  render(<TagSchemasPage {...props} />);
283
282
 
284
283
  expect(ToolbarPanels).toHaveBeenCalledWith(
285
- expect.objectContaining({ showLineNumbers: true }),
284
+ expect.not.objectContaining({ showLineNumbers: expect.anything() }),
286
285
  undefined,
287
286
  );
288
287
  });
@@ -420,7 +419,6 @@ describe('TagSchemasPage', () => {
420
419
  it('handles all props being defined', () => {
421
420
  const props = createMockProps({
422
421
  theme: 'dark',
423
- showLineNumbers: true,
424
422
  activePanel: 'settings',
425
423
  });
426
424
  render(<TagSchemasPage {...props} />);
@@ -22,7 +22,6 @@ export interface TagSchemasPageProps {
22
22
 
23
23
  // UI state
24
24
  theme: 'light' | 'dark' | 'system';
25
- showLineNumbers: boolean;
26
25
  activePanel: string | null;
27
26
 
28
27
  // Translations
@@ -54,7 +53,6 @@ export function TagSchemasPage({
54
53
  schemas,
55
54
  isLoading,
56
55
  theme,
57
- showLineNumbers,
58
56
  activePanel,
59
57
  translations: t,
60
58
  ToolbarPanels,
@@ -153,7 +151,6 @@ export function TagSchemasPage({
153
151
  <ToolbarPanels
154
152
  activePanel={activePanel}
155
153
  theme={theme}
156
- showLineNumbers={showLineNumbers}
157
154
  />
158
155
 
159
156
  <Toolbar
@@ -18,7 +18,6 @@ const createProps = (overrides?: Partial<LinkedDataPageProps>): LinkedDataPagePr
18
18
  isImporting: false,
19
19
  importPhase: null,
20
20
  theme: 'light',
21
- showLineNumbers: false,
22
21
  activePanel: null,
23
22
  translations: {
24
23
  title: 'Export & Import',
@@ -39,7 +39,6 @@ export interface LinkedDataPageProps {
39
39
 
40
40
  // UI state
41
41
  theme: 'light' | 'dark' | 'system';
42
- showLineNumbers: boolean;
43
42
  activePanel: string | null;
44
43
 
45
44
  // Translations
@@ -63,7 +62,6 @@ export function LinkedDataPage({
63
62
  importMessage,
64
63
  importResult,
65
64
  theme,
66
- showLineNumbers,
67
65
  activePanel,
68
66
  translations: t,
69
67
  ToolbarPanels,
@@ -109,7 +107,6 @@ export function LinkedDataPage({
109
107
  <ToolbarPanels
110
108
  activePanel={activePanel}
111
109
  theme={theme}
112
- showLineNumbers={showLineNumbers}
113
110
  />
114
111
  <Toolbar
115
112
  context="simple"
@@ -10,12 +10,14 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10
10
  import { ResourceComposePage } from '../components/ResourceComposePage';
11
11
  import type { ResourceComposePageProps } from '../components/ResourceComposePage';
12
12
  import { createTestSemiontWrapper } from '../../../test-utils';
13
+ import { LineNumbersProvider } from '../../../contexts/LineNumbersContext';
13
14
 
14
15
  // Mock CodeMirrorRenderer to avoid CodeMirror dependencies
15
16
  vi.mock('../../../components/CodeMirrorRenderer', () => ({
16
- CodeMirrorRenderer: ({ content, onChange, editable }: any) => (
17
+ CodeMirrorRenderer: ({ content, onChange, editable, showLineNumbers }: any) => (
17
18
  <textarea
18
19
  data-testid="code-editor"
20
+ data-line-numbers={String(showLineNumbers)}
19
21
  value={content}
20
22
  onChange={(e) => editable && onChange?.(e.target.value)}
21
23
  disabled={!editable}
@@ -62,7 +64,6 @@ const createMockProps = (overrides?: Partial<ResourceComposePageProps>): Resourc
62
64
  availableEntityTypes: ['Document', 'Article', 'Report'],
63
65
  initialLocale: 'en',
64
66
  theme: 'light',
65
- showLineNumbers: false,
66
67
  hoverDelayMs: 0,
67
68
  activePanel: null,
68
69
  onSaveResource: vi.fn().mockResolvedValue(undefined),
@@ -77,7 +78,11 @@ const createMockProps = (overrides?: Partial<ResourceComposePageProps>): Resourc
77
78
  // component reaches for via useSemiont/useObservable).
78
79
  const renderWithProviders = (ui: React.ReactElement) => {
79
80
  const { SemiontWrapper } = createTestSemiontWrapper();
80
- return render(<SemiontWrapper>{ui}</SemiontWrapper>);
81
+ return render(
82
+ <LineNumbersProvider>
83
+ <SemiontWrapper>{ui}</SemiontWrapper>
84
+ </LineNumbersProvider>,
85
+ );
81
86
  };
82
87
 
83
88
  describe('ResourceComposePage', () => {
@@ -500,11 +505,16 @@ describe('ResourceComposePage', () => {
500
505
  expect(editor.value).toBe('New content');
501
506
  });
502
507
 
503
- it('respects showLineNumbers prop', () => {
504
- const props = createMockProps({ showLineNumbers: true });
505
- renderWithProviders(<ResourceComposePage {...props} />);
508
+ it('feeds the editor the shared line-numbers setting from context', () => {
509
+ localStorage.setItem('showLineNumbers', 'true');
510
+ try {
511
+ const props = createMockProps({});
512
+ renderWithProviders(<ResourceComposePage {...props} />);
506
513
 
507
- expect(screen.getByTestId('code-editor')).toBeInTheDocument();
514
+ expect(screen.getByTestId('code-editor')).toHaveAttribute('data-line-numbers', 'true');
515
+ } finally {
516
+ localStorage.removeItem('showLineNumbers');
517
+ }
508
518
  });
509
519
  });
510
520
  });
@@ -15,6 +15,7 @@ import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state
15
15
  import { buttonStyles } from '../../../lib/button-styles';
16
16
  import { CodeMirrorRenderer } from '../../../components/CodeMirrorRenderer';
17
17
  import { useFormAnnouncements } from '../../../components/LiveRegion';
18
+ import { useLineNumbers } from '../../../contexts/LineNumbersContext';
18
19
  import { UploadProgressBar } from './UploadProgressBar';
19
20
 
20
21
  /**
@@ -54,7 +55,6 @@ export interface ResourceComposePageProps {
54
55
 
55
56
  // UI state
56
57
  theme: 'light' | 'dark';
57
- showLineNumbers: boolean;
58
58
  hoverDelayMs: number;
59
59
  activePanel: string | null;
60
60
 
@@ -133,7 +133,6 @@ export function ResourceComposePage({
133
133
  availableEntityTypes,
134
134
  initialLocale,
135
135
  theme,
136
- showLineNumbers,
137
136
  hoverDelayMs,
138
137
  activePanel,
139
138
  onSaveResource,
@@ -146,6 +145,10 @@ export function ResourceComposePage({
146
145
  // Form announcements
147
146
  const { announceFormSubmitting, announceFormSuccess, announceFormError } = useFormAnnouncements();
148
147
 
148
+ // Shared display setting (LineNumbersContext) — was a prop threaded from
149
+ // the route, which desynced it from the Settings toggle.
150
+ const { showLineNumbers } = useLineNumbers();
151
+
149
152
  // Form state
150
153
  const [newResourceName, setNewResourceName] = useState('');
151
154
  const [newResourceContent, setNewResourceContent] = useState('');
@@ -746,7 +749,6 @@ export function ResourceComposePage({
746
749
  <ToolbarPanels
747
750
  activePanel={activePanel}
748
751
  theme={theme}
749
- showLineNumbers={showLineNumbers}
750
752
  />
751
753
 
752
754
  {/* Toolbar - Always visible on the right */}
@@ -1,10 +1,12 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
+ import { asStates } from '../../../../__tests__/test-client';
3
+ import { sessionOf } from '../../../../__tests__/test-client';
2
4
  import { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';
3
5
  import { filter } from 'rxjs/operators';
4
6
  import type { SemiontClient } from '@semiont/sdk';
5
7
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
8
  import { createComposePageStateUnit } from '../compose-page-state-unit';
7
- import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
9
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing/axioms';
8
10
 
9
11
  /** Build an `UploadObservable`-shaped mock that emits started → finished. */
10
12
  function mockUpload(resourceId: string) {
@@ -32,7 +34,7 @@ function mockClient(overrides: {
32
34
  const entityTypes$ = overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person']);
33
35
  return {
34
36
  browse: {
35
- entityTypes: () => entityTypes$.asObservable(),
37
+ entityTypes: () => asStates(entityTypes$.asObservable()),
36
38
  },
37
39
  yield: {
38
40
  fromToken: overrides.fromToken ?? vi.fn().mockResolvedValue({ '@id': 'src-1', representations: [{ mediaType: 'text/plain' }] }),
@@ -51,7 +53,7 @@ function mockClient(overrides: {
51
53
 
52
54
  describe('createComposePageStateUnit', () => {
53
55
  it('defaults to "new" mode', async () => {
54
- const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {});
56
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {});
55
57
 
56
58
  const mode = await firstValueFrom(stateUnit.mode$);
57
59
  expect(mode).toBe('new');
@@ -63,7 +65,7 @@ describe('createComposePageStateUnit', () => {
63
65
  });
64
66
 
65
67
  it('detects reference mode from params', async () => {
66
- const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {
68
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
67
69
  annotationUri: 'ann-1',
68
70
  sourceDocumentId: 'doc-1',
69
71
  name: 'Reference Doc',
@@ -82,7 +84,7 @@ describe('createComposePageStateUnit', () => {
82
84
 
83
85
  it('parses storedContext in reference mode', async () => {
84
86
  const context = { annotation: { id: 'ann-1' }, sourceContext: 'text' };
85
- const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {
87
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
86
88
  annotationUri: 'ann-1',
87
89
  sourceDocumentId: 'doc-1',
88
90
  name: 'Ref',
@@ -96,7 +98,7 @@ describe('createComposePageStateUnit', () => {
96
98
  });
97
99
 
98
100
  it('ignores malformed storedContext', async () => {
99
- const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {
101
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
100
102
  annotationUri: 'ann-1',
101
103
  sourceDocumentId: 'doc-1',
102
104
  name: 'Ref',
@@ -113,9 +115,9 @@ describe('createComposePageStateUnit', () => {
113
115
  });
114
116
 
115
117
  it('exposes entity types', async () => {
116
- const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {});
118
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {});
117
119
 
118
- const types = await firstValueFrom(stateUnit.entityTypes$);
120
+ const types = await firstValueFrom(stateUnit.entityTypes.value$);
119
121
  expect(types).toEqual(['Person']);
120
122
 
121
123
  stateUnit.dispose();
@@ -123,7 +125,7 @@ describe('createComposePageStateUnit', () => {
123
125
 
124
126
  it('save in new mode calls yield.resource', async () => {
125
127
  const resource = mockUpload('new-3');
126
- const stateUnit = createComposePageStateUnit(mockClient({ resource }), mockBrowse(), {});
128
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ resource })), mockBrowse(), {});
127
129
 
128
130
  const id = await stateUnit.save({
129
131
  mode: 'new',
@@ -143,7 +145,7 @@ describe('createComposePageStateUnit', () => {
143
145
  it('save in reference mode calls yield.resource then bind.body', async () => {
144
146
  const resource = mockUpload('new-4');
145
147
  const body = vi.fn().mockResolvedValue(undefined);
146
- const stateUnit = createComposePageStateUnit(mockClient({ resource, body }), mockBrowse(), {
148
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ resource, body })), mockBrowse(), {
147
149
  annotationUri: 'ann-1',
148
150
  sourceDocumentId: 'doc-1',
149
151
  name: 'Ref',
@@ -167,7 +169,7 @@ describe('createComposePageStateUnit', () => {
167
169
 
168
170
  it('save in clone mode calls yield.createFromToken', async () => {
169
171
  const createFromToken = vi.fn().mockResolvedValue({ resourceId: 'cloned-1' });
170
- const stateUnit = createComposePageStateUnit(mockClient({ createFromToken }), mockBrowse(), {
172
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ createFromToken })), mockBrowse(), {
171
173
  mode: 'clone',
172
174
  token: 'tok-abc',
173
175
  });
@@ -192,9 +194,28 @@ describe('ComposePageStateUnit — StateUnit axioms', () => {
192
194
  assertStateUnitAxioms({
193
195
  setup: () => {
194
196
  const browse = disposeProbe();
195
- return { unit: createComposePageStateUnit(mockClient(), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
197
+ return { unit: createComposePageStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
196
198
  },
197
199
  surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.referenceData$, u.gatheredContext$, u.uploadProgress$],
198
200
  });
199
201
  });
200
202
  });
203
+
204
+ describe('createComposePageStateUnit — entity types load failure', () => {
205
+ // Lower severity than the two hangs: compose defaults to an empty picker,
206
+ // which misrepresents a failed load as "this KB has no entity types".
207
+ // See .plans/PANEL-FAILURE-STATES.md
208
+
209
+ it('surfaces the failure rather than presenting an empty picker as fact', async () => {
210
+ const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
211
+ const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ entityTypes$ })), mockBrowse(), {});
212
+
213
+ entityTypes$.next({ status: 'failed', error: new Error('boom') } as never);
214
+
215
+ expect(await firstValueFrom(stateUnit.entityTypes.loading$)).toBe(false);
216
+ expect(await firstValueFrom(stateUnit.entityTypes.error$)).not.toBeNull();
217
+ expect(await firstValueFrom(stateUnit.entityTypes.value$)).toEqual([]);
218
+
219
+ stateUnit.dispose();
220
+ });
221
+ });
@@ -1,10 +1,11 @@
1
- import { BehaviorSubject, type Observable, map } from 'rxjs';
1
+ import { BehaviorSubject, type Observable } from 'rxjs';
2
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/core';
6
6
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
7
- import type { SemiontClient } from '@semiont/sdk';
7
+ import { trackList, type ListState } from '../../../state/list-state';
8
+ import type { SemiontSession } from '@semiont/sdk';
8
9
  import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
9
10
  import type { UploadProgress } from '@semiont/sdk';
10
11
 
@@ -54,7 +55,7 @@ export interface ComposePageStateUnit extends StateUnit {
54
55
  cloneData$: Observable<CloneData | null>;
55
56
  referenceData$: Observable<ReferenceData | null>;
56
57
  gatheredContext$: Observable<GatheredContext | null>;
57
- entityTypes$: Observable<string[]>;
58
+ entityTypes: ListState<string[]>;
58
59
  /**
59
60
  * Live upload-progress for the in-flight `save(...)` call. Emits the
60
61
  * full `UploadProgress` lifecycle (started → finished) while a save is
@@ -66,11 +67,12 @@ export interface ComposePageStateUnit extends StateUnit {
66
67
  }
67
68
 
68
69
  export function createComposePageStateUnit(
69
- client: SemiontClient,
70
+ session: SemiontSession,
70
71
  browse: ShellStateUnit,
71
72
  params: ComposeParams,
72
73
  auth?: AccessToken,
73
74
  ): ComposePageStateUnit {
75
+ const { client } = session;
74
76
  const disposer = createDisposer();
75
77
  // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
76
78
  // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
@@ -87,9 +89,8 @@ export function createComposePageStateUnit(
87
89
  const gatheredContext$ = new BehaviorSubject<GatheredContext | null>(null);
88
90
  const uploadProgress$ = new BehaviorSubject<UploadProgress | null>(null);
89
91
 
90
- const entityTypes$: Observable<string[]> = client.browse.entityTypes().pipe(
91
- map((e) => e ?? []),
92
- );
92
+ const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
93
+ disposer.add(entityTypes.dispose);
93
94
 
94
95
  // Initialize based on mode
95
96
  if (isReferenceMode) {
@@ -192,7 +193,7 @@ export function createComposePageStateUnit(
192
193
  cloneData$: cloneData$.asObservable(),
193
194
  referenceData$: referenceData$.asObservable(),
194
195
  gatheredContext$: gatheredContext$.asObservable(),
195
- entityTypes$,
196
+ entityTypes: entityTypes.state,
196
197
  uploadProgress$: uploadProgress$.asObservable(),
197
198
  save,
198
199
  dispose: () => {
@@ -36,7 +36,6 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
36
36
  selectedEntityType: '',
37
37
  onSelectedEntityTypeChange: vi.fn(),
38
38
  theme: 'light',
39
- showLineNumbers: false,
40
39
  activePanel: null,
41
40
  onNavigateToResource: vi.fn(),
42
41
  onNavigateToCompose: vi.fn(),
@@ -56,7 +55,9 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
56
55
  composeFirstResource: 'Compose First Resource',
57
56
  archived: 'Archived',
58
57
  created: 'Created:',
59
- loadingKnowledgeBase: 'Loading knowledge base...',
58
+ recentFailed: 'Could not load resources.',
59
+ retry: 'Try again',
60
+ loadingKnowledgeBase: 'Loading knowledge base...',
60
61
  },
61
62
  ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
62
63
  ...overrides,