@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
@@ -1,79 +1,55 @@
1
1
  /**
2
- * GENERATE-FROM-BUTTON P2/P4 the resource-generate flow modal.
2
+ * GENERATE-FROM-BUTTON P2/P4 FLOW-LIFECYCLE-CONVERGENCE P3
3
+ * GATHER-AT-THE-TOP P2 — the resource-generate flow modal.
3
4
  *
4
- * Drives the step machine: configure-gather review configure-generation.
5
- * `useResourceGather` is mocked so we control the gathered context and assert
6
- * how the step wires `gather()` (incl. the Phase-4 exclusion threading) and
7
- * `onGenerateSubmit`. `useSemiont` is mocked to feed the entity-type options.
5
+ * One composite stack, no step machine: the gather controls sit at the TOP,
6
+ * the gathered evidence appears below them once gather fires, and the
7
+ * generation params render beneath the evidence once context arrives
8
+ * [gather zone] [evidence] [act zone], the same grammar the resolve
9
+ * wizard expresses. Gather state arrives as PROPS (the page reads
10
+ * `gather.resourceContext$` and friends off the state unit) and the gather
11
+ * itself is an `onGather` callback — the modal mocks no hook and reaches for
12
+ * no provider (FLC A6). Entity-type options are owner-supplied, so a failed
13
+ * load cannot surface as an empty vocabulary.
8
14
  */
9
15
  import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
10
16
  import { render, screen, fireEvent } from '@testing-library/react';
11
17
  import '@testing-library/jest-dom';
12
- import { BehaviorSubject, of } from 'rxjs';
13
18
  import type { GatheredContext } from '@semiont/core';
19
+ import type { ResourceGatherOptions } from '@semiont/sdk';
14
20
  import type { GenerationConfig } from '../ConfigureGenerationStep';
21
+ import { ResourceGenerateModal } from '../ResourceGenerateModal';
15
22
 
16
23
  const RESOURCE_CONTEXT = {
17
- focus: { kind: 'resource', resource: { id: 'res-1', name: 'My Resource' }, summary: 'A short summary' },
24
+ focus: { kind: 'resource', resource: { '@id': 'res-1', name: 'My Resource' }, summary: 'A short summary' },
18
25
  graph: { nodes: [{ id: 'res-1', type: 'resource', label: 'My Resource' }], edges: [] },
19
26
  metadata: {},
20
27
  } as unknown as GatheredContext;
21
28
 
22
- // Mutable hook state + spies, shared with the hoisted vi.mock factory.
23
- const h = vi.hoisted(() => ({
24
- gather: vi.fn(),
25
- reset: vi.fn(),
26
- state: { context: null as unknown, loading: false, error: null as Error | null },
27
- }));
28
-
29
- vi.mock('../../../hooks/useResourceGather', () => ({
30
- useResourceGather: () => ({
31
- context: h.state.context,
32
- loading: h.state.loading,
33
- error: h.state.error,
34
- gather: h.gather,
35
- reset: h.reset,
36
- }),
37
- }));
38
-
39
- // The modal no longer fetches entity types; the owner supplies them, so a
40
- // failed load cannot surface as an empty vocabulary. The stable-observable
41
- // note below is kept because the client mock still backs other calls.
42
- // See .plans/PANEL-FAILURE-STATES.md
43
- // Stable observable instance — the modal calls `entityTypes()` inline in
44
- // render, so a fresh observable each call would re-subscribe every render and
45
- // loop. The real SDK returns a cached observable; mirror that here.
46
- const entityTypes$ = of<string[]>(['Person', 'Topic']);
47
- const mockClient = { browse: { entityTypes: () => entityTypes$ } };
48
- const activeSession$ = new BehaviorSubject<unknown>({ client: mockClient });
49
- vi.mock('../../../session/SemiontProvider', async () => {
50
- const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>('../../../session/SemiontProvider');
51
- return { ...actual, useSemiont: () => ({ activeSession$ }) };
52
- });
53
-
54
- import { ResourceGenerateModal } from '../ResourceGenerateModal';
55
-
56
29
  const T = {
57
- gatherTitle: 'Configure Gather',
58
- reviewTitle: 'Review Context',
59
- configureTitle: 'Configure Generation',
60
- next: 'Next',
61
- back: 'Back',
62
- cancel: 'Cancel',
30
+ title: 'Generate from this Resource',
63
31
  gatherIntro: 'Choose what to include.',
64
32
  includeContent: 'Include content',
65
33
  includeSummary: 'Include summary',
66
34
  gatherDepth: 'Depth',
67
35
  gatherMaxResources: 'Max resources',
68
36
  gatherButton: 'Gather',
69
- excludeLabel: 'Exclude from recall',
37
+ recallLabel: 'Included in recall — deselect to exclude',
70
38
  loadingContext: 'Gathering…',
71
39
  failedContext: 'Failed',
72
40
  sourceContextLabel: 'Resource',
73
41
  connectionsLabel: 'Connections',
74
42
  citedByLabel: 'Cited by',
43
+ graphPaneTitle: 'In the graph',
44
+ graphEmpty: 'No links yet.', resourceLinkLabel: 'Resource link',
45
+ corpusPaneTitle: 'In the corpus',
46
+ corpusEmpty: 'Nothing similar in the corpus.',
47
+ excludedReceipt: '{{types}} excluded from this recall',
48
+ machineRead: 'OCR',
49
+ score: 'Score',
75
50
  resourceTitle: 'New resource title',
76
51
  resourceTitlePlaceholder: 'Title…',
52
+ saveLocation: 'Save location',
77
53
  additionalInstructions: 'Additional Instructions',
78
54
  additionalInstructionsPlaceholder: 'Optional…',
79
55
  language: 'Language',
@@ -83,11 +59,17 @@ const T = {
83
59
  creativityCreative: 'Creative',
84
60
  maxLength: 'Max Length',
85
61
  maxLengthHelp: 'Max help',
62
+ maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
86
63
  generate: 'Generate',
64
+ discardDraftPrompt: 'Discard this draft?',
65
+ discardDraft: 'Discard',
66
+ keepEditing: 'Keep editing',
67
+ editGather: 'Change gather settings',
87
68
  };
88
69
 
89
70
  let onClose: Mock<() => void>;
90
71
  let onGenerateSubmit: Mock<(resourceId: string, config: GenerationConfig) => void>;
72
+ let onGather: Mock<(options: ResourceGatherOptions) => void>;
91
73
 
92
74
  function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerateModal>> = {}) {
93
75
  return render(
@@ -99,6 +81,10 @@ function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerate
99
81
  locale="en"
100
82
  entityTypeOptions={['Person', 'Topic']}
101
83
  onGenerateSubmit={onGenerateSubmit}
84
+ gatherContext={null}
85
+ gatherLoading={false}
86
+ gatherError={null}
87
+ onGather={onGather}
102
88
  translations={T}
103
89
  {...props}
104
90
  />,
@@ -110,39 +96,81 @@ describe('ResourceGenerateModal', () => {
110
96
  vi.clearAllMocks();
111
97
  onClose = vi.fn<() => void>();
112
98
  onGenerateSubmit = vi.fn<(resourceId: string, config: GenerationConfig) => void>();
113
- h.state = { context: null, loading: false, error: null };
114
- activeSession$.next({ client: mockClient });
99
+ onGather = vi.fn<(options: ResourceGatherOptions) => void>();
115
100
  // jsdom doesn't implement scrollIntoView; GatherContextStep may call it.
116
101
  Element.prototype.scrollIntoView = vi.fn();
117
102
  });
118
103
 
119
104
  it('renders nothing when closed', () => {
120
105
  renderModal({ isOpen: false });
121
- expect(screen.queryByText('Configure Gather')).not.toBeInTheDocument();
106
+ expect(screen.queryByText(T.title)).not.toBeInTheDocument();
122
107
  });
123
108
 
124
- it('opens on the configure-gather step with the exclusion options', () => {
125
- renderModal();
126
- expect(screen.getByText('Configure Gather')).toBeInTheDocument(); // step title
127
- expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
128
- expect(screen.getByLabelText('Include content')).toBeInTheDocument();
109
+ it('opens with the gather controls at the top and nothing below them yet', () => {
110
+ // Stale slots must not leak a previous run's evidence into a fresh open:
111
+ // even with a context PROP already present, nothing renders below the
112
+ // controls until THIS run's gather fires.
113
+ const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
114
+ expect(screen.getByText(T.title)).toBeInTheDocument(); // the one modal title
115
+ expect(screen.getByText(T.gatherIntro)).toBeInTheDocument();
116
+ expect(screen.getByLabelText(T.includeContent)).toBeInTheDocument();
129
117
  expect(screen.getByRole('button', { name: 'Person' })).toBeInTheDocument();
130
118
  expect(screen.getByRole('button', { name: 'Topic' })).toBeInTheDocument();
119
+ expect(baseElement.querySelector('.semiont-wizard__step-scroll')).not.toBeNull();
120
+ expect(screen.queryByText(T.graphPaneTitle)).not.toBeInTheDocument();
121
+ expect(screen.queryByLabelText(T.resourceTitle)).not.toBeInTheDocument();
131
122
  });
132
123
 
133
- it('submitting the gather step calls gather() and advances to review (exclusion omitted when none picked)', () => {
124
+ it('Gather emits onGather; the spent controls collapse to a receipt of what fired', () => {
125
+ // The modal no longer knows how to gather — it says WHAT to gather and the
126
+ // page wires the state unit (FLC D3). No resourceId in the payload: the
127
+ // owner already holds it.
128
+ const { baseElement } = renderModal({ gatherLoading: true });
129
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
130
+ expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
131
+ expect(baseElement.querySelector('.semiont-gather__loading')).not.toBeNull();
132
+ // The form's job is done: it folds into a one-line receipt of what THIS
133
+ // gather asked for, and the evidence leads the pane. The form stays
134
+ // mounted (hidden) so its values survive for the expand path.
135
+ const receipt = screen.getByTitle(T.editGather);
136
+ expect(receipt.textContent).toContain(`${T.gatherDepth} 2`);
137
+ expect(receipt.textContent).toContain(`${T.gatherMaxResources} 10`);
138
+ expect(receipt.textContent).toContain(T.includeContent);
139
+ expect(screen.getByText(T.gatherIntro)).not.toBeVisible();
140
+ });
141
+
142
+ it('the receipt names the exclusions of THIS recall', () => {
134
143
  renderModal();
144
+ fireEvent.click(screen.getByRole('button', { name: 'Person' }));
135
145
  fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
136
- expect(h.gather).toHaveBeenCalledWith('res-1', { includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
137
- expect(screen.getByText('Review Context')).toBeInTheDocument(); // step title flipped
138
- expect(screen.getByRole('button', { name: /Next/ })).toBeDisabled(); // no context yet
146
+ expect(screen.getByTitle(T.editGather).textContent)
147
+ .toContain('Person excluded from this recall');
139
148
  });
140
149
 
141
- it('threads picked entity types into the gather call', () => {
150
+ it('the receipt expands back to the form — values preserved — and re-gather collapses again', () => {
151
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
152
+ fireEvent.change(screen.getByLabelText(T.gatherDepth), { target: { value: '4' } });
153
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
154
+ expect(screen.getByTitle(T.editGather).textContent).toContain(`${T.gatherDepth} 4`);
155
+
156
+ fireEvent.click(screen.getByTitle(T.editGather));
157
+ const depthInput = screen.getByLabelText(T.gatherDepth);
158
+ expect(depthInput).toBeVisible();
159
+ expect(depthInput).toHaveValue(4); // not re-initialized — the form never unmounted
160
+ expect(screen.queryByTitle(T.editGather)).not.toBeInTheDocument();
161
+
162
+ fireEvent.change(depthInput, { target: { value: '3' } });
163
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
164
+ expect(onGather).toHaveBeenCalledTimes(2);
165
+ expect(onGather).toHaveBeenLastCalledWith(expect.objectContaining({ depth: 3 }));
166
+ expect(screen.getByTitle(T.editGather).textContent).toContain(`${T.gatherDepth} 3`);
167
+ });
168
+
169
+ it('threads crossed-off entity types into the gather options as the exclude list', () => {
142
170
  renderModal();
143
- fireEvent.click(screen.getByRole('button', { name: 'Person' })); // select to exclude
171
+ fireEvent.click(screen.getByRole('button', { name: 'Person' })); // cross off the recall
144
172
  fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
145
- expect(h.gather).toHaveBeenCalledWith('res-1', {
173
+ expect(onGather).toHaveBeenCalledWith({
146
174
  includeContent: true,
147
175
  includeSummary: true,
148
176
  depth: 2,
@@ -151,17 +179,34 @@ describe('ResourceGenerateModal', () => {
151
179
  });
152
180
  });
153
181
 
154
- it('walks gather review configure-generation emits onGenerateSubmit then closes', () => {
155
- h.state.context = RESOURCE_CONTEXT; // gather already resolved
182
+ it('every entity type starts INCLUDED deselecting is what excludes (recall chips)', () => {
183
+ // The SDK's wire is an exclude list, but the natural posture is
184
+ // "everything is in the recall until you say otherwise": chips render
185
+ // included by default and a click crosses one off.
156
186
  renderModal();
187
+ expect(screen.getByText(T.recallLabel)).toBeInTheDocument();
188
+ const person = screen.getByRole('button', { name: 'Person' });
189
+ const topic = screen.getByRole('button', { name: 'Topic' });
190
+ expect(person).toHaveAttribute('aria-pressed', 'true');
191
+ expect(person).toHaveAttribute('data-included', 'true');
192
+ expect(person.className).toContain('semiont-form__recall-chip');
193
+
194
+ fireEvent.click(person);
195
+ expect(person).toHaveAttribute('aria-pressed', 'false');
196
+ expect(person).toHaveAttribute('data-included', 'false');
197
+ expect(topic).toHaveAttribute('aria-pressed', 'true'); // the rest stay in
198
+
199
+ fireEvent.click(person); // back in — the toggle round-trips
200
+ expect(person).toHaveAttribute('aria-pressed', 'true');
201
+ });
202
+
203
+ it('params render beneath the evidence once context arrives; Generate emits and closes', () => {
204
+ renderModal({ gatherContext: RESOURCE_CONTEXT }); // gather resolves immediately
157
205
 
158
- fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
159
- const next = screen.getByRole('button', { name: /Next/ });
160
- expect(next).toBeEnabled();
161
- fireEvent.click(next); // → configure-generation
206
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
162
207
 
163
- fireEvent.change(screen.getByLabelText('New resource title'), { target: { value: 'Generated Doc' } });
164
- fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.md' } });
208
+ fireEvent.change(screen.getByLabelText(T.resourceTitle), { target: { value: 'Generated Doc' } });
209
+ fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.md' } });
165
210
  fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
166
211
 
167
212
  expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
@@ -176,22 +221,38 @@ describe('ResourceGenerateModal', () => {
176
221
  expect(onClose).toHaveBeenCalled();
177
222
  });
178
223
 
179
- it('Back from configure-generation returns to review', () => {
180
- h.state.context = RESOURCE_CONTEXT;
181
- renderModal();
182
- fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
183
- fireEvent.click(screen.getByRole('button', { name: /Next/ })); // → configure-generation
184
- expect(screen.getByText('Configure Generation')).toBeInTheDocument();
185
- fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → review
186
- expect(screen.getByText('Review Context')).toBeInTheDocument();
224
+ it('a failed gather shows its failure in place', () => {
225
+ // The props are the state unit's slots verbatim; the modal renders them
226
+ // without owning them (FLC A6).
227
+ renderModal({ gatherLoading: true });
228
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
229
+
230
+ render(
231
+ <ResourceGenerateModal
232
+ isOpen onClose={onClose} resourceId="res-1" defaultTitle="Default Title"
233
+ locale="en" entityTypeOptions={['Person', 'Topic']} onGenerateSubmit={onGenerateSubmit}
234
+ gatherContext={null} gatherLoading={false} gatherError={new Error('boom')}
235
+ onGather={onGather} translations={T}
236
+ />,
237
+ );
238
+ fireEvent.click(screen.getAllByRole('button', { name: /Gather/ }).at(-1)!);
239
+ expect(screen.getByText(T.failedContext)).toBeInTheDocument();
187
240
  });
188
241
 
189
- it('Back from review returns to the configure-gather step', () => {
190
- renderModal();
191
- fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // → review
192
- expect(screen.getByText('Review Context')).toBeInTheDocument();
193
- fireEvent.click(screen.getByRole('button', { name: /Back/ })); // → configure-gather
194
- expect(screen.getByText('Configure Gather')).toBeInTheDocument();
242
+ it('re-Gather refreshes the evidence in place — the stack stays', () => {
243
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
244
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
245
+ fireEvent.click(screen.getByTitle(T.editGather)); // expand the receipt
246
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // tweak-and-regather
247
+ expect(onGather).toHaveBeenCalledTimes(2);
248
+ expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument();
249
+ expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument();
250
+ });
251
+
252
+ it('no Back anywhere — a single stack has nothing to go back to (D6)', () => {
253
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
254
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
255
+ expect(screen.queryAllByRole('button', { name: /Back/ })).toHaveLength(0);
195
256
  });
196
257
 
197
258
  it('the header close button calls onClose', () => {
@@ -199,4 +260,135 @@ describe('ResourceGenerateModal', () => {
199
260
  fireEvent.click(screen.getByLabelText('Close'));
200
261
  expect(onClose).toHaveBeenCalledTimes(1);
201
262
  });
263
+
264
+ it('opening seeds the proposed title from defaultTitle, even one that arrived after mount (GFR A4)', () => {
265
+ // The page passes the source resource's name, which loads asynchronously —
266
+ // a draft seeded only in the useState initializer holds whatever was there
267
+ // at FIRST render, which for the real page was ''. Opening is the moment
268
+ // that matters: the draft re-seeds then.
269
+ const { rerender } = render(
270
+ <ResourceGenerateModal
271
+ isOpen={false} onClose={onClose} resourceId="res-1" defaultTitle=""
272
+ locale="en" entityTypeOptions={[]} onGenerateSubmit={onGenerateSubmit}
273
+ gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
274
+ onGather={onGather} translations={T}
275
+ />,
276
+ );
277
+ rerender(
278
+ <ResourceGenerateModal
279
+ isOpen onClose={onClose} resourceId="res-1" defaultTitle="PB"
280
+ locale="en" entityTypeOptions={[]} onGenerateSubmit={onGenerateSubmit}
281
+ gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
282
+ onGather={onGather} translations={T}
283
+ />,
284
+ );
285
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
286
+ expect(screen.getByLabelText(T.resourceTitle)).toHaveValue('PB');
287
+ });
288
+
289
+ it('evidence stays in view above the params — ONE scroll pane, nothing scrolls alone (A3)', () => {
290
+ const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
291
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
292
+
293
+ const scroll = baseElement.querySelector('.semiont-wizard__step-scroll');
294
+ expect(scroll).not.toBeNull();
295
+ expect(scroll!.querySelector('.semiont-gather__outer')).not.toBeNull(); // the evidence
296
+ expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument(); // graph pane up
297
+ expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument(); // the form, below
298
+ for (const form of Array.from(scroll!.querySelectorAll('form.semiont-form'))) {
299
+ expect(form.className).not.toContain('semiont-form--scrollable');
300
+ }
301
+ });
302
+
303
+ it('the panel carries the widened class (GEP P2, D2)', () => {
304
+ const { baseElement } = renderModal();
305
+ const panel = baseElement.querySelector('.semiont-search-modal__panel--gather');
306
+ expect(panel).not.toBeNull();
307
+ expect(panel!.className).toContain('semiont-search-modal__panel--wide');
308
+ });
309
+
310
+ it('every footer is the wizard footer — no dismissal, no flex (GFR A5)', () => {
311
+ // The modal renders via a HeadlessUI portal, so query the whole document.
312
+ const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
313
+ const footerPins = () => {
314
+ expect(baseElement.querySelector('.semiont-modal__actions--wizard')).not.toBeNull();
315
+ expect(baseElement.querySelectorAll('.semiont-button--flex')).toHaveLength(0);
316
+ const footerButtons = Array.from(
317
+ baseElement.querySelectorAll('.semiont-modal__actions button'),
318
+ ).map((b) => b.textContent ?? '');
319
+ expect(footerButtons.filter((l) => /cancel|✕/i.test(l))).toEqual([]);
320
+ };
321
+
322
+ footerPins(); // controls alone
323
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
324
+ footerPins(); // the full stack: Gather footer + Generate footer
325
+ });
326
+ });
327
+
328
+ // GATHER-AT-THE-TOP P1: dismissal guards typed work. D5 — typed text only
329
+ // (title beyond the seeded default, save location, instructions); checkbox,
330
+ // depth, and exclusion state is cheap to redo and never nags.
331
+ describe('ResourceGenerateModal — dismissal guards typed work (GATHER-AT-THE-TOP P1)', () => {
332
+ beforeEach(() => {
333
+ onClose = vi.fn();
334
+ onGenerateSubmit = vi.fn();
335
+ onGather = vi.fn();
336
+ });
337
+
338
+ it('a typed save location guards dismissal; Keep editing stays, Discard closes', () => {
339
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
340
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // reveal the stack
341
+ fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.md' } });
342
+
343
+ fireEvent.click(screen.getByLabelText('Close'));
344
+ expect(onClose).not.toHaveBeenCalled();
345
+ expect(screen.getByText(T.discardDraftPrompt)).toBeInTheDocument();
346
+
347
+ fireEvent.click(screen.getByRole('button', { name: T.keepEditing }));
348
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
349
+ expect(screen.getByLabelText(T.saveLocation)).toHaveValue('generated/out.md');
350
+ expect(onClose).not.toHaveBeenCalled();
351
+
352
+ fireEvent.click(screen.getByLabelText('Close'));
353
+ fireEvent.click(screen.getByRole('button', { name: T.discardDraft }));
354
+ expect(onClose).toHaveBeenCalledTimes(1);
355
+ });
356
+
357
+ it('a clean draft dismisses without any prompt', () => {
358
+ renderModal({ gatherContext: RESOURCE_CONTEXT });
359
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
360
+
361
+ fireEvent.click(screen.getByLabelText('Close'));
362
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
363
+ expect(onClose).toHaveBeenCalledTimes(1);
364
+ });
365
+
366
+ it('a defaultTitle arriving mid-flow does not fake a dirty draft', () => {
367
+ // The source resource's name loads asynchronously: the baseline moves
368
+ // while the modal is open, the user has typed nothing. Dirtiness compares
369
+ // against what was SEEDED, not the live prop.
370
+ const { rerender } = renderModal({ defaultTitle: '', gatherContext: RESOURCE_CONTEXT });
371
+ fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
372
+ rerender(
373
+ <ResourceGenerateModal
374
+ isOpen onClose={onClose} resourceId="res-1" defaultTitle="Loaded Name"
375
+ locale="en" entityTypeOptions={['Person', 'Topic']} onGenerateSubmit={onGenerateSubmit}
376
+ gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
377
+ onGather={onGather} translations={T}
378
+ />,
379
+ );
380
+
381
+ fireEvent.click(screen.getByLabelText('Close'));
382
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
383
+ expect(onClose).toHaveBeenCalledTimes(1);
384
+ });
385
+
386
+ it('non-text state alone never nags — exclusions and depth are cheap to redo (D5)', () => {
387
+ renderModal();
388
+ fireEvent.click(screen.getByRole('button', { name: 'Person' })); // cross off the recall
389
+
390
+ fireEvent.click(screen.getByLabelText('Close'));
391
+ expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
392
+ expect(onClose).toHaveBeenCalledTimes(1);
393
+ });
202
394
  });
@@ -14,7 +14,7 @@
14
14
  import { describe, it, expect, beforeEach, vi } from 'vitest';
15
15
  import { asStates } from '../../../__tests__/test-client';
16
16
  import { screen, fireEvent, waitFor, act } from '@testing-library/react';
17
- import { BehaviorSubject } from 'rxjs';
17
+ import { BehaviorSubject, map } from 'rxjs';
18
18
  import { renderWithProviders } from '../../../test-utils';
19
19
  import '@testing-library/jest-dom';
20
20
  import { SearchModal } from '../SearchModal';
@@ -34,7 +34,11 @@ vi.mock('@headlessui/react', () => ({
34
34
 
35
35
  // Mock the http-transport Observable surface
36
36
  const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
37
- const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable()));
37
+ const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable().pipe(
38
+ // Arrays get the list envelope `resources()` now emits; `undefined`
39
+ // passes through as the pending state.
40
+ map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: 'lexical' } : v)),
41
+ )));
38
42
 
39
43
  // Stable client reference — useSemiont is called on every render, so a
40
44
  // fresh object literal would invalidate useMemo deps and restart the RxJS
@@ -0,0 +1,61 @@
1
+ /**
2
+ * GATHER-EVIDENCE-PANES D10 (amended 2026-08-19) — `SearchResultsStep` is pure
3
+ * RESULTS: ranked rows with scores and Link buttons, nothing else.
4
+ *
5
+ * The evidence lives with the HOST: the wizard stacks the full display-only
6
+ * `GatherContextStep` (quotation, graph pane, corpus pane, collapsed strategy
7
+ * band) above this step, exactly as on the configure steps. The original D10
8
+ * concern — corpus matches beside real results reading as a second, staler
9
+ * ranking — dissolved with the stacking: evidence sits above the strategy
10
+ * band, results below it, no competing side-by-side columns. This step
11
+ * therefore renders NO context of its own; a gather pane appearing here means
12
+ * someone re-embedded the minimal context redux this amendment deleted.
13
+ */
14
+ import { describe, it, expect, vi } from 'vitest';
15
+ import { render, screen } from '@testing-library/react';
16
+ import '@testing-library/jest-dom';
17
+ import { SearchResultsStep } from '../SearchResultsStep';
18
+
19
+ const T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back' };
20
+
21
+ const RESULTS = [
22
+ { '@id': 'r1', name: 'Martin Pauls', score: 57.46, matchReason: 'connected; semantic match' },
23
+ { '@id': 'r2', name: 'Arthur Fisher', score: 25.46, description: 'a citing resource' },
24
+ ] as never[];
25
+
26
+ describe('SearchResultsStep — pure results (D10 amended)', () => {
27
+ it('renders ranked rows: name, score, reason, and a Link button each', () => {
28
+ render(
29
+ <SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
30
+ );
31
+ expect(screen.getByText('Martin Pauls')).toBeInTheDocument();
32
+ expect(screen.getByText(`${T.score}: 57.46`)).toBeInTheDocument();
33
+ expect(screen.getByText('connected; semantic match')).toBeInTheDocument();
34
+ expect(screen.getByText('a citing resource')).toBeInTheDocument();
35
+ expect(screen.getAllByRole('button', { name: `🔗 ${T.link}` })).toHaveLength(2);
36
+ });
37
+
38
+ it('links the row that was clicked, by @id', () => {
39
+ const onLink = vi.fn();
40
+ render(
41
+ <SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={onLink} translations={T} />,
42
+ );
43
+ screen.getAllByRole('button', { name: `🔗 ${T.link}` })[1]!.click();
44
+ expect(onLink).toHaveBeenCalledWith('r2');
45
+ });
46
+
47
+ it('shows the empty state when nothing matched', () => {
48
+ render(
49
+ <SearchResultsStep results={[]} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
50
+ );
51
+ expect(screen.getByText(T.noResults)).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders no context of its own — the host owns the evidence', () => {
55
+ const { container } = render(
56
+ <SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
57
+ );
58
+ expect(container.querySelector('.semiont-gather-pane')).toBeNull();
59
+ expect(container.querySelector('.semiont-search-results__two-pane')).toBeNull();
60
+ });
61
+ });