@semiont/react-ui 0.5.27 → 0.5.28

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 (162) hide show
  1. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +31 -4
  2. package/dist/{ar-OU3DQGC6.js → ar-FABPTJVB.js} +39 -23
  3. package/dist/{bn-PHWCR6AZ.js → bn-LKP6BDMH.js} +39 -23
  4. package/dist/{chunk-PCRRBU7H.js → chunk-2GOHTTHW.js} +39 -23
  5. package/dist/{chunk-5VDTWRXJ.js → chunk-2YHEE63S.js} +31 -31
  6. package/dist/{cs-NEXYZ62I.js → cs-YQYNWHRG.js} +39 -23
  7. package/dist/{da-FAT43YCU.js → da-QHZOYKRY.js} +39 -23
  8. package/dist/{de-KRQ3WBCU.js → de-GKA7RD7J.js} +39 -23
  9. package/dist/{el-Q4IU3EKC.js → el-45T257AL.js} +39 -23
  10. package/dist/{en-2KADE7N4.js → en-RVW5EQ66.js} +2 -2
  11. package/dist/{es-2IO3R2MK.js → es-AVER7RU4.js} +39 -23
  12. package/dist/{fa-Q5GOQFXX.js → fa-K3VPK3O2.js} +39 -23
  13. package/dist/{fi-R6NMRWBE.js → fi-5NEKF7EB.js} +39 -23
  14. package/dist/{fr-QY4D5E6J.js → fr-FUMNDLBZ.js} +39 -23
  15. package/dist/{he-X6Y77XHY.js → he-3JGNA3PR.js} +39 -23
  16. package/dist/{hi-XL7IOEQM.js → hi-IJY375DF.js} +39 -23
  17. package/dist/{id-QZ4ZZS67.js → id-LWHKJ7ZD.js} +39 -23
  18. package/dist/index.css +279 -74
  19. package/dist/index.d.ts +170 -145
  20. package/dist/index.js +2743 -2365
  21. package/dist/{it-ME2Q7OB6.js → it-EBVOV4RW.js} +39 -23
  22. package/dist/{ja-HX7QASRI.js → ja-KWRIX2NF.js} +39 -23
  23. package/dist/{ko-EQO5AHAV.js → ko-L5CJMFDS.js} +39 -23
  24. package/dist/{ms-UILFSF3I.js → ms-LSNMJDR2.js} +39 -23
  25. package/dist/{nl-NNI45PMK.js → nl-ZB32MB7U.js} +39 -23
  26. package/dist/{no-MAL5F6P6.js → no-4GTNY7M5.js} +39 -23
  27. package/dist/{pl-4RMY6JAJ.js → pl-LUHBLTHB.js} +39 -23
  28. package/dist/{pt-VQH2X3QQ.js → pt-C4H3UNQ6.js} +39 -23
  29. package/dist/{ro-XXCQO2OG.js → ro-GGITS5V4.js} +39 -23
  30. package/dist/{sv-7Z6EJ6U4.js → sv-T56RTLBP.js} +39 -23
  31. package/dist/test-utils.js +2 -2
  32. package/dist/{th-L3DXXYVQ.js → th-22X2IWCB.js} +39 -23
  33. package/dist/{tr-P3DFVLFJ.js → tr-77TFG62H.js} +39 -23
  34. package/dist/{uk-7C5D3PXS.js → uk-MIIL6NXT.js} +39 -23
  35. package/dist/{vi-ZLTLEO7P.js → vi-7JYOIGNV.js} +39 -23
  36. package/dist/{zh-5FHVWDIN.js → zh-ID6DRYU4.js} +39 -23
  37. package/package.json +9 -8
  38. package/src/components/AssistProgress.tsx +36 -2
  39. package/src/components/CodeMirrorRenderer.tsx +8 -8
  40. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  41. package/src/components/modals/ComposeStep.tsx +218 -0
  42. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  43. package/src/components/modals/ConfigureGenerationStep.tsx +35 -19
  44. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  45. package/src/components/modals/ContextSummary.tsx +185 -72
  46. package/src/components/modals/CorpusPane.tsx +75 -0
  47. package/src/components/modals/DiscardPrompt.tsx +38 -0
  48. package/src/components/modals/GatherContextStep.tsx +237 -107
  49. package/src/components/modals/ReferenceWizardModal.tsx +238 -50
  50. package/src/components/modals/ResourceGenerateModal.tsx +216 -108
  51. package/src/components/modals/SearchModal.css +337 -101
  52. package/src/components/modals/SearchResultsStep.tsx +61 -68
  53. package/src/components/modals/WizardFooter.tsx +7 -1
  54. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  55. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  56. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +35 -0
  57. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  58. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  59. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +417 -38
  60. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +276 -85
  61. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  62. package/src/components/modals/__tests__/WizardFooter.test.tsx +113 -4
  63. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  64. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  65. package/src/components/resource/AnnotateView.tsx +3 -3
  66. package/src/components/resource/BrowseView.tsx +5 -5
  67. package/src/components/resource/ResourceViewer.tsx +4 -4
  68. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  69. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  70. package/src/components/resource/__tests__/BrowseView.test.tsx +9 -9
  71. package/src/components/resource/annotate-renderers.tsx +3 -3
  72. package/src/components/resource/panels/AssistSection.css +35 -0
  73. package/src/components/resource/panels/ReferenceEntry.tsx +9 -2
  74. package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
  75. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  76. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  77. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  78. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +33 -1
  79. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +148 -6
  80. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  81. package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
  82. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  83. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  84. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
  85. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +133 -59
  86. package/src/styles/core/forms.css +35 -8
  87. package/src/styles/features/resource-viewer.css +0 -22
  88. package/src/styles/features/resource.css +0 -4
  89. package/translations/ar.json +38 -22
  90. package/translations/bn.json +38 -22
  91. package/translations/cs.json +38 -22
  92. package/translations/da.json +38 -22
  93. package/translations/de.json +38 -22
  94. package/translations/el.json +38 -22
  95. package/translations/en.json +38 -22
  96. package/translations/es.json +38 -22
  97. package/translations/fa.json +38 -22
  98. package/translations/fi.json +38 -22
  99. package/translations/fr.json +38 -22
  100. package/translations/he.json +38 -22
  101. package/translations/hi.json +38 -22
  102. package/translations/id.json +38 -22
  103. package/translations/it.json +38 -22
  104. package/translations/ja.json +38 -22
  105. package/translations/ko.json +38 -22
  106. package/translations/ms.json +38 -22
  107. package/translations/nl.json +38 -22
  108. package/translations/no.json +38 -22
  109. package/translations/pl.json +38 -22
  110. package/translations/pt.json +38 -22
  111. package/translations/ro.json +38 -22
  112. package/translations/sv.json +38 -22
  113. package/translations/th.json +38 -22
  114. package/translations/tr.json +38 -22
  115. package/translations/uk.json +38 -22
  116. package/translations/vi.json +38 -22
  117. package/translations/zh.json +38 -22
  118. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  119. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  120. package/dist/ar-OU3DQGC6.js.map +0 -1
  121. package/dist/bn-PHWCR6AZ.js.map +0 -1
  122. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  123. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  124. package/dist/chunk-O23TLLXW.js.map +0 -1
  125. package/dist/chunk-PCRRBU7H.js.map +0 -1
  126. package/dist/chunk-QT7LYM72.js.map +0 -1
  127. package/dist/chunk-VXASXU4K.js.map +0 -1
  128. package/dist/cs-NEXYZ62I.js.map +0 -1
  129. package/dist/da-FAT43YCU.js.map +0 -1
  130. package/dist/de-KRQ3WBCU.js.map +0 -1
  131. package/dist/el-Q4IU3EKC.js.map +0 -1
  132. package/dist/en-2KADE7N4.js.map +0 -1
  133. package/dist/es-2IO3R2MK.js.map +0 -1
  134. package/dist/fa-Q5GOQFXX.js.map +0 -1
  135. package/dist/fi-R6NMRWBE.js.map +0 -1
  136. package/dist/fr-QY4D5E6J.js.map +0 -1
  137. package/dist/he-X6Y77XHY.js.map +0 -1
  138. package/dist/hi-XL7IOEQM.js.map +0 -1
  139. package/dist/id-QZ4ZZS67.js.map +0 -1
  140. package/dist/index.css.map +0 -1
  141. package/dist/index.js.map +0 -1
  142. package/dist/integrations/css-modules-helper.js.map +0 -1
  143. package/dist/integrations/styled-components-theme.js.map +0 -1
  144. package/dist/it-ME2Q7OB6.js.map +0 -1
  145. package/dist/ja-HX7QASRI.js.map +0 -1
  146. package/dist/ko-EQO5AHAV.js.map +0 -1
  147. package/dist/ms-UILFSF3I.js.map +0 -1
  148. package/dist/nl-NNI45PMK.js.map +0 -1
  149. package/dist/no-MAL5F6P6.js.map +0 -1
  150. package/dist/pl-4RMY6JAJ.js.map +0 -1
  151. package/dist/pt-VQH2X3QQ.js.map +0 -1
  152. package/dist/ro-XXCQO2OG.js.map +0 -1
  153. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  154. package/dist/test-utils.css.map +0 -1
  155. package/dist/test-utils.js.map +0 -1
  156. package/dist/th-L3DXXYVQ.js.map +0 -1
  157. package/dist/tr-P3DFVLFJ.js.map +0 -1
  158. package/dist/uk-7C5D3PXS.js.map +0 -1
  159. package/dist/vi-ZLTLEO7P.js.map +0 -1
  160. package/dist/zh-5FHVWDIN.js.map +0 -1
  161. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  162. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
@@ -16,6 +16,8 @@ import { WizardFooter } from '../WizardFooter';
16
16
  import { ConfigureSearchStep } from '../ConfigureSearchStep';
17
17
  import { ConfigureGenerationStep } from '../ConfigureGenerationStep';
18
18
  import { SearchResultsStep } from '../SearchResultsStep';
19
+ import { ConfigureGatherStep } from '../ConfigureGatherStep';
20
+ import { GatherContextStep } from '../GatherContextStep';
19
21
  import type { GatheredContext } from '@semiont/core';
20
22
 
21
23
  /** Anything that reads as "get me out of here" belongs in the corner, not the footer. */
@@ -78,6 +80,25 @@ describe('WizardFooter', () => {
78
80
  await userEvent.click(screen.getByText(/Back/));
79
81
  expect(onBack).not.toHaveBeenCalled();
80
82
  });
83
+
84
+ it('a disabled primary is not clickable, but Back stays live', async () => {
85
+ // `disabled` is the action's PRECONDITION loading (review step waiting on
86
+ // gather) — distinct from `pending`, which is the action itself running and
87
+ // rightly freezes retreat too. While a precondition loads, retreat must work.
88
+ const onBack = vi.fn();
89
+ const onAdvance = vi.fn();
90
+ render(
91
+ <WizardFooter
92
+ backLabel="Back"
93
+ onBack={onBack}
94
+ primary={{ label: 'Next', type: 'button', onClick: onAdvance, disabled: true }}
95
+ />,
96
+ );
97
+ await userEvent.click(screen.getByText(/Next/));
98
+ expect(onAdvance).not.toHaveBeenCalled();
99
+ await userEvent.click(screen.getByText(/Back/));
100
+ expect(onBack).toHaveBeenCalledTimes(1);
101
+ });
81
102
  });
82
103
 
83
104
  // ─────────────────────────────────────────────────────────────────────
@@ -87,10 +108,11 @@ describe('WizardFooter', () => {
87
108
  const SEARCH_T = {
88
109
  maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
89
110
  semanticScoringHelp: 'help', back: 'Back', search: 'Search', searching: 'Searching…',
111
+ searchFailed: 'Search failed',
90
112
  };
91
113
 
92
114
  const GEN_T = {
93
- resourceTitle: 'Title', resourceTitlePlaceholder: '', additionalInstructions: 'Instructions',
115
+ resourceTitle: 'Title', resourceTitlePlaceholder: '', saveLocation: 'Save location', additionalInstructions: 'Instructions',
94
116
  additionalInstructionsPlaceholder: '', language: 'Language', languageHelp: '',
95
117
  creativity: 'Creativity', creativityFocused: 'Focused', creativityCreative: 'Creative',
96
118
  maxLength: 'Max Length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{max}} by {{model}}.',
@@ -99,13 +121,18 @@ const GEN_T = {
99
121
 
100
122
  const CONTEXT_T = {
101
123
  sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
102
- userHintLabel: 'Hint', userHintPlaceholder: '',
124
+ graphPaneTitle: 'In the graph', graphEmpty: 'No links yet.', resourceLinkLabel: 'Resource link',
125
+ };
126
+
127
+ const GATHER_T = {
128
+ intro: 'Choose what to include.', includeContent: 'Content', includeSummary: 'Summary',
129
+ depth: 'Depth', maxResources: 'Max', gather: 'Gather',
103
130
  };
104
131
 
105
132
  const RESULTS_T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back', ...CONTEXT_T };
106
133
 
107
134
  const CONTEXT = {
108
- focus: { kind: 'annotation', annotation: { id: 'a1' }, sourceResource: { id: 'r1', name: 'Src' } },
135
+ focus: { kind: 'annotation', annotation: { id: 'a1' }, sourceResource: { '@id': 'r1', name: 'Src' } },
109
136
  // `graph` is not optional in practice: ContextSummary hands it straight to
110
137
  // `deriveViews`, which dereferences `.nodes`.
111
138
  graph: { nodes: [], edges: [] },
@@ -130,7 +157,6 @@ describe('every step footer follows the grammar (A1, A2)', () => {
130
157
  it('SearchResultsStep — footer is retreat only; Link lives on the rows', () => {
131
158
  const { container } = render(
132
159
  <SearchResultsStep
133
- context={CONTEXT}
134
160
  results={[{ id: 'r1', name: 'Black Sea', score: 54.22 } as never]}
135
161
  onBack={vi.fn()}
136
162
  onLink={vi.fn()}
@@ -161,6 +187,89 @@ describe('every step footer follows the grammar (A1, A2)', () => {
161
187
  expect(labels).toHaveLength(2);
162
188
  expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
163
189
  });
190
+
191
+ it('ConfigureGatherStep — first step of the resource-generate flow: advance only (GFR A5)', () => {
192
+ const { container } = render(
193
+ <ConfigureGatherStep onGather={vi.fn()} translations={GATHER_T} />,
194
+ );
195
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
196
+ expect(labels).toHaveLength(1); // nowhere to go back to, and dismissal is the corner's job
197
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
198
+ });
199
+ });
200
+
201
+ // ─────────────────────────────────────────────────────────────────────
202
+ // The THIRD footer species: the resolution CHOOSER. The one deliberate
203
+ // exception to the WizardFooter grammar — a three-way fork the
204
+ // one-retreat-plus-one-advance component cannot express. Its grammar is
205
+ // its own, and this pin is what keeps it a species instead of drift.
206
+ // ─────────────────────────────────────────────────────────────────────
207
+ describe('the resolution chooser (GatherContextStep) — the named exception', () => {
208
+ const ANNOTATE = {
209
+ userHint: '',
210
+ onUserHintChange: vi.fn(),
211
+ onBind: vi.fn(),
212
+ onGenerate: vi.fn(),
213
+ onCompose: vi.fn(),
214
+ translations: {
215
+ search: 'Search', generate: 'Generate', compose: 'Compose',
216
+ resolutionStrategyLabel: 'Strategy', userHintLabel: 'Hint',
217
+ userHintEffect: 'steers Search and Generate', userHintPlaceholder: '',
218
+ },
219
+ };
220
+ const DISPLAY_T = {
221
+ loadingContext: 'Loading…', failedContext: 'Failed', ...CONTEXT_T,
222
+ corpusPaneTitle: 'In the corpus', corpusEmpty: 'Nothing similar.',
223
+ excludedReceipt: '{{types}} excluded', machineRead: 'OCR', score: 'Score',
224
+ };
225
+
226
+ const chooserButtons = (container: HTMLElement) =>
227
+ Array.from(container.querySelectorAll('.semiont-gather__actions button'));
228
+
229
+ it('three equal-width peers, one of them quiet, none of them dismissal', () => {
230
+ const { container } = render(
231
+ <GatherContextStep
232
+ context={CONTEXT} contextLoading={false} contextError={null}
233
+ annotate={ANNOTATE} translations={DISPLAY_T}
234
+ />,
235
+ );
236
+ const buttons = chooserButtons(container);
237
+ expect(buttons).toHaveLength(3);
238
+ // Equal width is HONEST here — these are actual peers. This is the same
239
+ // `--flex` the shared footer bans for making retreat read as advance's peer.
240
+ for (const b of buttons) expect(b.className).toContain('semiont-button--flex');
241
+ // The AI paths are primary; the manual path's demotion to secondary is the
242
+ // recorded convention, not drift.
243
+ const secondary = buttons.filter((b) => b.className.includes('semiont-button--secondary'));
244
+ expect(secondary).toHaveLength(1);
245
+ expect(secondary[0]!.textContent).toContain('Compose');
246
+ // Dismissal stays in the corner, as on every footer.
247
+ expect(buttons.map((b) => b.textContent ?? '').filter((l) => DISMISSAL.test(l))).toEqual([]);
248
+ });
249
+
250
+ it('ellipses mark step-vs-act: the step-leading choices carry one, the acting one does not', () => {
251
+ // The "…" is component-owned markup, not translated copy: Search and
252
+ // Generate lead to another step; Compose acts immediately (navigates away).
253
+ const { container } = render(
254
+ <GatherContextStep
255
+ context={CONTEXT} contextLoading={false} contextError={null}
256
+ annotate={ANNOTATE} translations={DISPLAY_T}
257
+ />,
258
+ );
259
+ const labels = chooserButtons(container).map((b) => (b.textContent ?? '').trim());
260
+ expect(labels.filter((l) => l.endsWith('…'))).toHaveLength(2);
261
+ expect(labels.find((l) => l.includes('Compose'))!.endsWith('…')).toBe(false);
262
+ });
263
+
264
+ it('every choice waits for context', () => {
265
+ const { container } = render(
266
+ <GatherContextStep
267
+ context={CONTEXT} contextLoading contextError={null}
268
+ annotate={ANNOTATE} translations={DISPLAY_T}
269
+ />,
270
+ );
271
+ for (const b of chooserButtons(container)) expect(b).toBeDisabled();
272
+ });
164
273
  });
165
274
 
166
275
  // ─────────────────────────────────────────────────────────────────────
@@ -685,7 +685,7 @@ export function PdfAnnotationCanvas({
685
685
 
686
686
  const uri = resourceUri;
687
687
  const outcome = session.client.browse.resourceAnchoredText(toResourceId(uri)).then(
688
- (served) => (served && !('declined' in served) ? served : null),
688
+ (served) => (served && served.kind === 'extracted' ? served : null),
689
689
  () => {
690
690
  if (resourceAnchoredRef.current?.uri === uri) resourceAnchoredRef.current = null;
691
691
  return null;
@@ -861,6 +861,39 @@ export function PdfAnnotationCanvas({
861
861
 
862
862
  const stripRef = useRef<HTMLDivElement>(null);
863
863
 
864
+ // The strip's CSS scrollport ceiling is 100vh — right when the nearest
865
+ // scroller is the window, a lie inside an inner-scrolled panel: the sticky
866
+ // strip then extends below the panel's clip, and `nearest` (below) considers
867
+ // a tick in that hidden band already in view, so the active page vanished
868
+ // whenever it crossed the strip's bottom (never the top, whose edges
869
+ // coincide). Size the scrollport to the scroller that actually clips it;
870
+ // with no such scroller the CSS ceiling stands.
871
+ useEffect(() => {
872
+ const strip = stripRef.current;
873
+ if (pageLayout !== 'scroll' || !strip) return;
874
+ let scroller: HTMLElement | null = strip.parentElement;
875
+ while (scroller) {
876
+ const overflowY = getComputedStyle(scroller).overflowY;
877
+ if (overflowY === 'auto' || overflowY === 'scroll') break;
878
+ scroller = scroller.parentElement;
879
+ }
880
+ if (!scroller) return;
881
+ const clip = scroller;
882
+ const size = () => { strip.style.maxHeight = `${clip.clientHeight}px`; };
883
+ size();
884
+ let observer: ResizeObserver | null = null;
885
+ try {
886
+ observer = new ResizeObserver(size);
887
+ observer.observe(clip);
888
+ } catch {
889
+ window.addEventListener('resize', size);
890
+ }
891
+ return () => {
892
+ if (observer) observer.disconnect();
893
+ else window.removeEventListener('resize', size);
894
+ };
895
+ }, [pageLayout, numPages, pageShape]);
896
+
864
897
  useEffect(() => {
865
898
  // `nearest` so following the reader never yanks the strip around; it
866
899
  // scrolls only when the current rectangle would otherwise leave view.
@@ -508,6 +508,7 @@ describe('PdfAnnotationCanvas', () => {
508
508
  beckon: { hover: vi.fn() },
509
509
  browse: {
510
510
  resourceAnchoredText: vi.fn().mockResolvedValue({
511
+ kind: 'extracted',
511
512
  text: 'Hello world again',
512
513
  items: [
513
514
  { start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 },
@@ -560,6 +561,7 @@ describe('PdfAnnotationCanvas', () => {
560
561
  } as unknown as Awaited<ReturnType<typeof loadPdfDocument>>);
561
562
 
562
563
  const resourceAnchoredText = vi.fn().mockResolvedValue({
564
+ kind: 'extracted',
563
565
  text: 'Hello world again',
564
566
  items: [
565
567
  { start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 },
@@ -727,6 +729,7 @@ describe('PdfAnnotationCanvas', () => {
727
729
  scannedDoc();
728
730
 
729
731
  const resourceAnchoredText = vi.fn().mockResolvedValue({
732
+ kind: 'extracted',
730
733
  text: 'Hello world again',
731
734
  items: [{ start: 0, end: 5, page: 1, x: 72, y: 700, width: 30, height: 12 }],
732
735
  });
@@ -889,6 +892,40 @@ describe('PdfAnnotationCanvas', () => {
889
892
  });
890
893
  });
891
894
 
895
+ test('the strip sizes its scrollport to the panel that clips it — not the window', async () => {
896
+ // The strip is sticky with a CSS ceiling of 100vh — right when the
897
+ // nearest scroller is the window, a lie inside an inner-scrolled panel:
898
+ // there the strip's scrollport extends below the panel's clip, and
899
+ // `scrollIntoView({block:'nearest'})` considers a tick in that hidden
900
+ // band already in view — the active page vanished when it crossed the
901
+ // strip's bottom (never the top, whose edges coincide). Measured live:
902
+ // strip scrollport 126→846 vs panel 126→639. The fix sizes the
903
+ // scrollport to the scroller that actually clips it.
904
+ const io = stubIntersectionObserver();
905
+ scannedDoc();
906
+
907
+ const scroller = document.createElement('div');
908
+ scroller.style.overflowY = 'auto';
909
+ Object.defineProperty(scroller, 'clientHeight', { value: 513, configurable: true });
910
+ document.body.appendChild(scroller);
911
+
912
+ render(
913
+ <PdfAnnotationCanvas resourceUri="res-1"
914
+ pdfUrl={mockPdfUrl}
915
+ drawingMode={null}
916
+ pageLayout="scroll"
917
+ />,
918
+ { container: scroller },
919
+ );
920
+ await waitFor(() => {
921
+ expect(document.querySelectorAll('.semiont-pdf-annotation-canvas__strip-page')).toHaveLength(5);
922
+ });
923
+ io.fire([1]);
924
+
925
+ const strip = document.querySelector('.semiont-pdf-annotation-canvas__strip') as HTMLElement;
926
+ await waitFor(() => expect(strip.style.maxHeight).toBe('513px'));
927
+ });
928
+
892
929
  test('clicking a strip rectangle jumps to that page', async () => {
893
930
  const io = stubIntersectionObserver();
894
931
  scannedDoc();
@@ -1244,7 +1281,7 @@ describe('PdfAnnotationCanvas', () => {
1244
1281
 
1245
1282
  const resourceAnchoredText = vi.fn()
1246
1283
  .mockRejectedValueOnce(new Error('transport down'))
1247
- .mockResolvedValue({ text: 'later', items: [] });
1284
+ .mockResolvedValue({ kind: 'extracted', text: 'later', items: [] });
1248
1285
  const session = {
1249
1286
  client: { beckon: { hover: vi.fn() }, browse: { resourceAnchoredText } },
1250
1287
  } as unknown as import('@semiont/sdk').SemiontSession;
@@ -39,7 +39,7 @@ interface Props {
39
39
  /** Session for the shown resource — its client emits mark:* / mark.request; its bus feeds toolbar + beckon events. */
40
40
  session: SemiontSession | null;
41
41
  /** Recently-created annotation ids to sparkle (host-provided; was ResourceAnnotationsContext). */
42
- newAnnotationIds?: Set<string>;
42
+ sparkleAnnotationIds?: Set<string>;
43
43
  /** The bar's Mode control reports the chosen mode here (the owner applies it). */
44
44
  onModeChange?: (mode: boolean) => void;
45
45
  /** Render the built-in bar (default). false → no bar; selection capture and drawing stay live. */
@@ -69,7 +69,7 @@ export function AnnotateView({
69
69
  hoverDelayMs = 150,
70
70
  annotateMode,
71
71
  session,
72
- newAnnotationIds,
72
+ sparkleAnnotationIds,
73
73
  onModeChange,
74
74
  showToolbar = true,
75
75
  renderers,
@@ -267,7 +267,7 @@ export function AnnotateView({
267
267
  selectedMotivation={selectedMotivation}
268
268
  hoveredAnnotationId={hoveredAnnotationId}
269
269
  hoverDelayMs={hoverDelayMs}
270
- newAnnotationIds={newAnnotationIds}
270
+ sparkleAnnotationIds={sparkleAnnotationIds}
271
271
  scrollToAnnotationId={scrollToAnnotationId}
272
272
  showLineNumbers={showLineNumbers}
273
273
  enableWidgets={enableWidgets}
@@ -35,7 +35,7 @@ interface Props {
35
35
  /** Session for the shown resource — emits browse:click / beckon:hover; its bus feeds beckon events. */
36
36
  session: SemiontSession | null;
37
37
  /** Recently-created annotation ids to sparkle (host-provided; was ResourceAnnotationsContext). */
38
- newAnnotationIds?: Set<string>;
38
+ sparkleAnnotationIds?: Set<string>;
39
39
  /** Override the read-only media renderers (render mode → renderer); merged over the defaults. */
40
40
  renderers?: BrowseMediaRenderers;
41
41
  /** A content link (`<a href>` in the rendered content) was clicked. The viewer preventDefaults and
@@ -87,7 +87,7 @@ export const BrowseView = memo(function BrowseView({
87
87
  annotateMode,
88
88
  hoverDelayMs = 150,
89
89
  session,
90
- newAnnotationIds,
90
+ sparkleAnnotationIds,
91
91
  renderers,
92
92
  onLinkClick,
93
93
  onReferenceHover,
@@ -227,11 +227,11 @@ export const BrowseView = memo(function BrowseView({
227
227
  };
228
228
 
229
229
  // Apply animation classes to new annotations
230
- if (newAnnotationIds) {
230
+ if (sparkleAnnotationIds) {
231
231
  const annotationSpans = container.querySelectorAll('[data-annotation-id]');
232
232
  annotationSpans.forEach((span) => {
233
233
  const annotationId = span.getAttribute('data-annotation-id');
234
- if (annotationId && newAnnotationIds.has(annotationId)) {
234
+ if (annotationId && sparkleAnnotationIds.has(annotationId)) {
235
235
  span.classList.add('annotation-sparkle');
236
236
  }
237
237
  });
@@ -248,7 +248,7 @@ export const BrowseView = memo(function BrowseView({
248
248
  cleanupHover();
249
249
  referentSub?.unsubscribe(); // in-flight referent load dies with the effect
250
250
  };
251
- }, [content, allAnnotations, newAnnotationIds, hoverDelayMs, session, onReferenceHover]);
251
+ }, [content, allAnnotations, sparkleAnnotationIds, hoverDelayMs, session, onReferenceHover]);
252
252
 
253
253
  // Helper to scroll annotation into view with pulse effect
254
254
  const scrollToAnnotation = useCallback((annotationId: string | null, removePulse = false) => {
@@ -50,7 +50,7 @@ interface Props {
50
50
  /** Inline display variant: auto-height, no inner scroll, no pane chrome (browse path). Default: fill-the-pane. */
51
51
  inline?: boolean;
52
52
  /** Recently-created annotation ids to sparkle (threaded to the browse/annotate subtree). */
53
- newAnnotationIds?: Set<string>;
53
+ sparkleAnnotationIds?: Set<string>;
54
54
  generatingReferenceId?: string | null;
55
55
  showLineNumbers?: boolean;
56
56
  hoverDelayMs?: number;
@@ -116,7 +116,7 @@ export function ResourceViewer({
116
116
  onLinkClick,
117
117
  onReferenceHover,
118
118
  inline = false,
119
- newAnnotationIds,
119
+ sparkleAnnotationIds,
120
120
  generatingReferenceId,
121
121
  showLineNumbers = false,
122
122
  hoverDelayMs,
@@ -356,7 +356,7 @@ export function ResourceViewer({
356
356
  onModeChange={changeAnnotateMode}
357
357
  showToolbar={showToolbar}
358
358
  session={session}
359
- newAnnotationIds={newAnnotationIds}
359
+ sparkleAnnotationIds={sparkleAnnotationIds}
360
360
  {...(annotateRenderers && { renderers: annotateRenderers })}
361
361
  />
362
362
  ) : (
@@ -372,7 +372,7 @@ export function ResourceViewer({
372
372
  onClickActionChange={changeClickAction}
373
373
  showToolbar={showToolbar}
374
374
  session={session}
375
- newAnnotationIds={newAnnotationIds}
375
+ sparkleAnnotationIds={sparkleAnnotationIds}
376
376
  onLinkClick={onLinkClick}
377
377
  onReferenceHover={onReferenceHover}
378
378
  inline={inline}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * EMBEDDABLE-RESOURCE-VIEWER step 1b — AnnotateView + AnnotateToolbar provider-free.
3
3
  *
4
- * AnnotateView takes `session` + `newAnnotationIds` as props; the REAL
4
+ * AnnotateView takes `session` + `sparkleAnnotationIds` as props; the REAL
5
5
  * AnnotateToolbar (not mocked — its decoupling is the crux of 1b) takes `session`
6
6
  * as a prop. CodeMirrorRenderer is mocked (heavy; already prop-based).
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * EMBEDDABLE-RESOURCE-VIEWER step 1a — BrowseView renders provider-free.
3
3
  *
4
- * BrowseView takes its `session` + `newAnnotationIds` as props (not
4
+ * BrowseView takes its `session` + `sparkleAnnotationIds` as props (not
5
5
  * `useSemiont()` / `useResourceAnnotations()`), and subscribes to session-scoped
6
6
  * beckon events via `session.subscribe`. `AnnotateToolbar` is mocked here — its
7
7
  * own provider decoupling is step 1b — so this spec isolates BrowseView's body.
@@ -8,7 +8,7 @@ import { createTestSemiontWrapper } from '../../../test-utils';
8
8
 
9
9
  import type { Annotation, AnnotationId } from '@semiont/core';
10
10
 
11
- // BrowseView takes its `session` + `newAnnotationIds` as props now (step 1a) — no
11
+ // BrowseView takes its `session` + `sparkleAnnotationIds` as props now (step 1a) — no
12
12
  // ResourceAnnotationsContext / SemiontProvider reach-in. `makeSession` (below)
13
13
  // wraps the fake client so browse:click / beckon:hover land on the bus the
14
14
  // trackers listen on, and session.subscribe registers beckon:* on that bus.
@@ -125,14 +125,14 @@ function makeSession(client: SemiontClient): SemiontSession {
125
125
 
126
126
  const renderWithProviders = (
127
127
  component: React.ReactElement<React.ComponentProps<typeof BrowseView>>,
128
- options: { newAnnotationIds?: Set<string> } = {}
128
+ options: { sparkleAnnotationIds?: Set<string> } = {}
129
129
  ) => {
130
130
  const { SemiontWrapper, client } = createTestSemiontWrapper();
131
131
  const Wrapper = ({ children }: { children: React.ReactNode }) => (
132
132
  <SemiontWrapper>{children}</SemiontWrapper>
133
133
  );
134
134
  return render(
135
- React.cloneElement(component, { session: makeSession(client), newAnnotationIds: options.newAnnotationIds }),
135
+ React.cloneElement(component, { session: makeSession(client), sparkleAnnotationIds: options.sparkleAnnotationIds }),
136
136
  { wrapper: Wrapper },
137
137
  );
138
138
  };
@@ -140,7 +140,7 @@ const renderWithProviders = (
140
140
  const renderWithEventTracking = (
141
141
  component: React.ReactElement<React.ComponentProps<typeof BrowseView>>,
142
142
  tracker: ReturnType<typeof createEventTracker>,
143
- options: { newAnnotationIds?: Set<string> } = {}
143
+ options: { sparkleAnnotationIds?: Set<string> } = {}
144
144
  ) => {
145
145
  const { SemiontWrapper, eventBus, client } = createTestSemiontWrapper();
146
146
  tracker._attach(eventBus);
@@ -148,7 +148,7 @@ const renderWithEventTracking = (
148
148
  <SemiontWrapper>{children}</SemiontWrapper>
149
149
  );
150
150
  return render(
151
- React.cloneElement(component, { session: makeSession(client), newAnnotationIds: options.newAnnotationIds }),
151
+ React.cloneElement(component, { session: makeSession(client), sparkleAnnotationIds: options.sparkleAnnotationIds }),
152
152
  { wrapper: Wrapper },
153
153
  );
154
154
  };
@@ -545,7 +545,7 @@ describe('BrowseView Component', () => {
545
545
 
546
546
  describe('Annotation Animation Classes', () => {
547
547
  it('should apply sparkle class to new annotations', () => {
548
- const newAnnotationIds = new Set(['new-annotation-1']);
548
+ const sparkleAnnotationIds = new Set(['new-annotation-1']);
549
549
 
550
550
  const annotations = {
551
551
  ...defaultProps.annotations,
@@ -553,12 +553,12 @@ describe('BrowseView Component', () => {
553
553
  };
554
554
 
555
555
  renderWithProviders(<BrowseView {...defaultProps} annotations={annotations} />, {
556
- newAnnotationIds
556
+ sparkleAnnotationIds
557
557
  });
558
558
 
559
- // Verify the newAnnotationIds set contains the expected annotation
559
+ // Verify the sparkleAnnotationIds set contains the expected annotation
560
560
  // In the actual component, this triggers the sparkle class application
561
- expect(newAnnotationIds.has('new-annotation-1')).toBe(true);
561
+ expect(sparkleAnnotationIds.has('new-annotation-1')).toBe(true);
562
562
  });
563
563
  });
564
564
 
@@ -51,7 +51,7 @@ export interface AnnotateMediaRendererProps {
51
51
  // Text-path extras. Optional on the shared interface: a custom image or PDF
52
52
  // renderer simply ignores them, the same way `MediaRendererProps.session` is
53
53
  // documented to be ignorable.
54
- newAnnotationIds?: Set<string>;
54
+ sparkleAnnotationIds?: Set<string>;
55
55
  scrollToAnnotationId?: string | null;
56
56
  showLineNumbers?: boolean;
57
57
  enableWidgets?: boolean;
@@ -66,7 +66,7 @@ export function TextAnnotateRenderer({
66
66
  content,
67
67
  annotations,
68
68
  session,
69
- newAnnotationIds,
69
+ sparkleAnnotationIds,
70
70
  hoveredAnnotationId,
71
71
  scrollToAnnotationId,
72
72
  showLineNumbers = false,
@@ -84,7 +84,7 @@ export function TextAnnotateRenderer({
84
84
  content={content}
85
85
  segments={segments}
86
86
  editable={false}
87
- newAnnotationIds={newAnnotationIds}
87
+ sparkleAnnotationIds={sparkleAnnotationIds}
88
88
  {...(hoveredAnnotationId !== undefined && { hoveredAnnotationId })}
89
89
  {...(scrollToAnnotationId !== undefined && { scrollToAnnotationId })}
90
90
  sourceView={true}
@@ -173,6 +173,41 @@
173
173
  font-variant-numeric: tabular-nums;
174
174
  }
175
175
 
176
+ /* D8: the finished run's artifact, offered as a link. */
177
+ /* The finished run's artifact — the ended frame's primary affordance
178
+ (GENERATION-ARRIVAL P1, D3's fallback): a button-shaped invitation to open
179
+ what was made, not an underlined footnote beside the bar. */
180
+ .semiont-assist-progress__outcome {
181
+ align-self: stretch;
182
+ padding: 0.5rem 0.75rem;
183
+ border: 1px solid var(--semiont-color-primary-300);
184
+ border-radius: 0.5rem;
185
+ background: var(--semiont-color-primary-50);
186
+ cursor: pointer;
187
+ font-size: var(--semiont-text-sm);
188
+ font-weight: var(--semiont-font-medium);
189
+ color: var(--semiont-color-primary-700);
190
+ text-align: left;
191
+ }
192
+
193
+ .semiont-assist-progress__outcome:hover {
194
+ border-color: var(--semiont-color-primary-400);
195
+ background: var(--semiont-color-primary-100);
196
+ color: var(--semiont-color-primary-800);
197
+ }
198
+
199
+ [data-theme="dark"] .semiont-assist-progress__outcome {
200
+ border-color: var(--semiont-color-primary-700);
201
+ background: var(--semiont-color-primary-900);
202
+ color: var(--semiont-color-primary-300);
203
+ }
204
+
205
+ [data-theme="dark"] .semiont-assist-progress__outcome:hover {
206
+ border-color: var(--semiont-color-primary-600);
207
+ background: var(--semiont-color-primary-800);
208
+ color: var(--semiont-color-primary-200);
209
+ }
210
+
176
211
  .semiont-assist-progress__params {
177
212
  display: flex;
178
213
  flex-wrap: wrap;
@@ -27,6 +27,8 @@ interface ReferenceEntryProps {
27
27
  onOpenResource?: (resourceId: string) => void;
28
28
  annotateMode?: boolean;
29
29
  isGenerating?: boolean;
30
+ /** The reference just got attention-worthy (created or resolved) — glow the icon (RESOLUTION-SPARKLE D6). */
31
+ sparkle?: boolean;
30
32
  ref?: Ref<HTMLDivElement>;
31
33
  }
32
34
 
@@ -38,6 +40,7 @@ export function ReferenceEntry({
38
40
  onOpenResource,
39
41
  annotateMode = true,
40
42
  isGenerating = false,
43
+ sparkle = false,
41
44
  ref,
42
45
  }: ReferenceEntryProps) {
43
46
  const t = useTranslations('ReferencesPanel');
@@ -125,7 +128,7 @@ export function ReferenceEntry({
125
128
  <div className="semiont-annotation-entry__header">
126
129
  <div className="semiont-reference-icon-group">
127
130
  <button
128
- className={`semiont-reference-icon${iconIsClickable ? ' semiont-reference-icon--clickable' : ''}`}
131
+ className={`semiont-reference-icon${iconIsClickable ? ' semiont-reference-icon--clickable' : ''}${sparkle ? ' annotation-sparkle' : ''}`}
129
132
  title={isResolved ? t('open') : annotateMode ? t('resolve') : t('stub')}
130
133
  onClick={iconIsClickable ? handleIconClick : undefined}
131
134
  data-generating={!isResolved && isGenerating ? 'true' : 'false'}
@@ -151,7 +154,11 @@ export function ReferenceEntry({
151
154
  )}
152
155
  {!selectedText && (
153
156
  <div className="semiont-annotation-entry__meta">
154
- {annotationType}
157
+ {/* A resource-level annotation with a resolved, named target is
158
+ a derivation (the shape generation mints — the vocabulary
159
+ deliberately has no 'deriving' purpose): qualify the link
160
+ line below instead of showing the generic type label. */}
161
+ {!selector && resolvedDocumentName ? t('derived') : annotationType}
155
162
  </div>
156
163
  )}
157
164
  {resolvedDocumentName && (
@@ -83,6 +83,8 @@ interface Props {
83
83
  scrollToAnnotationId?: string | null;
84
84
  onScrollCompleted?: () => void;
85
85
  hoveredAnnotationId?: string | null;
86
+ /** Annotations currently sparkling (recently created or resolved) — entries in the set glow their icon (RESOLUTION-SPARKLE D6). */
87
+ sparkleAnnotationIds?: Set<string>;
86
88
 
87
89
  /** User UI locale — stamped on the unresolved-reference body's `language` field. */
88
90
  locale?: string;
@@ -119,6 +121,7 @@ export function ReferencesPanel({
119
121
  scrollToAnnotationId,
120
122
  onScrollCompleted,
121
123
  hoveredAnnotationId,
124
+ sparkleAnnotationIds,
122
125
  locale,
123
126
  sourceLanguage,
124
127
  }: Props) {
@@ -483,6 +486,7 @@ export function ReferencesPanel({
483
486
  onOpenResource={onOpenResource}
484
487
  annotateMode={annotateMode}
485
488
  isGenerating={reference.id === generatingReferenceId}
489
+ sparkle={sparkleAnnotationIds?.has(reference.id) ?? false}
486
490
  ref={(el) => setEntryRef(reference.id, el)}
487
491
  />
488
492
  ))