@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
@@ -7,76 +7,56 @@ import type { components } from '@semiont/core';
7
7
 
8
8
  type JobProgress = components['schemas']['JobProgress'];
9
9
 
10
+ // The widget no longer reads translations internally — the caller supplies the
11
+ // copy + cancel job type. These are the annotation-flow values for these tests.
12
+ const tr = {
13
+ title: 'Annotating',
14
+ cancel: 'Cancel',
15
+ inProgress: 'In progress',
16
+ complete: 'Complete',
17
+ failed: 'Failed',
18
+ found: (count: number) => `Found ${count}`,
19
+ current: (entityType: string) => `Current ${entityType}`,
20
+ };
21
+
22
+ function renderWidget(progress: JobProgress | null, opts?: { returnEventBus?: boolean }) {
23
+ return renderWithProviders(
24
+ <AnnotateReferencesProgressWidget progress={progress} cancelJobType="annotation" translations={tr} />,
25
+ opts,
26
+ );
27
+ }
28
+
10
29
  describe('AnnotateReferencesProgressWidget', () => {
11
30
  it('returns null when progress is null', () => {
12
- const { container } = renderWithProviders(
13
- <AnnotateReferencesProgressWidget progress={null} />
14
- );
15
-
31
+ const { container } = renderWidget(null);
16
32
  expect(container.firstChild).toBeNull();
17
33
  });
18
34
 
19
35
  it('renders progress with stage message', () => {
20
- const progress: JobProgress = {
21
- stage: 'in-progress',
22
- percentage: 50,
23
- message: 'Processing entities...',
24
- };
25
-
26
- renderWithProviders(
27
- <AnnotateReferencesProgressWidget progress={progress} />
28
- );
29
-
36
+ const progress: JobProgress = { stage: 'in-progress', percentage: 50, message: 'Processing entities...' };
37
+ renderWidget(progress);
30
38
  expect(screen.getByText('Processing entities...')).toBeInTheDocument();
31
39
  });
32
40
 
33
41
  it('shows cancel button when not complete', () => {
34
- const progress: JobProgress = {
35
- stage: 'in-progress',
36
- percentage: 30,
37
- message: 'Working...',
38
- };
39
-
40
- renderWithProviders(
41
- <AnnotateReferencesProgressWidget progress={progress} />
42
- );
43
-
44
- const cancelButton = screen.getByTitle('ReferencesPanel.cancelAnnotation');
45
- expect(cancelButton).toBeInTheDocument();
42
+ const progress: JobProgress = { stage: 'in-progress', percentage: 30, message: 'Working...' };
43
+ renderWidget(progress);
44
+ expect(screen.getByTitle('Cancel')).toBeInTheDocument();
46
45
  });
47
46
 
48
47
  it('hides cancel button when complete', () => {
49
- const progress: JobProgress = {
50
- stage: 'complete',
51
- percentage: 100,
52
- message: '',
53
- };
54
-
55
- renderWithProviders(
56
- <AnnotateReferencesProgressWidget progress={progress} />
57
- );
58
-
59
- expect(screen.queryByTitle('ReferencesPanel.cancelAnnotation')).not.toBeInTheDocument();
48
+ const progress: JobProgress = { stage: 'complete', percentage: 100, message: '' };
49
+ renderWidget(progress);
50
+ expect(screen.queryByTitle('Cancel')).not.toBeInTheDocument();
60
51
  });
61
52
 
62
53
  it('emits job:cancel-requested on cancel click', () => {
63
54
  const handler = vi.fn();
64
- const progress: JobProgress = {
65
- stage: 'in-progress',
66
- percentage: 40,
67
- message: 'Working...',
68
- };
69
-
70
- const { eventBus } = renderWithProviders(
71
- <AnnotateReferencesProgressWidget progress={progress} />,
72
- { returnEventBus: true }
73
- );
55
+ const progress: JobProgress = { stage: 'in-progress', percentage: 40, message: 'Working...' };
56
+ const { eventBus } = renderWidget(progress, { returnEventBus: true });
74
57
 
75
58
  const subscription = eventBus!.get('job:cancel-requested').subscribe(handler);
76
-
77
- const cancelButton = screen.getByTitle('ReferencesPanel.cancelAnnotation');
78
- fireEvent.click(cancelButton);
79
-
59
+ fireEvent.click(screen.getByTitle('Cancel'));
80
60
  expect(handler).toHaveBeenCalledWith({ jobType: 'annotation' });
81
61
 
82
62
  subscription.unsubscribe();
@@ -92,60 +72,30 @@ describe('AnnotateReferencesProgressWidget', () => {
92
72
  { entityType: 'Organization', foundCount: 3 },
93
73
  ],
94
74
  };
95
-
96
- renderWithProviders(
97
- <AnnotateReferencesProgressWidget progress={progress} />
98
- );
99
-
75
+ renderWidget(progress);
100
76
  expect(screen.getByText('Person:')).toBeInTheDocument();
101
77
  expect(screen.getByText('Organization:')).toBeInTheDocument();
102
- // Translation mock returns "ReferencesPanel.found" for each entity type
103
- const foundLabels = screen.getAllByText('ReferencesPanel.found');
104
- expect(foundLabels).toHaveLength(2);
78
+ expect(screen.getByText('Found 5')).toBeInTheDocument();
79
+ expect(screen.getByText('Found 3')).toBeInTheDocument();
105
80
  });
106
81
 
107
82
  it('shows complete icon for complete stage', () => {
108
- const progress: JobProgress = {
109
- stage: 'complete',
110
- percentage: 100,
111
- message: '',
112
- };
113
-
114
- const { container } = renderWithProviders(
115
- <AnnotateReferencesProgressWidget progress={progress} />
116
- );
117
-
83
+ const progress: JobProgress = { stage: 'complete', percentage: 100, message: '' };
84
+ const { container } = renderWidget(progress);
118
85
  expect(container.querySelector('[data-status="complete"]')).toBeInTheDocument();
119
- expect(screen.getByText('ReferencesPanel.complete')).toBeInTheDocument();
86
+ expect(screen.getByText('Complete')).toBeInTheDocument();
120
87
  });
121
88
 
122
89
  it('shows error message for error stage', () => {
123
- const progress: JobProgress = {
124
- stage: 'error',
125
- percentage: 0,
126
- message: 'Something went wrong',
127
- };
128
-
129
- const { container } = renderWithProviders(
130
- <AnnotateReferencesProgressWidget progress={progress} />
131
- );
132
-
90
+ const progress: JobProgress = { stage: 'error', percentage: 0, message: 'Something went wrong' };
91
+ const { container } = renderWidget(progress);
133
92
  expect(container.querySelector('[data-status="error"]')).toBeInTheDocument();
134
93
  expect(screen.getByText('Something went wrong')).toBeInTheDocument();
135
94
  });
136
95
 
137
96
  it('shows current entity type processing details', () => {
138
- const progress: JobProgress = {
139
- stage: 'in-progress',
140
- percentage: 50,
141
- message: '',
142
- currentEntityType: 'Location',
143
- };
144
-
145
- renderWithProviders(
146
- <AnnotateReferencesProgressWidget progress={progress} />
147
- );
148
-
97
+ const progress: JobProgress = { stage: 'in-progress', percentage: 50, message: '', currentEntityType: 'Location' };
98
+ renderWidget(progress);
149
99
  expect(screen.getByText(/Processing: Location/)).toBeInTheDocument();
150
100
  });
151
101
  });
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+
5
+ export interface ResourceGatherConfig {
6
+ depth: number;
7
+ maxResources: number;
8
+ includeContent: boolean;
9
+ includeSummary: boolean;
10
+ }
11
+
12
+ export interface ConfigureGatherStepProps {
13
+ defaults?: Partial<ResourceGatherConfig>;
14
+ onGather: (config: ResourceGatherConfig) => void;
15
+ onCancel: () => void;
16
+ translations: {
17
+ intro: string;
18
+ includeContent: string;
19
+ includeSummary: string;
20
+ depth: string;
21
+ maxResources: string;
22
+ cancel: string;
23
+ gather: string;
24
+ };
25
+ /** Slot for the exclusion multi-select (GENERATE-FROM-BUTTON Phase 4). */
26
+ children?: React.ReactNode;
27
+ }
28
+
29
+ /**
30
+ * First step of the resource-generate flow: pick the gather options before
31
+ * `gather.resource` runs. The `children` slot hosts the entity-type exclusion
32
+ * multi-select (Phase 4).
33
+ */
34
+ export function ConfigureGatherStep({ defaults, onGather, onCancel, translations: t, children }: ConfigureGatherStepProps) {
35
+ const [includeContent, setIncludeContent] = useState(defaults?.includeContent ?? true);
36
+ const [includeSummary, setIncludeSummary] = useState(defaults?.includeSummary ?? true);
37
+ const [depth, setDepth] = useState(defaults?.depth ?? 2);
38
+ const [maxResources, setMaxResources] = useState(defaults?.maxResources ?? 10);
39
+
40
+ const handleSubmit = (e: React.FormEvent) => {
41
+ e.preventDefault();
42
+ onGather({ includeContent, includeSummary, depth, maxResources });
43
+ };
44
+
45
+ return (
46
+ <form onSubmit={handleSubmit} className="semiont-form semiont-form--scrollable">
47
+ <p className="semiont-form__helper-text">{t.intro}</p>
48
+
49
+ <div className="semiont-form__checkbox-field">
50
+ <label className="semiont-form__checkbox-label">
51
+ <input type="checkbox" checked={includeContent} onChange={(e) => setIncludeContent(e.target.checked)} />
52
+ {t.includeContent}
53
+ </label>
54
+ </div>
55
+ <div className="semiont-form__checkbox-field">
56
+ <label className="semiont-form__checkbox-label">
57
+ <input type="checkbox" checked={includeSummary} onChange={(e) => setIncludeSummary(e.target.checked)} />
58
+ {t.includeSummary}
59
+ </label>
60
+ </div>
61
+
62
+ <div className="semiont-form__inline-row">
63
+ <div className="semiont-form__field semiont-form__field--inline semiont-form__field--narrow">
64
+ <label htmlFor="gather-depth" className="semiont-form__label">{t.depth}</label>
65
+ <input
66
+ id="gather-depth"
67
+ type="number"
68
+ min="1"
69
+ max="5"
70
+ value={depth}
71
+ onChange={(e) => setDepth(parseInt(e.target.value))}
72
+ className="semiont-input"
73
+ />
74
+ </div>
75
+ <div className="semiont-form__field semiont-form__field--inline semiont-form__field--narrow">
76
+ <label htmlFor="gather-max" className="semiont-form__label">{t.maxResources}</label>
77
+ <input
78
+ id="gather-max"
79
+ type="number"
80
+ min="1"
81
+ max="50"
82
+ value={maxResources}
83
+ onChange={(e) => setMaxResources(parseInt(e.target.value))}
84
+ className="semiont-input"
85
+ />
86
+ </div>
87
+ </div>
88
+
89
+ {/* Exclusion multi-select slot (Phase 4) */}
90
+ {children}
91
+
92
+ <div className="semiont-modal__actions" style={{ paddingTop: '0.5rem' }}>
93
+ <button type="button" onClick={onCancel} className="semiont-button--secondary semiont-button--flex">
94
+ ✕ {t.cancel}
95
+ </button>
96
+ <button type="submit" className="semiont-button--primary semiont-button--flex">
97
+ ✨ {t.gather}
98
+ </button>
99
+ </div>
100
+ </form>
101
+ );
102
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import type { GatheredContext } from '@semiont/core';
3
+ import { deriveViews, type GatheredContext } from '@semiont/core';
4
4
 
5
5
  export interface ContextSummaryTranslations {
6
6
  sourceContextLabel: string;
@@ -16,14 +16,23 @@ export interface ContextSummaryProps {
16
16
  }
17
17
 
18
18
  export function ContextSummary({ context, translations: t }: ContextSummaryProps) {
19
- const graphContext = context.graphContext;
20
- const connections = graphContext?.connections ?? [];
21
- const citedBy = graphContext?.citedBy ?? [];
22
- const citedByCount = graphContext?.citedByCount ?? 0;
19
+ // Graph views are focus-agnostic — only the focal resource id (and an optional
20
+ // focal annotation id) differ by kind.
21
+ const mainResourceId = context.focus.kind === 'annotation'
22
+ ? String(context.focus.sourceResource.id)
23
+ : String(context.focus.resource.id);
24
+ const focalAnnotationId = context.focus.kind === 'annotation'
25
+ ? context.focus.annotation.id
26
+ : undefined;
27
+ const { connections, citedBy, citedByCount } = deriveViews(
28
+ context.graph,
29
+ mainResourceId,
30
+ focalAnnotationId,
31
+ );
23
32
 
24
33
  return (
25
34
  <>
26
- {graphContext && (connections.length > 0 || citedByCount > 0) && (
35
+ {(connections.length > 0 || citedByCount > 0) && (
27
36
  <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
28
37
 
29
38
  {connections.length > 0 && (
@@ -9,11 +9,12 @@ export interface GatherContextStepProps {
9
9
  context: GatheredContext | null;
10
10
  contextLoading: boolean;
11
11
  contextError: Error | null;
12
- userHint: string;
13
- onUserHintChange: (value: string) => void;
14
- onBind: () => void;
15
- onGenerate: () => void;
16
- onCompose: () => void;
12
+ /** Annotation-wizard controls. Omit for a display-only (e.g. resource-focus) render. */
13
+ userHint?: string;
14
+ onUserHintChange?: (value: string) => void;
15
+ onBind?: () => void;
16
+ onGenerate?: () => void;
17
+ onCompose?: () => void;
17
18
  translations: {
18
19
  title: string;
19
20
  loadingContext: string;
@@ -29,7 +30,7 @@ export function GatherContextStep({
29
30
  context,
30
31
  contextLoading,
31
32
  contextError,
32
- userHint,
33
+ userHint = '',
33
34
  onUserHintChange,
34
35
  onBind,
35
36
  onGenerate,
@@ -38,7 +39,8 @@ export function GatherContextStep({
38
39
  }: GatherContextStepProps) {
39
40
  const [sourceExpanded, setSourceExpanded] = useState(false);
40
41
  const contextReady = !contextLoading && !contextError && !!context;
41
- const sourceContext = context?.sourceContext;
42
+ const focus = context?.focus.kind === 'annotation' ? context.focus : null;
43
+ const resourceFocus = context?.focus.kind === 'resource' ? context.focus : null;
42
44
  const highlightRef = useRef<HTMLSpanElement>(null);
43
45
 
44
46
  // Scroll the highlighted term into view when context loads
@@ -69,16 +71,16 @@ export function GatherContextStep({
69
71
 
70
72
  {context && (
71
73
  <>
72
- {/* Full-width source context strip */}
73
- {sourceContext && (
74
+ {/* Full-width source context strip — annotation focus */}
75
+ {focus?.selected && (
74
76
  <div className="semiont-gather__source-strip">
75
77
  <label className="semiont-form__label" style={{ marginBottom: '0.375rem' }}>
76
- {t.sourceContextLabel}{context.sourceResource?.name ? ` "${context.sourceResource.name}"` : ''}
78
+ {t.sourceContextLabel}{focus.sourceResource.name ? ` "${focus.sourceResource.name}"` : ''}
77
79
  </label>
78
80
  <div className={`semiont-gather__source-box${sourceExpanded ? ' semiont-gather__source-box--expanded' : ''}`}>
79
81
  <div className="semiont-gather__source-context">
80
82
  <div style={{ fontSize: 'var(--semiont-text-sm)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
81
- {sourceContext.before && <span>{sourceContext.before}</span>}
83
+ {focus.selected.before && <span>{focus.selected.before}</span>}
82
84
  <span
83
85
  ref={highlightRef}
84
86
  style={{
@@ -88,19 +90,19 @@ export function GatherContextStep({
88
90
  color: 'var(--semiont-color-primary-900)',
89
91
  }}
90
92
  >
91
- {sourceContext.selected}
93
+ {focus.selected.text}
92
94
  </span>
93
95
  {(context.metadata?.entityTypes ?? []).map(et => (
94
96
  <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400, verticalAlign: 'middle', marginLeft: '0.25rem' }}>
95
97
  {et}
96
98
  </span>
97
99
  ))}
98
- {context.annotation?.motivation && (
100
+ {focus.annotation.motivation && (
99
101
  <span className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400, verticalAlign: 'middle', marginLeft: '0.25rem' }}>
100
- {context.annotation.motivation}
102
+ {focus.annotation.motivation}
101
103
  </span>
102
104
  )}
103
- {sourceContext.after && <span>{sourceContext.after}</span>}
105
+ {focus.selected.after && <span>{focus.selected.after}</span>}
104
106
  </div>
105
107
  </div>
106
108
  <button
@@ -114,60 +116,103 @@ export function GatherContextStep({
114
116
  </div>
115
117
  )}
116
118
 
119
+ {/* Full-width source context strip — resource focus */}
120
+ {resourceFocus && (
121
+ <div className="semiont-gather__source-strip">
122
+ <label className="semiont-form__label" style={{ marginBottom: '0.375rem' }}>
123
+ {t.sourceContextLabel}{resourceFocus.resource.name ? ` "${resourceFocus.resource.name}"` : ''}
124
+ </label>
125
+ {(resourceFocus.summary || resourceFocus.content?.main) && (
126
+ <div className={`semiont-gather__source-box${sourceExpanded ? ' semiont-gather__source-box--expanded' : ''}`}>
127
+ <div className="semiont-gather__source-context">
128
+ <div style={{ fontSize: 'var(--semiont-text-sm)', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
129
+ {resourceFocus.summary ?? resourceFocus.content?.main}
130
+ {(context.metadata?.entityTypes ?? []).map(et => (
131
+ <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400, verticalAlign: 'middle', marginLeft: '0.25rem' }}>
132
+ {et}
133
+ </span>
134
+ ))}
135
+ </div>
136
+ </div>
137
+ <button
138
+ type="button"
139
+ className="semiont-gather__expand-btn"
140
+ onClick={() => setSourceExpanded(v => !v)}
141
+ >
142
+ {sourceExpanded ? '▲ less' : '▼ more'}
143
+ </button>
144
+ </div>
145
+ )}
146
+ {resourceFocus.suggestedReferences && resourceFocus.suggestedReferences.length > 0 && (
147
+ <div style={{ marginTop: '0.5rem' }}>
148
+ {resourceFocus.suggestedReferences.map(ref => (
149
+ <span key={ref} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', marginRight: '0.25rem' }}>
150
+ {ref}
151
+ </span>
152
+ ))}
153
+ </div>
154
+ )}
155
+ </div>
156
+ )}
157
+
117
158
  {/* Two-column body */}
118
159
  <div className="semiont-gather__body">
119
- {/* Left: annotation metadata */}
160
+ {/* Left: context summary (graph views) */}
120
161
  <div className="semiont-gather__left">
121
162
  <ContextSummary context={context} translations={t} />
122
163
  </div>
123
164
 
124
- {/* Right: hint textarea */}
125
- <div className="semiont-gather__right">
126
- <div className="semiont-form__field">
127
- <label className="semiont-form__label">
128
- {t.userHintLabel}
129
- </label>
130
- <textarea
131
- value={userHint}
132
- onChange={(e) => onUserHintChange(e.target.value)}
133
- placeholder={t.userHintPlaceholder}
134
- className="semiont-search-modal__search-input semiont-gather__hint-textarea"
135
- style={{ resize: 'vertical', fontFamily: 'inherit' }}
136
- />
165
+ {/* Right: hint textarea (annotation focus only) */}
166
+ {focus && (
167
+ <div className="semiont-gather__right">
168
+ <div className="semiont-form__field">
169
+ <label className="semiont-form__label">
170
+ {t.userHintLabel}
171
+ </label>
172
+ <textarea
173
+ value={userHint}
174
+ onChange={(e) => onUserHintChange?.(e.target.value)}
175
+ placeholder={t.userHintPlaceholder}
176
+ className="semiont-search-modal__search-input semiont-gather__hint-textarea"
177
+ style={{ resize: 'vertical', fontFamily: 'inherit' }}
178
+ />
179
+ </div>
137
180
  </div>
138
- </div>
181
+ )}
139
182
  </div>
140
183
 
141
- {/* Full-width footer: resolution strategy */}
142
- <div className="semiont-gather__footer">
143
- <div className="semiont-gather__footer-label">{t.resolutionStrategyLabel}</div>
144
- <div className="semiont-gather__actions">
145
- <button
146
- type="button"
147
- onClick={onBind}
148
- disabled={!contextReady}
149
- className="semiont-button--primary semiont-button--flex"
150
- >
151
- 🔍 {t.search}…
152
- </button>
153
- <button
154
- type="button"
155
- onClick={onGenerate}
156
- disabled={!contextReady}
157
- className="semiont-button--primary semiont-button--flex"
158
- >
159
- ✨ {t.generate}…
160
- </button>
161
- <button
162
- type="button"
163
- onClick={onCompose}
164
- disabled={!contextReady}
165
- className="semiont-button--secondary semiont-button--flex"
166
- >
167
- ✍️ {t.compose}
168
- </button>
184
+ {/* Full-width footer: resolution strategy (annotation focus only) */}
185
+ {focus && (
186
+ <div className="semiont-gather__footer">
187
+ <div className="semiont-gather__footer-label">{t.resolutionStrategyLabel}</div>
188
+ <div className="semiont-gather__actions">
189
+ <button
190
+ type="button"
191
+ onClick={onBind}
192
+ disabled={!contextReady}
193
+ className="semiont-button--primary semiont-button--flex"
194
+ >
195
+ 🔍 {t.search}…
196
+ </button>
197
+ <button
198
+ type="button"
199
+ onClick={onGenerate}
200
+ disabled={!contextReady}
201
+ className="semiont-button--primary semiont-button--flex"
202
+ >
203
+ ✨ {t.generate}…
204
+ </button>
205
+ <button
206
+ type="button"
207
+ onClick={onCompose}
208
+ disabled={!contextReady}
209
+ className="semiont-button--secondary semiont-button--flex"
210
+ >
211
+ ✍️ {t.compose}
212
+ </button>
213
+ </div>
169
214
  </div>
170
- </div>
215
+ )}
171
216
  </>
172
217
  )}
173
218
  </div>