@semiont/react-ui 0.5.8 → 0.5.10

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 (165) hide show
  1. package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js → PdfAnnotationCanvas.client-P37SXBWQ.js} +2 -2
  2. package/dist/{ar-WA47UUWA.js → ar-ZHKO4XDR.js} +45 -4
  3. package/dist/ar-ZHKO4XDR.js.map +1 -0
  4. package/dist/{bn-5ANDRIU6.js → bn-3BUDKAK5.js} +45 -4
  5. package/dist/bn-3BUDKAK5.js.map +1 -0
  6. package/dist/{chunk-WHUGODTB.js → chunk-5LFFAVDI.js} +32 -32
  7. package/dist/{chunk-YBGN4ACY.js → chunk-O23TLLXW.js} +1 -6
  8. package/dist/{chunk-GERSK2DY.js → chunk-R52YAGHK.js} +42 -1
  9. package/dist/chunk-R52YAGHK.js.map +1 -0
  10. package/dist/{cs-3RU7F4JX.js → cs-2EFEUKTW.js} +45 -4
  11. package/dist/cs-2EFEUKTW.js.map +1 -0
  12. package/dist/{da-GSW5P5HG.js → da-SUD3Q44Q.js} +45 -4
  13. package/dist/da-SUD3Q44Q.js.map +1 -0
  14. package/dist/{de-JUAUYF4Z.js → de-6IC5QRX7.js} +45 -4
  15. package/dist/de-6IC5QRX7.js.map +1 -0
  16. package/dist/{el-JNLWCKEC.js → el-Q4NAXFRC.js} +45 -4
  17. package/dist/el-Q4NAXFRC.js.map +1 -0
  18. package/dist/{en-RBN2GUHF.js → en-P2L4X27N.js} +5 -3
  19. package/dist/{es-WPOX225H.js → es-YUKZBD4W.js} +45 -4
  20. package/dist/es-YUKZBD4W.js.map +1 -0
  21. package/dist/{fa-TAEP6N77.js → fa-OJ47LILE.js} +45 -4
  22. package/dist/fa-OJ47LILE.js.map +1 -0
  23. package/dist/{fi-ZVZHANSP.js → fi-QFFL5CUB.js} +45 -4
  24. package/dist/fi-QFFL5CUB.js.map +1 -0
  25. package/dist/{fr-VLZW7M4N.js → fr-BOQZZWP4.js} +45 -4
  26. package/dist/fr-BOQZZWP4.js.map +1 -0
  27. package/dist/{he-QFAFYA77.js → he-LXDY3TFA.js} +45 -4
  28. package/dist/he-LXDY3TFA.js.map +1 -0
  29. package/dist/{hi-AO3DQPCV.js → hi-HOJER77V.js} +45 -4
  30. package/dist/hi-HOJER77V.js.map +1 -0
  31. package/dist/{id-GTXF42WM.js → id-I5WQ6EWD.js} +45 -4
  32. package/dist/id-I5WQ6EWD.js.map +1 -0
  33. package/dist/index.d.ts +119 -9
  34. package/dist/index.js +4417 -18858
  35. package/dist/index.js.map +1 -1
  36. package/dist/integrations/css-modules-helper.js +1 -1
  37. package/dist/integrations/styled-components-theme.js +28 -1449
  38. package/dist/integrations/styled-components-theme.js.map +1 -1
  39. package/dist/{it-AS5GM232.js → it-UYH642IJ.js} +45 -4
  40. package/dist/it-UYH642IJ.js.map +1 -0
  41. package/dist/{ja-GZ4HLUOF.js → ja-JTVWAC4L.js} +45 -4
  42. package/dist/ja-JTVWAC4L.js.map +1 -0
  43. package/dist/{ko-A4XUXFGJ.js → ko-IHXFMOIZ.js} +45 -4
  44. package/dist/ko-IHXFMOIZ.js.map +1 -0
  45. package/dist/{ms-YBAO3S6K.js → ms-JH25VFLO.js} +45 -4
  46. package/dist/ms-JH25VFLO.js.map +1 -0
  47. package/dist/{nl-3TJGIIIU.js → nl-SURXW7IQ.js} +45 -4
  48. package/dist/nl-SURXW7IQ.js.map +1 -0
  49. package/dist/{no-4AXIQPRQ.js → no-HXIKQFDZ.js} +45 -4
  50. package/dist/no-HXIKQFDZ.js.map +1 -0
  51. package/dist/{pl-5GP6GKXO.js → pl-7A4KU6XK.js} +45 -4
  52. package/dist/pl-7A4KU6XK.js.map +1 -0
  53. package/dist/{pt-26Y6JFG5.js → pt-6MGYP7Q5.js} +45 -4
  54. package/dist/pt-6MGYP7Q5.js.map +1 -0
  55. package/dist/{ro-C7UXFRWJ.js → ro-IROCHNIA.js} +45 -4
  56. package/dist/ro-IROCHNIA.js.map +1 -0
  57. package/dist/{sv-44DVD76T.js → sv-WDJD75CV.js} +45 -4
  58. package/dist/sv-WDJD75CV.js.map +1 -0
  59. package/dist/test-utils.js +5 -5
  60. package/dist/test-utils.js.map +1 -1
  61. package/dist/{th-GIQRTBOY.js → th-RIO2E6B3.js} +45 -4
  62. package/dist/th-RIO2E6B3.js.map +1 -0
  63. package/dist/{tr-WMQWO4D6.js → tr-TZ7ERCIN.js} +45 -4
  64. package/dist/tr-TZ7ERCIN.js.map +1 -0
  65. package/dist/{uk-I7ML6RGG.js → uk-ZFWDKLUE.js} +45 -4
  66. package/dist/uk-ZFWDKLUE.js.map +1 -0
  67. package/dist/{vi-FGWECASQ.js → vi-UIOSP2JG.js} +45 -4
  68. package/dist/vi-UIOSP2JG.js.map +1 -0
  69. package/dist/{zh-L5FN73XC.js → zh-6R6YUY46.js} +45 -4
  70. package/dist/zh-6R6YUY46.js.map +1 -0
  71. package/package.json +23 -7
  72. package/src/components/AnnotateReferencesProgressWidget.tsx +35 -16
  73. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +41 -91
  74. package/src/components/modals/ConfigureGatherStep.tsx +102 -0
  75. package/src/components/modals/ContextSummary.tsx +15 -6
  76. package/src/components/modals/GatherContextStep.tsx +104 -59
  77. package/src/components/modals/ResourceGenerateModal.tsx +258 -0
  78. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +82 -0
  79. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +107 -0
  80. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +197 -0
  81. package/src/components/resource/panels/ReferencesPanel.tsx +10 -0
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +22 -0
  83. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +26 -0
  84. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +14 -0
  85. package/src/features/admin-exchange/state/exchange-state-unit.ts +4 -2
  86. package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +14 -0
  87. package/src/features/admin-security/state/admin-security-state-unit.ts +4 -2
  88. package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +12 -5
  89. package/src/features/admin-users/state/admin-users-state-unit.ts +4 -2
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +11 -0
  91. package/src/features/auth-welcome/state/welcome-state-unit.ts +1 -1
  92. package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +14 -0
  93. package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +4 -2
  94. package/src/features/resource-compose/components/ResourceComposePage.tsx +13 -7
  95. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +13 -0
  96. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -2
  97. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +15 -9
  98. package/src/features/resource-discovery/state/discover-state-unit.ts +5 -2
  99. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +79 -0
  100. package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +11 -0
  101. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +30 -0
  102. package/src/features/resource-viewer/state/resource-loader-state-unit.ts +1 -1
  103. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +5 -2
  104. package/translations/ar.json +41 -2
  105. package/translations/bn.json +41 -2
  106. package/translations/cs.json +41 -2
  107. package/translations/da.json +41 -2
  108. package/translations/de.json +41 -2
  109. package/translations/el.json +41 -2
  110. package/translations/en.json +39 -0
  111. package/translations/es.json +41 -2
  112. package/translations/fa.json +41 -2
  113. package/translations/fi.json +41 -2
  114. package/translations/fr.json +41 -2
  115. package/translations/he.json +41 -2
  116. package/translations/hi.json +41 -2
  117. package/translations/id.json +41 -2
  118. package/translations/it.json +41 -2
  119. package/translations/ja.json +41 -2
  120. package/translations/ko.json +41 -2
  121. package/translations/ms.json +41 -2
  122. package/translations/nl.json +41 -2
  123. package/translations/no.json +41 -2
  124. package/translations/pl.json +41 -2
  125. package/translations/pt.json +41 -2
  126. package/translations/ro.json +41 -2
  127. package/translations/sv.json +41 -2
  128. package/translations/th.json +41 -2
  129. package/translations/tr.json +41 -2
  130. package/translations/uk.json +41 -2
  131. package/translations/vi.json +41 -2
  132. package/translations/zh.json +41 -2
  133. package/dist/ar-WA47UUWA.js.map +0 -1
  134. package/dist/bn-5ANDRIU6.js.map +0 -1
  135. package/dist/chunk-GERSK2DY.js.map +0 -1
  136. package/dist/cs-3RU7F4JX.js.map +0 -1
  137. package/dist/da-GSW5P5HG.js.map +0 -1
  138. package/dist/de-JUAUYF4Z.js.map +0 -1
  139. package/dist/el-JNLWCKEC.js.map +0 -1
  140. package/dist/es-WPOX225H.js.map +0 -1
  141. package/dist/fa-TAEP6N77.js.map +0 -1
  142. package/dist/fi-ZVZHANSP.js.map +0 -1
  143. package/dist/fr-VLZW7M4N.js.map +0 -1
  144. package/dist/he-QFAFYA77.js.map +0 -1
  145. package/dist/hi-AO3DQPCV.js.map +0 -1
  146. package/dist/id-GTXF42WM.js.map +0 -1
  147. package/dist/it-AS5GM232.js.map +0 -1
  148. package/dist/ja-GZ4HLUOF.js.map +0 -1
  149. package/dist/ko-A4XUXFGJ.js.map +0 -1
  150. package/dist/ms-YBAO3S6K.js.map +0 -1
  151. package/dist/nl-3TJGIIIU.js.map +0 -1
  152. package/dist/no-4AXIQPRQ.js.map +0 -1
  153. package/dist/pl-5GP6GKXO.js.map +0 -1
  154. package/dist/pt-26Y6JFG5.js.map +0 -1
  155. package/dist/ro-C7UXFRWJ.js.map +0 -1
  156. package/dist/sv-44DVD76T.js.map +0 -1
  157. package/dist/th-GIQRTBOY.js.map +0 -1
  158. package/dist/tr-WMQWO4D6.js.map +0 -1
  159. package/dist/uk-I7ML6RGG.js.map +0 -1
  160. package/dist/vi-FGWECASQ.js.map +0 -1
  161. package/dist/zh-L5FN73XC.js.map +0 -1
  162. /package/dist/{PdfAnnotationCanvas.client-U4EVBZEV.js.map → PdfAnnotationCanvas.client-P37SXBWQ.js.map} +0 -0
  163. /package/dist/{chunk-WHUGODTB.js.map → chunk-5LFFAVDI.js.map} +0 -0
  164. /package/dist/{chunk-YBGN4ACY.js.map → chunk-O23TLLXW.js.map} +0 -0
  165. /package/dist/{en-RBN2GUHF.js.map → en-P2L4X27N.js.map} +0 -0
@@ -23,6 +23,12 @@ interface Props {
23
23
  wasAttributedTo?: Agent | Agent[] | undefined;
24
24
  wasDerivedFrom?: string | string[] | undefined;
25
25
  generator?: Agent | Agent[] | undefined;
26
+ /**
27
+ * Open the resource-generate flow. UI-only — the SDK isn't involved in
28
+ * *opening* the display (if collaborative gather ever lands, this graduates
29
+ * to a verb). Omit to hide the Generate action.
30
+ */
31
+ onGenerate?: () => void;
26
32
  }
27
33
 
28
34
  /**
@@ -45,6 +51,7 @@ export function ResourceInfoPanel({
45
51
  wasAttributedTo,
46
52
  wasDerivedFrom,
47
53
  generator,
54
+ onGenerate,
48
55
  }: Props) {
49
56
  const t = useTranslations('ResourceInfoPanel');
50
57
  const session = useObservable(useSemiont().activeSession$);
@@ -177,6 +184,21 @@ export function ResourceInfoPanel({
177
184
  </div>
178
185
  )}
179
186
 
187
+ {/* Generate Action — opens the resource-generate flow (UI-only) */}
188
+ {onGenerate && (
189
+ <div className="semiont-resource-info-panel__action-section">
190
+ <button
191
+ onClick={onGenerate}
192
+ className="semiont-resource-button semiont-resource-button--secondary"
193
+ >
194
+ ✨ {t('generate')}
195
+ </button>
196
+ <p className="semiont-resource-info-panel__description">
197
+ {t('generateDescription')}
198
+ </p>
199
+ </div>
200
+ )}
201
+
180
202
  {/* Clone Action */}
181
203
  <div className="semiont-resource-info-panel__action-section">
182
204
  <button
@@ -20,6 +20,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
20
20
  storageUri: 'Storage',
21
21
  clone: 'Clone',
22
22
  cloneDescription: 'Generate a shareable clone link for this resource',
23
+ generate: 'Generate',
24
+ generateDescription: "Generate a new resource from this one's context",
23
25
  archive: 'Archive',
24
26
  archiveDescription: 'Move this resource to archived status',
25
27
  unarchive: 'Unarchive',
@@ -269,6 +271,30 @@ describe('ResourceInfoPanel Component', () => {
269
271
  });
270
272
  });
271
273
 
274
+ describe('Generate Action', () => {
275
+ it('renders the Generate button when onGenerate is provided', () => {
276
+ renderWithEventBus(
277
+ <ResourceInfoPanel {...defaultProps} onGenerate={() => {}} />
278
+ );
279
+ expect(screen.getByRole('button', { name: /Generate/i })).toBeInTheDocument();
280
+ expect(screen.getByText("Generate a new resource from this one's context")).toBeInTheDocument();
281
+ });
282
+
283
+ it('hides the Generate button when onGenerate is omitted', () => {
284
+ renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
285
+ expect(screen.queryByRole('button', { name: /Generate/i })).not.toBeInTheDocument();
286
+ });
287
+
288
+ it('calls onGenerate when clicked', () => {
289
+ const onGenerate = vi.fn();
290
+ renderWithEventBus(
291
+ <ResourceInfoPanel {...defaultProps} onGenerate={onGenerate} />
292
+ );
293
+ fireEvent.click(screen.getByRole('button', { name: /Generate/i }));
294
+ expect(onGenerate).toHaveBeenCalledTimes(1);
295
+ });
296
+ });
297
+
272
298
  describe('Provenance Section', () => {
273
299
  it('should not render provenance section when no provenance data', () => {
274
300
  renderWithEventBus(<ResourceInfoPanel {...defaultProps} />);
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
2
2
  import { firstValueFrom, of, throwError } from 'rxjs';
3
3
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
4
4
  import { createExchangeStateUnit } from '../exchange-state-unit';
5
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
5
6
 
6
7
  function mockBrowse(): ShellStateUnit {
7
8
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -169,3 +170,16 @@ describe('createExchangeStateUnit', () => {
169
170
  stateUnit.dispose();
170
171
  });
171
172
  });
173
+
174
+ describe('ExchangeStateUnit — StateUnit axioms', () => {
175
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
176
+ assertStateUnitAxioms({
177
+ setup: () => {
178
+ const browse = disposeProbe();
179
+ return { unit: createExchangeStateUnit(browse as unknown as ShellStateUnit, vi.fn(), vi.fn()), passedIn: [browse] };
180
+ },
181
+ surfaces: (u) => [u.selectedFile$, u.preview$, u.importPhase$, u.importMessage$, u.importResult$, u.isExporting$, u.isImporting$],
182
+ invocations: (u) => [() => u.cancelImport(), () => u.doImport()],
183
+ });
184
+ });
185
+ });
@@ -1,7 +1,7 @@
1
1
  import { BehaviorSubject, lastValueFrom, type Observable } from 'rxjs';
2
2
  import { tap } from 'rxjs/operators';
3
3
  import { createDisposer } from '@semiont/sdk';
4
- import type { StateUnit } from '@semiont/sdk';
4
+ import type { StateUnit } from '@semiont/core';
5
5
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
6
6
  import type { BackendDownload, ProgressEvent } from '@semiont/core';
7
7
 
@@ -33,7 +33,9 @@ export function createExchangeStateUnit(
33
33
  importFn: (file: File) => Observable<ProgressEvent>,
34
34
  ): ExchangeStateUnit {
35
35
  const disposer = createDisposer();
36
- disposer.add(browse);
36
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
37
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
38
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
37
39
 
38
40
  const selectedFile$ = new BehaviorSubject<File | null>(null);
39
41
  const preview$ = new BehaviorSubject<ImportPreview | null>(null);
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createAdminSecurityStateUnit } from '../admin-security-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  function mockBrowse(): ShellStateUnit {
9
10
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -66,3 +67,16 @@ describe('createAdminSecurityStateUnit', () => {
66
67
  stateUnit.dispose();
67
68
  });
68
69
  });
70
+
71
+ describe('AdminSecurityStateUnit — StateUnit axioms', () => {
72
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
73
+ assertStateUnitAxioms({
74
+ setup: () => {
75
+ const browse = disposeProbe();
76
+ const client = mockClient(vi.fn().mockResolvedValue({ providers: [], allowedDomains: [] }));
77
+ return { unit: createAdminSecurityStateUnit(client, browse as unknown as ShellStateUnit), passedIn: [browse] };
78
+ },
79
+ surfaces: (u) => [u.providers$, u.allowedDomains$, u.isLoading$],
80
+ });
81
+ });
82
+ });
@@ -1,6 +1,6 @@
1
1
  import { BehaviorSubject, type Observable } from 'rxjs';
2
2
  import { createDisposer } from '@semiont/sdk';
3
- import type { StateUnit } from '@semiont/sdk';
3
+ import type { StateUnit } from '@semiont/core';
4
4
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
5
5
  import type { SemiontClient } from '@semiont/sdk';
6
6
 
@@ -16,7 +16,9 @@ export function createAdminSecurityStateUnit(
16
16
  browse: ShellStateUnit,
17
17
  ): AdminSecurityStateUnit {
18
18
  const disposer = createDisposer();
19
- disposer.add(browse);
19
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
20
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
21
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
20
22
 
21
23
  const providers$ = new BehaviorSubject<unknown[]>([]);
22
24
  const allowedDomains$ = new BehaviorSubject<string[]>([]);
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createAdminUsersStateUnit } from '../admin-users-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  function mockBrowse(): ShellStateUnit {
9
10
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -77,10 +78,16 @@ describe('createAdminUsersStateUnit', () => {
77
78
  stateUnit.dispose();
78
79
  });
79
80
 
80
- it('disposes browse on dispose', () => {
81
- const browse = mockBrowse();
82
- const stateUnit = createAdminUsersStateUnit(mockClient(), browse);
83
- stateUnit.dispose();
84
- expect(browse.dispose).toHaveBeenCalled();
81
+ });
82
+
83
+ describe('AdminUsersStateUnit StateUnit axioms', () => {
84
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
85
+ assertStateUnitAxioms({
86
+ setup: () => {
87
+ const browse = disposeProbe();
88
+ return { unit: createAdminUsersStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
89
+ },
90
+ surfaces: (u) => [u.users$, u.stats$, u.usersLoading$, u.statsLoading$],
91
+ });
85
92
  });
86
93
  });
@@ -1,7 +1,7 @@
1
1
  import { BehaviorSubject, type Observable } from 'rxjs';
2
2
  import { userDID } from '@semiont/core';
3
3
  import { createDisposer } from '@semiont/sdk';
4
- import type { StateUnit } from '@semiont/sdk';
4
+ import type { StateUnit } from '@semiont/core';
5
5
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
6
6
  import type { SemiontClient } from '@semiont/sdk';
7
7
 
@@ -19,7 +19,9 @@ export function createAdminUsersStateUnit(
19
19
  browse: ShellStateUnit,
20
20
  ): AdminUsersStateUnit {
21
21
  const disposer = createDisposer();
22
- disposer.add(browse);
22
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
23
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
24
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
23
25
 
24
26
  const users$ = new BehaviorSubject<unknown[]>([]);
25
27
  const stats$ = new BehaviorSubject<unknown | null>(null);
@@ -3,6 +3,7 @@ import { firstValueFrom } from 'rxjs';
3
3
  import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import { createWelcomeStateUnit } from '../welcome-state-unit';
6
+ import { assertStateUnitAxioms } from '@semiont/core/testing';
6
7
 
7
8
  function mockClient(overrides: {
8
9
  getMe?: ReturnType<typeof vi.fn>;
@@ -84,3 +85,13 @@ describe('createWelcomeStateUnit', () => {
84
85
  stateUnit.dispose();
85
86
  });
86
87
  });
88
+
89
+ describe('WelcomeStateUnit — StateUnit axioms', () => {
90
+ it('satisfies the StateUnit axioms', () => {
91
+ assertStateUnitAxioms({
92
+ setup: () => createWelcomeStateUnit(mockClient()),
93
+ surfaces: (u) => [u.userData$, u.isProcessing$],
94
+ invocations: (u) => [() => u.acceptTerms()],
95
+ });
96
+ });
97
+ });
@@ -1,6 +1,6 @@
1
1
  import { BehaviorSubject, type Observable } from 'rxjs';
2
2
  import { createDisposer } from '@semiont/sdk';
3
- import type { StateUnit } from '@semiont/sdk';
3
+ import type { StateUnit } from '@semiont/core';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
 
6
6
  export interface WelcomeStateUnit extends StateUnit {
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createEntityTagsStateUnit } from '../entity-tags-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  function mockBrowse(): ShellStateUnit {
9
10
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -100,3 +101,16 @@ describe('createEntityTagsStateUnit', () => {
100
101
  stateUnit.dispose();
101
102
  });
102
103
  });
104
+
105
+ describe('EntityTagsStateUnit — StateUnit axioms', () => {
106
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
107
+ assertStateUnitAxioms({
108
+ setup: () => {
109
+ const browse = disposeProbe();
110
+ return { unit: createEntityTagsStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
111
+ },
112
+ surfaces: (u) => [u.newTag$, u.error$, u.isAdding$],
113
+ invocations: (u) => [() => u.setNewTag(''), () => u.addTag()],
114
+ });
115
+ });
116
+ });
@@ -1,6 +1,6 @@
1
1
  import { BehaviorSubject, type Observable, map } from 'rxjs';
2
2
  import { createDisposer } from '@semiont/sdk';
3
- import type { StateUnit } from '@semiont/sdk';
3
+ import type { StateUnit } from '@semiont/core';
4
4
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
5
5
  import type { SemiontClient } from '@semiont/sdk';
6
6
 
@@ -20,7 +20,9 @@ export function createEntityTagsStateUnit(
20
20
  browse: ShellStateUnit,
21
21
  ): EntityTagsStateUnit {
22
22
  const disposer = createDisposer();
23
- disposer.add(browse);
23
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
24
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
25
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
24
26
 
25
27
  const newTag$ = new BehaviorSubject<string>('');
26
28
  const error$ = new BehaviorSubject<string>('');
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import React, { useState, useEffect } from 'react';
10
- import type { GatheredContext } from '@semiont/core';
10
+ import { deriveViews, type GatheredContext } from '@semiont/core';
11
11
  import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
12
12
  import type { UploadProgress } from '@semiont/sdk';
13
13
  import { type CloneData, type ReferenceData } from '../state/compose-page-state-unit';
@@ -283,6 +283,12 @@ export function ResourceComposePage({
283
283
  const isClone = mode === 'clone';
284
284
  const isReferenceCompletion = mode === 'reference';
285
285
 
286
+ // Gathered-context panel — annotation-focus only for now.
287
+ const composeFocus = gatheredContext?.focus.kind === 'annotation' ? gatheredContext.focus : null;
288
+ const composeConnections = gatheredContext && composeFocus
289
+ ? deriveViews(gatheredContext.graph, String(composeFocus.sourceResource.id)).connections
290
+ : [];
291
+
286
292
  return (
287
293
  <div className={`semiont-page${activePanel && COMMON_PANELS.includes(activePanel as ToolbarPanelType) ? ' semiont-page--panel-open' : ''}`}>
288
294
  {/* Main Content Area */}
@@ -316,7 +322,7 @@ export function ResourceComposePage({
316
322
  border: '1px solid var(--semiont-border-primary)',
317
323
  }}>
318
324
  <h3 className="semiont-form__label" style={{ marginBottom: '0.75rem' }}>Gathered Context</h3>
319
- {gatheredContext.sourceContext && (
325
+ {composeFocus?.selected && (
320
326
  <div style={{
321
327
  padding: '0.75rem',
322
328
  backgroundColor: 'var(--semiont-bg-primary)',
@@ -327,27 +333,27 @@ export function ResourceComposePage({
327
333
  marginBottom: '0.5rem',
328
334
  }}>
329
335
  <div style={{ fontSize: 'var(--semiont-text-sm)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
330
- {gatheredContext.sourceContext.before && <span>{gatheredContext.sourceContext.before}</span>}
336
+ {composeFocus.selected.before && <span>{composeFocus.selected.before}</span>}
331
337
  <span style={{
332
338
  backgroundColor: 'var(--semiont-color-primary-100)',
333
339
  padding: '0 0.25rem',
334
340
  fontWeight: 600,
335
341
  color: 'var(--semiont-color-primary-900)',
336
342
  }}>
337
- {gatheredContext.sourceContext.selected}
343
+ {composeFocus.selected.text}
338
344
  </span>
339
- {gatheredContext.sourceContext.after && <span>{gatheredContext.sourceContext.after}</span>}
345
+ {composeFocus.selected.after && <span>{composeFocus.selected.after}</span>}
340
346
  </div>
341
347
  </div>
342
348
  )}
343
- {gatheredContext.graphContext && gatheredContext.graphContext.connections && gatheredContext.graphContext.connections.length > 0 && (
349
+ {composeConnections.length > 0 && (
344
350
  <div style={{
345
351
  padding: '0.5rem 0',
346
352
  fontSize: 'var(--semiont-text-sm)',
347
353
  color: 'var(--semiont-text-secondary)',
348
354
  }}>
349
355
  <span style={{ fontSize: 'var(--semiont-text-xs)', fontWeight: 500, marginRight: '0.5rem' }}>Connections:</span>
350
- {gatheredContext.graphContext.connections.map(conn => (
356
+ {composeConnections.map(conn => (
351
357
  <span key={conn.resourceId} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.5rem', marginRight: '0.25rem' }}>
352
358
  {conn.resourceName}
353
359
  </span>
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createComposePageStateUnit } from '../compose-page-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  /** Build an `UploadObservable`-shaped mock that emits started → finished. */
9
10
  function mockUpload(resourceId: string) {
@@ -185,3 +186,15 @@ describe('createComposePageStateUnit', () => {
185
186
  stateUnit.dispose();
186
187
  });
187
188
  });
189
+
190
+ describe('ComposePageStateUnit — StateUnit axioms', () => {
191
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
192
+ assertStateUnitAxioms({
193
+ setup: () => {
194
+ const browse = disposeProbe();
195
+ return { unit: createComposePageStateUnit(mockClient(), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
196
+ },
197
+ surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.referenceData$, u.gatheredContext$, u.uploadProgress$],
198
+ });
199
+ });
200
+ });
@@ -2,7 +2,7 @@ import { BehaviorSubject, type Observable, map } from 'rxjs';
2
2
  import type { GatheredContext, AnnotationId, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
3
3
  import { resourceId as makeResourceId, annotationId as makeAnnotationId } from '@semiont/core';
4
4
  import { createDisposer } from '@semiont/sdk';
5
- import type { StateUnit } from '@semiont/sdk';
5
+ import type { StateUnit } from '@semiont/core';
6
6
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
7
7
  import type { SemiontClient } from '@semiont/sdk';
8
8
  import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
@@ -72,7 +72,9 @@ export function createComposePageStateUnit(
72
72
  auth?: AccessToken,
73
73
  ): ComposePageStateUnit {
74
74
  const disposer = createDisposer();
75
- disposer.add(browse);
75
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
76
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
77
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
76
78
 
77
79
  const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
78
80
  const isCloneMode = params.mode === 'clone' && Boolean(params.token);
@@ -199,6 +201,7 @@ export function createComposePageStateUnit(
199
201
  cloneData$.complete();
200
202
  referenceData$.complete();
201
203
  gatheredContext$.complete();
204
+ uploadProgress$.complete();
202
205
  disposer.dispose();
203
206
  },
204
207
  };
@@ -4,6 +4,7 @@ import { filter, skip, take, toArray } from 'rxjs/operators';
4
4
  import type { SemiontClient } from '@semiont/sdk';
5
5
  import type { ShellStateUnit } from '../../../../state/shell-state-unit';
6
6
  import { createDiscoverStateUnit } from '../discover-state-unit';
7
+ import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
7
8
 
8
9
  function mockBrowse(): ShellStateUnit {
9
10
  return { dispose: vi.fn() } as unknown as ShellStateUnit;
@@ -100,15 +101,6 @@ describe('createDiscoverStateUnit', () => {
100
101
  stateUnit.dispose();
101
102
  });
102
103
 
103
- it('disposes browse and search on dispose', () => {
104
- const browse = mockBrowse();
105
- const { client } = mockClient();
106
- const stateUnit = createDiscoverStateUnit(client, browse);
107
- stateUnit.dispose();
108
-
109
- expect(browse.dispose).toHaveBeenCalled();
110
- });
111
-
112
104
  it('initial selectedEntityType$ is empty and recent fetch carries no entityType', async () => {
113
105
  const { client, resourceCalls } = mockClient();
114
106
  const stateUnit = createDiscoverStateUnit(client, mockBrowse());
@@ -267,3 +259,17 @@ describe('createDiscoverStateUnit', () => {
267
259
  stateUnit.dispose();
268
260
  });
269
261
  });
262
+
263
+ describe('DiscoverStateUnit — StateUnit axioms', () => {
264
+ it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
265
+ assertStateUnitAxioms({
266
+ setup: () => {
267
+ const browse = disposeProbe();
268
+ const { client } = mockClient();
269
+ return { unit: createDiscoverStateUnit(client, browse as unknown as ShellStateUnit), passedIn: [browse] };
270
+ },
271
+ surfaces: (u) => [u.selectedEntityType$],
272
+ invocations: (u) => [() => u.setSelectedEntityType('')],
273
+ });
274
+ });
275
+ });
@@ -1,7 +1,8 @@
1
1
  import { BehaviorSubject, Subject, combineLatest, of, type Observable } from 'rxjs';
2
2
  import { debounceTime, distinctUntilChanged, map, startWith, switchMap, shareReplay } from 'rxjs/operators';
3
3
  import type { ResourceDescriptor } from '@semiont/core';
4
- import type { SemiontClient, StateUnit } from '@semiont/sdk';
4
+ import type { SemiontClient } from '@semiont/sdk';
5
+ import type { StateUnit } from '@semiont/core';
5
6
  import { createDisposer } from '@semiont/sdk';
6
7
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
7
8
 
@@ -30,7 +31,9 @@ export function createDiscoverStateUnit(
30
31
  browse: ShellStateUnit,
31
32
  ): DiscoverStateUnit {
32
33
  const disposer = createDisposer();
33
- disposer.add(browse);
34
+ // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
35
+ // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
36
+ // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
34
37
 
35
38
  // Selected entity-type chip on the Discover page. Drives both the
36
39
  // `recent` list and the search results — filtering happens on the
@@ -35,6 +35,8 @@ import { useStateUnit } from '../../../hooks/useStateUnit';
35
35
  import { useShellStateUnit } from '../../../hooks/useShellStateUnit';
36
36
  import { useTranslations } from '../../../contexts/TranslationContext';
37
37
  import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
38
+ import { ResourceGenerateModal } from '../../../components/modals/ResourceGenerateModal';
39
+ import { AnnotateReferencesProgressWidget } from '../../../components/AnnotateReferencesProgressWidget';
38
40
  import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
39
41
 
40
42
  type SemiontResource = ResourceDescriptor;
@@ -128,6 +130,7 @@ export function ResourceViewerPage({
128
130
  }: ResourceViewerPageProps) {
129
131
  // Translations
130
132
  const tw = useTranslations('ReferenceWizard');
133
+ const tg = useTranslations('ResourceGenerate');
131
134
 
132
135
  const browser = useSemiont();
133
136
  const session = useObservable(browser.activeSession$);
@@ -187,6 +190,7 @@ export function ResourceViewerPage({
187
190
  const wizardResourceId = wizardState?.resourceId ?? null;
188
191
  const wizardDefaultTitle = wizardState?.defaultTitle ?? '';
189
192
  const wizardEntityTypes = wizardState?.entityTypes ?? [];
193
+ const [generateOpen, setGenerateOpen] = useState(false);
190
194
 
191
195
  const handleWizardClose = useCallback(() => {
192
196
  stateUnit.closeWizard();
@@ -209,6 +213,23 @@ export function ResourceViewerPage({
209
213
  });
210
214
  }, [stateUnit, clearNewAnnotationId, resource]);
211
215
 
216
+ // Resource-generate flow (GENERATE-FROM-BUTTON): drive the SAME yield progress$
217
+ // the annotation path uses so the full AnnotateReferencesProgressWidget shows —
218
+ // NOT a toast. `generateFromResource` is Phase 6 (the @semiont/sdk session);
219
+ // this is declared RED until that method lands. Do not re-impl it here.
220
+ const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
221
+ stateUnit.yield.generateFromResource({
222
+ title: config.title,
223
+ storageUri: config.storagePath,
224
+ ...(config.prompt ? { prompt: config.prompt } : {}),
225
+ language: config.language,
226
+ sourceLanguage: getLanguage(resource),
227
+ temperature: config.temperature,
228
+ maxTokens: config.maxTokens,
229
+ context: config.context,
230
+ });
231
+ }, [stateUnit, resource]);
232
+
212
233
  const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
213
234
  if (!semiont) return;
214
235
  try {
@@ -452,6 +473,21 @@ export function ResourceViewerPage({
452
473
  </h2>
453
474
  </div>
454
475
  </div>
476
+ {/* Resource-generation progress (GENERATE-FROM-BUTTON P7) — no annotationId ⇒ a resource-gen job */}
477
+ {generationProgress && !generationProgress.annotationId && (
478
+ <AnnotateReferencesProgressWidget
479
+ progress={generationProgress}
480
+ annotationType="generation"
481
+ cancelJobType="generation"
482
+ translations={{
483
+ title: tg('progressTitle'),
484
+ cancel: tg('progressCancel'),
485
+ inProgress: tg('progressInProgress'),
486
+ complete: tg('progressComplete'),
487
+ failed: tg('progressFailed'),
488
+ }}
489
+ />
490
+ )}
455
491
  {/* Scrollable body wrapper - contains document content, header is sibling above */}
456
492
  <div className="semiont-document-viewer__scrollable-body" lang={getLanguage(resource) || undefined}>
457
493
  <ErrorBoundary
@@ -565,6 +601,7 @@ export function ResourceViewerPage({
565
601
  wasAttributedTo={resource.wasAttributedTo}
566
602
  wasDerivedFrom={resource.wasDerivedFrom}
567
603
  generator={resource.generator as components['schemas']['Agent'] | components['schemas']['Agent'][] | undefined}
604
+ onGenerate={() => setGenerateOpen(true)}
568
605
  />
569
606
  )}
570
607
 
@@ -645,6 +682,48 @@ export function ResourceViewerPage({
645
682
  semanticScoringHelp: tw('semanticScoringHelp'),
646
683
  }}
647
684
  />
685
+
686
+ {/* Resource-generate flow (GENERATE-FROM-BUTTON) */}
687
+ <ResourceGenerateModal
688
+ isOpen={generateOpen}
689
+ onClose={() => setGenerateOpen(false)}
690
+ resourceId={rUri}
691
+ defaultTitle=""
692
+ locale={locale}
693
+ onGenerateSubmit={handleResourceGenerateSubmit}
694
+ translations={{
695
+ gatherTitle: tg('gatherTitle'),
696
+ reviewTitle: tg('reviewTitle'),
697
+ configureTitle: tg('configureTitle'),
698
+ next: tg('next'),
699
+ back: tg('back'),
700
+ cancel: tg('cancel'),
701
+ gatherIntro: tg('gatherIntro'),
702
+ includeContent: tg('includeContent'),
703
+ includeSummary: tg('includeSummary'),
704
+ gatherDepth: tg('gatherDepth'),
705
+ gatherMaxResources: tg('gatherMaxResources'),
706
+ gatherButton: tg('gatherButton'),
707
+ excludeLabel: tg('excludeLabel'),
708
+ loadingContext: tg('loadingContext'),
709
+ failedContext: tg('failedContext'),
710
+ sourceContextLabel: tg('sourceContextLabel'),
711
+ connectionsLabel: tg('connectionsLabel'),
712
+ citedByLabel: tg('citedByLabel'),
713
+ resourceTitle: tg('resourceTitle'),
714
+ resourceTitlePlaceholder: tg('resourceTitlePlaceholder'),
715
+ additionalInstructions: tg('additionalInstructions'),
716
+ additionalInstructionsPlaceholder: tg('additionalInstructionsPlaceholder'),
717
+ language: tg('language'),
718
+ languageHelp: tg('languageHelp'),
719
+ creativity: tg('creativity'),
720
+ creativityFocused: tg('creativityFocused'),
721
+ creativityCreative: tg('creativityCreative'),
722
+ maxLength: tg('maxLength'),
723
+ maxLengthHelp: tg('maxLengthHelp'),
724
+ generate: tg('generate'),
725
+ }}
726
+ />
648
727
  </div>
649
728
  );
650
729
  }
@@ -4,6 +4,7 @@ import { filter } from 'rxjs/operators';
4
4
  import { resourceId as makeResourceId } from '@semiont/core';
5
5
  import type { SemiontClient } from '@semiont/sdk';
6
6
  import { createResourceLoaderStateUnit } from '../resource-loader-state-unit';
7
+ import { assertStateUnitAxioms } from '@semiont/core/testing';
7
8
 
8
9
  const RID = makeResourceId('res-1');
9
10
 
@@ -44,3 +45,13 @@ describe('createResourceLoaderStateUnit', () => {
44
45
  stateUnit.dispose();
45
46
  });
46
47
  });
48
+
49
+ describe('ResourceLoaderStateUnit — StateUnit axioms', () => {
50
+ it('satisfies the StateUnit axioms', () => {
51
+ // No owned surfaces: resource$/isLoading$ are derived from client.browse; dispose is a no-op.
52
+ assertStateUnitAxioms({
53
+ setup: () => createResourceLoaderStateUnit(mockClient(), RID),
54
+ invocations: (u) => [() => u.invalidate()],
55
+ });
56
+ });
57
+ });