@semiont/react-ui 0.5.27 → 0.5.29

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 (192) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
  3. package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
  4. package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
  8. package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
  9. package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
  10. package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
  11. package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
  12. package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
  14. package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
  15. package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
  16. package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
  17. package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
  18. package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
  19. package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
  20. package/dist/index.css +279 -74
  21. package/dist/index.d.ts +318 -393
  22. package/dist/index.js +2619 -2687
  23. package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
  24. package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
  25. package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
  26. package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
  27. package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
  28. package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
  29. package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
  30. package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
  31. package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
  32. package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
  33. package/dist/test-utils.js +16 -7
  34. package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
  35. package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
  36. package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
  37. package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
  38. package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
  39. package/package.json +16 -15
  40. package/src/components/AssistProgress.tsx +36 -2
  41. package/src/components/CodeMirrorRenderer.tsx +8 -8
  42. package/src/components/StatusDisplay.tsx +13 -13
  43. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  44. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  45. package/src/components/modals/ComposeStep.tsx +233 -0
  46. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  47. package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
  48. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  49. package/src/components/modals/ContextSummary.tsx +185 -72
  50. package/src/components/modals/CorpusPane.tsx +75 -0
  51. package/src/components/modals/DiscardPrompt.tsx +38 -0
  52. package/src/components/modals/GatherContextStep.tsx +237 -107
  53. package/src/components/modals/ReferenceWizardModal.tsx +252 -59
  54. package/src/components/modals/ResourceGenerateModal.tsx +225 -112
  55. package/src/components/modals/SearchModal.css +337 -101
  56. package/src/components/modals/SearchResultsStep.tsx +61 -68
  57. package/src/components/modals/WizardFooter.tsx +7 -1
  58. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  59. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  60. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
  61. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  62. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  63. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
  64. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
  65. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  66. package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
  67. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  68. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  69. package/src/components/resource/AnnotateView.tsx +3 -3
  70. package/src/components/resource/BrowseView.tsx +8 -8
  71. package/src/components/resource/DownloadFileLink.tsx +1 -1
  72. package/src/components/resource/ResourceViewer.tsx +4 -4
  73. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  74. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  75. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  76. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
  77. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  78. package/src/components/resource/annotate-renderers.tsx +3 -3
  79. package/src/components/resource/panels/AssistSection.css +35 -0
  80. package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
  81. package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  83. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  84. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  85. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
  87. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
  88. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  89. package/src/features/auth/components/SignInForm.tsx +53 -53
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  91. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  95. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  96. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  97. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  98. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
  99. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  100. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
  101. package/src/features/resource-viewer/generation-options.ts +29 -0
  102. package/src/styles/core/forms.css +35 -8
  103. package/src/styles/features/resource-viewer.css +0 -22
  104. package/src/styles/features/resource.css +0 -4
  105. package/src/styles/index.css +0 -1
  106. package/translations/ar.json +42 -22
  107. package/translations/bn.json +42 -22
  108. package/translations/cs.json +42 -22
  109. package/translations/da.json +42 -22
  110. package/translations/de.json +42 -22
  111. package/translations/el.json +42 -22
  112. package/translations/en.json +42 -22
  113. package/translations/es.json +42 -22
  114. package/translations/fa.json +42 -22
  115. package/translations/fi.json +42 -22
  116. package/translations/fr.json +42 -22
  117. package/translations/he.json +42 -22
  118. package/translations/hi.json +42 -22
  119. package/translations/id.json +42 -22
  120. package/translations/it.json +42 -22
  121. package/translations/ja.json +42 -22
  122. package/translations/ko.json +42 -22
  123. package/translations/ms.json +42 -22
  124. package/translations/nl.json +42 -22
  125. package/translations/no.json +42 -22
  126. package/translations/pl.json +42 -22
  127. package/translations/pt.json +42 -22
  128. package/translations/ro.json +42 -22
  129. package/translations/sv.json +42 -22
  130. package/translations/th.json +42 -22
  131. package/translations/tr.json +42 -22
  132. package/translations/uk.json +42 -22
  133. package/translations/vi.json +42 -22
  134. package/translations/zh.json +42 -22
  135. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  136. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  137. package/dist/ar-OU3DQGC6.js.map +0 -1
  138. package/dist/bn-PHWCR6AZ.js.map +0 -1
  139. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  140. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  141. package/dist/chunk-O23TLLXW.js.map +0 -1
  142. package/dist/chunk-PCRRBU7H.js.map +0 -1
  143. package/dist/chunk-QT7LYM72.js.map +0 -1
  144. package/dist/chunk-VXASXU4K.js.map +0 -1
  145. package/dist/cs-NEXYZ62I.js.map +0 -1
  146. package/dist/da-FAT43YCU.js.map +0 -1
  147. package/dist/de-KRQ3WBCU.js.map +0 -1
  148. package/dist/el-Q4IU3EKC.js.map +0 -1
  149. package/dist/en-2KADE7N4.js.map +0 -1
  150. package/dist/es-2IO3R2MK.js.map +0 -1
  151. package/dist/fa-Q5GOQFXX.js.map +0 -1
  152. package/dist/fi-R6NMRWBE.js.map +0 -1
  153. package/dist/fr-QY4D5E6J.js.map +0 -1
  154. package/dist/he-X6Y77XHY.js.map +0 -1
  155. package/dist/hi-XL7IOEQM.js.map +0 -1
  156. package/dist/id-QZ4ZZS67.js.map +0 -1
  157. package/dist/index.css.map +0 -1
  158. package/dist/index.js.map +0 -1
  159. package/dist/integrations/css-modules-helper.js.map +0 -1
  160. package/dist/integrations/styled-components-theme.js.map +0 -1
  161. package/dist/it-ME2Q7OB6.js.map +0 -1
  162. package/dist/ja-HX7QASRI.js.map +0 -1
  163. package/dist/ko-EQO5AHAV.js.map +0 -1
  164. package/dist/ms-UILFSF3I.js.map +0 -1
  165. package/dist/nl-NNI45PMK.js.map +0 -1
  166. package/dist/no-MAL5F6P6.js.map +0 -1
  167. package/dist/pl-4RMY6JAJ.js.map +0 -1
  168. package/dist/pt-VQH2X3QQ.js.map +0 -1
  169. package/dist/ro-XXCQO2OG.js.map +0 -1
  170. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  171. package/dist/test-utils.css.map +0 -1
  172. package/dist/test-utils.js.map +0 -1
  173. package/dist/th-L3DXXYVQ.js.map +0 -1
  174. package/dist/tr-P3DFVLFJ.js.map +0 -1
  175. package/dist/uk-7C5D3PXS.js.map +0 -1
  176. package/dist/vi-ZLTLEO7P.js.map +0 -1
  177. package/dist/zh-5FHVWDIN.js.map +0 -1
  178. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  179. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
  180. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  181. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  182. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  183. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  184. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  185. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  186. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  187. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  188. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  189. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  190. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  191. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  192. package/src/styles/features/exchange.css +0 -374
@@ -2,8 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import React from 'react';
3
3
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
- import type { EventBus } from '@semiont/core';
6
- import type { SemiontClient, SemiontSession } from '@semiont/sdk';
5
+ import { BehaviorSubject } from 'rxjs';
6
+ import type { EventBus, ResourceDescriptor } from '@semiont/core';
7
+ import { resourceId as makeResourceId } from '@semiont/core';
8
+ import type { CacheObservable, CacheState, SemiontClient, SemiontSession } from '@semiont/sdk';
7
9
  import { ResourceInfoPanel } from '../ResourceInfoPanel';
8
10
  import { createTestSemiontWrapper } from '../../../../test-utils';
9
11
 
@@ -71,7 +73,7 @@ function createEventTracker() {
71
73
  eventBus.get('yield:clone').subscribe((payload: any) => {
72
74
  events.push({ event: 'yield:clone', payload });
73
75
  });
74
- // `mark:archive` / `mark:unarchive` are backend-routed via `actor.emit`;
76
+ // `mark:archive` / `mark:unarchive` are gateway-routed via `actor.emit`;
75
77
  // spy on the namespace methods instead of subscribing to a local bus.
76
78
  const origArchive = client.mark.archive.bind(client.mark);
77
79
  const origUnarchive = client.mark.unarchive.bind(client.mark);
@@ -281,17 +283,19 @@ describe('ResourceInfoPanel Component', () => {
281
283
  });
282
284
 
283
285
  describe('Generate Action', () => {
286
+ // The shell's collapsible header is ALSO a button named "Generate", so the
287
+ // form control is targeted by its exact accessible name (the ✨ is part of it).
284
288
  it('renders the Generate button when onGenerate is provided', () => {
285
289
  renderWithEventBus(
286
290
  <ResourceInfoPanel {...defaultProps} onGenerate={() => {}} />
287
291
  );
288
- expect(screen.getByRole('button', { name: /Generate/i })).toBeInTheDocument();
292
+ expect(screen.getByRole('button', { name: '✨ Generate' })).toBeInTheDocument();
289
293
  expect(screen.getByText("Generate a new resource from this one's context")).toBeInTheDocument();
290
294
  });
291
295
 
292
296
  it('hides the Generate button when onGenerate is omitted', () => {
293
297
  renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
294
- expect(screen.queryByRole('button', { name: /Generate/i })).not.toBeInTheDocument();
298
+ expect(screen.queryByRole('button', { name: '✨ Generate' })).not.toBeInTheDocument();
295
299
  });
296
300
 
297
301
  it('calls onGenerate when clicked', () => {
@@ -299,9 +303,117 @@ describe('ResourceInfoPanel Component', () => {
299
303
  renderWithEventBus(
300
304
  <ResourceInfoPanel {...defaultProps} onGenerate={onGenerate} />
301
305
  );
302
- fireEvent.click(screen.getByRole('button', { name: /Generate/i }));
306
+ fireEvent.click(screen.getByRole('button', { name: '✨ Generate' }));
303
307
  expect(onGenerate).toHaveBeenCalledTimes(1);
304
308
  });
309
+
310
+ // ── GENERATE-FROM-RESOURCE P2 (D7/D8): the panel is the progress surface ──
311
+
312
+ it('the Generate control lives inside the assist shell (D7)', () => {
313
+ const { container } = renderWithEventBus(
314
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
315
+ isGenerating={false} generationProgress={null} />
316
+ );
317
+ const shell = container.querySelector('.semiont-assist-widget[data-type="generation"]');
318
+ expect(shell).not.toBeNull();
319
+ expect(shell!.querySelector('button')).not.toBeNull(); // the form IS the Generate control
320
+ });
321
+
322
+ it('progress replaces the Generate form while a generation runs', () => {
323
+ const { container } = renderWithEventBus(
324
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
325
+ isGenerating
326
+ generationProgress={{ percentage: 40, message: { code: 'generating-resource' } }} />
327
+ );
328
+ expect(container.querySelector('.semiont-assist-progress')).not.toBeNull();
329
+ expect(screen.queryByRole('button', { name: '✨ Generate' })).toBeNull();
330
+ });
331
+
332
+ it('the ended frame links the generated resource by name, and dismisses (D8)', () => {
333
+ const onDismissProgress = vi.fn();
334
+ const { client } = renderWithEventBus(
335
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
336
+ isGenerating={false}
337
+ generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: false } }}
338
+ generationOutcome={{
339
+ resourceId: makeResourceId('urn:semiont:resource:new1'),
340
+ resourceName: 'Summary of PB',
341
+ truncated: false,
342
+ }}
343
+ onDismissProgress={onDismissProgress} />
344
+ );
345
+ const openSpy = vi.spyOn(client.browse, 'openResource');
346
+
347
+ fireEvent.click(screen.getByRole('button', { name: 'Summary of PB' }));
348
+ expect(openSpy).toHaveBeenCalledTimes(1);
349
+ expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:new1');
350
+
351
+ fireEvent.click(screen.getByTestId('semiont-assist-control'));
352
+ expect(onDismissProgress).toHaveBeenCalledTimes(1);
353
+ });
354
+
355
+ it('a truncated completion says so; a natural stop does not (GFR P3b, D6)', () => {
356
+ // The mocked translator echoes unknown keys, so the assertion reads the
357
+ // KEY the real assistProgressCopy branch picked — structure, not copy.
358
+ const base = { ...defaultProps, onGenerate: () => {}, isGenerating: false };
359
+ const { unmount } = renderWithEventBus(
360
+ <ResourceInfoPanel {...base}
361
+ generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: true } }} />
362
+ );
363
+ expect(screen.getByTestId('semiont-assist-status').textContent)
364
+ .toContain('codeCompleteGeneratedTruncated');
365
+ unmount();
366
+
367
+ renderWithEventBus(
368
+ <ResourceInfoPanel {...base}
369
+ generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: false } }} />
370
+ );
371
+ expect(screen.getByTestId('semiont-assist-status').textContent)
372
+ .toContain('codeCompleteGenerated');
373
+ expect(screen.getByTestId('semiont-assist-status').textContent)
374
+ .not.toContain('Truncated');
375
+ });
376
+
377
+ it('the ended frame speaks completion even when the final progress frame lost the race (GENERATION-ARRIVAL D4/D5)', () => {
378
+ // The screenshot case: last frame is the 95% "creating" payload, the
379
+ // terminal 100% frame never arrived — but the outcome did. The sentence
380
+ // derives from the OUTCOME, not the racing frame.
381
+ renderWithEventBus(
382
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
383
+ isGenerating={false}
384
+ generationProgress={{ percentage: 95, message: { code: 'creating-resource' } }}
385
+ generationOutcome={{
386
+ resourceId: makeResourceId('urn:semiont:resource:new1'),
387
+ resourceName: 'Summary of PB',
388
+ truncated: false,
389
+ }} />
390
+ );
391
+ expect(screen.getByTestId('semiont-assist-status').textContent).toBe('codeCompleteGenerated');
392
+ });
393
+
394
+ it('a truncated outcome says so — the bit rides the outcome, not the racing frame (D5)', () => {
395
+ renderWithEventBus(
396
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
397
+ isGenerating={false}
398
+ generationProgress={{ percentage: 95, message: { code: 'creating-resource' } }}
399
+ generationOutcome={{
400
+ resourceId: makeResourceId('urn:semiont:resource:new1'),
401
+ resourceName: 'Summary of PB',
402
+ truncated: true,
403
+ }} />
404
+ );
405
+ expect(screen.getByTestId('semiont-assist-status').textContent).toBe('codeCompleteGeneratedTruncated');
406
+ });
407
+
408
+ it('no outcome, no link: the ended frame renders without one until job:complete arrives', () => {
409
+ renderWithEventBus(
410
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}}
411
+ isGenerating={false}
412
+ generationProgress={{ percentage: 100, message: { code: 'complete-generated', truncated: false } }}
413
+ generationOutcome={null} />
414
+ );
415
+ expect(screen.queryByText('Summary of PB')).toBeNull();
416
+ });
305
417
  });
306
418
 
307
419
  describe('Provenance Section', () => {
@@ -350,7 +462,9 @@ describe('ResourceInfoPanel Component', () => {
350
462
  expect(screen.getByText('Alice, Bob')).toBeInTheDocument();
351
463
  });
352
464
 
353
- it('should render wasDerivedFrom when provided', () => {
465
+ it('should render wasDerivedFrom when provided — the id is the unresolved fallback', () => {
466
+ // The factory client's fetch fails in jsdom (no server), so the
467
+ // descriptor never resolves and the raw id stays as the honest label.
354
468
  renderWithEventBus(
355
469
  <ResourceInfoPanel
356
470
  {...defaultProps}
@@ -382,6 +496,34 @@ describe('ResourceInfoPanel Component', () => {
382
496
  expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:abc123');
383
497
  });
384
498
 
499
+ it('resolves the derived-from id to its resource NAME; clicks still navigate by id', () => {
500
+ // The id is wire identity, not presentation. Once `browse.resource`
501
+ // serves the source's descriptor, the link wears its name — the raw
502
+ // hash only survives while the descriptor is unresolved (pin below).
503
+ const { SemiontWrapper, session, client } = createTestSemiontWrapper();
504
+ vi.spyOn(client.browse, 'resource').mockReturnValue(
505
+ new BehaviorSubject<CacheState<ResourceDescriptor>>({
506
+ status: 'ready',
507
+ value: { '@id': 'urn:semiont:resource:abc123', name: 'Source Doc' } as ResourceDescriptor,
508
+ }) as unknown as CacheObservable<ResourceDescriptor>,
509
+ );
510
+ const openSpy = vi.spyOn(client.browse, 'openResource');
511
+ render(
512
+ <ResourceInfoPanel
513
+ {...defaultProps}
514
+ session={session}
515
+ wasDerivedFrom="urn:semiont:resource:abc123"
516
+ />,
517
+ { wrapper: ({ children }) => <SemiontWrapper>{children}</SemiontWrapper> },
518
+ );
519
+
520
+ expect(screen.getByText('Source Doc')).toBeInTheDocument();
521
+ expect(screen.queryByText('urn:semiont:resource:abc123')).toBeNull();
522
+
523
+ fireEvent.click(screen.getByText('Source Doc'));
524
+ expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:abc123');
525
+ });
526
+
385
527
  it('renders every id when the resource derives from several', () => {
386
528
  renderWithEventBus(
387
529
  <ResourceInfoPanel
@@ -29,8 +29,8 @@ const mockTranslations = {
29
29
  welcomeBack: 'Welcome back to Semiont',
30
30
  signInPrompt: 'Sign in to your knowledge workspace',
31
31
  continueWithGoogle: 'Continue with Google',
32
- backendUrlLabel: 'Backend URL',
33
- backendUrlPlaceholder: 'http://localhost:4000',
32
+ gatewayUrlLabel: 'Gateway URL',
33
+ gatewayUrlPlaceholder: 'http://localhost:4000',
34
34
  emailLabel: 'Email',
35
35
  emailPlaceholder: 'your@email.com',
36
36
  passwordLabel: 'Password',
@@ -44,9 +44,9 @@ const mockTranslations = {
44
44
  signUpInstead: 'Sign Up Instead',
45
45
  errorEmailRequired: 'Email is required',
46
46
  errorPasswordRequired: 'Password is required',
47
- errorBackendUrlRequired: 'Backend URL is required',
48
- errorBackendUrlInvalid: 'Backend URL is not valid',
49
- errorBackendUrlUnreachable: 'Backend URL is unreachable',
47
+ errorGatewayUrlRequired: 'Gateway URL is required',
48
+ errorGatewayUrlInvalid: 'Gateway URL is not valid',
49
+ errorGatewayUrlUnreachable: 'Gateway URL is unreachable',
50
50
  tagline: 'make meaning',
51
51
  };
52
52
 
@@ -58,7 +58,7 @@ describe('SignInForm - Accessibility', () => {
58
58
  const { container } = render(
59
59
  <SignInForm
60
60
  onGoogleSignIn={onGoogleSignIn}
61
- backendUrl="http://localhost:4000"
61
+ gatewayUrl="http://localhost:4000"
62
62
  Link={MockLink}
63
63
  translations={mockTranslations}
64
64
  />
@@ -206,7 +206,7 @@ describe('SignInForm - Accessibility', () => {
206
206
  render(
207
207
  <SignInForm
208
208
  onGoogleSignIn={onGoogleSignIn}
209
- backendUrl="http://localhost:4000"
209
+ gatewayUrl="http://localhost:4000"
210
210
  Link={MockLink}
211
211
  translations={mockTranslations}
212
212
  />
@@ -244,7 +244,7 @@ describe('SignInForm - Accessibility', () => {
244
244
  render(
245
245
  <SignInForm
246
246
  onGoogleSignIn={onGoogleSignIn}
247
- backendUrl="http://localhost:4000"
247
+ gatewayUrl="http://localhost:4000"
248
248
  Link={MockLink}
249
249
  translations={mockTranslations}
250
250
  />
@@ -261,7 +261,7 @@ describe('SignInForm - Accessibility', () => {
261
261
  render(
262
262
  <SignInForm
263
263
  onGoogleSignIn={onGoogleSignIn}
264
- backendUrl="http://localhost:4000"
264
+ gatewayUrl="http://localhost:4000"
265
265
  Link={MockLink}
266
266
  translations={mockTranslations}
267
267
  />
@@ -361,7 +361,7 @@ describe('SignInForm - Accessibility', () => {
361
361
  render(
362
362
  <SignInForm
363
363
  onGoogleSignIn={onGoogleSignIn}
364
- backendUrl="http://localhost:4000"
364
+ gatewayUrl="http://localhost:4000"
365
365
  Link={MockLink}
366
366
  translations={mockTranslations}
367
367
  />
@@ -379,7 +379,7 @@ describe('SignInForm - Accessibility', () => {
379
379
  const { container } = render(
380
380
  <SignInForm
381
381
  onGoogleSignIn={onGoogleSignIn}
382
- backendUrl="http://localhost:4000"
382
+ gatewayUrl="http://localhost:4000"
383
383
  Link={MockLink}
384
384
  translations={mockTranslations}
385
385
  />
@@ -397,7 +397,7 @@ describe('SignInForm - Accessibility', () => {
397
397
  const { container } = render(
398
398
  <SignInForm
399
399
  onGoogleSignIn={onGoogleSignIn}
400
- backendUrl="http://localhost:4000"
400
+ gatewayUrl="http://localhost:4000"
401
401
  Link={MockLink}
402
402
  translations={mockTranslations}
403
403
  />
@@ -4,8 +4,8 @@
4
4
  * Supports both Google OAuth and email/password credentials.
5
5
  * No Next.js dependencies - all data via props.
6
6
  *
7
- * When backendUrl is provided it is shown as a locked read-only field.
8
- * When backendUrl is omitted the user must enter a backend URL to connect to.
7
+ * When gatewayUrl is provided it is shown as a locked read-only field.
8
+ * When gatewayUrl is omitted the user must enter a gateway URL to connect to.
9
9
  */
10
10
 
11
11
  import React from 'react';
@@ -15,21 +15,21 @@ import '../auth.css';
15
15
  export interface SignInFormProps {
16
16
  /**
17
17
  * Callback when user clicks Google sign-in.
18
- * Receives the backend URL (either the locked one or what the user typed).
18
+ * Receives the gateway URL (either the locked one or what the user typed).
19
19
  */
20
- onGoogleSignIn: (backendUrl: string) => Promise<void>;
20
+ onGoogleSignIn: (gatewayUrl: string) => Promise<void>;
21
21
 
22
22
  /**
23
23
  * Callback when user submits email/password credentials.
24
- * Receives the backend URL along with email and password.
24
+ * Receives the gateway URL along with email and password.
25
25
  */
26
- onCredentialsSignIn?: ((backendUrl: string, email: string, password: string) => Promise<void>) | undefined;
26
+ onCredentialsSignIn?: ((gatewayUrl: string, email: string, password: string) => Promise<void>) | undefined;
27
27
 
28
28
  /**
29
- * Pre-filled backend URL. When provided the field is read-only (re-auth to known workspace).
29
+ * Pre-filled gateway URL. When provided the field is read-only (re-auth to known workspace).
30
30
  * When omitted the user enters the URL themselves (new connection).
31
31
  */
32
- backendUrl?: string;
32
+ gatewayUrl?: string;
33
33
 
34
34
  /**
35
35
  * Error message to display (if any)
@@ -59,8 +59,8 @@ export interface SignInFormProps {
59
59
  welcomeBack: string;
60
60
  signInPrompt: string;
61
61
  continueWithGoogle: string;
62
- backendUrlLabel: string;
63
- backendUrlPlaceholder: string;
62
+ gatewayUrlLabel: string;
63
+ gatewayUrlPlaceholder: string;
64
64
  emailLabel: string;
65
65
  emailPlaceholder: string;
66
66
  passwordLabel: string;
@@ -72,9 +72,9 @@ export interface SignInFormProps {
72
72
  backToHome: string;
73
73
  learnMore: string;
74
74
  signUpInstead: string;
75
- errorBackendUrlRequired: string;
76
- errorBackendUrlInvalid: string;
77
- errorBackendUrlUnreachable: string;
75
+ errorGatewayUrlRequired: string;
76
+ errorGatewayUrlInvalid: string;
77
+ errorGatewayUrlUnreachable: string;
78
78
  errorEmailRequired: string;
79
79
  errorPasswordRequired: string;
80
80
  tagline: string;
@@ -117,23 +117,23 @@ function normalizeUrlForProbe(raw: string): string | null {
117
117
  }
118
118
 
119
119
  /**
120
- * CredentialsAuthForm - Backend URL + email/password form.
121
- * When lockedBackendUrl is provided, the URL field is shown read-only.
120
+ * CredentialsAuthForm - Gateway URL + email/password form.
121
+ * When lockedGatewayUrl is provided, the URL field is shown read-only.
122
122
  */
123
123
  function CredentialsAuthForm({
124
- lockedBackendUrl,
124
+ lockedGatewayUrl,
125
125
  onSubmit,
126
126
  translations: t,
127
127
  }: {
128
- lockedBackendUrl: string | undefined;
129
- onSubmit: (backendUrl: string, email: string, password: string) => Promise<void>;
128
+ lockedGatewayUrl: string | undefined;
129
+ onSubmit: (gatewayUrl: string, email: string, password: string) => Promise<void>;
130
130
  translations: SignInFormProps['translations'];
131
131
  }) {
132
- const [backendUrl, setBackendUrl] = React.useState(lockedBackendUrl ?? '');
132
+ const [gatewayUrl, setGatewayUrl] = React.useState(lockedGatewayUrl ?? '');
133
133
  const [email, setEmail] = React.useState('');
134
134
  const [password, setPassword] = React.useState('');
135
135
  const [validationError, setValidationError] = React.useState<string | null>(null);
136
- const [fieldErrors, setFieldErrors] = React.useState<{ backendUrl?: string; email?: string; password?: string }>({});
136
+ const [fieldErrors, setFieldErrors] = React.useState<{ gatewayUrl?: string; email?: string; password?: string }>({});
137
137
  const [urlProbeStatus, setUrlProbeStatus] = React.useState<UrlProbeStatus>('idle');
138
138
 
139
139
  const probeUrl = async (raw: string) => {
@@ -141,7 +141,7 @@ function CredentialsAuthForm({
141
141
  if (!url) return;
142
142
  // Validate format
143
143
  try { new URL(url); } catch {
144
- setFieldErrors(prev => ({ ...prev, backendUrl: t.errorBackendUrlInvalid }));
144
+ setFieldErrors(prev => ({ ...prev, gatewayUrl: t.errorGatewayUrlInvalid }));
145
145
  setUrlProbeStatus('error');
146
146
  return;
147
147
  }
@@ -150,19 +150,19 @@ function CredentialsAuthForm({
150
150
  const res = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(5000) });
151
151
  setUrlProbeStatus(res.ok ? 'ok' : 'error');
152
152
  if (!res.ok) {
153
- setFieldErrors(prev => ({ ...prev, backendUrl: t.errorBackendUrlUnreachable }));
153
+ setFieldErrors(prev => ({ ...prev, gatewayUrl: t.errorGatewayUrlUnreachable }));
154
154
  }
155
155
  } catch {
156
156
  setUrlProbeStatus('error');
157
- setFieldErrors(prev => ({ ...prev, backendUrl: t.errorBackendUrlUnreachable }));
157
+ setFieldErrors(prev => ({ ...prev, gatewayUrl: t.errorGatewayUrlUnreachable }));
158
158
  }
159
159
  };
160
160
 
161
161
  const handleSubmit = async (e: React.FormEvent) => {
162
162
  e.preventDefault();
163
- const errors: { backendUrl?: string; email?: string; password?: string } = {};
163
+ const errors: { gatewayUrl?: string; email?: string; password?: string } = {};
164
164
 
165
- if (!backendUrl.trim()) errors.backendUrl = t.errorBackendUrlRequired;
165
+ if (!gatewayUrl.trim()) errors.gatewayUrl = t.errorGatewayUrlRequired;
166
166
  if (!email) errors.email = t.errorEmailRequired;
167
167
  if (!password) errors.password = t.errorPasswordRequired;
168
168
 
@@ -174,10 +174,10 @@ function CredentialsAuthForm({
174
174
 
175
175
  setFieldErrors({});
176
176
  setValidationError(null);
177
- await onSubmit(backendUrl.trim(), email, password);
177
+ await onSubmit(gatewayUrl.trim(), email, password);
178
178
  };
179
179
 
180
- const probeIndicator = !lockedBackendUrl && urlProbeStatus !== 'idle' ? (
180
+ const probeIndicator = !lockedGatewayUrl && urlProbeStatus !== 'idle' ? (
181
181
  <span
182
182
  className={`semiont-form__url-status semiont-form__url-status--${urlProbeStatus}`}
183
183
  aria-live="polite"
@@ -198,33 +198,33 @@ function CredentialsAuthForm({
198
198
 
199
199
  <form onSubmit={handleSubmit} className="semiont-auth__form" noValidate>
200
200
  <div className="semiont-form__field">
201
- <label htmlFor="backend-url" className="semiont-form__label">
202
- {t.backendUrlLabel}
201
+ <label htmlFor="gateway-url" className="semiont-form__label">
202
+ {t.gatewayUrlLabel}
203
203
  </label>
204
204
  <div className="semiont-form__input-row">
205
205
  <input
206
- id="backend-url"
206
+ id="gateway-url"
207
207
  type="url"
208
- value={backendUrl}
208
+ value={gatewayUrl}
209
209
  onChange={(e) => {
210
- if (lockedBackendUrl) return;
211
- setBackendUrl(e.target.value);
210
+ if (lockedGatewayUrl) return;
211
+ setGatewayUrl(e.target.value);
212
212
  setUrlProbeStatus('idle');
213
- if (fieldErrors.backendUrl) setFieldErrors({ ...fieldErrors, backendUrl: undefined });
213
+ if (fieldErrors.gatewayUrl) setFieldErrors({ ...fieldErrors, gatewayUrl: undefined });
214
214
  }}
215
- onBlur={() => { if (!lockedBackendUrl) probeUrl(backendUrl); }}
216
- readOnly={!!lockedBackendUrl}
217
- placeholder={t.backendUrlPlaceholder}
218
- className={`semiont-input${lockedBackendUrl ? ' semiont-input--readonly' : ''}`}
219
- aria-invalid={!!fieldErrors.backendUrl}
220
- aria-describedby={fieldErrors.backendUrl ? 'backend-url-error' : undefined}
215
+ onBlur={() => { if (!lockedGatewayUrl) probeUrl(gatewayUrl); }}
216
+ readOnly={!!lockedGatewayUrl}
217
+ placeholder={t.gatewayUrlPlaceholder}
218
+ className={`semiont-input${lockedGatewayUrl ? ' semiont-input--readonly' : ''}`}
219
+ aria-invalid={!!fieldErrors.gatewayUrl}
220
+ aria-describedby={fieldErrors.gatewayUrl ? 'gateway-url-error' : undefined}
221
221
  required
222
222
  />
223
223
  {probeIndicator}
224
224
  </div>
225
- {fieldErrors.backendUrl && (
226
- <span id="backend-url-error" className="semiont-form__error" role="alert">
227
- {fieldErrors.backendUrl}
225
+ {fieldErrors.gatewayUrl && (
226
+ <span id="gateway-url-error" className="semiont-form__error" role="alert">
227
+ {fieldErrors.gatewayUrl}
228
228
  </span>
229
229
  )}
230
230
  </div>
@@ -295,23 +295,23 @@ function CredentialsAuthForm({
295
295
  /**
296
296
  * SignInForm - Main sign-in / connect component.
297
297
  *
298
- * When backendUrl is provided (re-auth to known workspace) the URL field is locked.
299
- * When backendUrl is omitted (new connection) the user enters the URL themselves.
298
+ * When gatewayUrl is provided (re-auth to known workspace) the URL field is locked.
299
+ * When gatewayUrl is omitted (new connection) the user enters the URL themselves.
300
300
  */
301
301
  export function SignInForm({
302
302
  onGoogleSignIn,
303
303
  onCredentialsSignIn,
304
- backendUrl,
304
+ gatewayUrl,
305
305
  error,
306
306
  showCredentialsAuth = false,
307
307
  isLoading = false,
308
308
  translations: t,
309
309
  }: SignInFormProps) {
310
310
  const handleGoogleClick = () => {
311
- // For Google auth we need the backend URL from the locked prop or we cannot proceed.
312
- // The caller is responsible for ensuring backendUrl is set when Google sign-in is offered.
313
- if (backendUrl) {
314
- onGoogleSignIn(backendUrl);
311
+ // For Google auth we need the gateway URL from the locked prop or we cannot proceed.
312
+ // The caller is responsible for ensuring gatewayUrl is set when Google sign-in is offered.
313
+ if (gatewayUrl) {
314
+ onGoogleSignIn(gatewayUrl);
315
315
  }
316
316
  };
317
317
 
@@ -348,14 +348,14 @@ export function SignInForm({
348
348
  <>
349
349
  {showCredentialsAuth && onCredentialsSignIn && (
350
350
  <CredentialsAuthForm
351
- lockedBackendUrl={backendUrl}
351
+ lockedGatewayUrl={gatewayUrl}
352
352
  onSubmit={onCredentialsSignIn}
353
353
  translations={t}
354
354
  />
355
355
  )}
356
356
 
357
- {/* Google sign-in only shown when a backend URL is known */}
358
- {backendUrl && (
357
+ {/* Google sign-in only shown when a gateway URL is known */}
358
+ {gatewayUrl && (
359
359
  <button onClick={handleGoogleClick} className={`${buttonStyles.primary.base} semiont-button--full-width`}>
360
360
  <GoogleIcon />
361
361
  {t.continueWithGoogle}
@@ -3,7 +3,7 @@ import { firstValueFrom } from 'rxjs';
3
3
  import { filter } from 'rxjs/operators';
4
4
  import type { SemiontSession } from '@semiont/sdk';
5
5
  import { AuthNamespace } from '@semiont/sdk';
6
- import { createTestSession, stubBackend } from '@semiont/sdk/testing';
6
+ import { createTestSession, stubGateway } from '@semiont/sdk/testing';
7
7
  import { createWelcomeStateUnit } from '../welcome-state-unit';
8
8
  import { assertStateUnitAxioms } from '@semiont/core/testing/axioms';
9
9
 
@@ -25,7 +25,7 @@ afterEach(() => {
25
25
  });
26
26
 
27
27
  function makeSession(): SemiontSession {
28
- return createTestSession({ backend: stubBackend() }).session;
28
+ return createTestSession({ gateway: stubGateway() }).session;
29
29
  }
30
30
 
31
31
  describe('createWelcomeStateUnit', () => {
@@ -28,10 +28,7 @@ vi.mock('../../../components/CodeMirrorRenderer', () => ({
28
28
  const createMockTranslations = () => ({
29
29
  title: 'Compose Resource',
30
30
  titleEditClone: 'Edit Cloned Resource',
31
- titleCompleteReference: 'Complete Reference',
32
31
  subtitleClone: 'Editing a cloned resource',
33
- subtitleReference: 'Creating a new resource for reference',
34
- linkedNoticePrefix: 'This resource will be linked',
35
32
  resourceName: 'Resource Name',
36
33
  resourceNamePlaceholder: 'Enter resource name',
37
34
  entityTypes: 'Entity Types',
@@ -53,9 +50,7 @@ const createMockTranslations = () => ({
53
50
  cancel: 'Cancel',
54
51
  saving: 'Saving...',
55
52
  creating: 'Creating...',
56
- creatingAndLinking: 'Creating and linking...',
57
53
  saveClonedResource: 'Save Cloned Resource',
58
- createAndLinkResource: 'Create and Link Resource',
59
54
  createResource: 'Create Resource',
60
55
  });
61
56
 
@@ -207,66 +202,6 @@ describe('ResourceComposePage', () => {
207
202
  });
208
203
  });
209
204
 
210
- describe('Reference Completion Mode', () => {
211
- it('shows reference completion title', () => {
212
- const props = createMockProps({
213
- mode: 'reference',
214
- referenceData: {
215
- annotationUri: 'http://localhost:8080/annotations/ref-1',
216
- sourceDocumentId: 'doc-1',
217
- name: 'Referenced Resource',
218
- entityTypes: ['Document'],
219
- },
220
- });
221
- renderWithProviders(<ResourceComposePage {...props} />);
222
-
223
- expect(screen.getByText('Complete Reference')).toBeInTheDocument();
224
- expect(screen.getByText('Creating a new resource for reference')).toBeInTheDocument();
225
- expect(screen.getByText('This resource will be linked')).toBeInTheDocument();
226
- });
227
-
228
- it('initializes with reference data', () => {
229
- const props = createMockProps({
230
- mode: 'reference',
231
- referenceData: {
232
- annotationUri: 'http://localhost:8080/annotations/ref-1',
233
- sourceDocumentId: 'doc-1',
234
- name: 'Referenced Resource',
235
- entityTypes: ['Document', 'Article'],
236
- },
237
- });
238
- renderWithProviders(<ResourceComposePage {...props} />);
239
-
240
- const nameInput = screen.getByLabelText('Resource Name') as HTMLInputElement;
241
- expect(nameInput.value).toBe('Referenced Resource');
242
-
243
- expect(screen.getByText('Document')).toBeInTheDocument();
244
- expect(screen.getByText('Article')).toBeInTheDocument();
245
- });
246
-
247
- it('shows entity types as read-only when provided', () => {
248
- const props = createMockProps({
249
- mode: 'reference',
250
- referenceData: {
251
- annotationUri: 'http://localhost:8080/annotations/ref-1',
252
- sourceDocumentId: 'doc-1',
253
- name: 'Referenced Resource',
254
- entityTypes: ['Document'],
255
- },
256
- });
257
- renderWithProviders(<ResourceComposePage {...props} />);
258
-
259
- // Should show read-only entity types
260
- expect(screen.getByText('Document')).toBeInTheDocument();
261
-
262
- // Should not show selectable buttons
263
- expect(screen.queryByRole('button', { name: /Document entity type/ })).not.toBeInTheDocument();
264
- });
265
- });
266
-
267
- // Entity Type Selection tests removed - functionality not present in current component
268
- // The component only displays entity types in reference mode, doesn't have selection buttons
269
-
270
205
  describe('Content Input Method', () => {
271
206
  it('defaults to write mode', () => {
272
207
  const props = createMockProps();