@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
@@ -20,13 +20,14 @@ import { JsonLdPanel } from '@semiont/react-ui';
20
20
  import { Toolbar } from '@semiont/react-ui';
21
21
  import { useResourceLoadingAnnouncements } from '@semiont/react-ui';
22
22
  import { ResourceViewer } from '@semiont/react-ui';
23
+ import type { BrowseMediaRenderers, AnnotateMediaRenderers } from '@semiont/react-ui';
23
24
  import { useObservable } from '@semiont/react-ui';
24
25
  import { useResourceContent } from '../../../hooks/useResourceContent';
25
26
  import { useMediaToken } from '../../../hooks/useMediaToken';
26
27
  import { useToast } from '../../../components/Toast';
27
28
  import { useOutcomeToasts } from '../../../hooks/useOutcomeToasts';
28
29
  import { useTheme } from '../../../contexts/ThemeContext';
29
- import { useLineNumbers } from '../../../hooks/useLineNumbers';
30
+ import { useLineNumbers } from '../../../contexts/LineNumbersContext';
30
31
  import { useHoverDelay } from '../../../hooks/useHoverDelay';
31
32
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
32
33
  import { useObservableExternalNavigation } from '../../../hooks/useObservableBrowse';
@@ -35,7 +36,7 @@ import { getSelectorType } from '../../../lib/media-shapes';
35
36
  import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
36
37
  import { useSemiont } from '../../../session/SemiontProvider';
37
38
  import { createResourceViewerPageStateUnit } from '../state/resource-viewer-page-state-unit';
38
- import { useStateUnit } from '../../../hooks/useStateUnit';
39
+ import { useSessionStateUnit } from '../../../hooks/useSessionStateUnit';
39
40
  import { useShellStateUnit } from '../../../hooks/useShellStateUnit';
40
41
  import { useTranslations } from '../../../contexts/TranslationContext';
41
42
  import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
@@ -92,6 +93,14 @@ export interface ResourceViewerPageProps {
92
93
  * Name of the active knowledge base (for display in panels)
93
94
  */
94
95
  knowledgeBaseName?: string | undefined;
96
+
97
+ /**
98
+ * Media-renderer overrides, forwarded to `ResourceViewer`. Present at this
99
+ * tier too so a host embedding the whole page — not just the viewer — can
100
+ * still swap a renderer. See .plans/ANNOTATE-RENDERER-REGISTRY.md (D5)
101
+ */
102
+ browseRenderers?: BrowseMediaRenderers;
103
+ annotateRenderers?: AnnotateMediaRenderers;
95
104
  }
96
105
 
97
106
  /**
@@ -107,8 +116,6 @@ export interface ResourceViewerPageProps {
107
116
  * @subscribes yield:clone - Clone the current resource
108
117
  * @subscribes beckon:sparkle - Trigger sparkle animation
109
118
  * @subscribes mark:added - Annotation was created (sparkle)
110
- * @subscribes settings:theme-changed - UI theme changed
111
- * @subscribes settings:line-numbers-toggled - Line numbers display toggled
112
119
  * @subscribes browse:reference-navigate - Navigate to a referenced document
113
120
  * @subscribes browse:entity-type-clicked - Navigate filtered by entity type
114
121
  *
@@ -126,6 +133,8 @@ export function ResourceViewerPage({
126
133
  refetchDocument,
127
134
  streamStatus,
128
135
  knowledgeBaseName,
136
+ browseRenderers,
137
+ annotateRenderers,
129
138
  }: ResourceViewerPageProps) {
130
139
  // Translations
131
140
  const tw = useTranslations('ReferenceWizard');
@@ -148,8 +157,8 @@ export function ResourceViewerPage({
148
157
 
149
158
  // UI state hooks
150
159
  const { showError, showSuccess } = useToast();
151
- const { theme, setTheme } = useTheme();
152
- const { showLineNumbers, toggleLineNumbers } = useLineNumbers();
160
+ const { theme } = useTheme();
161
+ const { showLineNumbers } = useLineNumbers();
153
162
  const { hoverDelayMs } = useHoverDelay();
154
163
  const { triggerSparkleAnimation, clearNewAnnotationId, newAnnotationIds } = useResourceAnnotations();
155
164
 
@@ -185,27 +194,40 @@ export function ResourceViewerPage({
185
194
 
186
195
  // Composite state unit — owns all flow VMs, wizard state, annotations, entity types
187
196
  const browseStateUnit = useShellStateUnit();
188
- const stateUnit = useStateUnit(() => createResourceViewerPageStateUnit(semiont!, rUri, locale, browseStateUnit));
189
-
190
- const annotations = useObservable(stateUnit.annotations$) ?? [];
191
- const groups = useObservable(stateUnit.annotationGroups$);
192
- const allEntityTypes = useObservable(stateUnit.entityTypes$) ?? [];
193
- const referencedByRaw = useObservable(stateUnit.referencedBy$);
194
- const referencedBy = referencedByRaw ?? [];
195
- const referencedByLoading = referencedByRaw === undefined;
196
- const hoveredAnnotationId = useObservable(stateUnit.beckon.hoveredAnnotationId$) ?? null;
197
- const pendingAnnotation = useObservable(stateUnit.mark.pendingAnnotation$) ?? null;
198
- const assistingMotivation = useObservable(stateUnit.mark.assistingMotivation$) ?? null;
199
- const progress = useObservable(stateUnit.mark.progress$) ?? null;
200
- const activePanel = useObservable(stateUnit.browse.activePanel$) ?? null;
201
- const scrollToAnnotationId = useObservable(stateUnit.browse.scrollToAnnotationId$) ?? null;
202
- const panelInitialTab = useObservable(stateUnit.browse.panelInitialTab$) ?? null;
203
- const onScrollCompleted = stateUnit.browse.onScrollCompleted;
204
- const generationProgress = useObservable(stateUnit.yield.progress$) ?? null;
205
- const gatherContext = useObservable(stateUnit.gather.context$) ?? null;
206
- const gatherLoading = useObservable(stateUnit.gather.loading$) ?? false;
207
- const gatherError = useObservable(stateUnit.gather.error$) ?? null;
208
- const wizardState = useObservable(stateUnit.wizard$);
197
+ // Session-typed + session-keyed (SESSION-TYPED-FACTORIES.md): no `!`, no
198
+ // construction without a session, dispose-first rebuild on session swap.
199
+ const stateUnit = useSessionStateUnit(
200
+ session ?? undefined,
201
+ (s) => createResourceViewerPageStateUnit(s, rUri, locale, browseStateUnit),
202
+ );
203
+
204
+ const annotations = useObservable(stateUnit?.annotations.value$) ?? [];
205
+ const annotationsError = useObservable(stateUnit?.annotations.error$) ?? null;
206
+ const groups = useObservable(stateUnit?.annotationGroups$);
207
+ const allEntityTypes = useObservable(stateUnit?.entityTypes.value$) ?? [];
208
+ const entityTypesError = useObservable(stateUnit?.entityTypes.error$) ?? null;
209
+ // Three states, not two: a terminally failed list has no value EITHER, so
210
+ // deriving "loading" from `undefined` leaves a dead request spinning for
211
+ // ever. See .plans/PANEL-FAILURE-STATES.md
212
+ const referencedBy = useObservable(stateUnit?.referencedBy.value$) ?? [];
213
+ const referencedByLoading = useObservable(stateUnit?.referencedBy.loading$) ?? true;
214
+ const referencedByError = useObservable(stateUnit?.referencedBy.error$) ?? null;
215
+ const events = useObservable(stateUnit?.events.value$) ?? [];
216
+ const eventsLoading = useObservable(stateUnit?.events.loading$) ?? true;
217
+ const eventsError = useObservable(stateUnit?.events.error$) ?? null;
218
+ const hoveredAnnotationId = useObservable(stateUnit?.beckon.hoveredAnnotationId$) ?? null;
219
+ const pendingAnnotation = useObservable(stateUnit?.mark.pendingAnnotation$) ?? null;
220
+ const assistingMotivation = useObservable(stateUnit?.mark.assistingMotivation$) ?? null;
221
+ const progress = useObservable(stateUnit?.mark.progress$) ?? null;
222
+ const activePanel = useObservable(stateUnit?.browse.activePanel$) ?? null;
223
+ const scrollToAnnotationId = useObservable(stateUnit?.browse.scrollToAnnotationId$) ?? null;
224
+ const panelInitialTab = useObservable(stateUnit?.browse.panelInitialTab$) ?? null;
225
+ const onScrollCompleted = stateUnit?.browse.onScrollCompleted;
226
+ const generationProgress = useObservable(stateUnit?.yield.progress$) ?? null;
227
+ const gatherContext = useObservable(stateUnit?.gather.context$) ?? null;
228
+ const gatherLoading = useObservable(stateUnit?.gather.loading$) ?? false;
229
+ const gatherError = useObservable(stateUnit?.gather.error$) ?? null;
230
+ const wizardState = useObservable(stateUnit?.wizard$);
209
231
  const wizardOpen = wizardState?.open ?? false;
210
232
  const wizardAnnotationId = wizardState?.annotationId ?? null;
211
233
  const wizardResourceId = wizardState?.resourceId ?? null;
@@ -214,12 +236,12 @@ export function ResourceViewerPage({
214
236
  const [generateOpen, setGenerateOpen] = useState(false);
215
237
 
216
238
  const handleWizardClose = useCallback(() => {
217
- stateUnit.closeWizard();
239
+ stateUnit?.closeWizard();
218
240
  }, [stateUnit]);
219
241
 
220
242
  const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
221
243
  clearNewAnnotationId(annotationId(referenceId));
222
- stateUnit.yield.generate(referenceId, {
244
+ stateUnit?.yield.generate(referenceId, {
223
245
  title: config.title,
224
246
  storageUri: config.storagePath,
225
247
  prompt: config.prompt,
@@ -239,7 +261,7 @@ export function ResourceViewerPage({
239
261
  // NOT a toast. `generateFromResource` is Phase 6 (the @semiont/sdk session);
240
262
  // this is declared RED until that method lands. Do not re-impl it here.
241
263
  const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
242
- stateUnit.yield.generateFromResource({
264
+ stateUnit?.yield.generateFromResource({
243
265
  title: config.title,
244
266
  storageUri: config.storagePath,
245
267
  ...(config.prompt ? { prompt: config.prompt } : {}),
@@ -286,14 +308,15 @@ export function ResourceViewerPage({
286
308
  });
287
309
  }, [browser]);
288
310
 
289
- // Add resource to open tabs when it loads
311
+ // Add resource to open tabs when it loads, and record it as this KB's
312
+ // last-viewed for the /know landing route to resume from. Both are per-KB
313
+ // state owned by the browser — a global last-viewed id sends that redirect
314
+ // into the previously active KB's resource after a switch.
290
315
  useEffect(() => {
291
316
  if (resource && rUri) {
292
317
  const mediaType = getPrimaryMediaType(resource);
293
318
  browser.addOpenResource(rUri, resource.name, mediaType || undefined, resource.storageUri);
294
- if (typeof localStorage !== 'undefined') {
295
- localStorage.setItem('lastViewedDocumentId', rUri);
296
- }
319
+ browser.setLastViewedResource(rUri);
297
320
  }
298
321
  }, [resource, rUri, browser]);
299
322
 
@@ -355,8 +378,6 @@ export function ResourceViewerPage({
355
378
  triggerSparkleAnimation(stored.payload.annotation.id);
356
379
  }, [triggerSparkleAnimation]);
357
380
 
358
- const handleSettingsThemeChanged = useCallback(({ theme }: { theme: any }) => setTheme(theme), [setTheme]);
359
-
360
381
  const handleReferenceNavigate = useCallback(({ resourceId }: { resourceId: string }) => {
361
382
  if (routes.resourceDetail) {
362
383
  const path = routes.resourceDetail(resourceId);
@@ -386,8 +407,6 @@ export function ResourceViewerPage({
386
407
  'yield:clone': handleResourceClone,
387
408
  'beckon:sparkle': handleAnnotationSparkle,
388
409
  'mark:added': handleAnnotationAdded,
389
- 'settings:theme-changed': handleSettingsThemeChanged,
390
- 'settings:line-numbers-toggled': toggleLineNumbers,
391
410
  'browse:reference-navigate': handleReferenceNavigate,
392
411
  'browse:entity-type-clicked': handleEntityTypeClicked,
393
412
  });
@@ -503,6 +522,8 @@ export function ResourceViewerPage({
503
522
  showLineNumbers={showLineNumbers}
504
523
  hoverDelayMs={hoverDelayMs}
505
524
  hoveredAnnotationId={hoveredAnnotationId}
525
+ {...(browseRenderers && { browseRenderers })}
526
+ {...(annotateRenderers && { annotateRenderers })}
506
527
  />
507
528
  )}
508
529
  </ErrorBoundary>
@@ -515,7 +536,6 @@ export function ResourceViewerPage({
515
536
  <ToolbarPanels
516
537
  activePanel={activePanel}
517
538
  theme={theme}
518
- showLineNumbers={showLineNumbers}
519
539
  width={
520
540
  activePanel === 'jsonld' ? 'w-[600px]' :
521
541
  activePanel === 'annotations' ? 'w-[400px]' :
@@ -543,9 +563,14 @@ export function ResourceViewerPage({
543
563
  progress={progress}
544
564
  pendingAnnotation={pendingAnnotation}
545
565
  allEntityTypes={allEntityTypes}
566
+ annotationsError={annotationsError}
567
+ onRetryAnnotations={stateUnit?.annotations.retry}
568
+ entityTypesError={entityTypesError}
546
569
  generatingReferenceId={generationProgress?.annotationId ?? null}
547
570
  referencedBy={referencedBy}
548
571
  referencedByLoading={referencedByLoading}
572
+ referencedByError={referencedByError}
573
+ onRetryReferencedBy={stateUnit?.referencedBy.retry}
549
574
  resourceId={rUri}
550
575
  locale={locale}
551
576
  sourceLanguage={getLanguage(resource)}
@@ -562,7 +587,11 @@ export function ResourceViewerPage({
562
587
  {/* History Panel */}
563
588
  {activePanel === 'history' && (
564
589
  <AnnotationHistory
565
- rUri={rUri}
590
+ events={events}
591
+ eventsLoading={eventsLoading}
592
+ eventsError={eventsError}
593
+ onRetryEvents={stateUnit?.events.retry}
594
+ annotations={annotations}
566
595
  hoveredAnnotationId={hoveredAnnotationId}
567
596
  onEventHover={handleEventHover}
568
597
  onEventClick={handleEventClick}
@@ -676,6 +705,7 @@ export function ResourceViewerPage({
676
705
  resourceId={rUri}
677
706
  defaultTitle=""
678
707
  locale={locale}
708
+ entityTypeOptions={allEntityTypes}
679
709
  onGenerateSubmit={handleResourceGenerateSubmit}
680
710
  translations={{
681
711
  gatherTitle: tg('gatherTitle'),
@@ -1,10 +1,12 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { BehaviorSubject, firstValueFrom } from 'rxjs';
2
+ import { asStates } from '../../../../__tests__/test-client';
3
+ import { sessionOf } from '../../../../__tests__/test-client';
4
+ import { BehaviorSubject, Subject, firstValueFrom } from 'rxjs';
3
5
  import { filter } from 'rxjs/operators';
4
6
  import { resourceId as makeResourceId } from '@semiont/core';
5
7
  import type { SemiontClient } from '@semiont/sdk';
6
8
  import { createResourceLoaderStateUnit } from '../resource-loader-state-unit';
7
- import { assertStateUnitAxioms } from '@semiont/core/testing';
9
+ import { assertStateUnitAxioms } from '@semiont/core/testing/axioms';
8
10
 
9
11
  const RID = makeResourceId('res-1');
10
12
 
@@ -13,15 +15,47 @@ function mockClient(resource$?: BehaviorSubject<unknown>): SemiontClient {
13
15
  const invalidate = vi.fn();
14
16
  return {
15
17
  browse: {
16
- resource: () => subject.asObservable(),
18
+ resource: () => asStates(subject.asObservable()),
17
19
  invalidateResourceDetail: invalidate,
18
20
  },
19
21
  } as unknown as SemiontClient;
20
22
  }
21
23
 
24
+ /**
25
+ * A client whose `browse.resource()` mirrors the cache's B15 contract: each
26
+ * `observe()` call hands back a FRESH observable (the failure marker is
27
+ * cleared and a new attempt chain starts), and a terminal failure arrives as
28
+ * an RxJS error notification on that key.
29
+ */
30
+ function failingClient() {
31
+ const attempts: Array<Subject<unknown>> = [];
32
+ const invalidate = vi.fn();
33
+ const client = {
34
+ browse: {
35
+ resource: () => {
36
+ const s = new Subject<unknown>();
37
+ attempts.push(s);
38
+ return s.asObservable();
39
+ },
40
+ invalidateResourceDetail: invalidate,
41
+ },
42
+ } as unknown as SemiontClient;
43
+ return {
44
+ client,
45
+ invalidate,
46
+ /** Fail the most recent attempt the way B15 does. */
47
+ failLatest: (message: string) =>
48
+ attempts[attempts.length - 1]!.next({ status: 'failed', error: new Error(message) }),
49
+ /** Resolve the most recent attempt with a value. */
50
+ resolveLatest: (value: unknown) =>
51
+ attempts[attempts.length - 1]!.next({ status: 'ready', value }),
52
+ attemptCount: () => attempts.length,
53
+ };
54
+ }
55
+
22
56
  describe('createResourceLoaderStateUnit', () => {
23
57
  it('exposes resource from browse namespace', async () => {
24
- const stateUnit = createResourceLoaderStateUnit(mockClient(), RID);
58
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(mockClient()), RID);
25
59
  const resource = await firstValueFrom(stateUnit.resource$.pipe(filter((r) => r !== undefined)));
26
60
  expect((resource as { name: string }).name).toBe('Test');
27
61
  stateUnit.dispose();
@@ -29,7 +63,7 @@ describe('createResourceLoaderStateUnit', () => {
29
63
 
30
64
  it('reports loading when resource is undefined', async () => {
31
65
  const subject = new BehaviorSubject<unknown>(undefined);
32
- const stateUnit = createResourceLoaderStateUnit(mockClient(subject), RID);
66
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(mockClient(subject)), RID);
33
67
  expect(await firstValueFrom(stateUnit.isLoading$)).toBe(true);
34
68
 
35
69
  subject.next({ '@id': 'res-1' });
@@ -39,18 +73,87 @@ describe('createResourceLoaderStateUnit', () => {
39
73
 
40
74
  it('invalidate calls browse.invalidateResourceDetail', () => {
41
75
  const client = mockClient();
42
- const stateUnit = createResourceLoaderStateUnit(client, RID);
76
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(client), RID);
43
77
  stateUnit.invalidate();
44
78
  expect(client.browse.invalidateResourceDetail).toHaveBeenCalledWith(RID);
45
79
  stateUnit.dispose();
46
80
  });
47
81
  });
48
82
 
83
+ describe('createResourceLoaderStateUnit — terminal failure (B15)', () => {
84
+ // "Loading" cannot be defined as "no value yet": a key that fails with
85
+ // nothing cached has no value EITHER, so a two-state model reports a dead
86
+ // request as an eternal spinner and drops the reason on the floor. The
87
+ // failure must be a state of its own.
88
+ // See .plans/bugs/resource-page-frozen-on-disposed-client-after-kb-switch.md (D4)
89
+
90
+ it('surfaces a terminal failure on error$ instead of leaving it undelivered', async () => {
91
+ const h = failingClient();
92
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(h.client), RID);
93
+ const seen: Array<Error | null> = [];
94
+ stateUnit.error$.subscribe((e) => seen.push(e));
95
+
96
+ expect(seen).toEqual([null]);
97
+ h.failLatest('Resource not found');
98
+
99
+ expect(seen.length).toBe(2);
100
+ expect(seen[1]?.message).toBe('Resource not found');
101
+ stateUnit.dispose();
102
+ });
103
+
104
+ it('stops reporting loading once the request has terminally failed', async () => {
105
+ const h = failingClient();
106
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(h.client), RID);
107
+ const loading: boolean[] = [];
108
+ stateUnit.isLoading$.subscribe((l) => loading.push(l));
109
+
110
+ expect(loading[loading.length - 1]).toBe(true);
111
+ h.failLatest('Resource not found');
112
+
113
+ // The defect: without a failure state this stays true forever, and the
114
+ // page sits on "Loading resource..." with no way out.
115
+ expect(loading[loading.length - 1]).toBe(false);
116
+ stateUnit.dispose();
117
+ });
118
+
119
+ it('an error notification does not tear the unit down — invalidate starts a fresh attempt that can succeed', async () => {
120
+ const h = failingClient();
121
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(h.client), RID);
122
+ h.failLatest('Resource not found');
123
+ expect(await firstValueFrom(stateUnit.error$)).not.toBeNull();
124
+
125
+ stateUnit.invalidate();
126
+ expect(h.invalidate).toHaveBeenCalledWith(RID);
127
+ // B15: a fresh observe() clears the marker and restarts the chain, so the
128
+ // unit must be listening to a NEW attempt, not the errored one.
129
+ expect(h.attemptCount()).toBeGreaterThan(1);
130
+ expect(await firstValueFrom(stateUnit.error$)).toBeNull();
131
+
132
+ h.resolveLatest({ '@id': 'res-1', name: 'Recovered' });
133
+ const resource = await firstValueFrom(stateUnit.resource$.pipe(filter((r) => r !== undefined)));
134
+ expect((resource as { name: string }).name).toBe('Recovered');
135
+ expect(await firstValueFrom(stateUnit.isLoading$)).toBe(false);
136
+ stateUnit.dispose();
137
+ });
138
+
139
+ it('a value arriving after a failure clears the error', async () => {
140
+ const h = failingClient();
141
+ const stateUnit = createResourceLoaderStateUnit(sessionOf(h.client), RID);
142
+ h.failLatest('transient');
143
+ stateUnit.invalidate();
144
+ h.resolveLatest({ '@id': 'res-1', name: 'Test' });
145
+
146
+ expect(await firstValueFrom(stateUnit.error$)).toBeNull();
147
+ stateUnit.dispose();
148
+ });
149
+ });
150
+
49
151
  describe('ResourceLoaderStateUnit — StateUnit axioms', () => {
50
152
  it('satisfies the StateUnit axioms', () => {
51
- // No owned surfaces: resource$/isLoading$ are derived from client.browse; dispose is a no-op.
153
+ // Owns resource$/error$ and a live subscription to client.browse, so
154
+ // dispose completes them and detaches; invalidate is inert afterwards.
52
155
  assertStateUnitAxioms({
53
- setup: () => createResourceLoaderStateUnit(mockClient(), RID),
156
+ setup: () => createResourceLoaderStateUnit(sessionOf(mockClient()), RID),
54
157
  invocations: (u) => [() => u.invalidate()],
55
158
  });
56
159
  });