@semiont/react-ui 0.5.26 → 0.5.28

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 (242) hide show
  1. package/README.md +28 -4
  2. package/dist/{PdfAnnotationCanvas.client-6ZSF6ZTJ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +33 -6
  3. package/dist/{PdfAnnotationCanvas.client-JCJMXWWJ.css → PdfAnnotationCanvas.client-TGDPPGXJ.css} +6 -1
  4. package/dist/{ar-YTUUGK2O.js → ar-FABPTJVB.js} +88 -26
  5. package/dist/{bn-ASGNGPMD.js → bn-LKP6BDMH.js} +88 -26
  6. package/dist/{chunk-L3WKRWP3.js → chunk-2GOHTTHW.js} +88 -26
  7. package/dist/{chunk-OBQOZCJE.js → chunk-2YHEE63S.js} +31 -31
  8. package/dist/{cs-WMYVAOZY.js → cs-YQYNWHRG.js} +88 -26
  9. package/dist/{da-FSHHN4D6.js → da-QHZOYKRY.js} +88 -26
  10. package/dist/{de-FJFO3TPT.js → de-GKA7RD7J.js} +88 -26
  11. package/dist/{el-XOH4QIAG.js → el-45T257AL.js} +88 -26
  12. package/dist/{en-3FCBW3YD.js → en-RVW5EQ66.js} +4 -2
  13. package/dist/{es-HDX4QI7E.js → es-AVER7RU4.js} +88 -26
  14. package/dist/{fa-DJ6WTFTJ.js → fa-K3VPK3O2.js} +88 -26
  15. package/dist/{fi-4S7H7W7U.js → fi-5NEKF7EB.js} +88 -26
  16. package/dist/{fr-6UJSYHXC.js → fr-FUMNDLBZ.js} +88 -26
  17. package/dist/{he-ZZJOHJX2.js → he-3JGNA3PR.js} +88 -26
  18. package/dist/{hi-HZJTAW7U.js → hi-IJY375DF.js} +88 -26
  19. package/dist/{id-E3I6ILEM.js → id-LWHKJ7ZD.js} +88 -26
  20. package/dist/index.css +380 -524
  21. package/dist/index.d.ts +318 -192
  22. package/dist/index.js +2960 -2421
  23. package/dist/integrations/css-modules-helper.js +1 -3
  24. package/dist/{it-ULAPAD7U.js → it-EBVOV4RW.js} +88 -26
  25. package/dist/{ja-SAHZQ4CZ.js → ja-KWRIX2NF.js} +88 -26
  26. package/dist/{ko-DFU2ADQ4.js → ko-L5CJMFDS.js} +88 -26
  27. package/dist/{ms-NWL76XNT.js → ms-LSNMJDR2.js} +88 -26
  28. package/dist/{nl-ATCM76WN.js → nl-ZB32MB7U.js} +88 -26
  29. package/dist/{no-UIAQ5RJF.js → no-4GTNY7M5.js} +88 -26
  30. package/dist/{pl-NOECQDXD.js → pl-LUHBLTHB.js} +88 -26
  31. package/dist/{pt-SVWIIUPC.js → pt-C4H3UNQ6.js} +88 -26
  32. package/dist/{ro-NR2DRTZG.js → ro-GGITS5V4.js} +88 -26
  33. package/dist/{sv-STVJ37JP.js → sv-T56RTLBP.js} +88 -26
  34. package/dist/test-utils.css +1 -1
  35. package/dist/test-utils.d.ts +1 -1
  36. package/dist/test-utils.js +2 -2
  37. package/dist/{th-UWQR4K54.js → th-22X2IWCB.js} +88 -26
  38. package/dist/{tr-WNXZSD27.js → tr-77TFG62H.js} +88 -26
  39. package/dist/{uk-SJRMB47Y.js → uk-MIIL6NXT.js} +88 -26
  40. package/dist/{vi-TMRQFES2.js → vi-7JYOIGNV.js} +88 -26
  41. package/dist/{zh-QOL3LYEM.js → zh-ID6DRYU4.js} +88 -26
  42. package/package.json +11 -10
  43. package/src/components/AssistProgress.tsx +178 -134
  44. package/src/components/CodeMirrorRenderer.tsx +8 -8
  45. package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
  46. package/src/components/ProtectedErrorBoundary.css +3 -1
  47. package/src/components/StatusDisplay.css +1 -1
  48. package/src/components/Toast.css +3 -1
  49. package/src/components/__tests__/AssistProgress.test.tsx +268 -141
  50. package/src/components/annotation/annotation-entries.css +0 -4
  51. package/src/components/annotation/references.css +0 -4
  52. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  53. package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
  54. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
  55. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
  56. package/src/components/layout/Header.css +0 -4
  57. package/src/components/layout/LeftSidebar.css +0 -4
  58. package/src/components/modals/ComposeStep.tsx +218 -0
  59. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  60. package/src/components/modals/ConfigureGenerationStep.tsx +131 -54
  61. package/src/components/modals/ConfigureSearchStep.tsx +42 -40
  62. package/src/components/modals/ContextSummary.tsx +185 -72
  63. package/src/components/modals/CorpusPane.tsx +75 -0
  64. package/src/components/modals/DiscardPrompt.tsx +38 -0
  65. package/src/components/modals/GatherContextStep.tsx +237 -107
  66. package/src/components/modals/ReferenceWizardModal.tsx +287 -60
  67. package/src/components/modals/ResourceGenerateModal.tsx +233 -109
  68. package/src/components/modals/SearchModal.css +335 -111
  69. package/src/components/modals/SearchModal.tsx +1 -1
  70. package/src/components/modals/SearchResultsStep.tsx +63 -88
  71. package/src/components/modals/WizardFooter.tsx +69 -0
  72. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  73. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  74. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +290 -0
  75. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +95 -0
  76. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  77. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +635 -0
  78. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +277 -85
  79. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
  80. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  81. package/src/components/modals/__tests__/WizardFooter.test.tsx +344 -0
  82. package/src/components/modals/modals.css +16 -42
  83. package/src/components/navigation/NavigationTabs.css +0 -20
  84. package/src/components/pdf-annotation/PdfAnnotationCanvas.css +21 -2
  85. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +36 -3
  86. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +41 -6
  87. package/src/components/resource/AnnotateView.tsx +19 -8
  88. package/src/components/resource/AnnotationHistory.tsx +2 -0
  89. package/src/components/resource/BrowseView.tsx +20 -9
  90. package/src/components/resource/ResourceViewer.tsx +27 -28
  91. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  92. package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
  93. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
  94. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  95. package/src/components/resource/__tests__/BrowseView.test.tsx +38 -12
  96. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
  97. package/src/components/resource/annotate-renderers.tsx +3 -3
  98. package/src/components/resource/event-formatting.ts +0 -3
  99. package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
  100. package/src/components/resource/panels/AssessmentPanel.css +5 -6
  101. package/src/components/resource/panels/AssistSection.css +95 -263
  102. package/src/components/resource/panels/AssistSection.tsx +4 -4
  103. package/src/components/resource/panels/AssistShell.tsx +12 -4
  104. package/src/components/resource/panels/CollaborationPanel.css +47 -22
  105. package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
  106. package/src/components/resource/panels/CommentEntry.tsx +1 -1
  107. package/src/components/resource/panels/CommentsPanel.css +0 -16
  108. package/src/components/resource/panels/HighlightEntry.tsx +1 -1
  109. package/src/components/resource/panels/HighlightPanel.css +0 -7
  110. package/src/components/resource/panels/JsonLdPanel.css +0 -4
  111. package/src/components/resource/panels/ReferenceEntry.tsx +10 -3
  112. package/src/components/resource/panels/ReferencesPanel.css +0 -12
  113. package/src/components/resource/panels/ReferencesPanel.tsx +16 -20
  114. package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  116. package/src/components/resource/panels/StatisticsPanel.css +0 -20
  117. package/src/components/resource/panels/TagEntry.tsx +1 -1
  118. package/src/components/resource/panels/TaggingPanel.css +0 -4
  119. package/src/components/resource/panels/TaggingPanel.tsx +2 -10
  120. package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
  121. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  122. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
  123. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
  124. package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
  125. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
  126. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
  127. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
  128. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
  129. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -1
  130. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
  131. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +67 -54
  132. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +185 -7
  133. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
  134. package/src/components/settings/SettingsPanel.css +0 -4
  135. package/src/features/auth/auth.css +2 -26
  136. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  137. package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
  138. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -76
  139. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -48
  140. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
  141. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
  142. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
  143. package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
  144. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
  145. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +166 -71
  146. package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
  147. package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
  148. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
  149. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
  150. package/src/styles/core/buttons.css +3 -1
  151. package/src/styles/core/forms.css +35 -29
  152. package/src/styles/core/indicators.css +0 -4
  153. package/src/styles/core/inputs.css +0 -4
  154. package/src/styles/core/progress.css +2 -34
  155. package/src/styles/core/sliders.css +0 -4
  156. package/src/styles/core/toggles.css +0 -4
  157. package/src/styles/features/resource-discovery.css +13 -0
  158. package/src/styles/features/resource-viewer.css +0 -42
  159. package/src/styles/features/resource.css +0 -4
  160. package/src/styles/motivations/motivation-assessment.css +2 -2
  161. package/src/styles/panels/history-panel.css +0 -40
  162. package/src/styles/panels/user-panel.css +0 -3
  163. package/src/styles/patterns/panel-helpers.css +0 -27
  164. package/src/styles/patterns/panel-sections.css +0 -59
  165. package/src/styles/patterns/panels-base.css +0 -32
  166. package/translations/ar.json +84 -24
  167. package/translations/bn.json +84 -24
  168. package/translations/cs.json +84 -24
  169. package/translations/da.json +84 -24
  170. package/translations/de.json +84 -24
  171. package/translations/el.json +84 -24
  172. package/translations/en.json +84 -24
  173. package/translations/es.json +84 -24
  174. package/translations/fa.json +84 -24
  175. package/translations/fi.json +84 -24
  176. package/translations/fr.json +84 -24
  177. package/translations/he.json +84 -24
  178. package/translations/hi.json +84 -24
  179. package/translations/id.json +84 -24
  180. package/translations/it.json +84 -24
  181. package/translations/ja.json +84 -24
  182. package/translations/ko.json +84 -24
  183. package/translations/ms.json +84 -24
  184. package/translations/nl.json +84 -24
  185. package/translations/no.json +84 -24
  186. package/translations/pl.json +84 -24
  187. package/translations/pt.json +84 -24
  188. package/translations/ro.json +84 -24
  189. package/translations/sv.json +84 -24
  190. package/translations/th.json +84 -24
  191. package/translations/tr.json +84 -24
  192. package/translations/uk.json +84 -24
  193. package/translations/vi.json +84 -24
  194. package/translations/zh.json +84 -24
  195. package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +0 -1
  196. package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +0 -1
  197. package/dist/ar-YTUUGK2O.js.map +0 -1
  198. package/dist/bn-ASGNGPMD.js.map +0 -1
  199. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  200. package/dist/chunk-L3WKRWP3.js.map +0 -1
  201. package/dist/chunk-O23TLLXW.js.map +0 -1
  202. package/dist/chunk-OBQOZCJE.js.map +0 -1
  203. package/dist/chunk-QT7LYM72.js.map +0 -1
  204. package/dist/chunk-VXASXU4K.js.map +0 -1
  205. package/dist/chunk-XUDKYAVC.js +0 -21
  206. package/dist/chunk-XUDKYAVC.js.map +0 -1
  207. package/dist/cs-WMYVAOZY.js.map +0 -1
  208. package/dist/da-FSHHN4D6.js.map +0 -1
  209. package/dist/de-FJFO3TPT.js.map +0 -1
  210. package/dist/el-XOH4QIAG.js.map +0 -1
  211. package/dist/en-3FCBW3YD.js.map +0 -1
  212. package/dist/es-HDX4QI7E.js.map +0 -1
  213. package/dist/fa-DJ6WTFTJ.js.map +0 -1
  214. package/dist/fi-4S7H7W7U.js.map +0 -1
  215. package/dist/fr-6UJSYHXC.js.map +0 -1
  216. package/dist/he-ZZJOHJX2.js.map +0 -1
  217. package/dist/hi-HZJTAW7U.js.map +0 -1
  218. package/dist/id-E3I6ILEM.js.map +0 -1
  219. package/dist/index.css.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/integrations/css-modules-helper.js.map +0 -1
  222. package/dist/integrations/styled-components-theme.js.map +0 -1
  223. package/dist/it-ULAPAD7U.js.map +0 -1
  224. package/dist/ja-SAHZQ4CZ.js.map +0 -1
  225. package/dist/ko-DFU2ADQ4.js.map +0 -1
  226. package/dist/ms-NWL76XNT.js.map +0 -1
  227. package/dist/nl-ATCM76WN.js.map +0 -1
  228. package/dist/no-UIAQ5RJF.js.map +0 -1
  229. package/dist/pl-NOECQDXD.js.map +0 -1
  230. package/dist/pt-SVWIIUPC.js.map +0 -1
  231. package/dist/ro-NR2DRTZG.js.map +0 -1
  232. package/dist/sv-STVJ37JP.js.map +0 -1
  233. package/dist/test-utils.css.map +0 -1
  234. package/dist/test-utils.js.map +0 -1
  235. package/dist/th-UWQR4K54.js.map +0 -1
  236. package/dist/tr-WNXZSD27.js.map +0 -1
  237. package/dist/uk-SJRMB47Y.js.map +0 -1
  238. package/dist/vi-TMRQFES2.js.map +0 -1
  239. package/dist/zh-QOL3LYEM.js.map +0 -1
  240. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  241. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -159
  242. package/src/integrations/tailwind-plugin.cjs +0 -224
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
2
2
  import { asStates } from '../../../../__tests__/test-client';
3
3
  import { sessionOf } from '../../../../__tests__/test-client';
4
4
  import { BehaviorSubject, firstValueFrom } from 'rxjs';
5
- import { filter, skip, take, toArray } from 'rxjs/operators';
5
+ import { filter, map, skip, take, toArray } from 'rxjs/operators';
6
6
  import type { SemiontClient } from '@semiont/sdk';
7
7
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
8
8
  import { createDiscoverStateUnit } from '../discover-state-unit';
@@ -23,6 +23,8 @@ function mockClient(overrides: {
23
23
  resources$?: BehaviorSubject<unknown[] | undefined>;
24
24
  entityTypes$?: BehaviorSubject<string[] | undefined>;
25
25
  resourcesFn?: (filters: BrowseFilters) => BehaviorSubject<unknown[] | undefined>;
26
+ /** What the stubbed list envelope reports as its match kind. */
27
+ matchKind?: 'lexical' | 'semantic';
26
28
  } = {}): { client: SemiontClient; resourceCalls: BrowseFilters[] } {
27
29
  const resourceCalls: BrowseFilters[] = [];
28
30
  const defaultResources$ =
@@ -36,7 +38,11 @@ function mockClient(overrides: {
36
38
  browse: {
37
39
  resources: (filters: BrowseFilters = {}) => {
38
40
  resourceCalls.push(filters);
39
- return asStates(resourcesFn(filters).asObservable());
41
+ return asStates(resourcesFn(filters).asObservable().pipe(
42
+ // Arrays get the list envelope `resources()` now emits; explicit
43
+ // status objects (B15 fixtures) and `undefined` pass through.
44
+ map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: overrides.matchKind ?? 'lexical' } : v)),
45
+ ));
40
46
  },
41
47
  entityTypes: () => asStates(entityTypes$.asObservable()),
42
48
  },
@@ -45,6 +51,50 @@ function mockClient(overrides: {
45
51
  return { client, resourceCalls };
46
52
  }
47
53
 
54
+ describe('createDiscoverStateUnit — search match kind (SEMANTIC-FALLBACK P3b)', () => {
55
+ // S10 is tier-agnostic: the label and the resources it describes must arrive
56
+ // as ONE value. Exposing `matchKind$` as a second observable beside
57
+ // `state$.results` would satisfy the component and still let a render pair
58
+ // this query's label with the previous query's list.
59
+ it('emits matchKind in the SAME value as the results it labels', async () => {
60
+ vi.useFakeTimers();
61
+ try {
62
+ const results$ = new BehaviorSubject<unknown[] | undefined>([{ '@id': 'sem-1' }]);
63
+ const { client } = mockClient({ resourcesFn: () => results$, matchKind: 'semantic' });
64
+ const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
65
+
66
+ // Subscribe BEFORE setQuery: `queryInput$` is a plain Subject, so a value
67
+ // pushed with nothing listening is gone.
68
+ const collected: Array<{ results: unknown[]; matchKind?: string }> = [];
69
+ const sub = stateUnit.search.state$.subscribe((st) => collected.push(st));
70
+
71
+ stateUnit.search.setQuery('kitten');
72
+ await vi.advanceTimersByTimeAsync(300);
73
+
74
+ const last = collected.at(-1)!;
75
+ expect(last.results).toEqual([{ '@id': 'sem-1' }]);
76
+ expect(last.matchKind).toBe('semantic');
77
+
78
+ sub.unsubscribe();
79
+ stateUnit.dispose();
80
+ } finally {
81
+ vi.useRealTimers();
82
+ }
83
+ });
84
+
85
+ it('carries no matchKind on the empty-query view', async () => {
86
+ // Clearing the box returns to `recent`, which no label describes.
87
+ const { client } = mockClient();
88
+ const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
89
+
90
+ const first = await firstValueFrom(stateUnit.search.state$);
91
+ expect(first.results).toEqual([]);
92
+ expect(first.matchKind).toBeUndefined();
93
+
94
+ stateUnit.dispose();
95
+ });
96
+ });
97
+
48
98
  describe('createDiscoverStateUnit', () => {
49
99
  it('exposes recent resources from browse namespace', async () => {
50
100
  const { client } = mockClient();
@@ -201,7 +251,7 @@ describe('createDiscoverStateUnit', () => {
201
251
  stateUnit.search.setQuery('lincoln');
202
252
  await vi.advanceTimersByTimeAsync(300);
203
253
 
204
- expect(collected.at(-1)).toEqual({ results: [{ '@id': 'hit' }], isSearching: false });
254
+ expect(collected.at(-1)).toEqual({ results: [{ '@id': 'hit' }], isSearching: false, matchKind: 'lexical' });
205
255
 
206
256
  sub.unsubscribe();
207
257
  stateUnit.dispose();
@@ -1,5 +1,5 @@
1
1
  import { BehaviorSubject, Subject, combineLatest, of, type Observable } from 'rxjs';
2
- import { readyValue } from '@semiont/sdk';
2
+ import { readyValue, type CacheState } from '@semiont/sdk';
3
3
  import { debounceTime, distinctUntilChanged, map, startWith, switchMap, shareReplay } from 'rxjs/operators';
4
4
  import type { ResourceDescriptor } from '@semiont/core';
5
5
  import type { SemiontSession } from '@semiont/sdk';
@@ -14,7 +14,18 @@ const DEBOUNCE_MS = 250;
14
14
 
15
15
  export interface DiscoverSearchPipeline {
16
16
  query$: Observable<string>;
17
- state$: Observable<{ results: ResourceDescriptor[]; isSearching: boolean }>;
17
+ /**
18
+ * The page AND the label that describes it, as one value. `matchKind` is
19
+ * carried here rather than beside it because SEMANTIC-FALLBACK S10 is
20
+ * tier-agnostic: two separately-observable pieces of state let a render pair
21
+ * this query's label with the previous query's list. Undefined only before
22
+ * the first answer (and while a query is empty), never as a third kind.
23
+ */
24
+ state$: Observable<{
25
+ results: ResourceDescriptor[];
26
+ isSearching: boolean;
27
+ matchKind?: 'lexical' | 'semantic';
28
+ }>;
18
29
  setQuery(value: string): void;
19
30
  }
20
31
 
@@ -58,7 +69,11 @@ export function createDiscoverStateUnit(
58
69
  limit: RECENT_LIMIT,
59
70
  archived: false,
60
71
  ...(et ? { entityType: et } : {}),
61
- }),
72
+ }).pipe(
73
+ // This unit renders only the page; project it out of the list
74
+ // envelope (`matchKind` rendering is SEMANTIC-FALLBACK P3b's).
75
+ map((st): CacheState<ResourceDescriptor[]> => (st.status === 'ready' ? { status: 'ready', value: st.value.resources } : st)),
76
+ ),
62
77
  ),
63
78
  ),
64
79
  [],
@@ -74,11 +89,13 @@ export function createDiscoverStateUnit(
74
89
  distinctUntilChanged(),
75
90
  );
76
91
 
77
- const state$: Observable<{ results: ResourceDescriptor[]; isSearching: boolean }> =
92
+ const state$: DiscoverSearchPipeline['state$'] =
78
93
  combineLatest([debouncedQuery$, selectedEntityType$]).pipe(
79
94
  switchMap(([q, et]) => {
80
95
  const trimmed = q.trim();
81
96
  if (!trimmed) {
97
+ // No query, no answer to label: `recent` is what renders, and no
98
+ // matchKind describes it.
82
99
  return of({ results: [] as ResourceDescriptor[], isSearching: false });
83
100
  }
84
101
  return client.browse
@@ -88,10 +105,14 @@ export function createDiscoverStateUnit(
88
105
  ...(et ? { entityType: et } : {}),
89
106
  })
90
107
  .pipe(
91
- map((st) => ({
92
- results: readyValue(st) ?? [],
93
- isSearching: st.status === 'pending',
94
- })),
108
+ map((st) => {
109
+ const ready = readyValue(st);
110
+ return {
111
+ results: ready?.resources ?? [],
112
+ isSearching: st.status === 'pending',
113
+ ...(ready ? { matchKind: ready.matchKind } : {}),
114
+ };
115
+ }),
95
116
  startWith({ results: [] as ResourceDescriptor[], isSearching: true }),
96
117
  );
97
118
  }),
@@ -14,7 +14,8 @@ import { ToastProvider } from '../../../components/Toast';
14
14
  import { ThemeProvider } from '../../../contexts/ThemeContext';
15
15
  import { LineNumbersProvider } from '../../../contexts/LineNumbersContext';
16
16
  import { createTestSemiontWrapper } from '../../../test-utils';
17
- import type { ResourceId } from '@semiont/core';
17
+ import type { BodyOperation, EventMap, ResourceId, UserId } from '@semiont/core';
18
+ import { annotationId as makeAnnotationId, resourceId as makeResourceId } from '@semiont/core';
18
19
 
19
20
  // jsdom doesn't implement window.matchMedia — mock it for useTheme
20
21
  Object.defineProperty(window, 'matchMedia', {
@@ -102,24 +103,22 @@ useDebouncedCallback: (fn: any) => fn,
102
103
  announceResourceLoaded: vi.fn(),
103
104
  }),
104
105
  useEventSubscriptions: vi.fn(),
105
- useResourceAnnotations: () => ({
106
- clearNewAnnotationId: vi.fn(),
107
- newAnnotationIds: new Set(),
108
- markAnnotation: vi.fn(),
109
- deleteAnnotation: vi.fn(),
110
- triggerSparkleAnimation: vi.fn(),
111
- }),
106
+ useResourceAnnotations: () => sparkleContext,
112
107
  };
113
108
  });
114
109
 
110
+ // Shared, assertable spies — the page's sparkle wiring is pinned against
111
+ // these (a per-call `vi.fn()` object would be unobservable from the tests).
112
+ const sparkleContext = vi.hoisted(() => ({
113
+ clearSparkle: vi.fn(),
114
+ sparkleAnnotationIds: new Set<string>(),
115
+ markAnnotation: vi.fn(),
116
+ deleteAnnotation: vi.fn(),
117
+ triggerSparkleAnimation: vi.fn(),
118
+ }));
119
+
115
120
  vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
116
- useResourceAnnotations: () => ({
117
- clearNewAnnotationId: vi.fn(),
118
- newAnnotationIds: new Set(),
119
- markAnnotation: vi.fn(),
120
- deleteAnnotation: vi.fn(),
121
- triggerSparkleAnimation: vi.fn(),
122
- }),
121
+ useResourceAnnotations: () => sparkleContext,
123
122
  ResourceAnnotationsProvider: ({ children }: any) => children,
124
123
  }));
125
124
 
@@ -206,6 +205,63 @@ describe('ResourceViewerPage — outcome toasts reach the user', () => {
206
205
  });
207
206
  });
208
207
 
208
+ // RESOLUTION-SPARKLE D2: a reference resolving — from ANY strategy, local or
209
+ // remote — arrives as mark:body-updated with a linking-add operation, and the
210
+ // page turns exactly that into a sparkle. The harness mocks
211
+ // useEventSubscriptions, so the pin drives the captured handler directly.
212
+ describe('ResourceViewerPage — resolution sparkles', () => {
213
+ const bodyUpdated = (operations: BodyOperation[]): EventMap['mark:body-updated'] => ({
214
+ id: 'evt-1',
215
+ timestamp: '2026-08-21T12:00:00Z',
216
+ resourceId: makeResourceId('test-123'),
217
+ userId: 'did:web:example.com:users:alice' as UserId,
218
+ version: 1,
219
+ type: 'mark:body-updated',
220
+ payload: { annotationId: makeAnnotationId('ann-7'), operations },
221
+ metadata: { sequenceNumber: 1 },
222
+ });
223
+
224
+ const renderAndGetHandler = () => {
225
+ mockUseEventSubscriptions.mockClear();
226
+ sparkleContext.triggerSparkleAnimation.mockClear();
227
+ renderWithProviders(<ResourceViewerPage {...createMockProps()} />);
228
+ const map = mockUseEventSubscriptions.mock.calls.at(-1)?.[0] as
229
+ Record<string, (event: EventMap['mark:body-updated']) => void>;
230
+ return map['mark:body-updated'];
231
+ };
232
+
233
+ it('an operation adding a linking SpecificResource sparkles the annotation', () => {
234
+ const handler = renderAndGetHandler();
235
+ expect(handler).toBeDefined();
236
+
237
+ handler!(bodyUpdated([
238
+ { op: 'add', item: { type: 'SpecificResource', source: 'res-target', purpose: 'linking' } },
239
+ ]));
240
+
241
+ expect(sparkleContext.triggerSparkleAnimation).toHaveBeenCalledWith('ann-7');
242
+ });
243
+
244
+ it('unlink (remove-only operations) stays dark — A2', () => {
245
+ const handler = renderAndGetHandler();
246
+
247
+ handler?.(bodyUpdated([
248
+ { op: 'remove', item: { type: 'SpecificResource', source: 'res-target', purpose: 'linking' } },
249
+ ]));
250
+
251
+ expect(sparkleContext.triggerSparkleAnimation).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it('a TextualBody add stays dark', () => {
255
+ const handler = renderAndGetHandler();
256
+
257
+ handler?.(bodyUpdated([
258
+ { op: 'add', item: { type: 'TextualBody', value: 'Person', purpose: 'tagging' } },
259
+ ]));
260
+
261
+ expect(sparkleContext.triggerSparkleAnimation).not.toHaveBeenCalled();
262
+ });
263
+ });
264
+
209
265
  describe('ResourceViewerPage', () => {
210
266
  beforeEach(() => {
211
267
  });