@semiont/react-ui 0.5.28 → 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 (112) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-C2IGG4SW.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +4 -4
  3. package/dist/{ar-FABPTJVB.js → ar-AZBRADEB.js} +5 -1
  4. package/dist/{bn-LKP6BDMH.js → bn-CCEW6AOS.js} +5 -1
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-2YHEE63S.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-2GOHTTHW.js → chunk-SDKFZLEX.js} +5 -1
  8. package/dist/{cs-YQYNWHRG.js → cs-HXPF2CTE.js} +5 -1
  9. package/dist/{da-QHZOYKRY.js → da-K5NTK7LH.js} +5 -1
  10. package/dist/{de-GKA7RD7J.js → de-GMRQEDZI.js} +5 -1
  11. package/dist/{el-45T257AL.js → el-EUB4CI4G.js} +5 -1
  12. package/dist/{en-RVW5EQ66.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-AVER7RU4.js → es-Z32DAZ6W.js} +5 -1
  14. package/dist/{fa-K3VPK3O2.js → fa-G7HN6B32.js} +5 -1
  15. package/dist/{fi-5NEKF7EB.js → fi-3YBLZO7I.js} +5 -1
  16. package/dist/{fr-FUMNDLBZ.js → fr-VUBH7BBS.js} +5 -1
  17. package/dist/{he-3JGNA3PR.js → he-WQQEY26B.js} +5 -1
  18. package/dist/{hi-IJY375DF.js → hi-HJZ6W4JB.js} +5 -1
  19. package/dist/{id-LWHKJ7ZD.js → id-ZOGZ37MI.js} +5 -1
  20. package/dist/index.d.ts +157 -257
  21. package/dist/index.js +763 -1209
  22. package/dist/{it-EBVOV4RW.js → it-DT6UW66F.js} +5 -1
  23. package/dist/{ja-KWRIX2NF.js → ja-RUKQCOLO.js} +5 -1
  24. package/dist/{ko-L5CJMFDS.js → ko-ZH5ADKZM.js} +5 -1
  25. package/dist/{ms-LSNMJDR2.js → ms-Y2AFPZTU.js} +5 -1
  26. package/dist/{nl-ZB32MB7U.js → nl-OQEWIXNA.js} +5 -1
  27. package/dist/{no-4GTNY7M5.js → no-KPAYJZWY.js} +5 -1
  28. package/dist/{pl-LUHBLTHB.js → pl-WSXK2UUD.js} +5 -1
  29. package/dist/{pt-C4H3UNQ6.js → pt-NWRJRUCK.js} +5 -1
  30. package/dist/{ro-GGITS5V4.js → ro-3CGVMOC6.js} +5 -1
  31. package/dist/{sv-T56RTLBP.js → sv-HGSEBA7S.js} +5 -1
  32. package/dist/test-utils.js +16 -7
  33. package/dist/{th-22X2IWCB.js → th-NZ4G352Z.js} +5 -1
  34. package/dist/{tr-77TFG62H.js → tr-LGGS4DLW.js} +5 -1
  35. package/dist/{uk-MIIL6NXT.js → uk-AIX536L4.js} +5 -1
  36. package/dist/{vi-7JYOIGNV.js → vi-V6CVQZLQ.js} +5 -1
  37. package/dist/{zh-ID6DRYU4.js → zh-AGKPGI3L.js} +5 -1
  38. package/package.json +12 -12
  39. package/src/components/StatusDisplay.tsx +13 -13
  40. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  41. package/src/components/modals/ComposeStep.tsx +19 -4
  42. package/src/components/modals/ConfigureGenerationStep.tsx +143 -15
  43. package/src/components/modals/ReferenceWizardModal.tsx +14 -9
  44. package/src/components/modals/ResourceGenerateModal.tsx +10 -5
  45. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +171 -8
  46. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +25 -1
  47. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +24 -1
  48. package/src/components/modals/__tests__/WizardFooter.test.tsx +3 -5
  49. package/src/components/resource/BrowseView.tsx +3 -3
  50. package/src/components/resource/DownloadFileLink.tsx +1 -1
  51. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  52. package/src/components/resource/__tests__/BrowseView.test.tsx +2 -2
  53. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  54. package/src/components/resource/panels/ReferenceEntry.tsx +2 -2
  55. package/src/components/resource/panels/ReferencesPanel.tsx +5 -1
  56. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  57. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -2
  58. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -1
  59. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  60. package/src/features/auth/components/SignInForm.tsx +53 -53
  61. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  62. package/src/features/resource-compose/components/ResourceComposePage.tsx +18 -3
  63. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  64. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  65. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  66. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
  67. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  68. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +16 -25
  69. package/src/features/resource-viewer/generation-options.ts +29 -0
  70. package/src/styles/index.css +0 -1
  71. package/translations/ar.json +4 -0
  72. package/translations/bn.json +4 -0
  73. package/translations/cs.json +4 -0
  74. package/translations/da.json +4 -0
  75. package/translations/de.json +4 -0
  76. package/translations/el.json +4 -0
  77. package/translations/en.json +4 -0
  78. package/translations/es.json +4 -0
  79. package/translations/fa.json +4 -0
  80. package/translations/fi.json +4 -0
  81. package/translations/fr.json +4 -0
  82. package/translations/he.json +4 -0
  83. package/translations/hi.json +4 -0
  84. package/translations/id.json +4 -0
  85. package/translations/it.json +4 -0
  86. package/translations/ja.json +4 -0
  87. package/translations/ko.json +4 -0
  88. package/translations/ms.json +4 -0
  89. package/translations/nl.json +4 -0
  90. package/translations/no.json +4 -0
  91. package/translations/pl.json +4 -0
  92. package/translations/pt.json +4 -0
  93. package/translations/ro.json +4 -0
  94. package/translations/sv.json +4 -0
  95. package/translations/th.json +4 -0
  96. package/translations/tr.json +4 -0
  97. package/translations/uk.json +4 -0
  98. package/translations/vi.json +4 -0
  99. package/translations/zh.json +4 -0
  100. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  101. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  102. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  103. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  104. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  105. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  106. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  107. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  108. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  109. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  110. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  111. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  112. package/src/styles/features/exchange.css +0 -374
@@ -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', () => {
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import React, { useState, useEffect } from 'react';
11
- import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
11
+ import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES, folderOf, proposeStoragePath, getStorageUri } from '@semiont/core';
12
+ import type { SupportedMediaType } from '@semiont/core';
12
13
  import type { UploadProgress } from '@semiont/sdk';
13
14
  import { type CloneData } from '../state/compose-page-state-unit';
14
15
  import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
@@ -168,6 +169,20 @@ export function ResourceComposePage({
168
169
  // Archive original checkbox (for clones only)
169
170
  const [archiveOriginal, setArchiveOriginal] = useState(true);
170
171
 
172
+ // D11 — the Save location starts filled and follows the title until the user
173
+ // takes it over; `pathTouched` is derived, so clearing restores the proposal.
174
+ // A clone proposes beside its source; a new resource proposes at the root.
175
+ const composeFormat: SupportedMediaType =
176
+ isSupportedMediaType(selectedFormat) ? selectedFormat : 'text/markdown';
177
+ const pathTouched = storagePath !== '';
178
+ const effectivePath = pathTouched
179
+ ? storagePath
180
+ : proposeStoragePath(
181
+ mode === 'clone' && cloneData ? folderOf(getStorageUri(cloneData.sourceResource)) : '',
182
+ newResourceName,
183
+ composeFormat,
184
+ );
185
+
171
186
  // Initialize form data based on mode
172
187
  useEffect(() => {
173
188
  if (mode === 'clone' && cloneData) {
@@ -234,7 +249,7 @@ export function ResourceComposePage({
234
249
  const params: SaveResourceParams = {
235
250
  mode,
236
251
  name: newResourceName,
237
- storageUri: `file://${storagePath}`,
252
+ storageUri: `file://${effectivePath}`,
238
253
  content: newResourceContent,
239
254
  format: uploadedFile ? fileMimeType : selectedFormat,
240
255
  entityTypes: selectedEntityTypes,
@@ -316,7 +331,7 @@ export function ResourceComposePage({
316
331
  <input
317
332
  id="storagePath"
318
333
  type="text"
319
- value={storagePath}
334
+ value={effectivePath}
320
335
  onChange={(e) => setStoragePath(e.target.value)}
321
336
  placeholder="docs/my-resource.md"
322
337
  required
@@ -354,7 +354,7 @@ describe('ResourceDiscoveryPage', () => {
354
354
  });
355
355
 
356
356
  it('renders the recentDocuments prop as-is without applying any post-filter', () => {
357
- // The component is now controlled — backend filtering means
357
+ // The component is now controlled — gateway filtering means
358
358
  // `recentDocuments` already contains only the resources matching the
359
359
  // active `selectedEntityType`. The component must not re-filter.
360
360
  const props = createMockProps({
@@ -44,7 +44,7 @@ export interface ResourceDiscoveryPageProps {
44
44
  onSearchQueryChange: (query: string) => void;
45
45
 
46
46
  // Controlled entity-type filter — owned by the state unit so filtering
47
- // pushes to the backend rather than running as a post-fetch array filter.
47
+ // pushes to the gateway rather than running as a post-fetch array filter.
48
48
  selectedEntityType: string;
49
49
  onSelectedEntityTypeChange: (entityType: string) => void;
50
50
 
@@ -106,7 +106,7 @@ export function ResourceDiscoveryPage({
106
106
  const hasSearchQuery = searchQuery.trim() !== '';
107
107
 
108
108
  // When searching, render search results; otherwise render recent.
109
- // Both already arrive entity-type-filtered from the backend — no post-filter here.
109
+ // Both already arrive entity-type-filtered from the gateway — no post-filter here.
110
110
  const filteredResources = hasSearchQuery ? searchDocuments : recentDocuments;
111
111
 
112
112
  // Roving tabindex for entity type filters
@@ -50,7 +50,7 @@ export function createDiscoverStateUnit(
50
50
 
51
51
  // Selected entity-type chip on the Discover page. Drives both the
52
52
  // `recent` list and the search results — filtering happens on the
53
- // backend, not via post-fetch array filtering.
53
+ // gateway, not via post-fetch array filtering.
54
54
  const selectedEntityType$ = new BehaviorSubject<string>('');
55
55
  disposer.add(() => selectedEntityType$.complete());
56
56
 
@@ -47,8 +47,8 @@ const { stubBrowser, stubClient } = vi.hoisted(() => {
47
47
  const { SemiontClient, HttpTransport, HttpContentTransport } = require('@semiont/sdk');
48
48
  const { baseUrl } = require('@semiont/core');
49
49
  const transport = new HttpTransport({ baseUrl: baseUrl('http://localhost:4000') });
50
- // HttpTransport implements both ITransport and IBackendOperations; pass it
51
- // as backend so `client.auth` (used by useMediaToken) is wired.
50
+ // HttpTransport implements both ITransport and IGatewayOperations; pass it
51
+ // as gateway so `client.auth` (used by useMediaToken) is wired.
52
52
  const client = new SemiontClient(transport, new HttpContentTransport(transport), transport);
53
53
  const stubActiveSession$ = new BehaviorSubject({ client });
54
54
  const stubOpenResources$ = new BehaviorSubject([]);
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GENERATION-OUTPUT-FORMAT D8 — the submitted config becomes generation
3
+ * options in ONE place, by spread.
4
+ *
5
+ * Both page handlers used to build the options object field-by-field, so any
6
+ * field not explicitly listed was silently dropped. That is precisely what
7
+ * stranded `outputMediaType` in the SDK one layer down (P1), relocated one
8
+ * layer up. These pins state the PROPERTY — everything the config carries is
9
+ * forwarded — rather than enumerating today's fields, so a knob added later
10
+ * cannot regress the mode back into existence.
11
+ */
12
+ import { describe, it, expect } from 'vitest';
13
+ import type { GatheredContext } from '@semiont/core';
14
+ import { toGenerationOptions } from '../generation-options';
15
+ import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
16
+
17
+ const context = { focus: { kind: 'resource' } } as unknown as GatheredContext;
18
+
19
+ const config: GenerationConfig = {
20
+ title: 'Cedar County',
21
+ storageUri: 'file://research/notes.pdf',
22
+ prompt: 'lead with the treaty',
23
+ language: 'en',
24
+ temperature: 0.7,
25
+ maxTokens: 500,
26
+ outputMediaType: 'application/pdf',
27
+ context,
28
+ };
29
+
30
+ describe('toGenerationOptions', () => {
31
+ it('forwards EVERY field the config carries except the context', () => {
32
+ // Stated structurally on purpose: the expectation is derived from the
33
+ // same object, so adding a field to GenerationConfig extends this pin
34
+ // automatically. A field-by-field assertion would have passed happily
35
+ // while the real handler dropped the new one.
36
+ const { context: _positional, ...forwarded } = config;
37
+ expect(toGenerationOptions(config, 'fr')).toMatchObject(forwarded);
38
+ });
39
+
40
+ it('does not pass the context as an option — it is the positional argument', () => {
41
+ expect(toGenerationOptions(config, 'fr')).not.toHaveProperty('context');
42
+ });
43
+
44
+ it('injects the sourceLanguage, which comes from the viewed resource and not the form', () => {
45
+ expect(toGenerationOptions(config, 'fr').sourceLanguage).toBe('fr');
46
+ });
47
+
48
+ it('omits sourceLanguage entirely when the resource has no language', () => {
49
+ // Absence must stay absence: a manufactured '' would tell the prompt the
50
+ // source is in a language named empty string.
51
+ expect(toGenerationOptions(config, undefined)).not.toHaveProperty('sourceLanguage');
52
+ });
53
+ });
@@ -9,9 +9,9 @@ import React, { useState, useEffect, useCallback } from 'react';
9
9
  import { useResourceViewedReport } from '../hooks/useResourceViewedReport';
10
10
  import type { components, ResourceDescriptor, ResourceId, EventMap } from '@semiont/core';
11
11
  import type { ConnectionState } from '@semiont/core';
12
- import { annotationId } from '@semiont/core';
12
+ import { annotationId, folderOf } from '@semiont/core';
13
13
  import type { ComposeParams } from '../../../components/modals/ComposeStep';
14
- import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, capabilitiesOf, extensionForMediaType } from '@semiont/core';
14
+ import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, getStorageUri, capabilitiesOf, extensionForMediaType } from '@semiont/core';
15
15
  import { ANNOTATORS } from '@semiont/react-ui';
16
16
  import { ErrorBoundary } from '@semiont/react-ui';
17
17
  import { AnnotationHistory } from '@semiont/react-ui';
@@ -47,6 +47,7 @@ import { useTranslations } from '../../../contexts/TranslationContext';
47
47
  import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
48
48
  import { ResourceGenerateModal } from '../../../components/modals/ResourceGenerateModal';
49
49
  import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
50
+ import { toGenerationOptions } from '../generation-options';
50
51
 
51
52
  type SemiontResource = ResourceDescriptor;
52
53
 
@@ -275,18 +276,10 @@ export function ResourceViewerPage({
275
276
 
276
277
  const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
277
278
  clearSparkle(annotationId(referenceId));
278
- stateUnit?.yield.generate(config.context, {
279
- title: config.title,
280
- storageUri: config.storagePath,
281
- prompt: config.prompt,
282
- language: config.language,
283
- // The source resource is the one the user is viewing — fed into the
284
- // prompt so the LLM understands the embedded context (selected
285
- // passage, surrounding text) regardless of UI/target language.
286
- sourceLanguage: getLanguage(resource),
287
- temperature: config.temperature,
288
- maxTokens: config.maxTokens,
289
- });
279
+ // D8: forwarded by spread in ONE place, so a knob added to the form is
280
+ // never dropped on the way to the wire. `sourceLanguage` is the viewed
281
+ // resource's language — a page fact the form cannot know.
282
+ stateUnit?.yield.generate(config.context, toGenerationOptions(config, getLanguage(resource)));
290
283
  }, [stateUnit, clearSparkle, resource]);
291
284
 
292
285
  // Resource-generate flow (GENERATE-FROM-BUTTON): drive the SAME yield progress$
@@ -294,15 +287,7 @@ export function ResourceViewerPage({
294
287
  // toast. Both paths are one `generate(context, options)` now: the context's
295
288
  // focus.kind (resource here, annotation above) decides the shape.
296
289
  const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
297
- stateUnit?.yield.generate(config.context, {
298
- title: config.title,
299
- storageUri: config.storagePath,
300
- ...(config.prompt ? { prompt: config.prompt } : {}),
301
- language: config.language,
302
- sourceLanguage: getLanguage(resource),
303
- temperature: config.temperature,
304
- maxTokens: config.maxTokens,
305
- });
290
+ stateUnit?.yield.generate(config.context, toGenerationOptions(config, getLanguage(resource)));
306
291
  }, [stateUnit, resource]);
307
292
 
308
293
  const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
@@ -364,7 +349,7 @@ export function ResourceViewerPage({
364
349
  useEffect(() => {
365
350
  if (resource && rUri) {
366
351
  const mediaType = getPrimaryMediaType(resource);
367
- browser.addOpenResource(rUri, resource.name, mediaType || undefined, resource.storageUri);
352
+ browser.addOpenResource(rUri, resource.name, mediaType || undefined, getStorageUri(resource));
368
353
  browser.setLastViewedResource(rUri);
369
354
  }
370
355
  }, [resource, rUri, browser]);
@@ -671,7 +656,7 @@ export function ResourceViewerPage({
671
656
  documentLocale={getLanguage(resource)}
672
657
  primaryMediaType={primaryMediaType}
673
658
  primaryByteSize={primaryByteSize}
674
- storageUri={resource.storageUri}
659
+ storageUri={getStorageUri(resource)}
675
660
  isArchived={resource.archived ?? false}
676
661
  dateCreated={resource.dateCreated}
677
662
  dateModified={resource.dateModified}
@@ -724,6 +709,7 @@ export function ResourceViewerPage({
724
709
  annotationId={wizardAnnotationId}
725
710
  resourceId={wizardResourceId}
726
711
  defaultTitle={wizardDefaultTitle}
712
+ defaultFolder={folderOf(getStorageUri(resource))}
727
713
  entityTypes={wizardEntityTypes}
728
714
  resourceName={resource.name}
729
715
  locale={locale}
@@ -774,6 +760,8 @@ export function ResourceViewerPage({
774
760
  maxLength: tw('maxLength'),
775
761
  maxLengthHelp: tw('maxLengthHelp'),
776
762
  maxLengthCeiling: tw('maxLengthCeiling'),
763
+ outputFormat: tw('outputFormat'),
764
+ formatExtensionMismatch: tw('formatExtensionMismatch'),
777
765
  maxResults: tw('maxResults'),
778
766
  semanticScoring: tw('semanticScoring'),
779
767
  semanticScoringHelp: tw('semanticScoringHelp'),
@@ -797,6 +785,7 @@ export function ResourceViewerPage({
797
785
  // Seed the proposed title from the source resource's name (GFR D4/A4);
798
786
  // the field stays editable and required.
799
787
  defaultTitle={resource.name}
788
+ defaultFolder={folderOf(getStorageUri(resource))}
800
789
  locale={locale}
801
790
  entityTypeOptions={allEntityTypes}
802
791
  onGenerateSubmit={handleResourceGenerateSubmit}
@@ -840,6 +829,8 @@ export function ResourceViewerPage({
840
829
  maxLength: tg('maxLength'),
841
830
  maxLengthHelp: tg('maxLengthHelp'),
842
831
  maxLengthCeiling: tg('maxLengthCeiling'),
832
+ outputFormat: tg('outputFormat'),
833
+ formatExtensionMismatch: tg('formatExtensionMismatch'),
843
834
  generate: tg('generate'),
844
835
  discardDraftPrompt: tg('discardDraftPrompt'),
845
836
  discardDraft: tg('discardDraft'),
@@ -0,0 +1,29 @@
1
+ import type { GenerationOptions } from '@semiont/sdk';
2
+ import type { GenerationConfig } from '../../components/modals/ConfigureGenerationStep';
3
+
4
+ /**
5
+ * The submitted form config becomes generation options — in ONE place, by
6
+ * spread (GENERATION-OUTPUT-FORMAT D8).
7
+ *
8
+ * Both page handlers previously built this object field-by-field, so a knob
9
+ * the list didn't mention was silently dropped on the way to the wire. That
10
+ * is exactly how `outputMediaType` came to be unreachable one layer down
11
+ * (P1's finding), and copying the pattern here would have re-created it for
12
+ * every future knob. Spreading means the default is "forwarded"; only the two
13
+ * genuine transformations are spelled out:
14
+ *
15
+ * - `context` leaves the bag — it is `fromContext`'s positional argument.
16
+ * - `sourceLanguage` joins it — the language of the resource being VIEWED,
17
+ * which the form cannot know and the page can. Omitted entirely when
18
+ * unknown, so absence stays absence rather than becoming `''`.
19
+ */
20
+ export function toGenerationOptions(
21
+ config: GenerationConfig,
22
+ sourceLanguage: string | undefined,
23
+ ): GenerationOptions {
24
+ const { context: _positional, ...options } = config;
25
+ return {
26
+ ...options,
27
+ ...(sourceLanguage ? { sourceLanguage } : {}),
28
+ };
29
+ }
@@ -43,7 +43,6 @@
43
43
  @import './features/admin.css';
44
44
  @import './features/compose.css';
45
45
  @import './features/devops.css';
46
- @import './features/exchange.css';
47
46
  @import './features/entity-tags.css';
48
47
  @import './features/recent-docs.css';
49
48
  @import './features/resource.css';