@semiont/react-ui 0.5.24 → 0.5.26

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 (179) hide show
  1. package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js +677 -0
  2. package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +1 -0
  3. package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css +180 -0
  4. package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +1 -0
  5. package/dist/{ar-JRTKCMTF.js → ar-YTUUGK2O.js} +32 -6
  6. package/dist/ar-YTUUGK2O.js.map +1 -0
  7. package/dist/{bn-LO6EDSCC.js → bn-ASGNGPMD.js} +32 -6
  8. package/dist/bn-ASGNGPMD.js.map +1 -0
  9. package/dist/{chunk-4QMTEPQ3.js → chunk-BTUG3VJJ.js} +18 -298
  10. package/dist/chunk-BTUG3VJJ.js.map +1 -0
  11. package/dist/{chunk-5Y5JDCKC.js → chunk-L3WKRWP3.js} +32 -6
  12. package/dist/chunk-L3WKRWP3.js.map +1 -0
  13. package/dist/chunk-OBQOZCJE.js +287 -0
  14. package/dist/chunk-OBQOZCJE.js.map +1 -0
  15. package/dist/{cs-LCHHOR4O.js → cs-WMYVAOZY.js} +32 -6
  16. package/dist/cs-WMYVAOZY.js.map +1 -0
  17. package/dist/{da-TARGPMJR.js → da-FSHHN4D6.js} +32 -6
  18. package/dist/da-FSHHN4D6.js.map +1 -0
  19. package/dist/{de-SBH4QBPJ.js → de-FJFO3TPT.js} +32 -6
  20. package/dist/de-FJFO3TPT.js.map +1 -0
  21. package/dist/{el-2D2OLGWB.js → el-XOH4QIAG.js} +32 -6
  22. package/dist/el-XOH4QIAG.js.map +1 -0
  23. package/dist/{en-QFIEG3CA.js → en-3FCBW3YD.js} +6 -2
  24. package/dist/{es-U6YK2LX6.js → es-HDX4QI7E.js} +32 -6
  25. package/dist/es-HDX4QI7E.js.map +1 -0
  26. package/dist/{fa-S7SZE37M.js → fa-DJ6WTFTJ.js} +32 -6
  27. package/dist/fa-DJ6WTFTJ.js.map +1 -0
  28. package/dist/{fi-CBL3NIXN.js → fi-4S7H7W7U.js} +32 -6
  29. package/dist/fi-4S7H7W7U.js.map +1 -0
  30. package/dist/{fr-EABUHV6S.js → fr-6UJSYHXC.js} +32 -6
  31. package/dist/fr-6UJSYHXC.js.map +1 -0
  32. package/dist/{he-THWD244Y.js → he-ZZJOHJX2.js} +32 -6
  33. package/dist/he-ZZJOHJX2.js.map +1 -0
  34. package/dist/{hi-GHGFZJ44.js → hi-HZJTAW7U.js} +32 -6
  35. package/dist/hi-HZJTAW7U.js.map +1 -0
  36. package/dist/{id-POZOUMDW.js → id-E3I6ILEM.js} +32 -6
  37. package/dist/id-E3I6ILEM.js.map +1 -0
  38. package/dist/index.css +140 -94
  39. package/dist/index.css.map +1 -1
  40. package/dist/index.d.ts +85 -24
  41. package/dist/index.js +1782 -1697
  42. package/dist/index.js.map +1 -1
  43. package/dist/{it-KDYJADSH.js → it-ULAPAD7U.js} +32 -6
  44. package/dist/it-ULAPAD7U.js.map +1 -0
  45. package/dist/{ja-Z5Z6ZF3P.js → ja-SAHZQ4CZ.js} +32 -6
  46. package/dist/ja-SAHZQ4CZ.js.map +1 -0
  47. package/dist/{ko-7X3XUXRN.js → ko-DFU2ADQ4.js} +32 -6
  48. package/dist/ko-DFU2ADQ4.js.map +1 -0
  49. package/dist/{ms-BTWEIW43.js → ms-NWL76XNT.js} +32 -6
  50. package/dist/ms-NWL76XNT.js.map +1 -0
  51. package/dist/{nl-IS2CGF5I.js → nl-ATCM76WN.js} +32 -6
  52. package/dist/nl-ATCM76WN.js.map +1 -0
  53. package/dist/{no-AAMEVA26.js → no-UIAQ5RJF.js} +32 -6
  54. package/dist/no-UIAQ5RJF.js.map +1 -0
  55. package/dist/{pl-PVXUQYFT.js → pl-NOECQDXD.js} +32 -6
  56. package/dist/pl-NOECQDXD.js.map +1 -0
  57. package/dist/{pt-3ITQ76TL.js → pt-SVWIIUPC.js} +32 -6
  58. package/dist/pt-SVWIIUPC.js.map +1 -0
  59. package/dist/{ro-FRB4IIVS.js → ro-NR2DRTZG.js} +32 -6
  60. package/dist/ro-NR2DRTZG.js.map +1 -0
  61. package/dist/{sv-DS3RO24M.js → sv-STVJ37JP.js} +32 -6
  62. package/dist/sv-STVJ37JP.js.map +1 -0
  63. package/dist/test-utils.js +5 -3
  64. package/dist/test-utils.js.map +1 -1
  65. package/dist/{th-AX66MWWB.js → th-UWQR4K54.js} +32 -6
  66. package/dist/th-UWQR4K54.js.map +1 -0
  67. package/dist/{tr-7KTB67TT.js → tr-WNXZSD27.js} +32 -6
  68. package/dist/tr-WNXZSD27.js.map +1 -0
  69. package/dist/{uk-SMQS2MAQ.js → uk-SJRMB47Y.js} +32 -6
  70. package/dist/uk-SJRMB47Y.js.map +1 -0
  71. package/dist/{vi-D3JXL2EZ.js → vi-TMRQFES2.js} +32 -6
  72. package/dist/vi-TMRQFES2.js.map +1 -0
  73. package/dist/{zh-S754AWFS.js → zh-QOL3LYEM.js} +32 -6
  74. package/dist/zh-QOL3LYEM.js.map +1 -0
  75. package/package.json +8 -8
  76. package/src/components/AssistProgress.tsx +42 -30
  77. package/src/components/EntityFoundLog.tsx +38 -0
  78. package/src/components/StatusDisplay.css +35 -35
  79. package/src/components/__tests__/AssistProgress.test.tsx +86 -28
  80. package/src/components/annotation/annotations.css +1 -1
  81. package/src/components/modals/SearchModal.css +5 -5
  82. package/src/components/pdf-annotation/PdfAnnotationCanvas.css +143 -6
  83. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +689 -198
  84. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1008 -3
  85. package/src/components/pdf-annotation/__tests__/estimate-slot-height.test.ts +53 -0
  86. package/src/components/pdf-annotation/estimate-slot-height.ts +31 -0
  87. package/src/components/resource/AnnotateView.tsx +14 -7
  88. package/src/components/resource/BrowseView.tsx +2 -7
  89. package/src/components/resource/DownloadFileLink.tsx +49 -0
  90. package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +58 -0
  91. package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +33 -0
  92. package/src/components/resource/__tests__/download-link.test.tsx +129 -0
  93. package/src/components/resource/annotate-renderers.tsx +10 -1
  94. package/src/components/resource/browse-renderers.tsx +10 -1
  95. package/src/components/resource/panels/AssistSection.css +0 -43
  96. package/src/components/resource/panels/AssistSection.tsx +11 -2
  97. package/src/components/resource/panels/AssistShell.tsx +5 -1
  98. package/src/components/resource/panels/ReferencesPanel.tsx +12 -13
  99. package/src/components/resource/panels/TaggingPanel.tsx +21 -1
  100. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +2 -2
  101. package/src/components/resource/panels/__tests__/AssistShell.test.tsx +9 -4
  102. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -1
  103. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +26 -0
  104. package/src/features/auth/auth.css +10 -10
  105. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +58 -3
  106. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +18 -7
  107. package/src/styles/core/inputs.css +1 -1
  108. package/src/styles/features/compose.css +1 -1
  109. package/src/styles/features/resource-viewer.css +1 -1
  110. package/src/styles/features/static-pages.css +11 -11
  111. package/src/styles/motivations/motivation-comment.css +2 -2
  112. package/src/styles/motivations/motivation-reference.css +2 -2
  113. package/src/styles/patterns/panels-base.css +4 -4
  114. package/src/styles/utilities/motion.css +15 -15
  115. package/src/styles/variables.css +29 -1
  116. package/translations/ar.json +27 -5
  117. package/translations/bn.json +27 -5
  118. package/translations/cs.json +27 -5
  119. package/translations/da.json +27 -5
  120. package/translations/de.json +27 -5
  121. package/translations/el.json +27 -5
  122. package/translations/en.json +27 -5
  123. package/translations/es.json +27 -5
  124. package/translations/fa.json +27 -5
  125. package/translations/fi.json +27 -5
  126. package/translations/fr.json +27 -5
  127. package/translations/he.json +27 -5
  128. package/translations/hi.json +27 -5
  129. package/translations/id.json +27 -5
  130. package/translations/it.json +27 -5
  131. package/translations/ja.json +27 -5
  132. package/translations/ko.json +27 -5
  133. package/translations/ms.json +27 -5
  134. package/translations/nl.json +27 -5
  135. package/translations/no.json +27 -5
  136. package/translations/pl.json +27 -5
  137. package/translations/pt.json +27 -5
  138. package/translations/ro.json +27 -5
  139. package/translations/sv.json +27 -5
  140. package/translations/th.json +27 -5
  141. package/translations/tr.json +27 -5
  142. package/translations/uk.json +27 -5
  143. package/translations/vi.json +27 -5
  144. package/translations/zh.json +27 -5
  145. package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css +0 -98
  146. package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css.map +0 -1
  147. package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js +0 -427
  148. package/dist/PdfAnnotationCanvas.client-V7Q3BEEQ.js.map +0 -1
  149. package/dist/ar-JRTKCMTF.js.map +0 -1
  150. package/dist/bn-LO6EDSCC.js.map +0 -1
  151. package/dist/chunk-4QMTEPQ3.js.map +0 -1
  152. package/dist/chunk-5Y5JDCKC.js.map +0 -1
  153. package/dist/cs-LCHHOR4O.js.map +0 -1
  154. package/dist/da-TARGPMJR.js.map +0 -1
  155. package/dist/de-SBH4QBPJ.js.map +0 -1
  156. package/dist/el-2D2OLGWB.js.map +0 -1
  157. package/dist/es-U6YK2LX6.js.map +0 -1
  158. package/dist/fa-S7SZE37M.js.map +0 -1
  159. package/dist/fi-CBL3NIXN.js.map +0 -1
  160. package/dist/fr-EABUHV6S.js.map +0 -1
  161. package/dist/he-THWD244Y.js.map +0 -1
  162. package/dist/hi-GHGFZJ44.js.map +0 -1
  163. package/dist/id-POZOUMDW.js.map +0 -1
  164. package/dist/it-KDYJADSH.js.map +0 -1
  165. package/dist/ja-Z5Z6ZF3P.js.map +0 -1
  166. package/dist/ko-7X3XUXRN.js.map +0 -1
  167. package/dist/ms-BTWEIW43.js.map +0 -1
  168. package/dist/nl-IS2CGF5I.js.map +0 -1
  169. package/dist/no-AAMEVA26.js.map +0 -1
  170. package/dist/pl-PVXUQYFT.js.map +0 -1
  171. package/dist/pt-3ITQ76TL.js.map +0 -1
  172. package/dist/ro-FRB4IIVS.js.map +0 -1
  173. package/dist/sv-DS3RO24M.js.map +0 -1
  174. package/dist/th-AX66MWWB.js.map +0 -1
  175. package/dist/tr-7KTB67TT.js.map +0 -1
  176. package/dist/uk-SMQS2MAQ.js.map +0 -1
  177. package/dist/vi-D3JXL2EZ.js.map +0 -1
  178. package/dist/zh-S754AWFS.js.map +0 -1
  179. /package/dist/{en-QFIEG3CA.js.map → en-3FCBW3YD.js.map} +0 -0
@@ -82,6 +82,14 @@ export function TaggingPanel({
82
82
  sourceLanguage,
83
83
  }: TaggingPanelProps) {
84
84
  const t = useTranslations('TaggingPanel');
85
+ const ta = useTranslations('AssistProgress');
86
+
87
+ // Dismiss parity: tag was the one assist surface with no way to clear a
88
+ // finished progress display, because it had no `closeProgress` key to
89
+ // label the control. Same wiring as every other surface.
90
+ const handleDismissProgress = useCallback(() => {
91
+ session?.client.mark.dismissProgress();
92
+ }, [session]);
85
93
 
86
94
  // Subscribe to the per-KB tag-schema registry. Schemas are runtime-
87
95
  // registered by the KB at session start (see frame.addTagSchema).
@@ -355,7 +363,19 @@ export function TaggingPanel({
355
363
  title={t('annotateTags')}
356
364
  isAssisting={isAssisting}
357
365
  progress={progress}
358
- progressProps={{ showPercentBar: true }}
366
+ progressProps={{
367
+ showPercentBar: true,
368
+ onDismiss: handleDismissProgress,
369
+ translations: {
370
+ cancel: t('cancel'),
371
+ inProgress: t('annotating'),
372
+ complete: t('complete'),
373
+ failed: t('failed'),
374
+ paramsTitle: ta('paramsTitle'),
375
+ processing: (label) => ta('processing', { label }),
376
+ close: ta('close'),
377
+ },
378
+ }}
359
379
  form={
360
380
  <>
361
381
  {/* Empty-state — registry has resolved with no schemas. */}
@@ -126,7 +126,7 @@ describe('AssistSection', () => {
126
126
  />
127
127
  );
128
128
 
129
- expect(screen.getByText('Request Parameters:')).toBeInTheDocument();
129
+ expect(screen.getByText('paramsTitle')).toBeInTheDocument();
130
130
  expect(screen.getByText('Find important points')).toBeInTheDocument();
131
131
  expect(screen.getByText(/Instructions:/)).toBeInTheDocument();
132
132
  expect(screen.getByText('5')).toBeInTheDocument();
@@ -506,7 +506,7 @@ describe('AssistSection', () => {
506
506
  />
507
507
  );
508
508
 
509
- expect(screen.queryByText('Request Parameters:')).not.toBeInTheDocument();
509
+ expect(screen.queryByText('paramsTitle')).not.toBeInTheDocument();
510
510
  });
511
511
  });
512
512
  });
@@ -13,6 +13,11 @@ import { AssistShell } from '../AssistShell';
13
13
 
14
14
  const progress = { stage: 'analyzing', percentage: 50, message: 'working' };
15
15
 
16
+ /** AssistProgress requires a full translation set; the shell just passes it through. */
17
+ const TR = { cancel: 'tr.cancel', inProgress: 'tr.inProgress', complete: 'tr.complete',
18
+ failed: 'tr.failed', close: 'tr.close', paramsTitle: 'tr.paramsTitle',
19
+ processing: (l: string) => `tr.processing(${l})` };
20
+
16
21
  describe('AssistShell', () => {
17
22
  beforeEach(() => {
18
23
  localStorage.clear();
@@ -21,12 +26,12 @@ describe('AssistShell', () => {
21
26
  it('renders the form when there is no progress, the progress when there is', () => {
22
27
  const { rerender } = render(
23
28
  <AssistShell assistType="tag" title="Annotate Tags" isAssisting={false} progress={null}
24
- form={<button type="button">the form</button>} />,
29
+ form={<button type="button">the form</button>} progressProps={{ translations: TR }} />,
25
30
  );
26
31
  expect(screen.getByText('the form')).toBeInTheDocument();
27
32
  rerender(
28
33
  <AssistShell assistType="tag" title="Annotate Tags" isAssisting={true} progress={progress}
29
- form={<button type="button">the form</button>} />,
34
+ form={<button type="button">the form</button>} progressProps={{ translations: TR }} />,
30
35
  );
31
36
  expect(screen.queryByText('the form')).not.toBeInTheDocument();
32
37
  expect(screen.getByText('working')).toBeInTheDocument();
@@ -37,7 +42,7 @@ describe('AssistShell', () => {
37
42
  const props = {
38
43
  assistType: 'highlight', title: 'Annotate Highlights', progress,
39
44
  form: <span>form</span>,
40
- progressProps: { onDismiss, translations: { close: 'Close' } },
45
+ progressProps: { onDismiss, translations: { ...TR, close: 'Close' } },
41
46
  };
42
47
  const { rerender } = render(<AssistShell {...props} isAssisting={true} />);
43
48
  expect(screen.queryByLabelText('Close')).not.toBeInTheDocument();
@@ -50,7 +55,7 @@ describe('AssistShell', () => {
50
55
  it('persists the expand state per assist type', async () => {
51
56
  render(
52
57
  <AssistShell assistType="reference" title="Annotate References" isAssisting={false} progress={null}
53
- form={<span>form</span>} />,
58
+ form={<span>form</span>} progressProps={{ translations: TR }} />,
54
59
  );
55
60
  await userEvent.click(screen.getByRole('button', { name: /Annotate References/ }));
56
61
  expect(screen.queryByText('form')).not.toBeInTheDocument();
@@ -169,7 +169,7 @@ describe('HighlightPanel + AssistSection Integration', () => {
169
169
  />
170
170
  );
171
171
 
172
- expect(screen.getByText('Request Parameters:')).toBeInTheDocument();
172
+ expect(screen.getByText('paramsTitle')).toBeInTheDocument();
173
173
  expect(screen.getByText('Find important points')).toBeInTheDocument();
174
174
  expect(screen.getByText('5')).toBeInTheDocument();
175
175
  });
@@ -409,6 +409,32 @@ describe('ReferencesPanel Component', () => {
409
409
  expect(screen.getByText('Organization:')).toBeInTheDocument();
410
410
  });
411
411
 
412
+ it('renders the entity log with the SAME markup the progress display uses', () => {
413
+ // ASSIST-SURFACE-WARTS Lane B: this form-side log and AssistProgress's
414
+ // completed-entity log are the same concept. They had two class families
415
+ // (semiont-assist-widget__log* here, semiont-annotation-log* there) one
416
+ // panel apart — one concept, one markup.
417
+ const { container, rerender } = renderWithEventBus(
418
+ <ReferencesPanel
419
+ {...panelProps()}
420
+ isAssisting={false}
421
+ progress={{
422
+ stage: 'complete',
423
+ percentage: 100,
424
+ message: 'Annotation complete',
425
+ completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
426
+ }}
427
+ />
428
+ );
429
+ rerender(
430
+ <ReferencesPanel {...panelProps()} isAssisting={false} progress={null} />
431
+ );
432
+
433
+ expect(container.querySelector('.semiont-annotation-log')).toBeInTheDocument();
434
+ expect(container.querySelector('.semiont-annotation-log-item')).toBeInTheDocument();
435
+ expect(container.querySelector('.semiont-assist-widget__log')).not.toBeInTheDocument();
436
+ });
437
+
412
438
  it('should hide entity type selection during detection', () => {
413
439
  renderWithEventBus(
414
440
  <ReferencesPanel
@@ -316,12 +316,12 @@
316
316
  }
317
317
 
318
318
  .semiont-form__input-row:focus-visible {
319
- outline: 2px solid var(--semiont-focus-ring, #3b82f6);
319
+ outline: 2px solid var(--semiont-focus-ring);
320
320
  outline-offset: 2px;
321
321
  }
322
322
 
323
323
  [data-theme="dark"] .semiont-form__input-row:focus-visible {
324
- outline-color: var(--semiont-focus-ring, #60a5fa);
324
+ outline-color: var(--semiont-focus-ring);
325
325
  }
326
326
 
327
327
  .semiont-form__input-row .semiont-input {
@@ -329,12 +329,12 @@
329
329
  }
330
330
 
331
331
  .semiont-form__input-row .semiont-input:focus-visible {
332
- outline: 2px solid var(--semiont-focus-ring, #3b82f6);
332
+ outline: 2px solid var(--semiont-focus-ring);
333
333
  outline-offset: 2px;
334
334
  }
335
335
 
336
336
  [data-theme="dark"] .semiont-form__input-row .semiont-input:focus-visible {
337
- outline-color: var(--semiont-focus-ring, #60a5fa);
337
+ outline-color: var(--semiont-focus-ring);
338
338
  }
339
339
 
340
340
  .semiont-form__url-status {
@@ -345,27 +345,27 @@
345
345
  }
346
346
 
347
347
  .semiont-form__url-status--checking {
348
- color: var(--semiont-text-muted, #888888);
348
+ color: var(--semiont-text-tertiary);
349
349
  animation: spin 1s linear infinite;
350
350
  display: inline-block;
351
351
  }
352
352
 
353
353
  [data-theme="dark"] .semiont-form__url-status--checking {
354
- color: var(--semiont-text-muted, #888888);
354
+ color: var(--semiont-text-tertiary);
355
355
  }
356
356
 
357
357
  .semiont-form__url-status--ok {
358
- color: var(--semiont-success, #22c55e);
358
+ color: var(--semiont-color-success);
359
359
  }
360
360
 
361
361
  [data-theme="dark"] .semiont-form__url-status--ok {
362
- color: var(--semiont-success, #22c55e);
362
+ color: var(--semiont-color-success);
363
363
  }
364
364
 
365
365
  .semiont-form__url-status--error {
366
- color: var(--semiont-error, #ef4444);
366
+ color: var(--semiont-color-error);
367
367
  }
368
368
 
369
369
  [data-theme="dark"] .semiont-form__url-status--error {
370
- color: var(--semiont-error, #ef4444);
370
+ color: var(--semiont-color-error);
371
371
  }
@@ -41,7 +41,7 @@ vi.mock('../../../hooks/useResourceContent', () => ({
41
41
  // SemiontClient (wired to a dummy baseUrl). The real client surface lets
42
42
  // createResourceViewerPageStateUnit run against the full namespace API without us
43
43
  // hand-stubbing every method it touches.
44
- const { stubBrowser } = vi.hoisted(() => {
44
+ const { stubBrowser, stubClient } = vi.hoisted(() => {
45
45
  const { BehaviorSubject } = require('rxjs');
46
46
  const { SemiontClient, HttpTransport, HttpContentTransport } = require('@semiont/sdk');
47
47
  const { baseUrl } = require('@semiont/core');
@@ -63,7 +63,7 @@ const { stubBrowser } = vi.hoisted(() => {
63
63
  on: vi.fn(() => () => {}),
64
64
  stream: vi.fn(() => ({ subscribe: () => ({ unsubscribe: () => {} }) })),
65
65
  };
66
- return { stubBrowser };
66
+ return { stubBrowser, stubClient: client };
67
67
  });
68
68
 
69
69
  vi.mock('../../../session/SemiontProvider', async () => {
@@ -76,6 +76,8 @@ vi.mock('../../../session/SemiontProvider', async () => {
76
76
  };
77
77
  });
78
78
 
79
+ const capturedHistory = vi.hoisted(() => ({ props: null as any }));
80
+
79
81
  vi.mock('@semiont/react-ui', async () => {
80
82
  const actual = await vi.importActual('@semiont/react-ui');
81
83
  return {
@@ -84,7 +86,10 @@ vi.mock('@semiont/react-ui', async () => {
84
86
  Toolbar: () => <div data-testid="toolbar">Toolbar</div>,
85
87
  ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
86
88
  UnifiedAnnotationsPanel: () => <div data-testid="annotations-panel">Annotations</div>,
87
- AnnotationHistory: () => <div data-testid="history-panel">History</div>,
89
+ AnnotationHistory: (props: any) => {
90
+ capturedHistory.props = props;
91
+ return <div data-testid="history-panel">History</div>;
92
+ },
88
93
  ResourceInfoPanel: () => <div data-testid="info-panel">Info</div>,
89
94
  CollaborationPanel: () => <div data-testid="collaboration-panel">Collaboration</div>,
90
95
  JsonLdPanel: () => <div data-testid="jsonld-panel">JSON-LD</div>,
@@ -118,6 +123,15 @@ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
118
123
  ResourceAnnotationsProvider: ({ children }: any) => children,
119
124
  }));
120
125
 
126
+ // Capture what ResourceViewerPage passes to useOutcomeToasts. The real hook is a
127
+ // pure event subscription, so standing it in costs the other specs nothing.
128
+ const outcomeToastsCalls = vi.hoisted(() => [] as unknown[]);
129
+ vi.mock('../../../hooks/useOutcomeToasts', () => ({
130
+ useOutcomeToasts: (resourceId: unknown) => {
131
+ outcomeToastsCalls.push(resourceId);
132
+ },
133
+ }));
134
+
121
135
  // Mock useEventSubscription at the direct path used by ResourceViewerPage
122
136
  // (the barrel export mock doesn't intercept direct context imports)
123
137
  const mockUseEventSubscriptions = vi.fn();
@@ -175,6 +189,23 @@ const renderWithProviders = (ui: React.ReactElement) => {
175
189
  );
176
190
  };
177
191
 
192
+ describe('ResourceViewerPage — outcome toasts reach the user', () => {
193
+ // The decline/success toast was dropped for EVERY resource:
194
+ // `useOutcomeToasts(resource.id as string)` read a property
195
+ // `ResourceDescriptor` does not declare (it carries `@id`). The descriptor's
196
+ // open index signature made the access legal and the `as string` silenced its
197
+ // `unknown` type, so the hook's `event.resourceId !== resourceId` guard
198
+ // compared a real id against `undefined` and returned early every time.
199
+ it('subscribes with the resource id, not a property the descriptor lacks', () => {
200
+ outcomeToastsCalls.length = 0;
201
+ renderWithProviders(<ResourceViewerPage {...createMockProps()} />);
202
+
203
+ expect(outcomeToastsCalls).not.toHaveLength(0);
204
+ // Must be the resource's identity — `undefined` here silences every toast.
205
+ expect(outcomeToastsCalls[0]).toBe('test-123');
206
+ });
207
+ });
208
+
178
209
  describe('ResourceViewerPage', () => {
179
210
  beforeEach(() => {
180
211
  });
@@ -228,6 +259,30 @@ describe('ResourceViewerPage', () => {
228
259
  localStorage.clear();
229
260
  });
230
261
 
262
+ it('clicking a history event focuses that annotation in the content', () => {
263
+ // ASSIST-SURFACE-WARTS Lane D. `handleEventClick` used to be a no-op with
264
+ // a stale comment, while HistoryEvent still rendered a focusable button
265
+ // labelled "View annotation" — a promise to screen-reader users that
266
+ // nothing kept. `beckon:focus` is the existing "scroll to and highlight"
267
+ // contract (BrowseView already subscribes); this makes the page a producer.
268
+ localStorage.setItem('activeToolbarPanel', 'history');
269
+ capturedHistory.props = null;
270
+ const props = createMockProps();
271
+ renderWithProviders(<ResourceViewerPage {...props} />);
272
+
273
+ expect(capturedHistory.props).not.toBeNull();
274
+
275
+ const focused: Array<string | null> = [];
276
+ const sub = stubClient.bus.get('beckon:focus').subscribe(({ annotationId }: any) => focused.push(annotationId));
277
+
278
+ expect(capturedHistory.props?.onEventClick).toBeTypeOf('function');
279
+ capturedHistory.props!.onEventClick('ann-42');
280
+ expect(focused).toEqual(['ann-42']);
281
+
282
+ sub.unsubscribe();
283
+ localStorage.clear();
284
+ });
285
+
231
286
  it('shows history panel when activePanel is history', () => {
232
287
  localStorage.setItem('activeToolbarPanel', 'history');
233
288
  const props = createMockProps();
@@ -24,6 +24,7 @@ import type { BrowseMediaRenderers, AnnotateMediaRenderers } from '@semiont/reac
24
24
  import { useObservable } from '@semiont/react-ui';
25
25
  import { useResourceContent } from '../../../hooks/useResourceContent';
26
26
  import { useMediaToken } from '../../../hooks/useMediaToken';
27
+ import { mediaUrl } from '../../../lib/media-url';
27
28
  import { useToast } from '../../../components/Toast';
28
29
  import { useOutcomeToasts } from '../../../hooks/useOutcomeToasts';
29
30
  import { useTheme } from '../../../contexts/ThemeContext';
@@ -139,6 +140,7 @@ export function ResourceViewerPage({
139
140
  // Translations
140
141
  const tw = useTranslations('ReferenceWizard');
141
142
  const tg = useTranslations('ResourceGenerate');
143
+ const ta = useTranslations('AssistProgress');
142
144
 
143
145
  const browser = useSemiont();
144
146
  const session = useObservable(browser.activeSession$);
@@ -185,9 +187,7 @@ export function ResourceViewerPage({
185
187
 
186
188
  // Binary path: fetch short-lived media token, construct URL
187
189
  const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(semiont ?? null, rUri);
188
- const binaryContent = (isBinary && mediaToken && semiont)
189
- ? `${semiont.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
190
- : '';
190
+ const binaryContent = (isBinary ? mediaUrl(semiont, rUri, mediaToken) : undefined) ?? '';
191
191
 
192
192
  const content = isBinary ? binaryContent : textContent;
193
193
  const contentLoading = isBinary ? mediaTokenLoading : textLoading;
@@ -396,7 +396,7 @@ export function ResourceViewerPage({
396
396
  // assist timed-out) live in useOutcomeToasts — they need only the resource id
397
397
  // and the toast surface. The registration below keeps the handlers that need
398
398
  // page-local dependencies (SDK actions, sparkles, settings, navigation).
399
- useOutcomeToasts(resource.id as string);
399
+ useOutcomeToasts(rUri);
400
400
 
401
401
  // Single useEventSubscriptions call per file (enforced by
402
402
  // scripts/compliance/audit-hooks-ordering.ts); hooks like useOutcomeToasts
@@ -444,9 +444,17 @@ export function ResourceViewerPage({
444
444
  }
445
445
  }, [session]);
446
446
 
447
- const handleEventClick = useCallback((_annotationId: string | null) => {
448
- // ResourceViewer now manages scroll state internally
449
- }, []);
447
+ // Clicking a history row reveals that annotation in the content. HistoryEvent
448
+ // renders these rows as buttons labelled "View annotation", so this used to be
449
+ // a focusable, screen-reader-announced control wired to a no-op.
450
+ // `beckon:focus` is the existing "scroll to and highlight" contract rather
451
+ // than a new prop chain — BrowseView already subscribed to it, AnnotateView
452
+ // now does too. See .plans/ASSIST-SURFACE-WARTS.md Lane D.
453
+ const handleEventClick = useCallback((id: string | null) => {
454
+ if (id) {
455
+ stateUnit?.beckon.focus(annotationId(id));
456
+ }
457
+ }, [stateUnit]);
450
458
 
451
459
  // Document rendering
452
460
  return (
@@ -475,6 +483,9 @@ export function ResourceViewerPage({
475
483
  inProgress: tg('progressInProgress'),
476
484
  complete: tg('progressComplete'),
477
485
  failed: tg('progressFailed'),
486
+ paramsTitle: ta('paramsTitle'),
487
+ processing: (label) => ta('processing', { label }),
488
+ close: ta('close'),
478
489
  }}
479
490
  />
480
491
  )}
@@ -137,7 +137,7 @@
137
137
  align-items: center;
138
138
  padding: 0.5rem 0.5rem 0.5rem 0.75rem;
139
139
  font-size: var(--semiont-text-sm);
140
- font-family: var(--semiont-font-mono, monospace);
140
+ font-family: var(--semiont-font-mono);
141
141
  color: var(--semiont-text-tertiary);
142
142
  background-color: var(--semiont-color-gray-50);
143
143
  border-right: 1px solid var(--semiont-color-gray-300);
@@ -118,7 +118,7 @@
118
118
  highlight while a drag hovers. */
119
119
  .semiont-form__upload-dropzone[data-drag-over="true"] {
120
120
  border-color: var(--semiont-color-primary-500);
121
- background: var(--semiont-color-primary-50, rgba(59, 130, 246, 0.06));
121
+ background: var(--semiont-color-primary-50);
122
122
  }
123
123
 
124
124
  /* Dark mode needs its own tint: primary-50 is a near-white wash that would
@@ -324,7 +324,7 @@
324
324
  }
325
325
 
326
326
  [data-theme="dark"] .semiont-browse-view__content tr:nth-child(even) td {
327
- background: var(--semiont-color-gray-850, var(--semiont-color-gray-800));
327
+ background: var(--semiont-color-gray-800);
328
328
  }
329
329
 
330
330
  /* GFM task list checkboxes */
@@ -412,8 +412,8 @@
412
412
  }
413
413
 
414
414
  .semiont-static-warning-box {
415
- background: var(--semiont-color-warning-50);
416
- border-left: 4px solid var(--semiont-color-warning-500);
415
+ background: var(--semiont-color-amber-50);
416
+ border-left: 4px solid var(--semiont-color-amber-500);
417
417
  border-radius: 0.375rem;
418
418
  padding: 1rem 1.25rem;
419
419
  margin: 1.5rem 0;
@@ -427,7 +427,7 @@
427
427
 
428
428
  [data-theme="dark"] .semiont-static-warning-box {
429
429
  background: rgba(251, 146, 60, 0.1);
430
- border-left-color: var(--semiont-color-warning-400);
430
+ border-left-color: var(--semiont-color-amber-400);
431
431
  }
432
432
 
433
433
  /* ========================================
@@ -447,27 +447,27 @@
447
447
 
448
448
  .semiont-static-badge-planned {
449
449
  background: rgba(251, 146, 60, 0.1);
450
- color: var(--semiont-color-warning-700);
451
- border: 1px solid var(--semiont-color-warning-200);
450
+ color: var(--semiont-color-amber-700);
451
+ border: 1px solid var(--semiont-color-amber-200);
452
452
  }
453
453
 
454
454
  .semiont-static-badge-active {
455
455
  background: rgba(34, 197, 94, 0.1);
456
- color: var(--semiont-color-success-700);
457
- border: 1px solid var(--semiont-color-success-200);
456
+ color: var(--semiont-color-green-700);
457
+ border: 1px solid var(--semiont-color-green-200);
458
458
  }
459
459
 
460
460
  /* Dark mode */
461
461
  [data-theme="dark"] .semiont-static-badge-planned {
462
462
  background: rgba(251, 146, 60, 0.15);
463
- color: var(--semiont-color-warning-400);
464
- border-color: var(--semiont-color-warning-600);
463
+ color: var(--semiont-color-amber-400);
464
+ border-color: var(--semiont-color-amber-600);
465
465
  }
466
466
 
467
467
  [data-theme="dark"] .semiont-static-badge-active {
468
468
  background: rgba(34, 197, 94, 0.15);
469
- color: var(--semiont-color-success-400);
470
- border-color: var(--semiont-color-success-600);
469
+ color: var(--semiont-color-green-400);
470
+ border-color: var(--semiont-color-green-600);
471
471
  }
472
472
 
473
473
  /* ========================================
@@ -33,8 +33,8 @@
33
33
  --semiont-motivation-comment-panel-bg-dark: rgba(55, 65, 81, 0.2);
34
34
 
35
35
  /* Button gradients */
36
- --semiont-motivation-comment-button-bg: linear-gradient(to right, var(--semiont-color-purple-600), var(--semiont-color-indigo-600));
37
- --semiont-motivation-comment-button-bg-hover: linear-gradient(to right, var(--semiont-color-purple-700), var(--semiont-color-indigo-700));
36
+ --semiont-motivation-comment-button-bg: linear-gradient(to right, var(--semiont-color-purple-600), var(--semiont-color-blue-600));
37
+ --semiont-motivation-comment-button-bg-hover: linear-gradient(to right, var(--semiont-color-purple-700), var(--semiont-color-blue-700));
38
38
  }
39
39
 
40
40
  /* ============================================
@@ -14,8 +14,8 @@
14
14
  /* Light theme colors */
15
15
  --semiont-motivation-reference-primary: var(--semiont-color-cyan-600);
16
16
  --semiont-motivation-reference-secondary: var(--semiont-color-blue-600);
17
- --semiont-motivation-reference-bg: linear-gradient(to right, var(--semiont-color-cyan-200, #a5f3fc), var(--semiont-color-blue-200));
18
- --semiont-motivation-reference-bg-hover: linear-gradient(to right, var(--semiont-color-cyan-300, #67e8f9), var(--semiont-color-blue-300));
17
+ --semiont-motivation-reference-bg: linear-gradient(to right, var(--semiont-color-cyan-200), var(--semiont-color-blue-200));
18
+ --semiont-motivation-reference-bg-hover: linear-gradient(to right, var(--semiont-color-cyan-300), var(--semiont-color-blue-300));
19
19
  --semiont-motivation-reference-border: rgba(6, 182, 212, 0.3);
20
20
  --semiont-motivation-reference-border-hover: rgba(6, 182, 212, 0.5);
21
21
  --semiont-motivation-reference-text: var(--semiont-color-gray-900);
@@ -405,12 +405,12 @@
405
405
 
406
406
  /* Login form pulsing border for first-launch nudge */
407
407
  @keyframes semiont-pulse-border {
408
- 0%, 100% { border-color: var(--semiont-color-primary-300, #93c5fd); }
409
- 50% { border-color: var(--semiont-color-primary-600, #2563eb); }
408
+ 0%, 100% { border-color: var(--semiont-color-primary-300); }
409
+ 50% { border-color: var(--semiont-color-primary-600); }
410
410
  }
411
411
 
412
412
  .semiont-panel__login-form--pulsing {
413
- border: 2px solid var(--semiont-color-primary-400, #60a5fa);
414
- border-radius: var(--semiont-panel-border-radius, 0.5rem);
413
+ border: 2px solid var(--semiont-color-primary-400);
414
+ border-radius: var(--semiont-panel-border-radius);
415
415
  animation: semiont-pulse-border 2s ease-in-out infinite;
416
416
  }
@@ -124,7 +124,7 @@
124
124
  .semiont-transition {
125
125
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
126
126
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
127
- transition-duration: var(--semiont-duration-base, 150ms);
127
+ transition-duration: var(--semiont-duration-base);
128
128
  }
129
129
 
130
130
  .semiont-transition-none {
@@ -134,58 +134,58 @@
134
134
  .semiont-transition-all {
135
135
  transition-property: all;
136
136
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
137
- transition-duration: var(--semiont-duration-base, 150ms);
137
+ transition-duration: var(--semiont-duration-base);
138
138
  }
139
139
 
140
140
  .semiont-transition-colors {
141
141
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
142
142
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
143
- transition-duration: var(--semiont-duration-base, 150ms);
143
+ transition-duration: var(--semiont-duration-base);
144
144
  }
145
145
 
146
146
  .semiont-transition-opacity {
147
147
  transition-property: opacity;
148
148
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
149
- transition-duration: var(--semiont-duration-base, 150ms);
149
+ transition-duration: var(--semiont-duration-base);
150
150
  }
151
151
 
152
152
  .semiont-transition-transform {
153
153
  transition-property: transform;
154
154
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
155
- transition-duration: var(--semiont-duration-base, 150ms);
155
+ transition-duration: var(--semiont-duration-base);
156
156
  }
157
157
 
158
158
  /* Animation classes */
159
159
  .semiont-animate-fade-in {
160
- animation: fadeIn var(--semiont-duration-base, 150ms) ease-in-out;
160
+ animation: fadeIn var(--semiont-duration-base) ease-in-out;
161
161
  }
162
162
 
163
163
  .semiont-animate-fade-out {
164
- animation: fadeOut var(--semiont-duration-base, 150ms) ease-in-out;
164
+ animation: fadeOut var(--semiont-duration-base) ease-in-out;
165
165
  }
166
166
 
167
167
  .semiont-animate-slide-in-up {
168
- animation: slideInUp var(--semiont-duration-moderate, 300ms) ease-out;
168
+ animation: slideInUp var(--semiont-duration-base) ease-out;
169
169
  }
170
170
 
171
171
  .semiont-animate-slide-in-down {
172
- animation: slideInDown var(--semiont-duration-moderate, 300ms) ease-out;
172
+ animation: slideInDown var(--semiont-duration-base) ease-out;
173
173
  }
174
174
 
175
175
  .semiont-animate-slide-in-left {
176
- animation: slideInLeft var(--semiont-duration-moderate, 300ms) ease-out;
176
+ animation: slideInLeft var(--semiont-duration-base) ease-out;
177
177
  }
178
178
 
179
179
  .semiont-animate-slide-in-right {
180
- animation: slideInRight var(--semiont-duration-moderate, 300ms) ease-out;
180
+ animation: slideInRight var(--semiont-duration-base) ease-out;
181
181
  }
182
182
 
183
183
  .semiont-animate-scale-in {
184
- animation: scaleIn var(--semiont-duration-base, 150ms) ease-out;
184
+ animation: scaleIn var(--semiont-duration-base) ease-out;
185
185
  }
186
186
 
187
187
  .semiont-animate-scale-out {
188
- animation: scaleOut var(--semiont-duration-base, 150ms) ease-in;
188
+ animation: scaleOut var(--semiont-duration-base) ease-in;
189
189
  }
190
190
 
191
191
  .semiont-animate-spin {
@@ -299,13 +299,13 @@
299
299
  .semiont-motion-safe-transition-all {
300
300
  transition-property: all;
301
301
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
302
- transition-duration: var(--semiont-duration-base, 150ms);
302
+ transition-duration: var(--semiont-duration-base);
303
303
  }
304
304
 
305
305
  .semiont-motion-safe-transition-transform {
306
306
  transition-property: transform;
307
307
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
308
- transition-duration: var(--semiont-duration-base, 150ms);
308
+ transition-duration: var(--semiont-duration-base);
309
309
  }
310
310
  }
311
311