@semiont/react-ui 0.5.26 → 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 (242) hide show
  1. package/README.md +28 -4
  2. package/dist/{PdfAnnotationCanvas.client-6ZSF6ZTJ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +33 -6
  3. package/dist/{PdfAnnotationCanvas.client-JCJMXWWJ.css → PdfAnnotationCanvas.client-TGDPPGXJ.css} +6 -1
  4. package/dist/{ar-YTUUGK2O.js → ar-FABPTJVB.js} +88 -26
  5. package/dist/{bn-ASGNGPMD.js → bn-LKP6BDMH.js} +88 -26
  6. package/dist/{chunk-L3WKRWP3.js → chunk-2GOHTTHW.js} +88 -26
  7. package/dist/{chunk-OBQOZCJE.js → chunk-2YHEE63S.js} +31 -31
  8. package/dist/{cs-WMYVAOZY.js → cs-YQYNWHRG.js} +88 -26
  9. package/dist/{da-FSHHN4D6.js → da-QHZOYKRY.js} +88 -26
  10. package/dist/{de-FJFO3TPT.js → de-GKA7RD7J.js} +88 -26
  11. package/dist/{el-XOH4QIAG.js → el-45T257AL.js} +88 -26
  12. package/dist/{en-3FCBW3YD.js → en-RVW5EQ66.js} +4 -2
  13. package/dist/{es-HDX4QI7E.js → es-AVER7RU4.js} +88 -26
  14. package/dist/{fa-DJ6WTFTJ.js → fa-K3VPK3O2.js} +88 -26
  15. package/dist/{fi-4S7H7W7U.js → fi-5NEKF7EB.js} +88 -26
  16. package/dist/{fr-6UJSYHXC.js → fr-FUMNDLBZ.js} +88 -26
  17. package/dist/{he-ZZJOHJX2.js → he-3JGNA3PR.js} +88 -26
  18. package/dist/{hi-HZJTAW7U.js → hi-IJY375DF.js} +88 -26
  19. package/dist/{id-E3I6ILEM.js → id-LWHKJ7ZD.js} +88 -26
  20. package/dist/index.css +380 -524
  21. package/dist/index.d.ts +318 -192
  22. package/dist/index.js +2960 -2421
  23. package/dist/integrations/css-modules-helper.js +1 -3
  24. package/dist/{it-ULAPAD7U.js → it-EBVOV4RW.js} +88 -26
  25. package/dist/{ja-SAHZQ4CZ.js → ja-KWRIX2NF.js} +88 -26
  26. package/dist/{ko-DFU2ADQ4.js → ko-L5CJMFDS.js} +88 -26
  27. package/dist/{ms-NWL76XNT.js → ms-LSNMJDR2.js} +88 -26
  28. package/dist/{nl-ATCM76WN.js → nl-ZB32MB7U.js} +88 -26
  29. package/dist/{no-UIAQ5RJF.js → no-4GTNY7M5.js} +88 -26
  30. package/dist/{pl-NOECQDXD.js → pl-LUHBLTHB.js} +88 -26
  31. package/dist/{pt-SVWIIUPC.js → pt-C4H3UNQ6.js} +88 -26
  32. package/dist/{ro-NR2DRTZG.js → ro-GGITS5V4.js} +88 -26
  33. package/dist/{sv-STVJ37JP.js → sv-T56RTLBP.js} +88 -26
  34. package/dist/test-utils.css +1 -1
  35. package/dist/test-utils.d.ts +1 -1
  36. package/dist/test-utils.js +2 -2
  37. package/dist/{th-UWQR4K54.js → th-22X2IWCB.js} +88 -26
  38. package/dist/{tr-WNXZSD27.js → tr-77TFG62H.js} +88 -26
  39. package/dist/{uk-SJRMB47Y.js → uk-MIIL6NXT.js} +88 -26
  40. package/dist/{vi-TMRQFES2.js → vi-7JYOIGNV.js} +88 -26
  41. package/dist/{zh-QOL3LYEM.js → zh-ID6DRYU4.js} +88 -26
  42. package/package.json +11 -10
  43. package/src/components/AssistProgress.tsx +178 -134
  44. package/src/components/CodeMirrorRenderer.tsx +8 -8
  45. package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
  46. package/src/components/ProtectedErrorBoundary.css +3 -1
  47. package/src/components/StatusDisplay.css +1 -1
  48. package/src/components/Toast.css +3 -1
  49. package/src/components/__tests__/AssistProgress.test.tsx +268 -141
  50. package/src/components/annotation/annotation-entries.css +0 -4
  51. package/src/components/annotation/references.css +0 -4
  52. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  53. package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
  54. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
  55. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
  56. package/src/components/layout/Header.css +0 -4
  57. package/src/components/layout/LeftSidebar.css +0 -4
  58. package/src/components/modals/ComposeStep.tsx +218 -0
  59. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  60. package/src/components/modals/ConfigureGenerationStep.tsx +131 -54
  61. package/src/components/modals/ConfigureSearchStep.tsx +42 -40
  62. package/src/components/modals/ContextSummary.tsx +185 -72
  63. package/src/components/modals/CorpusPane.tsx +75 -0
  64. package/src/components/modals/DiscardPrompt.tsx +38 -0
  65. package/src/components/modals/GatherContextStep.tsx +237 -107
  66. package/src/components/modals/ReferenceWizardModal.tsx +287 -60
  67. package/src/components/modals/ResourceGenerateModal.tsx +233 -109
  68. package/src/components/modals/SearchModal.css +335 -111
  69. package/src/components/modals/SearchModal.tsx +1 -1
  70. package/src/components/modals/SearchResultsStep.tsx +63 -88
  71. package/src/components/modals/WizardFooter.tsx +69 -0
  72. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  73. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  74. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +290 -0
  75. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +95 -0
  76. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  77. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +635 -0
  78. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +277 -85
  79. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
  80. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  81. package/src/components/modals/__tests__/WizardFooter.test.tsx +344 -0
  82. package/src/components/modals/modals.css +16 -42
  83. package/src/components/navigation/NavigationTabs.css +0 -20
  84. package/src/components/pdf-annotation/PdfAnnotationCanvas.css +21 -2
  85. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +36 -3
  86. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +41 -6
  87. package/src/components/resource/AnnotateView.tsx +19 -8
  88. package/src/components/resource/AnnotationHistory.tsx +2 -0
  89. package/src/components/resource/BrowseView.tsx +20 -9
  90. package/src/components/resource/ResourceViewer.tsx +27 -28
  91. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  92. package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
  93. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
  94. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  95. package/src/components/resource/__tests__/BrowseView.test.tsx +38 -12
  96. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
  97. package/src/components/resource/annotate-renderers.tsx +3 -3
  98. package/src/components/resource/event-formatting.ts +0 -3
  99. package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
  100. package/src/components/resource/panels/AssessmentPanel.css +5 -6
  101. package/src/components/resource/panels/AssistSection.css +95 -263
  102. package/src/components/resource/panels/AssistSection.tsx +4 -4
  103. package/src/components/resource/panels/AssistShell.tsx +12 -4
  104. package/src/components/resource/panels/CollaborationPanel.css +47 -22
  105. package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
  106. package/src/components/resource/panels/CommentEntry.tsx +1 -1
  107. package/src/components/resource/panels/CommentsPanel.css +0 -16
  108. package/src/components/resource/panels/HighlightEntry.tsx +1 -1
  109. package/src/components/resource/panels/HighlightPanel.css +0 -7
  110. package/src/components/resource/panels/JsonLdPanel.css +0 -4
  111. package/src/components/resource/panels/ReferenceEntry.tsx +10 -3
  112. package/src/components/resource/panels/ReferencesPanel.css +0 -12
  113. package/src/components/resource/panels/ReferencesPanel.tsx +16 -20
  114. package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  116. package/src/components/resource/panels/StatisticsPanel.css +0 -20
  117. package/src/components/resource/panels/TagEntry.tsx +1 -1
  118. package/src/components/resource/panels/TaggingPanel.css +0 -4
  119. package/src/components/resource/panels/TaggingPanel.tsx +2 -10
  120. package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
  121. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  122. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
  123. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
  124. package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
  125. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
  126. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
  127. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
  128. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
  129. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -1
  130. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
  131. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +67 -54
  132. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +185 -7
  133. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
  134. package/src/components/settings/SettingsPanel.css +0 -4
  135. package/src/features/auth/auth.css +2 -26
  136. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  137. package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
  138. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -76
  139. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -48
  140. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
  141. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
  142. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
  143. package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
  144. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
  145. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +166 -71
  146. package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
  147. package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
  148. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
  149. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
  150. package/src/styles/core/buttons.css +3 -1
  151. package/src/styles/core/forms.css +35 -29
  152. package/src/styles/core/indicators.css +0 -4
  153. package/src/styles/core/inputs.css +0 -4
  154. package/src/styles/core/progress.css +2 -34
  155. package/src/styles/core/sliders.css +0 -4
  156. package/src/styles/core/toggles.css +0 -4
  157. package/src/styles/features/resource-discovery.css +13 -0
  158. package/src/styles/features/resource-viewer.css +0 -42
  159. package/src/styles/features/resource.css +0 -4
  160. package/src/styles/motivations/motivation-assessment.css +2 -2
  161. package/src/styles/panels/history-panel.css +0 -40
  162. package/src/styles/panels/user-panel.css +0 -3
  163. package/src/styles/patterns/panel-helpers.css +0 -27
  164. package/src/styles/patterns/panel-sections.css +0 -59
  165. package/src/styles/patterns/panels-base.css +0 -32
  166. package/translations/ar.json +84 -24
  167. package/translations/bn.json +84 -24
  168. package/translations/cs.json +84 -24
  169. package/translations/da.json +84 -24
  170. package/translations/de.json +84 -24
  171. package/translations/el.json +84 -24
  172. package/translations/en.json +84 -24
  173. package/translations/es.json +84 -24
  174. package/translations/fa.json +84 -24
  175. package/translations/fi.json +84 -24
  176. package/translations/fr.json +84 -24
  177. package/translations/he.json +84 -24
  178. package/translations/hi.json +84 -24
  179. package/translations/id.json +84 -24
  180. package/translations/it.json +84 -24
  181. package/translations/ja.json +84 -24
  182. package/translations/ko.json +84 -24
  183. package/translations/ms.json +84 -24
  184. package/translations/nl.json +84 -24
  185. package/translations/no.json +84 -24
  186. package/translations/pl.json +84 -24
  187. package/translations/pt.json +84 -24
  188. package/translations/ro.json +84 -24
  189. package/translations/sv.json +84 -24
  190. package/translations/th.json +84 -24
  191. package/translations/tr.json +84 -24
  192. package/translations/uk.json +84 -24
  193. package/translations/vi.json +84 -24
  194. package/translations/zh.json +84 -24
  195. package/dist/PdfAnnotationCanvas.client-6ZSF6ZTJ.js.map +0 -1
  196. package/dist/PdfAnnotationCanvas.client-JCJMXWWJ.css.map +0 -1
  197. package/dist/ar-YTUUGK2O.js.map +0 -1
  198. package/dist/bn-ASGNGPMD.js.map +0 -1
  199. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  200. package/dist/chunk-L3WKRWP3.js.map +0 -1
  201. package/dist/chunk-O23TLLXW.js.map +0 -1
  202. package/dist/chunk-OBQOZCJE.js.map +0 -1
  203. package/dist/chunk-QT7LYM72.js.map +0 -1
  204. package/dist/chunk-VXASXU4K.js.map +0 -1
  205. package/dist/chunk-XUDKYAVC.js +0 -21
  206. package/dist/chunk-XUDKYAVC.js.map +0 -1
  207. package/dist/cs-WMYVAOZY.js.map +0 -1
  208. package/dist/da-FSHHN4D6.js.map +0 -1
  209. package/dist/de-FJFO3TPT.js.map +0 -1
  210. package/dist/el-XOH4QIAG.js.map +0 -1
  211. package/dist/en-3FCBW3YD.js.map +0 -1
  212. package/dist/es-HDX4QI7E.js.map +0 -1
  213. package/dist/fa-DJ6WTFTJ.js.map +0 -1
  214. package/dist/fi-4S7H7W7U.js.map +0 -1
  215. package/dist/fr-6UJSYHXC.js.map +0 -1
  216. package/dist/he-ZZJOHJX2.js.map +0 -1
  217. package/dist/hi-HZJTAW7U.js.map +0 -1
  218. package/dist/id-E3I6ILEM.js.map +0 -1
  219. package/dist/index.css.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/integrations/css-modules-helper.js.map +0 -1
  222. package/dist/integrations/styled-components-theme.js.map +0 -1
  223. package/dist/it-ULAPAD7U.js.map +0 -1
  224. package/dist/ja-SAHZQ4CZ.js.map +0 -1
  225. package/dist/ko-DFU2ADQ4.js.map +0 -1
  226. package/dist/ms-NWL76XNT.js.map +0 -1
  227. package/dist/nl-ATCM76WN.js.map +0 -1
  228. package/dist/no-UIAQ5RJF.js.map +0 -1
  229. package/dist/pl-NOECQDXD.js.map +0 -1
  230. package/dist/pt-SVWIIUPC.js.map +0 -1
  231. package/dist/ro-NR2DRTZG.js.map +0 -1
  232. package/dist/sv-STVJ37JP.js.map +0 -1
  233. package/dist/test-utils.css.map +0 -1
  234. package/dist/test-utils.js.map +0 -1
  235. package/dist/th-UWQR4K54.js.map +0 -1
  236. package/dist/tr-WNXZSD27.js.map +0 -1
  237. package/dist/uk-SJRMB47Y.js.map +0 -1
  238. package/dist/vi-TMRQFES2.js.map +0 -1
  239. package/dist/zh-QOL3LYEM.js.map +0 -1
  240. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  241. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -159
  242. package/src/integrations/tailwind-plugin.cjs +0 -224
@@ -0,0 +1,344 @@
1
+ /**
2
+ * WIZARD-NAVIGATION P1 — one dismissal, one retreat, one advance.
3
+ *
4
+ * The wizard used to offer four ways out of one step (corner ✕, Esc, backdrop, and a
5
+ * footer `✕ Cancel`) and none at all on the step that starts the flow. D1 settled it:
6
+ * the corner control plus Esc and backdrop are the dismissal, and the footer is purely
7
+ * flow — where you came from, where you are going.
8
+ *
9
+ * These assert STRUCTURE, never copy: labels arrive as props and will be revised.
10
+ */
11
+ import { describe, it, expect, vi } from 'vitest';
12
+ import { render, screen } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import '@testing-library/jest-dom';
15
+ import { WizardFooter } from '../WizardFooter';
16
+ import { ConfigureSearchStep } from '../ConfigureSearchStep';
17
+ import { ConfigureGenerationStep } from '../ConfigureGenerationStep';
18
+ import { SearchResultsStep } from '../SearchResultsStep';
19
+ import { ConfigureGatherStep } from '../ConfigureGatherStep';
20
+ import { GatherContextStep } from '../GatherContextStep';
21
+ import type { GatheredContext } from '@semiont/core';
22
+
23
+ /** Anything that reads as "get me out of here" belongs in the corner, not the footer. */
24
+ const DISMISSAL = /cancel|close|dismiss|✕|✖|×/i;
25
+
26
+ const footerOf = (container: HTMLElement) =>
27
+ container.querySelector('.semiont-modal__actions') as HTMLElement | null;
28
+
29
+ const buttonsIn = (footer: HTMLElement) => Array.from(footer.querySelectorAll('button'));
30
+
31
+ describe('WizardFooter', () => {
32
+ it('renders retreat and advance, and nothing that dismisses', () => {
33
+ const { container } = render(
34
+ <WizardFooter
35
+ backLabel="Back"
36
+ onBack={vi.fn()}
37
+ primary={{ label: 'Search', type: 'submit' }}
38
+ />,
39
+ );
40
+ const footer = footerOf(container)!;
41
+ const labels = buttonsIn(footer).map((b) => b.textContent ?? '');
42
+ expect(labels).toHaveLength(2);
43
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
44
+ });
45
+
46
+ it('omits Back entirely when there is nowhere to go back to', () => {
47
+ const { container } = render(
48
+ <WizardFooter primary={{ label: 'Search', type: 'submit' }} />,
49
+ );
50
+ expect(buttonsIn(footerOf(container)!)).toHaveLength(1);
51
+ });
52
+
53
+ it('renders Back alone when the step has no footer-level action', () => {
54
+ // Search results: the action is per row, so the footer carries retreat only.
55
+ const { container } = render(<WizardFooter backLabel="Back" onBack={vi.fn()} />);
56
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent);
57
+ expect(labels).toEqual(['◀ Back']);
58
+ });
59
+
60
+ it('does not stretch its buttons to equal width', () => {
61
+ // `--flex` made retreat and advance read as peers. Retreat is chrome.
62
+ const { container } = render(
63
+ <WizardFooter backLabel="Back" onBack={vi.fn()} primary={{ label: 'Go', type: 'button', onClick: vi.fn() }} />,
64
+ );
65
+ for (const button of buttonsIn(footerOf(container)!)) {
66
+ expect(button.className).not.toContain('semiont-button--flex');
67
+ }
68
+ });
69
+
70
+ it('a pending primary disables itself and Back, and says so', async () => {
71
+ const onBack = vi.fn();
72
+ render(
73
+ <WizardFooter
74
+ backLabel="Back"
75
+ onBack={onBack}
76
+ primary={{ label: 'Search', pendingLabel: 'Searching…', pending: true, type: 'submit' }}
77
+ />,
78
+ );
79
+ expect(screen.getByText(/Searching…/)).toBeInTheDocument();
80
+ await userEvent.click(screen.getByText(/Back/));
81
+ expect(onBack).not.toHaveBeenCalled();
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
+ });
102
+ });
103
+
104
+ // ─────────────────────────────────────────────────────────────────────
105
+ // The same rule, asserted against the real steps — a footer component is
106
+ // only worth having if every step actually uses it.
107
+ // ─────────────────────────────────────────────────────────────────────
108
+ const SEARCH_T = {
109
+ maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
110
+ semanticScoringHelp: 'help', back: 'Back', search: 'Search', searching: 'Searching…',
111
+ searchFailed: 'Search failed',
112
+ };
113
+
114
+ const GEN_T = {
115
+ resourceTitle: 'Title', resourceTitlePlaceholder: '', saveLocation: 'Save location', additionalInstructions: 'Instructions',
116
+ additionalInstructionsPlaceholder: '', language: 'Language', languageHelp: '',
117
+ creativity: 'Creativity', creativityFocused: 'Focused', creativityCreative: 'Creative',
118
+ maxLength: 'Max Length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{max}} by {{model}}.',
119
+ back: 'Back', generate: 'Generate',
120
+ };
121
+
122
+ const CONTEXT_T = {
123
+ sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
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',
130
+ };
131
+
132
+ const RESULTS_T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back', ...CONTEXT_T };
133
+
134
+ const CONTEXT = {
135
+ focus: { kind: 'annotation', annotation: { id: 'a1' }, sourceResource: { '@id': 'r1', name: 'Src' } },
136
+ // `graph` is not optional in practice: ContextSummary hands it straight to
137
+ // `deriveViews`, which dereferences `.nodes`.
138
+ graph: { nodes: [], edges: [] },
139
+ } as unknown as GatheredContext;
140
+
141
+ describe('every step footer follows the grammar (A1, A2)', () => {
142
+ it('ConfigureSearchStep', () => {
143
+ const { container } = render(
144
+ <ConfigureSearchStep
145
+ config={{ limit: 10, useSemanticScoring: true }}
146
+ onConfigChange={vi.fn()}
147
+ onBack={vi.fn()}
148
+ onSearch={vi.fn()}
149
+ translations={SEARCH_T}
150
+ />,
151
+ );
152
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
153
+ expect(labels).toHaveLength(2);
154
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
155
+ });
156
+
157
+ it('SearchResultsStep — footer is retreat only; Link lives on the rows', () => {
158
+ const { container } = render(
159
+ <SearchResultsStep
160
+ results={[{ id: 'r1', name: 'Black Sea', score: 54.22 } as never]}
161
+ onBack={vi.fn()}
162
+ onLink={vi.fn()}
163
+ translations={RESULTS_T}
164
+ />,
165
+ );
166
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
167
+ expect(labels).toHaveLength(1);
168
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
169
+ expect(screen.getByText(/Link/)).toBeInTheDocument();
170
+ });
171
+
172
+ it('ConfigureGenerationStep', () => {
173
+ const { container } = render(
174
+ <ConfigureGenerationStep
175
+ context={CONTEXT}
176
+ config={{
177
+ title: 'Caspian Sea', storagePath: '', prompt: '', language: 'en',
178
+ temperature: 0.7, maxTokensText: '500',
179
+ }}
180
+ onConfigChange={vi.fn()}
181
+ onBack={vi.fn()}
182
+ onGenerate={vi.fn()}
183
+ translations={GEN_T}
184
+ />,
185
+ );
186
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
187
+ expect(labels).toHaveLength(2);
188
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
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
+ });
273
+ });
274
+
275
+ // ─────────────────────────────────────────────────────────────────────
276
+ // WIZARD-NAVIGATION P3 — the exits, pinned.
277
+ //
278
+ // HeadlessUI already wires Esc and the backdrop to `onClose`; these are the
279
+ // guard, not the fix. What they prevent is a later change that "improves" Esc
280
+ // into a step-back, or that disables the way out while a request is in flight.
281
+ // ─────────────────────────────────────────────────────────────────────
282
+ describe('the way out (A4, A5)', () => {
283
+ it('a pending step never disables the corner control — only the footer', () => {
284
+ // D5: dismissal must survive a search that never returns. The footer's own
285
+ // buttons DO disable (pressing Search twice is not a feature); the corner is
286
+ // not the footer's to disable, which is why it does not live there.
287
+ const { container } = render(
288
+ <ConfigureSearchStep
289
+ config={{ limit: 10, useSemanticScoring: true }}
290
+ onConfigChange={vi.fn()}
291
+ isSearching
292
+ onBack={vi.fn()}
293
+ onSearch={vi.fn()}
294
+ translations={SEARCH_T}
295
+ />,
296
+ );
297
+ const footer = footerOf(container)!;
298
+ for (const button of buttonsIn(footer)) expect(button).toBeDisabled();
299
+ // …and the step renders no dismissal of its own to be disabled with them.
300
+ expect(buttonsIn(footer).map((b) => b.textContent ?? '').filter((l) => DISMISSAL.test(l))).toEqual([]);
301
+ });
302
+
303
+ it('Back does not fire while the step is pending', async () => {
304
+ const onBack = vi.fn();
305
+ render(
306
+ <ConfigureSearchStep
307
+ config={{ limit: 10, useSemanticScoring: true }}
308
+ onConfigChange={vi.fn()}
309
+ isSearching
310
+ onBack={onBack}
311
+ onSearch={vi.fn()}
312
+ translations={SEARCH_T}
313
+ />,
314
+ );
315
+ await userEvent.click(screen.getByText(/Back/));
316
+ expect(onBack).not.toHaveBeenCalled();
317
+ });
318
+ });
319
+
320
+ describe('Back is lossless (A3)', () => {
321
+ it('the step reflects the wizard-owned draft, so remounting restores it', () => {
322
+ // The regression this replaces: `useState` inside the step meant stepping
323
+ // back unmounted it and every choice reverted to the default.
324
+ const config = { limit: 20, useSemanticScoring: false };
325
+ const { unmount } = render(
326
+ <ConfigureSearchStep
327
+ config={config} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
328
+ translations={SEARCH_T}
329
+ />,
330
+ );
331
+ expect((screen.getByLabelText('Max Results') as HTMLSelectElement).value).toBe('20');
332
+ unmount();
333
+
334
+ // Same draft, fresh mount — what "Back then forward" does.
335
+ render(
336
+ <ConfigureSearchStep
337
+ config={config} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
338
+ translations={SEARCH_T}
339
+ />,
340
+ );
341
+ expect((screen.getByLabelText('Max Results') as HTMLSelectElement).value).toBe('20');
342
+ expect((screen.getByLabelText(/Semantic Scoring/) as HTMLInputElement).checked).toBe(false);
343
+ });
344
+ });
@@ -111,11 +111,6 @@
111
111
  background-color: var(--semiont-bg-tertiary);
112
112
  }
113
113
 
114
- [data-theme="dark"] .semiont-modal__close:hover {
115
- color: var(--semiont-text-primary);
116
- background-color: var(--semiont-bg-tertiary);
117
- }
118
-
119
114
  .semiont-modal__close-icon {
120
115
  width: 1.25rem;
121
116
  height: 1.25rem;
@@ -256,6 +251,22 @@
256
251
  gap: 0.5rem;
257
252
  }
258
253
 
254
+ /*
255
+ * The wizard footer (WIZARD-NAVIGATION D2). Retreat sits at the leading edge and
256
+ * advance at the trailing one, and neither stretches — `--flex` on all three
257
+ * buttons is what made "Cancel", "Back" and "Search" read as three peers.
258
+ * `margin-inline-end: auto` rather than `space-between` so a footer with only
259
+ * one child still lands on the correct side.
260
+ */
261
+ .semiont-modal__actions--wizard {
262
+ justify-content: flex-end;
263
+ padding-top: 0.5rem;
264
+ }
265
+
266
+ .semiont-wizard-footer__back {
267
+ margin-inline-end: auto;
268
+ }
269
+
259
270
  /* Chip Styles */
260
271
  .semiont-chip {
261
272
  padding: 0.25rem 0.75rem;
@@ -352,12 +363,6 @@
352
363
  cursor: not-allowed;
353
364
  }
354
365
 
355
- [data-theme="dark"] .semiont-button--disabled,
356
- [data-theme="dark"] .semiont-button[data-disabled="true"] {
357
- background-color: var(--semiont-bg-tertiary);
358
- color: var(--semiont-text-tertiary);
359
- }
360
-
361
366
  /* Keyboard Shortcuts Styles */
362
367
  .semiont-shortcuts {
363
368
  display: flex;
@@ -544,10 +549,6 @@
544
549
  margin: 0;
545
550
  }
546
551
 
547
- [data-theme="dark"] .semiont-jsonld-view__title {
548
- color: var(--semiont-text-primary);
549
- }
550
-
551
552
  .semiont-jsonld-view__back-button,
552
553
  .semiont-jsonld-view__copy-button {
553
554
  padding: 0.5rem 1rem;
@@ -572,12 +573,6 @@
572
573
  border-color: var(--semiont-border-primary);
573
574
  }
574
575
 
575
- [data-theme="dark"] .semiont-jsonld-view__back-button:hover,
576
- [data-theme="dark"] .semiont-jsonld-view__copy-button:hover {
577
- background-color: var(--semiont-bg-secondary);
578
- border-color: var(--semiont-border-secondary);
579
- }
580
-
581
576
  .semiont-jsonld-view__editor {
582
577
  flex: 1;
583
578
  overflow: auto;
@@ -585,10 +580,6 @@
585
580
  background-color: var(--semiont-bg-primary);
586
581
  }
587
582
 
588
- [data-theme="dark"] .semiont-jsonld-view__editor {
589
- background-color: var(--semiont-bg-primary);
590
- }
591
-
592
583
  /* Ensure CodeMirror takes full height */
593
584
  .semiont-jsonld-view__editor .cm-editor {
594
585
  height: 100%;
@@ -620,10 +611,6 @@
620
611
  margin: 0;
621
612
  }
622
613
 
623
- [data-theme="dark"] .semiont-popup-header__title {
624
- color: var(--semiont-text-primary);
625
- }
626
-
627
614
  .semiont-popup-header__subtitle {
628
615
  font-size: 0.875rem;
629
616
  font-weight: normal;
@@ -631,10 +618,6 @@
631
618
  margin-left: 0.5rem;
632
619
  }
633
620
 
634
- [data-theme="dark"] .semiont-popup-header__subtitle {
635
- color: var(--semiont-text-secondary);
636
- }
637
-
638
621
  .semiont-popup-header__close-button {
639
622
  padding: 0.25rem;
640
623
  border-radius: 0.25rem;
@@ -657,15 +640,6 @@
657
640
  outline-offset: 2px;
658
641
  }
659
642
 
660
- [data-theme="dark"] .semiont-popup-header__close-button {
661
- color: var(--semiont-text-secondary);
662
- }
663
-
664
- [data-theme="dark"] .semiont-popup-header__close-button:hover {
665
- background-color: var(--semiont-bg-tertiary);
666
- color: var(--semiont-text-primary);
667
- }
668
-
669
643
  [data-theme="dark"] .semiont-popup-header__close-button:focus-visible {
670
644
  outline-color: var(--semiont-color-primary-400);
671
645
  }
@@ -36,10 +36,6 @@
36
36
  align-items: center;
37
37
  }
38
38
 
39
- [data-theme="dark"] .semiont-nav-section__header-button {
40
- color: inherit;
41
- }
42
-
43
39
  .semiont-nav-section__header-button:hover {
44
40
  opacity: 0.8;
45
41
  }
@@ -68,10 +64,6 @@
68
64
  text-align: left;
69
65
  }
70
66
 
71
- [data-theme="dark"] .semiont-nav-section__header-text {
72
- color: inherit;
73
- }
74
-
75
67
  .semiont-nav-section__header-icon {
76
68
  display: flex;
77
69
  align-items: center;
@@ -99,14 +91,6 @@
99
91
  outline-offset: 2px;
100
92
  }
101
93
 
102
- [data-theme="dark"] .semiont-nav-section__header-icon {
103
- color: var(--semiont-text-secondary);
104
- }
105
-
106
- [data-theme="dark"] .semiont-nav-section__header-icon:hover {
107
- background: var(--semiont-bg-tertiary);
108
- }
109
-
110
94
  [data-theme="dark"] .semiont-nav-section__header-icon:focus-visible {
111
95
  outline-color: var(--semiont-color-primary-400);
112
96
  }
@@ -361,10 +345,6 @@
361
345
  outline-color: var(--semiont-color-primary-400);
362
346
  }
363
347
 
364
- [data-theme="dark"] .semiont-resource-tab__link:visited {
365
- color: inherit !important;
366
- }
367
-
368
348
  /* Resource tab icon (emoji) */
369
349
  .semiont-resource-tab__icon {
370
350
  flex-shrink: 0;
@@ -101,9 +101,16 @@
101
101
  scrollbar-width: none; /* standard; Firefox + recent Chromium */
102
102
  }
103
103
 
104
- /* WebKit/Safari, which has no `scrollbar-width` support to rely on. */
104
+ /*
105
+ * WebKit/Safari, which has no `scrollbar-width` support to rely on.
106
+ *
107
+ * This hides a SCROLLBAR, not content: `scrollbar-width: none` above already
108
+ * does the same for every other engine, the rail still scrolls by wheel and
109
+ * drag, and each page is a focusable button — so tabbing through the rail
110
+ * scrolls it, which is the keyboard path the rule is protecting.
111
+ */
105
112
  .semiont-pdf-annotation-canvas__strip::-webkit-scrollbar {
106
- display: none;
113
+ display: none; /* stylelint-disable-line semiont/accessibility */
107
114
  }
108
115
 
109
116
  /*
@@ -160,6 +167,18 @@
160
167
  font-weight: var(--semiont-font-semibold);
161
168
  }
162
169
 
170
+ /*
171
+ * Palette tokens are one fixed hue in both themes, so this marker needs a real
172
+ * variant — the accent steps lighter against a dark surface (the treatment
173
+ * `.semiont-checkbox:checked` uses), and the number flips to dark type, which
174
+ * at this size reads far better on that lighter blue than white does.
175
+ */
176
+ [data-theme="dark"] .semiont-pdf-annotation-canvas__strip-page[aria-current="page"] {
177
+ border-color: var(--semiont-color-primary-400);
178
+ background: var(--semiont-color-primary-400);
179
+ color: var(--semiont-color-neutral-900);
180
+ }
181
+
163
182
  .semiont-pdf-annotation-canvas__strip-page:focus-visible {
164
183
  outline: 2px solid var(--semiont-focus-ring);
165
184
  outline-offset: 1px;
@@ -353,7 +353,7 @@ function PdfPageView({
353
353
  });
354
354
 
355
355
  if (hit) {
356
- session?.client.browse.click(hit.annId, hit.annotation.motivation, hitRect);
356
+ session?.client.browse.click(hit.annId, hitRect);
357
357
  setIsDrawing(false);
358
358
  setSelection(null);
359
359
  return;
@@ -515,7 +515,7 @@ function PdfPageView({
515
515
  cursor: 'pointer',
516
516
  opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7
517
517
  }}
518
- onClick={(e) => session?.client.browse.click(r.annId, r.annotation.motivation, e.currentTarget.getBoundingClientRect())}
518
+ onClick={(e) => session?.client.browse.click(r.annId, e.currentTarget.getBoundingClientRect())}
519
519
  onMouseEnter={() => handleMouseEnter(r.annId)}
520
520
  onMouseLeave={handleMouseLeave}
521
521
  />
@@ -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.