@semiont/react-ui 0.4.6 → 0.4.9

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 (190) hide show
  1. package/dist/{PdfAnnotationCanvas.client-LF6DDTCV.mjs → PdfAnnotationCanvas.client-CW6SKH2U.mjs} +7 -25
  2. package/dist/PdfAnnotationCanvas.client-CW6SKH2U.mjs.map +1 -0
  3. package/dist/{EventBusContext-DUIMowqQ.d.mts → TranslationManager-CudgH3gw.d.mts} +1 -74
  4. package/dist/{ar-3URRW77J.mjs → ar-R4CRNXEF.mjs} +3 -2
  5. package/dist/ar-R4CRNXEF.mjs.map +1 -0
  6. package/dist/{bn-DCQD3XZ5.mjs → bn-CZKGRHTA.mjs} +3 -2
  7. package/dist/bn-CZKGRHTA.mjs.map +1 -0
  8. package/dist/{chunk-XMCUHQ2Y.mjs → chunk-BQJWOK4C.mjs} +15 -34
  9. package/dist/chunk-BQJWOK4C.mjs.map +1 -0
  10. package/dist/{chunk-5JZFKRLW.mjs → chunk-HNZOXH4L.mjs} +33 -35
  11. package/dist/chunk-HNZOXH4L.mjs.map +1 -0
  12. package/dist/{chunk-PWIVZQ4X.mjs → chunk-HVMAGUFA.mjs} +3 -2
  13. package/dist/chunk-HVMAGUFA.mjs.map +1 -0
  14. package/dist/{chunk-4RMWYJUJ.mjs → chunk-OL5UST25.mjs} +31 -31
  15. package/dist/{cs-23KOZUFE.mjs → cs-4WIB2IHH.mjs} +3 -2
  16. package/dist/cs-4WIB2IHH.mjs.map +1 -0
  17. package/dist/{da-OIQ66A42.mjs → da-JWYEUYPX.mjs} +3 -2
  18. package/dist/da-JWYEUYPX.mjs.map +1 -0
  19. package/dist/{de-FCCLKE2X.mjs → de-GWUQZGER.mjs} +3 -2
  20. package/dist/de-GWUQZGER.mjs.map +1 -0
  21. package/dist/{el-3ADITCGI.mjs → el-DM2GT7P5.mjs} +3 -2
  22. package/dist/el-DM2GT7P5.mjs.map +1 -0
  23. package/dist/{en-LNW2A3RA.mjs → en-IUV4ZXKH.mjs} +2 -2
  24. package/dist/{es-POQEEYIW.mjs → es-6LVQIM3D.mjs} +3 -2
  25. package/dist/es-6LVQIM3D.mjs.map +1 -0
  26. package/dist/{fa-RQPXVELG.mjs → fa-IRUJY3QI.mjs} +3 -2
  27. package/dist/fa-IRUJY3QI.mjs.map +1 -0
  28. package/dist/{fi-UXOVOUGT.mjs → fi-53FBOEVT.mjs} +3 -2
  29. package/dist/fi-53FBOEVT.mjs.map +1 -0
  30. package/dist/{fr-6W2T3R7G.mjs → fr-Q5KY7QL6.mjs} +3 -2
  31. package/dist/fr-Q5KY7QL6.mjs.map +1 -0
  32. package/dist/{he-65UHPZIU.mjs → he-HJNKULBY.mjs} +3 -2
  33. package/dist/he-HJNKULBY.mjs.map +1 -0
  34. package/dist/{hi-SGJIVPTN.mjs → hi-UYZ4X6CR.mjs} +3 -2
  35. package/dist/hi-UYZ4X6CR.mjs.map +1 -0
  36. package/dist/{id-EYJJQCS2.mjs → id-UAQMH6U2.mjs} +3 -2
  37. package/dist/id-UAQMH6U2.mjs.map +1 -0
  38. package/dist/index.css +48 -0
  39. package/dist/index.css.map +1 -1
  40. package/dist/index.d.mts +176 -125
  41. package/dist/index.mjs +556 -781
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/{it-IZGQEDO7.mjs → it-C7QEBNFA.mjs} +3 -2
  44. package/dist/it-C7QEBNFA.mjs.map +1 -0
  45. package/dist/{ja-SR272JSY.mjs → ja-THS6AOSJ.mjs} +3 -2
  46. package/dist/ja-THS6AOSJ.mjs.map +1 -0
  47. package/dist/{ko-YWTXVVXE.mjs → ko-XKK3TWQG.mjs} +3 -2
  48. package/dist/ko-XKK3TWQG.mjs.map +1 -0
  49. package/dist/{ms-3K2XSJGM.mjs → ms-GSK7LIF7.mjs} +3 -2
  50. package/dist/ms-GSK7LIF7.mjs.map +1 -0
  51. package/dist/{nl-YIGP4SLE.mjs → nl-KUBWITGY.mjs} +3 -2
  52. package/dist/nl-KUBWITGY.mjs.map +1 -0
  53. package/dist/{no-IFYIL3ND.mjs → no-ECWZUHT6.mjs} +3 -2
  54. package/dist/no-ECWZUHT6.mjs.map +1 -0
  55. package/dist/{pl-6MWSASJR.mjs → pl-PLVWSZWS.mjs} +3 -2
  56. package/dist/pl-PLVWSZWS.mjs.map +1 -0
  57. package/dist/{pt-NZNN6WUN.mjs → pt-AL74ZTKB.mjs} +3 -2
  58. package/dist/pt-AL74ZTKB.mjs.map +1 -0
  59. package/dist/{ro-NF3SMUJS.mjs → ro-WTPHLHGS.mjs} +3 -2
  60. package/dist/ro-WTPHLHGS.mjs.map +1 -0
  61. package/dist/{sv-ZHM7GSTD.mjs → sv-QCLI7SG4.mjs} +3 -2
  62. package/dist/sv-QCLI7SG4.mjs.map +1 -0
  63. package/dist/test-utils.d.mts +1 -4
  64. package/dist/test-utils.mjs +4 -6
  65. package/dist/test-utils.mjs.map +1 -1
  66. package/dist/{th-LX4NO5BJ.mjs → th-WCKVZU6U.mjs} +3 -2
  67. package/dist/th-WCKVZU6U.mjs.map +1 -0
  68. package/dist/{tr-DZ4GDSRR.mjs → tr-2CAFS2XS.mjs} +3 -2
  69. package/dist/tr-2CAFS2XS.mjs.map +1 -0
  70. package/dist/{uk-KC5KVVBY.mjs → uk-TDE4JLCY.mjs} +3 -2
  71. package/dist/uk-TDE4JLCY.mjs.map +1 -0
  72. package/dist/{vi-KNCR3OXZ.mjs → vi-KKXZ4PCX.mjs} +3 -2
  73. package/dist/vi-KKXZ4PCX.mjs.map +1 -0
  74. package/dist/{zh-M2HV2A27.mjs → zh-VH4XN5PV.mjs} +3 -2
  75. package/dist/zh-VH4XN5PV.mjs.map +1 -0
  76. package/package.json +1 -1
  77. package/src/components/Toolbar.tsx +15 -0
  78. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +2 -6
  79. package/src/components/__tests__/Toolbar.test.tsx +2 -6
  80. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +1 -2
  81. package/src/components/image-annotation/SvgDrawingCanvas.tsx +4 -7
  82. package/src/components/modals/ConfigureGenerationStep.tsx +54 -60
  83. package/src/components/modals/ReferenceWizardModal.tsx +3 -3
  84. package/src/components/navigation/__tests__/ObservableLink.test.tsx +2 -6
  85. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +2 -6
  86. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +3 -9
  87. package/src/components/resource/AnnotateView.tsx +4 -5
  88. package/src/components/resource/AnnotationHistory.tsx +2 -5
  89. package/src/components/resource/BrowseView.tsx +2 -3
  90. package/src/components/resource/HistoryEvent.tsx +3 -3
  91. package/src/components/resource/__tests__/BrowseView.test.tsx +1 -2
  92. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +8 -8
  93. package/src/components/resource/event-formatting.ts +22 -19
  94. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +1 -2
  95. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +1 -2
  96. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +0 -2
  97. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
  98. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +1 -2
  99. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +1 -2
  100. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -2
  101. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +1 -2
  102. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +1 -2
  103. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -2
  104. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +1 -2
  105. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -2
  106. package/src/components/settings/__tests__/SettingsPanel.test.tsx +1 -2
  107. package/src/components/viewers/ImageViewer.tsx +2 -8
  108. package/src/components/viewers/__tests__/ImageViewer.test.tsx +3 -16
  109. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +8 -0
  110. package/src/features/auth/auth.css +62 -0
  111. package/src/features/auth/components/SignInForm.tsx +139 -29
  112. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +2 -6
  113. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -2
  114. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -2
  115. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +1 -2
  116. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +1 -2
  117. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +1 -2
  118. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +1 -2
  119. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +1 -2
  120. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +9 -10
  121. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +9 -6
  122. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +2 -12
  123. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +1 -2
  124. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +16 -6
  125. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +45 -75
  126. package/src/styles/core/forms.css +32 -0
  127. package/src/styles/core/sliders.css +5 -0
  128. package/translations/ar.json +3 -2
  129. package/translations/bn.json +3 -2
  130. package/translations/cs.json +3 -2
  131. package/translations/da.json +3 -2
  132. package/translations/de.json +3 -2
  133. package/translations/el.json +3 -2
  134. package/translations/en.json +4 -3
  135. package/translations/es.json +3 -2
  136. package/translations/fa.json +3 -2
  137. package/translations/fi.json +3 -2
  138. package/translations/fr.json +3 -2
  139. package/translations/he.json +3 -2
  140. package/translations/hi.json +3 -2
  141. package/translations/id.json +3 -2
  142. package/translations/it.json +3 -2
  143. package/translations/ja.json +3 -2
  144. package/translations/ko.json +3 -2
  145. package/translations/ms.json +3 -2
  146. package/translations/nl.json +3 -2
  147. package/translations/no.json +3 -2
  148. package/translations/pl.json +3 -2
  149. package/translations/pt.json +3 -2
  150. package/translations/ro.json +3 -2
  151. package/translations/sv.json +3 -2
  152. package/translations/th.json +3 -2
  153. package/translations/tr.json +3 -2
  154. package/translations/uk.json +3 -2
  155. package/translations/vi.json +3 -2
  156. package/translations/zh.json +3 -2
  157. package/dist/PdfAnnotationCanvas.client-LF6DDTCV.mjs.map +0 -1
  158. package/dist/ar-3URRW77J.mjs.map +0 -1
  159. package/dist/bn-DCQD3XZ5.mjs.map +0 -1
  160. package/dist/chunk-5JZFKRLW.mjs.map +0 -1
  161. package/dist/chunk-PWIVZQ4X.mjs.map +0 -1
  162. package/dist/chunk-XMCUHQ2Y.mjs.map +0 -1
  163. package/dist/cs-23KOZUFE.mjs.map +0 -1
  164. package/dist/da-OIQ66A42.mjs.map +0 -1
  165. package/dist/de-FCCLKE2X.mjs.map +0 -1
  166. package/dist/el-3ADITCGI.mjs.map +0 -1
  167. package/dist/es-POQEEYIW.mjs.map +0 -1
  168. package/dist/fa-RQPXVELG.mjs.map +0 -1
  169. package/dist/fi-UXOVOUGT.mjs.map +0 -1
  170. package/dist/fr-6W2T3R7G.mjs.map +0 -1
  171. package/dist/he-65UHPZIU.mjs.map +0 -1
  172. package/dist/hi-SGJIVPTN.mjs.map +0 -1
  173. package/dist/id-EYJJQCS2.mjs.map +0 -1
  174. package/dist/it-IZGQEDO7.mjs.map +0 -1
  175. package/dist/ja-SR272JSY.mjs.map +0 -1
  176. package/dist/ko-YWTXVVXE.mjs.map +0 -1
  177. package/dist/ms-3K2XSJGM.mjs.map +0 -1
  178. package/dist/nl-YIGP4SLE.mjs.map +0 -1
  179. package/dist/no-IFYIL3ND.mjs.map +0 -1
  180. package/dist/pl-6MWSASJR.mjs.map +0 -1
  181. package/dist/pt-NZNN6WUN.mjs.map +0 -1
  182. package/dist/ro-NF3SMUJS.mjs.map +0 -1
  183. package/dist/sv-ZHM7GSTD.mjs.map +0 -1
  184. package/dist/th-LX4NO5BJ.mjs.map +0 -1
  185. package/dist/tr-DZ4GDSRR.mjs.map +0 -1
  186. package/dist/uk-KC5KVVBY.mjs.map +0 -1
  187. package/dist/vi-KNCR3OXZ.mjs.map +0 -1
  188. package/dist/zh-M2HV2A27.mjs.map +0 -1
  189. /package/dist/{chunk-4RMWYJUJ.mjs.map → chunk-OL5UST25.mjs.map} +0 -0
  190. /package/dist/{en-LNW2A3RA.mjs.map → en-IUV4ZXKH.mjs.map} +0 -0
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import React from 'react';
3
3
  import { screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
- import { renderWithProviders, resetEventBusForTesting } from '../../../../test-utils';
5
+ import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
8
  import type { RouteBuilder } from '../../../../contexts/RoutingContext';
@@ -86,7 +86,6 @@ describe('ReferenceEntry', () => {
86
86
 
87
87
  beforeEach(() => {
88
88
  vi.clearAllMocks();
89
- resetEventBusForTesting();
90
89
  mockGetAnnotationExactText.mockReturnValue('referenced text');
91
90
  mockIsBodyResolved.mockReturnValue(false);
92
91
  mockGetBodySource.mockReturnValue(null);
@@ -4,7 +4,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import '@testing-library/jest-dom';
6
6
  import { ReferencesPanel } from '../ReferencesPanel';
7
- import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../../contexts/EventBusContext';
7
+ import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
8
8
 
9
9
  // Composition-based event tracker
10
10
  interface TrackedEvent {
@@ -127,7 +127,6 @@ describe('ReferencesPanel Component', () => {
127
127
  };
128
128
 
129
129
  beforeEach(() => {
130
- resetEventBusForTesting();
131
130
  vi.clearAllMocks();
132
131
  });
133
132
 
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
  import { ResourceInfoPanel } from '../ResourceInfoPanel';
6
- import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../../contexts/EventBusContext';
6
+ import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
7
7
 
8
8
  // Mock TranslationContext
9
9
  vi.mock('../../../../contexts/TranslationContext', () => ({
@@ -129,7 +129,6 @@ describe('ResourceInfoPanel Component', () => {
129
129
  };
130
130
 
131
131
  beforeEach(() => {
132
- resetEventBusForTesting();
133
132
  vi.clearAllMocks();
134
133
  });
135
134
 
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import React from 'react';
3
3
  import { screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
- import { renderWithProviders, resetEventBusForTesting } from '../../../../test-utils';
5
+ import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
8
 
@@ -73,7 +73,6 @@ describe('TagEntry', () => {
73
73
 
74
74
  beforeEach(() => {
75
75
  vi.clearAllMocks();
76
- resetEventBusForTesting();
77
76
  mockGetAnnotationExactText.mockReturnValue('Tagged text content');
78
77
  mockGetTagCategory.mockReturnValue('Entity');
79
78
  mockGetTagSchemaId.mockReturnValue(null);
@@ -5,7 +5,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import '@testing-library/jest-dom';
7
7
  import { TaggingPanel } from '../TaggingPanel';
8
- import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../../contexts/EventBusContext';
8
+ import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
9
9
  import type { components } from '@semiont/core';
10
10
 
11
11
  type Annotation = components['schemas']['Annotation'];
@@ -203,7 +203,6 @@ describe('TaggingPanel Component', () => {
203
203
  };
204
204
 
205
205
  beforeEach(() => {
206
- resetEventBusForTesting();
207
206
  vi.clearAllMocks();
208
207
 
209
208
  // Mock scrollIntoView for jsdom
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import React from 'react';
3
3
  import { screen, fireEvent } from '@testing-library/react';
4
- import { renderWithProviders, resetEventBusForTesting } from '../../../test-utils';
4
+ import { renderWithProviders } from '../../../test-utils';
5
5
  import '@testing-library/jest-dom';
6
6
  import { SettingsPanel } from '../SettingsPanel';
7
7
 
@@ -36,7 +36,6 @@ describe('SettingsPanel', () => {
36
36
 
37
37
  beforeEach(() => {
38
38
  vi.clearAllMocks();
39
- resetEventBusForTesting();
40
39
  });
41
40
 
42
41
  it('renders settings title', () => {
@@ -1,16 +1,10 @@
1
- import type { ResourceId } from '@semiont/core';
2
-
3
1
  interface ImageViewerProps {
4
- resourceUri: ResourceId;
2
+ imageUrl: string;
5
3
  mimeType: string;
6
4
  alt?: string;
7
5
  }
8
6
 
9
- export function ImageViewer({ resourceUri, alt = 'Resource image' }: ImageViewerProps) {
10
- // Use Next.js API route proxy instead of direct backend call
11
- // This allows us to add authentication headers which <img> tags can't send
12
- const imageUrl = `/api/resources/${resourceUri}`;
13
-
7
+ export function ImageViewer({ imageUrl, alt = 'Resource image' }: ImageViewerProps) {
14
8
  return (
15
9
  <div className="semiont-image-viewer">
16
10
  <img
@@ -4,19 +4,18 @@ import { screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../../test-utils';
6
6
  import { ImageViewer } from '../ImageViewer';
7
- import type { ResourceId } from '@semiont/core';
8
7
 
9
8
  describe('ImageViewer', () => {
10
9
  const defaultProps = {
11
- resourceUri: 'abc-123' as ResourceId,
10
+ imageUrl: 'http://localhost:4000/resources/abc-123?token=tok',
12
11
  mimeType: 'image/png',
13
12
  };
14
13
 
15
- it('should render an img element with correct src derived from URI', () => {
14
+ it('should render an img element with the provided src', () => {
16
15
  renderWithProviders(<ImageViewer {...defaultProps} />);
17
16
 
18
17
  const img = screen.getByRole('img');
19
- expect(img).toHaveAttribute('src', '/api/resources/abc-123');
18
+ expect(img).toHaveAttribute('src', defaultProps.imageUrl);
20
19
  });
21
20
 
22
21
  it('should use default alt text when none provided', () => {
@@ -35,18 +34,6 @@ describe('ImageViewer', () => {
35
34
  expect(img).toHaveAttribute('alt', 'A beautiful diagram');
36
35
  });
37
36
 
38
- it('should extract the last segment of the URI as resource ID', () => {
39
- renderWithProviders(
40
- <ImageViewer
41
- resourceUri={'resource-xyz' as ResourceId}
42
- mimeType="image/jpeg"
43
- />
44
- );
45
-
46
- const img = screen.getByRole('img');
47
- expect(img).toHaveAttribute('src', '/api/resources/resource-xyz');
48
- });
49
-
50
37
  it('should render with correct class names', () => {
51
38
  const { container } = renderWithProviders(<ImageViewer {...defaultProps} />);
52
39
 
@@ -29,6 +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: 'https://your-semiont-server.com',
32
34
  emailLabel: 'Email',
33
35
  emailPlaceholder: 'your@email.com',
34
36
  passwordLabel: 'Password',
@@ -42,6 +44,7 @@ const mockTranslations = {
42
44
  signUpInstead: 'Sign Up Instead',
43
45
  errorEmailRequired: 'Email is required',
44
46
  errorPasswordRequired: 'Password is required',
47
+ errorBackendUrlRequired: 'Backend URL is required',
45
48
  tagline: 'make meaning',
46
49
  };
47
50
 
@@ -53,6 +56,7 @@ describe('SignInForm - Accessibility', () => {
53
56
  const { container } = render(
54
57
  <SignInForm
55
58
  onGoogleSignIn={onGoogleSignIn}
59
+ backendUrl="http://localhost:4000"
56
60
  Link={MockLink}
57
61
  translations={mockTranslations}
58
62
  />
@@ -200,6 +204,7 @@ describe('SignInForm - Accessibility', () => {
200
204
  render(
201
205
  <SignInForm
202
206
  onGoogleSignIn={onGoogleSignIn}
207
+ backendUrl="http://localhost:4000"
203
208
  Link={MockLink}
204
209
  translations={mockTranslations}
205
210
  />
@@ -257,6 +262,7 @@ describe('SignInForm - Accessibility', () => {
257
262
  render(
258
263
  <SignInForm
259
264
  onGoogleSignIn={onGoogleSignIn}
265
+ backendUrl="http://localhost:4000"
260
266
  Link={MockLink}
261
267
  translations={mockTranslations}
262
268
  />
@@ -374,6 +380,7 @@ describe('SignInForm - Accessibility', () => {
374
380
  render(
375
381
  <SignInForm
376
382
  onGoogleSignIn={onGoogleSignIn}
383
+ backendUrl="http://localhost:4000"
377
384
  Link={MockLink}
378
385
  translations={mockTranslations}
379
386
  />
@@ -391,6 +398,7 @@ describe('SignInForm - Accessibility', () => {
391
398
  const { container } = render(
392
399
  <SignInForm
393
400
  onGoogleSignIn={onGoogleSignIn}
401
+ backendUrl="http://localhost:4000"
394
402
  Link={MockLink}
395
403
  translations={mockTranslations}
396
404
  />
@@ -307,3 +307,65 @@
307
307
  white-space: nowrap;
308
308
  border-width: 0;
309
309
  }
310
+
311
+ /* URL field with inline probe indicator */
312
+ .semiont-form__input-row {
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 0.5rem;
316
+ }
317
+
318
+ .semiont-form__input-row:focus-visible {
319
+ outline: 2px solid var(--semiont-focus-ring, #3b82f6);
320
+ outline-offset: 2px;
321
+ }
322
+
323
+ [data-theme="dark"] .semiont-form__input-row:focus-visible {
324
+ outline-color: var(--semiont-focus-ring, #60a5fa);
325
+ }
326
+
327
+ .semiont-form__input-row .semiont-input {
328
+ flex: 1;
329
+ }
330
+
331
+ .semiont-form__input-row .semiont-input:focus-visible {
332
+ outline: 2px solid var(--semiont-focus-ring, #3b82f6);
333
+ outline-offset: 2px;
334
+ }
335
+
336
+ [data-theme="dark"] .semiont-form__input-row .semiont-input:focus-visible {
337
+ outline-color: var(--semiont-focus-ring, #60a5fa);
338
+ }
339
+
340
+ .semiont-form__url-status {
341
+ flex-shrink: 0;
342
+ font-size: 1rem;
343
+ width: 1.25rem;
344
+ text-align: center;
345
+ }
346
+
347
+ .semiont-form__url-status--checking {
348
+ color: var(--semiont-text-muted, #888888);
349
+ animation: spin 1s linear infinite;
350
+ display: inline-block;
351
+ }
352
+
353
+ [data-theme="dark"] .semiont-form__url-status--checking {
354
+ color: var(--semiont-text-muted, #888888);
355
+ }
356
+
357
+ .semiont-form__url-status--ok {
358
+ color: var(--semiont-success, #22c55e);
359
+ }
360
+
361
+ [data-theme="dark"] .semiont-form__url-status--ok {
362
+ color: var(--semiont-success, #22c55e);
363
+ }
364
+
365
+ .semiont-form__url-status--error {
366
+ color: var(--semiont-error, #ef4444);
367
+ }
368
+
369
+ [data-theme="dark"] .semiont-form__url-status--error {
370
+ color: var(--semiont-error, #ef4444);
371
+ }
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Supports both Google OAuth and email/password credentials.
5
5
  * No Next.js dependencies - all data via props.
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.
6
9
  */
7
10
 
8
11
  import React from 'react';
@@ -11,14 +14,22 @@ import '../auth.css';
11
14
 
12
15
  export interface SignInFormProps {
13
16
  /**
14
- * Callback when user clicks Google sign-in
17
+ * Callback when user clicks Google sign-in.
18
+ * Receives the backend URL (either the locked one or what the user typed).
19
+ */
20
+ onGoogleSignIn: (backendUrl: string) => Promise<void>;
21
+
22
+ /**
23
+ * Callback when user submits email/password credentials.
24
+ * Receives the backend URL along with email and password.
15
25
  */
16
- onGoogleSignIn: () => Promise<void>;
26
+ onCredentialsSignIn?: ((backendUrl: string, email: string, password: string) => Promise<void>) | undefined;
17
27
 
18
28
  /**
19
- * Callback when user submits email/password credentials
29
+ * Pre-filled backend URL. When provided the field is read-only (re-auth to known workspace).
30
+ * When omitted the user enters the URL themselves (new connection).
20
31
  */
21
- onCredentialsSignIn?: ((email: string, password: string) => Promise<void>) | undefined;
32
+ backendUrl?: string;
22
33
 
23
34
  /**
24
35
  * Error message to display (if any)
@@ -48,6 +59,8 @@ export interface SignInFormProps {
48
59
  welcomeBack: string;
49
60
  signInPrompt: string;
50
61
  continueWithGoogle: string;
62
+ backendUrlLabel: string;
63
+ backendUrlPlaceholder: string;
51
64
  emailLabel: string;
52
65
  emailPlaceholder: string;
53
66
  passwordLabel: string;
@@ -59,6 +72,9 @@ export interface SignInFormProps {
59
72
  backToHome: string;
60
73
  learnMore: string;
61
74
  signUpInstead: string;
75
+ errorBackendUrlRequired: string;
76
+ errorBackendUrlInvalid: string;
77
+ errorBackendUrlUnreachable: string;
62
78
  errorEmailRequired: string;
63
79
  errorPasswordRequired: string;
64
80
  tagline: string;
@@ -91,43 +107,87 @@ function GoogleIcon() {
91
107
  );
92
108
  }
93
109
 
110
+ type UrlProbeStatus = 'idle' | 'checking' | 'ok' | 'error';
111
+
112
+ function normalizeUrlForProbe(raw: string): string | null {
113
+ const trimmed = raw.trim();
114
+ if (!trimmed) return null;
115
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
116
+ return `http://${trimmed}`;
117
+ }
118
+
94
119
  /**
95
- * CredentialsAuthForm - Email/password form component
120
+ * CredentialsAuthForm - Backend URL + email/password form.
121
+ * When lockedBackendUrl is provided, the URL field is shown read-only.
96
122
  */
97
123
  function CredentialsAuthForm({
124
+ lockedBackendUrl,
98
125
  onSubmit,
99
126
  translations: t,
100
127
  }: {
101
- onSubmit: (email: string, password: string) => Promise<void>;
128
+ lockedBackendUrl: string | undefined;
129
+ onSubmit: (backendUrl: string, email: string, password: string) => Promise<void>;
102
130
  translations: SignInFormProps['translations'];
103
131
  }) {
132
+ const [backendUrl, setBackendUrl] = React.useState(lockedBackendUrl ?? '');
104
133
  const [email, setEmail] = React.useState('');
105
134
  const [password, setPassword] = React.useState('');
106
135
  const [validationError, setValidationError] = React.useState<string | null>(null);
107
- const [fieldErrors, setFieldErrors] = React.useState<{ email?: string; password?: string }>({});
136
+ const [fieldErrors, setFieldErrors] = React.useState<{ backendUrl?: string; email?: string; password?: string }>({});
137
+ const [urlProbeStatus, setUrlProbeStatus] = React.useState<UrlProbeStatus>('idle');
138
+
139
+ const probeUrl = async (raw: string) => {
140
+ const url = normalizeUrlForProbe(raw);
141
+ if (!url) return;
142
+ // Validate format
143
+ try { new URL(url); } catch {
144
+ setFieldErrors(prev => ({ ...prev, backendUrl: t.errorBackendUrlInvalid }));
145
+ setUrlProbeStatus('error');
146
+ return;
147
+ }
148
+ setUrlProbeStatus('checking');
149
+ try {
150
+ const res = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(5000) });
151
+ setUrlProbeStatus(res.ok ? 'ok' : 'error');
152
+ if (!res.ok) {
153
+ setFieldErrors(prev => ({ ...prev, backendUrl: t.errorBackendUrlUnreachable }));
154
+ }
155
+ } catch {
156
+ setUrlProbeStatus('error');
157
+ setFieldErrors(prev => ({ ...prev, backendUrl: t.errorBackendUrlUnreachable }));
158
+ }
159
+ };
108
160
 
109
161
  const handleSubmit = async (e: React.FormEvent) => {
110
162
  e.preventDefault();
111
- const errors: { email?: string; password?: string } = {};
163
+ const errors: { backendUrl?: string; email?: string; password?: string } = {};
112
164
 
113
- if (!email) {
114
- errors.email = t.errorEmailRequired;
115
- }
116
- if (!password) {
117
- errors.password = t.errorPasswordRequired;
118
- }
165
+ if (!backendUrl.trim()) errors.backendUrl = t.errorBackendUrlRequired;
166
+ if (!email) errors.email = t.errorEmailRequired;
167
+ if (!password) errors.password = t.errorPasswordRequired;
119
168
 
120
169
  if (Object.keys(errors).length > 0) {
121
170
  setFieldErrors(errors);
122
- setValidationError(Object.values(errors)[0]); // Set first error for screen readers
171
+ setValidationError(Object.values(errors)[0]);
123
172
  return;
124
173
  }
125
174
 
126
175
  setFieldErrors({});
127
176
  setValidationError(null);
128
- await onSubmit(email, password);
177
+ await onSubmit(backendUrl.trim(), email, password);
129
178
  };
130
179
 
180
+ const probeIndicator = !lockedBackendUrl && urlProbeStatus !== 'idle' ? (
181
+ <span
182
+ className={`semiont-form__url-status semiont-form__url-status--${urlProbeStatus}`}
183
+ aria-live="polite"
184
+ >
185
+ {urlProbeStatus === 'checking' && '⟳'}
186
+ {urlProbeStatus === 'ok' && '✓'}
187
+ {urlProbeStatus === 'error' && '✗'}
188
+ </span>
189
+ ) : null;
190
+
131
191
  return (
132
192
  <>
133
193
  {validationError && (
@@ -137,6 +197,38 @@ function CredentialsAuthForm({
137
197
  )}
138
198
 
139
199
  <form onSubmit={handleSubmit} className="semiont-auth__form" noValidate>
200
+ <div className="semiont-form__field">
201
+ <label htmlFor="backend-url" className="semiont-form__label">
202
+ {t.backendUrlLabel}
203
+ </label>
204
+ <div className="semiont-form__input-row">
205
+ <input
206
+ id="backend-url"
207
+ type="url"
208
+ value={backendUrl}
209
+ onChange={(e) => {
210
+ if (lockedBackendUrl) return;
211
+ setBackendUrl(e.target.value);
212
+ setUrlProbeStatus('idle');
213
+ if (fieldErrors.backendUrl) setFieldErrors({ ...fieldErrors, backendUrl: undefined });
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}
221
+ required
222
+ />
223
+ {probeIndicator}
224
+ </div>
225
+ {fieldErrors.backendUrl && (
226
+ <span id="backend-url-error" className="semiont-form__error" role="alert">
227
+ {fieldErrors.backendUrl}
228
+ </span>
229
+ )}
230
+ </div>
231
+
140
232
  <div className="semiont-form__field">
141
233
  <label htmlFor="email" className="semiont-form__label">
142
234
  {t.emailLabel}
@@ -147,9 +239,7 @@ function CredentialsAuthForm({
147
239
  value={email}
148
240
  onChange={(e) => {
149
241
  setEmail(e.target.value);
150
- if (fieldErrors.email) {
151
- setFieldErrors({ ...fieldErrors, email: undefined });
152
- }
242
+ if (fieldErrors.email) setFieldErrors({ ...fieldErrors, email: undefined });
153
243
  }}
154
244
  placeholder={t.emailPlaceholder}
155
245
  className="semiont-input"
@@ -163,6 +253,7 @@ function CredentialsAuthForm({
163
253
  </span>
164
254
  )}
165
255
  </div>
256
+
166
257
  <div className="semiont-form__field">
167
258
  <label htmlFor="password" className="semiont-form__label">
168
259
  {t.passwordLabel}
@@ -173,9 +264,7 @@ function CredentialsAuthForm({
173
264
  value={password}
174
265
  onChange={(e) => {
175
266
  setPassword(e.target.value);
176
- if (fieldErrors.password) {
177
- setFieldErrors({ ...fieldErrors, password: undefined });
178
- }
267
+ if (fieldErrors.password) setFieldErrors({ ...fieldErrors, password: undefined });
179
268
  }}
180
269
  placeholder={t.passwordPlaceholder}
181
270
  className="semiont-input"
@@ -189,6 +278,7 @@ function CredentialsAuthForm({
189
278
  </span>
190
279
  )}
191
280
  </div>
281
+
192
282
  <button type="submit" className={`${buttonStyles.primary.base} semiont-button--full-width`}>
193
283
  {t.signInWithCredentials}
194
284
  </button>
@@ -203,17 +293,29 @@ function CredentialsAuthForm({
203
293
  }
204
294
 
205
295
  /**
206
- * SignInForm - Main sign-in component
296
+ * SignInForm - Main sign-in / connect component.
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.
207
300
  */
208
301
  export function SignInForm({
209
302
  onGoogleSignIn,
210
303
  onCredentialsSignIn,
304
+ backendUrl,
211
305
  error,
212
306
  showCredentialsAuth = false,
213
307
  isLoading = false,
214
308
  Link,
215
309
  translations: t,
216
310
  }: SignInFormProps) {
311
+ const handleGoogleClick = () => {
312
+ // For Google auth we need the backend URL from the locked prop or we cannot proceed.
313
+ // The caller is responsible for ensuring backendUrl is set when Google sign-in is offered.
314
+ if (backendUrl) {
315
+ onGoogleSignIn(backendUrl);
316
+ }
317
+ };
318
+
217
319
  return (
218
320
  <main className="semiont-auth__main" role="main">
219
321
  <div className="semiont-auth__container">
@@ -245,12 +347,21 @@ export function SignInForm({
245
347
  <div className="semiont-auth__forms">
246
348
  {!isLoading ? (
247
349
  <>
248
- {showCredentialsAuth && onCredentialsSignIn && <CredentialsAuthForm onSubmit={onCredentialsSignIn} translations={t} />}
350
+ {showCredentialsAuth && onCredentialsSignIn && (
351
+ <CredentialsAuthForm
352
+ lockedBackendUrl={backendUrl}
353
+ onSubmit={onCredentialsSignIn}
354
+ translations={t}
355
+ />
356
+ )}
249
357
 
250
- <button onClick={onGoogleSignIn} className={`${buttonStyles.primary.base} semiont-button--full-width`}>
251
- <GoogleIcon />
252
- {t.continueWithGoogle}
253
- </button>
358
+ {/* Google sign-in only shown when a backend URL is known */}
359
+ {backendUrl && (
360
+ <button onClick={handleGoogleClick} className={`${buttonStyles.primary.base} semiont-button--full-width`}>
361
+ <GoogleIcon />
362
+ {t.continueWithGoogle}
363
+ </button>
364
+ )}
254
365
 
255
366
  <div className="semiont-auth__info">
256
367
  {showCredentialsAuth ? t.credentialsAuthEnabled : t.approvedDomainsOnly}
@@ -258,7 +369,6 @@ export function SignInForm({
258
369
  </>
259
370
  ) : (
260
371
  <div className="semiont-auth__loading" aria-busy="true" aria-live="polite">
261
- {/* Placeholder to maintain consistent height while loading */}
262
372
  <div style={{ height: '200px' }}></div>
263
373
  </div>
264
374
  )}
@@ -5,11 +5,11 @@
5
5
  * No Next.js mocking required - all dependencies passed as props!
6
6
  */
7
7
 
8
- import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import { describe, it, expect, vi } from 'vitest';
9
9
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10
10
  import { ResourceComposePage } from '../components/ResourceComposePage';
11
11
  import type { ResourceComposePageProps, SaveResourceParams } from '../components/ResourceComposePage';
12
- import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/EventBusContext';
12
+ import { EventBusProvider } from '../../../contexts/EventBusContext';
13
13
 
14
14
  // Mock CodeMirrorRenderer to avoid CodeMirror dependencies
15
15
  vi.mock('../../../components/CodeMirrorRenderer', () => ({
@@ -78,10 +78,6 @@ const renderWithProviders = (ui: React.ReactElement) => {
78
78
  };
79
79
 
80
80
  describe('ResourceComposePage', () => {
81
- beforeEach(() => {
82
- resetEventBusForTesting();
83
- });
84
-
85
81
  describe('Basic Rendering - New Resource Mode', () => {
86
82
  it('renders without crashing', () => {
87
83
  const props = createMockProps();
@@ -9,7 +9,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
9
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10
10
  import { ResourceDiscoveryPage } from '../components/ResourceDiscoveryPage';
11
11
  import type { ResourceDiscoveryPageProps } from '../components/ResourceDiscoveryPage';
12
- import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/EventBusContext';
12
+ import { EventBusProvider } from '../../../contexts/EventBusContext';
13
13
 
14
14
  const createMockResource = (id: string, name: string, entityTypes: string[] = []) => ({
15
15
  '@context': 'https://www.w3.org/ns/anno.jsonld',
@@ -63,7 +63,6 @@ const renderWithProviders = (ui: React.ReactElement) => {
63
63
 
64
64
  describe('ResourceDiscoveryPage', () => {
65
65
  beforeEach(() => {
66
- resetEventBusForTesting();
67
66
  });
68
67
 
69
68
  describe('Basic Rendering', () => {
@@ -21,7 +21,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
21
21
  import { render, screen, waitFor } from '@testing-library/react';
22
22
  import { act } from 'react';
23
23
  import { useMarkFlow } from '../../../hooks/useMarkFlow';
24
- import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
24
+ import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
25
25
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
26
26
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
27
27
  import { SemiontApiClient } from '@semiont/api-client';
@@ -107,7 +107,6 @@ describe('Annotation creation clears pendingAnnotation', () => {
107
107
 
108
108
  beforeEach(() => {
109
109
  vi.clearAllMocks();
110
- resetEventBusForTesting();
111
110
  markAnnotationSpy = vi
112
111
  .spyOn(SemiontApiClient.prototype, 'markAnnotation')
113
112
  .mockResolvedValue({ annotationId: MOCK_ANNOTATION.id } as any);
@@ -27,7 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
27
27
  import { render, waitFor } from '@testing-library/react';
28
28
  import { act } from 'react';
29
29
  import { useMarkFlow } from '../../../hooks/useMarkFlow';
30
- import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
30
+ import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
31
31
 
32
32
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
33
33
  vi.mock('../../../components/Toast', () => ({
@@ -51,7 +51,6 @@ describe('Annotation Deletion - Feature Integration', () => {
51
51
 
52
52
  beforeEach(() => {
53
53
  vi.clearAllMocks();
54
- resetEventBusForTesting();
55
54
 
56
55
  // Mock the deleteAnnotation method on SemiontApiClient prototype
57
56
  deleteAnnotationSpy = vi.fn().mockResolvedValue({ success: true });