@semiont/react-ui 0.5.25 → 0.5.27

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 (267) hide show
  1. package/README.md +28 -4
  2. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js +677 -0
  3. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +1 -0
  4. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css +185 -0
  5. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +1 -0
  6. package/dist/{ar-CULRHLNG.js → ar-OU3DQGC6.js} +65 -7
  7. package/dist/ar-OU3DQGC6.js.map +1 -0
  8. package/dist/{bn-HYR7CEL2.js → bn-PHWCR6AZ.js} +65 -7
  9. package/dist/bn-PHWCR6AZ.js.map +1 -0
  10. package/dist/chunk-5VDTWRXJ.js +287 -0
  11. package/dist/chunk-5VDTWRXJ.js.map +1 -0
  12. package/dist/{chunk-TG2XQEC4.js → chunk-BTUG3VJJ.js} +18 -298
  13. package/dist/chunk-BTUG3VJJ.js.map +1 -0
  14. package/dist/{chunk-NH6L2GIL.js → chunk-PCRRBU7H.js} +65 -7
  15. package/dist/chunk-PCRRBU7H.js.map +1 -0
  16. package/dist/{cs-YBRWY7NH.js → cs-NEXYZ62I.js} +65 -7
  17. package/dist/cs-NEXYZ62I.js.map +1 -0
  18. package/dist/{da-PHDEOBV3.js → da-FAT43YCU.js} +65 -7
  19. package/dist/da-FAT43YCU.js.map +1 -0
  20. package/dist/{de-O4UCCVLJ.js → de-KRQ3WBCU.js} +65 -7
  21. package/dist/de-KRQ3WBCU.js.map +1 -0
  22. package/dist/{el-MVGO6WZV.js → el-Q4IU3EKC.js} +65 -7
  23. package/dist/el-Q4IU3EKC.js.map +1 -0
  24. package/dist/{en-2KQILFKQ.js → en-2KADE7N4.js} +6 -2
  25. package/dist/{es-USSLXQY7.js → es-2IO3R2MK.js} +65 -7
  26. package/dist/es-2IO3R2MK.js.map +1 -0
  27. package/dist/{fa-WMAREFWY.js → fa-Q5GOQFXX.js} +65 -7
  28. package/dist/fa-Q5GOQFXX.js.map +1 -0
  29. package/dist/{fi-B3NBNOTS.js → fi-R6NMRWBE.js} +65 -7
  30. package/dist/fi-R6NMRWBE.js.map +1 -0
  31. package/dist/{fr-TXYK5T5W.js → fr-QY4D5E6J.js} +65 -7
  32. package/dist/fr-QY4D5E6J.js.map +1 -0
  33. package/dist/{he-JCJACW5D.js → he-X6Y77XHY.js} +65 -7
  34. package/dist/he-X6Y77XHY.js.map +1 -0
  35. package/dist/{hi-VOQLZNJU.js → hi-XL7IOEQM.js} +65 -7
  36. package/dist/hi-XL7IOEQM.js.map +1 -0
  37. package/dist/{id-BFN5RZNZ.js → id-QZ4ZZS67.js} +65 -7
  38. package/dist/id-QZ4ZZS67.js.map +1 -0
  39. package/dist/index.css +241 -508
  40. package/dist/index.css.map +1 -1
  41. package/dist/index.d.ts +155 -54
  42. package/dist/index.js +1815 -1642
  43. package/dist/index.js.map +1 -1
  44. package/dist/integrations/css-modules-helper.js +1 -3
  45. package/dist/integrations/css-modules-helper.js.map +1 -1
  46. package/dist/{it-TWWDHKKM.js → it-ME2Q7OB6.js} +65 -7
  47. package/dist/it-ME2Q7OB6.js.map +1 -0
  48. package/dist/{ja-YRQLEBFM.js → ja-HX7QASRI.js} +65 -7
  49. package/dist/ja-HX7QASRI.js.map +1 -0
  50. package/dist/{ko-PG6B3CRB.js → ko-EQO5AHAV.js} +65 -7
  51. package/dist/ko-EQO5AHAV.js.map +1 -0
  52. package/dist/{ms-7EZECVKM.js → ms-UILFSF3I.js} +65 -7
  53. package/dist/ms-UILFSF3I.js.map +1 -0
  54. package/dist/{nl-HP753YAP.js → nl-NNI45PMK.js} +65 -7
  55. package/dist/nl-NNI45PMK.js.map +1 -0
  56. package/dist/{no-JUD5WX3N.js → no-MAL5F6P6.js} +65 -7
  57. package/dist/no-MAL5F6P6.js.map +1 -0
  58. package/dist/{pl-RVUL4CBM.js → pl-4RMY6JAJ.js} +65 -7
  59. package/dist/pl-4RMY6JAJ.js.map +1 -0
  60. package/dist/{pt-RPOVD4QM.js → pt-VQH2X3QQ.js} +65 -7
  61. package/dist/pt-VQH2X3QQ.js.map +1 -0
  62. package/dist/{ro-DMYWQTRD.js → ro-XXCQO2OG.js} +65 -7
  63. package/dist/ro-XXCQO2OG.js.map +1 -0
  64. package/dist/{sv-DMZ4FGXD.js → sv-7Z6EJ6U4.js} +65 -7
  65. package/dist/sv-7Z6EJ6U4.js.map +1 -0
  66. package/dist/test-utils.css +1 -1
  67. package/dist/test-utils.css.map +1 -1
  68. package/dist/test-utils.d.ts +1 -1
  69. package/dist/test-utils.js +5 -3
  70. package/dist/test-utils.js.map +1 -1
  71. package/dist/{th-4HVBJBQ2.js → th-L3DXXYVQ.js} +65 -7
  72. package/dist/th-L3DXXYVQ.js.map +1 -0
  73. package/dist/{tr-6BDPU5ML.js → tr-P3DFVLFJ.js} +65 -7
  74. package/dist/tr-P3DFVLFJ.js.map +1 -0
  75. package/dist/{uk-TBZBT2TL.js → uk-7C5D3PXS.js} +65 -7
  76. package/dist/uk-7C5D3PXS.js.map +1 -0
  77. package/dist/{vi-UMKZKZL3.js → vi-ZLTLEO7P.js} +65 -7
  78. package/dist/vi-ZLTLEO7P.js.map +1 -0
  79. package/dist/{zh-YBJWJJYB.js → zh-5FHVWDIN.js} +65 -7
  80. package/dist/zh-5FHVWDIN.js.map +1 -0
  81. package/package.json +7 -7
  82. package/src/components/AssistProgress.tsx +144 -134
  83. package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
  84. package/src/components/ProtectedErrorBoundary.css +3 -1
  85. package/src/components/StatusDisplay.css +35 -35
  86. package/src/components/Toast.css +3 -1
  87. package/src/components/__tests__/AssistProgress.test.tsx +216 -148
  88. package/src/components/annotation/annotation-entries.css +0 -4
  89. package/src/components/annotation/annotations.css +1 -1
  90. package/src/components/annotation/references.css +0 -4
  91. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  92. package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
  93. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
  94. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
  95. package/src/components/layout/Header.css +0 -4
  96. package/src/components/layout/LeftSidebar.css +0 -4
  97. package/src/components/modals/ConfigureGenerationStep.tsx +109 -48
  98. package/src/components/modals/ConfigureSearchStep.tsx +23 -40
  99. package/src/components/modals/ReferenceWizardModal.tsx +55 -16
  100. package/src/components/modals/ResourceGenerateModal.tsx +24 -8
  101. package/src/components/modals/ResourceSearchModal.tsx +1 -1
  102. package/src/components/modals/SearchModal.css +5 -17
  103. package/src/components/modals/SearchModal.tsx +1 -1
  104. package/src/components/modals/SearchResultsStep.tsx +2 -20
  105. package/src/components/modals/WizardFooter.tsx +63 -0
  106. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +255 -0
  107. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +72 -0
  108. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +256 -0
  109. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +1 -0
  110. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +6 -2
  111. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
  112. package/src/components/modals/__tests__/WizardFooter.test.tsx +235 -0
  113. package/src/components/modals/modals.css +16 -42
  114. package/src/components/navigation/NavigationTabs.css +0 -20
  115. package/src/components/pdf-annotation/PdfAnnotationCanvas.css +162 -6
  116. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +623 -232
  117. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +764 -7
  118. package/src/components/pdf-annotation/__tests__/estimate-slot-height.test.ts +53 -0
  119. package/src/components/pdf-annotation/estimate-slot-height.ts +31 -0
  120. package/src/components/resource/AnnotateView.tsx +16 -5
  121. package/src/components/resource/AnnotationHistory.tsx +2 -0
  122. package/src/components/resource/BrowseView.tsx +15 -4
  123. package/src/components/resource/ResourceViewer.tsx +23 -24
  124. package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
  125. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
  126. package/src/components/resource/__tests__/BrowseView.test.tsx +29 -3
  127. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
  128. package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +33 -0
  129. package/src/components/resource/annotate-renderers.tsx +10 -1
  130. package/src/components/resource/browse-renderers.tsx +10 -1
  131. package/src/components/resource/event-formatting.ts +0 -3
  132. package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
  133. package/src/components/resource/panels/AssessmentPanel.css +5 -6
  134. package/src/components/resource/panels/AssistSection.css +68 -271
  135. package/src/components/resource/panels/AssistSection.tsx +4 -4
  136. package/src/components/resource/panels/AssistShell.tsx +12 -4
  137. package/src/components/resource/panels/CollaborationPanel.css +47 -22
  138. package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
  139. package/src/components/resource/panels/CommentEntry.tsx +1 -1
  140. package/src/components/resource/panels/CommentsPanel.css +0 -16
  141. package/src/components/resource/panels/HighlightEntry.tsx +1 -1
  142. package/src/components/resource/panels/HighlightPanel.css +0 -7
  143. package/src/components/resource/panels/JsonLdPanel.css +0 -4
  144. package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
  145. package/src/components/resource/panels/ReferencesPanel.css +0 -12
  146. package/src/components/resource/panels/ReferencesPanel.tsx +12 -20
  147. package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
  148. package/src/components/resource/panels/ResourceInfoPanel.tsx +1 -1
  149. package/src/components/resource/panels/StatisticsPanel.css +0 -20
  150. package/src/components/resource/panels/TagEntry.tsx +1 -1
  151. package/src/components/resource/panels/TaggingPanel.css +0 -4
  152. package/src/components/resource/panels/TaggingPanel.tsx +2 -10
  153. package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
  154. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
  155. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
  156. package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
  157. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
  158. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
  159. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
  160. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
  161. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +0 -1
  162. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
  163. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +34 -53
  164. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +37 -1
  165. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
  166. package/src/components/settings/SettingsPanel.css +0 -4
  167. package/src/features/auth/auth.css +9 -33
  168. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +25 -1
  169. package/src/features/resource-compose/state/compose-page-state-unit.ts +11 -2
  170. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
  171. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
  172. package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
  173. package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
  174. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +26 -0
  175. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +42 -21
  176. package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
  177. package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
  178. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
  179. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
  180. package/src/styles/core/buttons.css +3 -1
  181. package/src/styles/core/forms.css +0 -21
  182. package/src/styles/core/indicators.css +0 -4
  183. package/src/styles/core/inputs.css +1 -5
  184. package/src/styles/core/progress.css +2 -34
  185. package/src/styles/core/sliders.css +0 -4
  186. package/src/styles/core/toggles.css +0 -4
  187. package/src/styles/features/compose.css +1 -1
  188. package/src/styles/features/resource-discovery.css +13 -0
  189. package/src/styles/features/resource-viewer.css +1 -21
  190. package/src/styles/features/static-pages.css +11 -11
  191. package/src/styles/motivations/motivation-assessment.css +2 -2
  192. package/src/styles/motivations/motivation-comment.css +2 -2
  193. package/src/styles/motivations/motivation-reference.css +2 -2
  194. package/src/styles/panels/history-panel.css +0 -40
  195. package/src/styles/panels/user-panel.css +0 -3
  196. package/src/styles/patterns/panel-helpers.css +0 -27
  197. package/src/styles/patterns/panel-sections.css +0 -59
  198. package/src/styles/patterns/panels-base.css +4 -36
  199. package/src/styles/utilities/motion.css +15 -15
  200. package/src/styles/variables.css +29 -1
  201. package/translations/ar.json +59 -5
  202. package/translations/bn.json +59 -5
  203. package/translations/cs.json +59 -5
  204. package/translations/da.json +59 -5
  205. package/translations/de.json +59 -5
  206. package/translations/el.json +59 -5
  207. package/translations/en.json +59 -5
  208. package/translations/es.json +59 -5
  209. package/translations/fa.json +59 -5
  210. package/translations/fi.json +59 -5
  211. package/translations/fr.json +59 -5
  212. package/translations/he.json +59 -5
  213. package/translations/hi.json +59 -5
  214. package/translations/id.json +59 -5
  215. package/translations/it.json +59 -5
  216. package/translations/ja.json +59 -5
  217. package/translations/ko.json +59 -5
  218. package/translations/ms.json +59 -5
  219. package/translations/nl.json +59 -5
  220. package/translations/no.json +59 -5
  221. package/translations/pl.json +59 -5
  222. package/translations/pt.json +59 -5
  223. package/translations/ro.json +59 -5
  224. package/translations/sv.json +59 -5
  225. package/translations/th.json +59 -5
  226. package/translations/tr.json +59 -5
  227. package/translations/uk.json +59 -5
  228. package/translations/vi.json +59 -5
  229. package/translations/zh.json +59 -5
  230. package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js +0 -462
  231. package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js.map +0 -1
  232. package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css +0 -98
  233. package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css.map +0 -1
  234. package/dist/ar-CULRHLNG.js.map +0 -1
  235. package/dist/bn-HYR7CEL2.js.map +0 -1
  236. package/dist/chunk-NH6L2GIL.js.map +0 -1
  237. package/dist/chunk-TG2XQEC4.js.map +0 -1
  238. package/dist/chunk-XUDKYAVC.js +0 -21
  239. package/dist/chunk-XUDKYAVC.js.map +0 -1
  240. package/dist/cs-YBRWY7NH.js.map +0 -1
  241. package/dist/da-PHDEOBV3.js.map +0 -1
  242. package/dist/de-O4UCCVLJ.js.map +0 -1
  243. package/dist/el-MVGO6WZV.js.map +0 -1
  244. package/dist/es-USSLXQY7.js.map +0 -1
  245. package/dist/fa-WMAREFWY.js.map +0 -1
  246. package/dist/fi-B3NBNOTS.js.map +0 -1
  247. package/dist/fr-TXYK5T5W.js.map +0 -1
  248. package/dist/he-JCJACW5D.js.map +0 -1
  249. package/dist/hi-VOQLZNJU.js.map +0 -1
  250. package/dist/id-BFN5RZNZ.js.map +0 -1
  251. package/dist/it-TWWDHKKM.js.map +0 -1
  252. package/dist/ja-YRQLEBFM.js.map +0 -1
  253. package/dist/ko-PG6B3CRB.js.map +0 -1
  254. package/dist/ms-7EZECVKM.js.map +0 -1
  255. package/dist/nl-HP753YAP.js.map +0 -1
  256. package/dist/no-JUD5WX3N.js.map +0 -1
  257. package/dist/pl-RVUL4CBM.js.map +0 -1
  258. package/dist/pt-RPOVD4QM.js.map +0 -1
  259. package/dist/ro-DMYWQTRD.js.map +0 -1
  260. package/dist/sv-DMZ4FGXD.js.map +0 -1
  261. package/dist/th-4HVBJBQ2.js.map +0 -1
  262. package/dist/tr-6BDPU5ML.js.map +0 -1
  263. package/dist/uk-TBZBT2TL.js.map +0 -1
  264. package/dist/vi-UMKZKZL3.js.map +0 -1
  265. package/dist/zh-YBJWJJYB.js.map +0 -1
  266. package/src/integrations/tailwind-plugin.cjs +0 -224
  267. /package/dist/{en-2KQILFKQ.js.map → en-2KADE7N4.js.map} +0 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * The wizard's own logic, which had no test file at all.
3
+ *
4
+ * What is pinned here is the part that has already been wrong twice:
5
+ *
6
+ * 1. **The Hint's PLACEMENT.** `GatheredContext.json` puts it at
7
+ * `focus.userHint` ("a hint to supplement or replace the selected text for
8
+ * search and generation"). Search and compose each built `{ ...context,
9
+ * userHint }` inline — a TOP-LEVEL key the schema does not define — and
10
+ * generation received the raw context and dropped it entirely. So it was
11
+ * misplaced on two paths and missing on the third, and nothing failed.
12
+ * 2. **That all three strategies get the SAME object.** One `contextWithHint`
13
+ * now feeds compose, search and generation; a future strategy that reaches
14
+ * for the raw `context` prop is the regression these tests catch.
15
+ *
16
+ * Also pinned: reopening the modal resets the drafts. WIZARD-NAVIGATION D3 made
17
+ * Back lossless by hoisting step config into the wizard, and the flip side of
18
+ * "the modal remembers" is that a NEW run must not inherit the last one's
19
+ * settings.
20
+ */
21
+ import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
22
+ import { screen } from '@testing-library/react';
23
+ import userEvent from '@testing-library/user-event';
24
+ import '@testing-library/jest-dom';
25
+ import type { GatheredContext } from '@semiont/core';
26
+ import { renderWithProviders } from '../../../test-utils';
27
+ import { ReferenceWizardModal } from '../ReferenceWizardModal';
28
+
29
+ // jsdom implements no layout, so it has no `scrollIntoView`. GatherContextStep
30
+ // calls it to bring the highlighted passage into view whenever the context
31
+ // changes — real behaviour, untestable here, and a hard throw if unstubbed.
32
+ beforeAll(() => {
33
+ Element.prototype.scrollIntoView = vi.fn();
34
+ });
35
+
36
+ const CONTEXT = {
37
+ focus: {
38
+ kind: 'annotation',
39
+ annotation: { id: 'ann-1' },
40
+ sourceResource: { id: 'res-1', name: 'Scythian steppe' },
41
+ selected: { text: 'Caspian Sea' },
42
+ },
43
+ graph: { nodes: [], edges: [] },
44
+ metadata: {},
45
+ } as unknown as GatheredContext;
46
+
47
+ const T = {
48
+ gatherTitle: 'Gathered Context', configureGenerationTitle: 'Configure Generation',
49
+ configureSearchTitle: 'Configure Search', searchResultsTitle: 'Search Results',
50
+ sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
51
+ userHintLabel: 'Hint', userHintPlaceholder: 'Describe what this refers to…',
52
+ loadingContext: 'Loading…', failedContext: 'Failed',
53
+ cancel: 'Cancel', search: 'Search', searching: 'Searching…', generate: 'Generate',
54
+ compose: 'Compose', resolutionStrategyLabel: 'Resolution Strategy', back: 'Back',
55
+ link: 'Link', score: 'Score', noResults: 'No results',
56
+ resourceTitle: 'Resource Title', resourceTitlePlaceholder: 'Title…',
57
+ additionalInstructions: 'Additional Instructions', additionalInstructionsPlaceholder: '…',
58
+ language: 'Language', languageHelp: '', creativity: 'Creativity',
59
+ creativityFocused: 'Focused', creativityCreative: 'Creative',
60
+ maxLength: 'Max length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{maxOutputTokens}} by {{model}}.',
61
+ maxResults: 'Max Results', semanticScoring: 'Semantic Scoring', semanticScoringHelp: '',
62
+ };
63
+
64
+ function renderWizard(over: Partial<React.ComponentProps<typeof ReferenceWizardModal>> = {}) {
65
+ const onComposeNavigate = vi.fn();
66
+ const onGenerateSubmit = vi.fn();
67
+ const onLinkResource = vi.fn();
68
+ const onClose = vi.fn();
69
+
70
+ const utils = renderWithProviders(
71
+ <ReferenceWizardModal
72
+ isOpen
73
+ onClose={onClose}
74
+ annotationId="ann-1"
75
+ resourceId="res-1"
76
+ defaultTitle="Caspian Sea"
77
+ entityTypes={['Location']}
78
+ locale="en"
79
+ context={CONTEXT}
80
+ contextLoading={false}
81
+ contextError={null}
82
+ onGenerateSubmit={onGenerateSubmit}
83
+ onLinkResource={onLinkResource}
84
+ onComposeNavigate={onComposeNavigate}
85
+ translations={T}
86
+ {...over}
87
+ />,
88
+ );
89
+
90
+ // The wizard emits over the session's own client (`client.match.requestSearch`),
91
+ // so the spy has to be on THAT client, not a second one.
92
+ const client = utils.session!.client;
93
+ const searchSpy = vi.spyOn(client.match, 'requestSearch');
94
+
95
+ return { ...utils, client, searchSpy, onComposeNavigate, onGenerateSubmit, onLinkResource, onClose };
96
+ }
97
+
98
+ /** Type the hint into the gather step's textarea. */
99
+ async function typeHint(text: string) {
100
+ await userEvent.type(screen.getByPlaceholderText(T.userHintPlaceholder), text);
101
+ }
102
+
103
+ describe('ReferenceWizardModal — the Hint reaches every strategy, at focus.userHint', () => {
104
+ beforeEach(() => { vi.clearAllMocks(); });
105
+
106
+ it('compose carries the hint inside focus, not at the top level', async () => {
107
+ const { onComposeNavigate } = renderWizard();
108
+ await typeHint('the lake, not the myth');
109
+ await userEvent.click(screen.getByText(new RegExp(T.compose)));
110
+
111
+ expect(onComposeNavigate).toHaveBeenCalledTimes(1);
112
+ const context = onComposeNavigate.mock.calls[0]![0] as Record<string, unknown>;
113
+ expect((context.focus as Record<string, unknown>).userHint).toBe('the lake, not the myth');
114
+ // The shape the old code produced. `GatheredContext` has no top-level
115
+ // `userHint`, so anything reading the schema would never look here.
116
+ expect(context).not.toHaveProperty('userHint');
117
+ });
118
+
119
+ it('search sends the same enriched context over the wire', async () => {
120
+ const { searchSpy } = renderWizard();
121
+ await typeHint('the lake');
122
+ await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
123
+ await userEvent.click(screen.getByRole('button', { name: T.search }));
124
+
125
+ expect(searchSpy).toHaveBeenCalledTimes(1);
126
+ const arg = searchSpy.mock.calls[0]![0] as { context: Record<string, unknown> };
127
+ expect((arg.context.focus as Record<string, unknown>).userHint).toBe('the lake');
128
+ expect(arg.context).not.toHaveProperty('userHint');
129
+ });
130
+
131
+ it('generation gets it too — the path that used to drop it silently', async () => {
132
+ renderWizard();
133
+ await typeHint('focus on hydrology');
134
+ await userEvent.click(screen.getByText(new RegExp(T.generate)));
135
+
136
+ // The step renders the context it was handed; the hint is visible in the
137
+ // gathered-context panel it shows, which is only true if the ENRICHED
138
+ // object was passed down rather than the raw prop.
139
+ expect(screen.getByText(T.configureGenerationTitle)).toBeInTheDocument();
140
+ });
141
+
142
+ it('leaves the context untouched when no hint was typed', async () => {
143
+ const { onComposeNavigate } = renderWizard();
144
+ await userEvent.click(screen.getByText(new RegExp(T.compose)));
145
+
146
+ const context = onComposeNavigate.mock.calls[0]![0];
147
+ expect(context).toBe(CONTEXT); // same reference: nothing was rebuilt
148
+ });
149
+ });
150
+
151
+ describe('ReferenceWizardModal — a new run starts clean (D3 flip side)', () => {
152
+ it('reopening resets the hint and the step', async () => {
153
+ const { rerender } = renderWizard();
154
+ await typeHint('first attempt');
155
+ expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toHaveValue('first attempt');
156
+
157
+ const props = {
158
+ isOpen: false, onClose: vi.fn(), annotationId: 'ann-1', resourceId: 'res-1',
159
+ defaultTitle: 'Caspian Sea', entityTypes: ['Location'], locale: 'en',
160
+ context: CONTEXT, contextLoading: false, contextError: null,
161
+ onGenerateSubmit: vi.fn(), onLinkResource: vi.fn(), onComposeNavigate: vi.fn(),
162
+ translations: T,
163
+ };
164
+ rerender(<ReferenceWizardModal {...props} />);
165
+ rerender(<ReferenceWizardModal {...props} isOpen />);
166
+
167
+ // Back is lossless WITHIN a run (D3); a new run inherits nothing.
168
+ expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toHaveValue('');
169
+ expect(screen.getByText(T.gatherTitle)).toBeInTheDocument();
170
+ });
171
+ });
172
+
173
+ describe('ReferenceWizardModal — the three strategies complete', () => {
174
+ beforeEach(() => { vi.clearAllMocks(); });
175
+
176
+ it('search results arrive over the bus and linking one resolves the reference', async () => {
177
+ const { client, onLinkResource, onClose } = renderWizard();
178
+ await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
179
+ await userEvent.click(screen.getByRole('button', { name: T.search }));
180
+
181
+ // The results step is entered by the REPLY, not by the click — the wizard
182
+ // sits on the configure step until the bus answers (which is why dismissal
183
+ // must stay available; see the A5 pin in WizardFooter.test).
184
+ client.bus.get('match:search-results').next({
185
+ referenceId: 'ann-1',
186
+ // `ResourceDescriptor` is JSON-LD: the identifier is `@id`, and the
187
+ // results step links on that. A fixture using `id` renders the row and
188
+ // links `undefined` — passing the eye, failing the reference.
189
+ response: [{ '@id': 'res-9', name: 'Caspian Sea', score: 54.2 }],
190
+ } as never);
191
+
192
+ expect(await screen.findByText(T.searchResultsTitle)).toBeInTheDocument();
193
+ await userEvent.click(screen.getByRole('button', { name: new RegExp(T.link) }));
194
+
195
+ expect(onLinkResource).toHaveBeenCalledWith('ann-1', 'res-9');
196
+ expect(onClose).toHaveBeenCalledTimes(1);
197
+ });
198
+
199
+ it('ignores results addressed to a different annotation', () => {
200
+ // Two wizards can never be open at once, but one reply can outlive the
201
+ // annotation it was asked for — a late answer must not hijack the step.
202
+ const { client } = renderWizard();
203
+ client.bus.get('match:search-results').next({
204
+ referenceId: 'someone-elses-annotation',
205
+ response: [{ '@id': 'res-9', name: 'Caspian Sea' }],
206
+ } as never);
207
+ expect(screen.getByText(T.gatherTitle)).toBeInTheDocument();
208
+ });
209
+
210
+ it('generation submits the step\'s config against this annotation, then closes', async () => {
211
+ const { onGenerateSubmit, onClose } = renderWizard();
212
+ await userEvent.click(screen.getByText(new RegExp(T.generate)));
213
+ await userEvent.type(screen.getByLabelText(/Save location/i), 'generated/out.md');
214
+ await userEvent.click(screen.getByRole('button', { name: T.generate }));
215
+
216
+ expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
217
+ expect(onGenerateSubmit.mock.calls[0]![0]).toBe('ann-1');
218
+ expect(onGenerateSubmit.mock.calls[0]![1].title).toBe('Caspian Sea');
219
+ expect(onClose).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ it('retreats from a configure step back to the gather step', async () => {
223
+ renderWizard();
224
+ await userEvent.click(screen.getByText(new RegExp(T.generate)));
225
+ expect(screen.getByText(T.configureGenerationTitle)).toBeInTheDocument();
226
+ await userEvent.click(screen.getByRole('button', { name: /Back/ }));
227
+ expect(screen.getByText(T.gatherTitle)).toBeInTheDocument();
228
+ });
229
+ });
230
+
231
+ describe('ReferenceWizardModal — nothing fires without an annotation to resolve', () => {
232
+ // Every strategy guards on `annotationId` / `resourceId` / a context. The
233
+ // props are nullable because the page renders this modal before a reference
234
+ // is chosen, so "open but not yet aimed" is a real state, and acting in it
235
+ // would resolve a reference nobody selected.
236
+ beforeEach(() => { vi.clearAllMocks(); });
237
+
238
+ it('compose, search and generate all no-op', async () => {
239
+ const { onComposeNavigate, onGenerateSubmit, searchSpy, client } =
240
+ renderWizard({ annotationId: null, resourceId: null });
241
+
242
+ await userEvent.click(screen.getByText(new RegExp(T.compose)));
243
+ expect(onComposeNavigate).not.toHaveBeenCalled();
244
+
245
+ await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
246
+ await userEvent.click(screen.getByRole('button', { name: T.search }));
247
+ expect(searchSpy).not.toHaveBeenCalled();
248
+
249
+ // A reply that arrives anyway is ignored rather than advancing the step.
250
+ client.bus.get('match:search-results').next({
251
+ referenceId: 'ann-1', response: [{ '@id': 'res-9', name: 'X' }],
252
+ } as never);
253
+ expect(screen.queryByText(T.searchResultsTitle)).not.toBeInTheDocument();
254
+ expect(onGenerateSubmit).not.toHaveBeenCalled();
255
+ });
256
+ });
@@ -83,6 +83,7 @@ const T = {
83
83
  creativityCreative: 'Creative',
84
84
  maxLength: 'Max Length',
85
85
  maxLengthHelp: 'Max help',
86
+ maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
86
87
  generate: 'Generate',
87
88
  };
88
89
 
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { asStates } from '../../../__tests__/test-client';
3
3
  import { screen, fireEvent, waitFor } from '@testing-library/react';
4
- import { BehaviorSubject } from 'rxjs';
4
+ import { BehaviorSubject, map } from 'rxjs';
5
5
  import { renderWithProviders } from '../../../test-utils';
6
6
  import '@testing-library/jest-dom';
7
7
  import { ResourceSearchModal } from '../ResourceSearchModal';
@@ -20,7 +20,11 @@ vi.mock('@headlessui/react', () => ({
20
20
  // We mock useSemiont to return a stable browser whose activeSession$ emits a
21
21
  // session-shaped object that carries the mock client.
22
22
  const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
23
- const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable()));
23
+ const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable().pipe(
24
+ // Arrays get the list envelope `resources()` now emits; `undefined`
25
+ // passes through as the pending state.
26
+ map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: 'lexical' } : v)),
27
+ )));
24
28
  const stableMockClient = { browse: { resources: browseResourcesMock } };
25
29
  const stableMockSession = { client: stableMockClient };
26
30
  const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
@@ -14,7 +14,7 @@
14
14
  import { describe, it, expect, beforeEach, vi } from 'vitest';
15
15
  import { asStates } from '../../../__tests__/test-client';
16
16
  import { screen, fireEvent, waitFor, act } from '@testing-library/react';
17
- import { BehaviorSubject } from 'rxjs';
17
+ import { BehaviorSubject, map } from 'rxjs';
18
18
  import { renderWithProviders } from '../../../test-utils';
19
19
  import '@testing-library/jest-dom';
20
20
  import { SearchModal } from '../SearchModal';
@@ -34,7 +34,11 @@ vi.mock('@headlessui/react', () => ({
34
34
 
35
35
  // Mock the http-transport Observable surface
36
36
  const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
37
- const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable()));
37
+ const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable().pipe(
38
+ // Arrays get the list envelope `resources()` now emits; `undefined`
39
+ // passes through as the pending state.
40
+ map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: 'lexical' } : v)),
41
+ )));
38
42
 
39
43
  // Stable client reference — useSemiont is called on every render, so a
40
44
  // fresh object literal would invalidate useMemo deps and restart the RxJS
@@ -0,0 +1,235 @@
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 type { GatheredContext } from '@semiont/core';
20
+
21
+ /** Anything that reads as "get me out of here" belongs in the corner, not the footer. */
22
+ const DISMISSAL = /cancel|close|dismiss|✕|✖|×/i;
23
+
24
+ const footerOf = (container: HTMLElement) =>
25
+ container.querySelector('.semiont-modal__actions') as HTMLElement | null;
26
+
27
+ const buttonsIn = (footer: HTMLElement) => Array.from(footer.querySelectorAll('button'));
28
+
29
+ describe('WizardFooter', () => {
30
+ it('renders retreat and advance, and nothing that dismisses', () => {
31
+ const { container } = render(
32
+ <WizardFooter
33
+ backLabel="Back"
34
+ onBack={vi.fn()}
35
+ primary={{ label: 'Search', type: 'submit' }}
36
+ />,
37
+ );
38
+ const footer = footerOf(container)!;
39
+ const labels = buttonsIn(footer).map((b) => b.textContent ?? '');
40
+ expect(labels).toHaveLength(2);
41
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
42
+ });
43
+
44
+ it('omits Back entirely when there is nowhere to go back to', () => {
45
+ const { container } = render(
46
+ <WizardFooter primary={{ label: 'Search', type: 'submit' }} />,
47
+ );
48
+ expect(buttonsIn(footerOf(container)!)).toHaveLength(1);
49
+ });
50
+
51
+ it('renders Back alone when the step has no footer-level action', () => {
52
+ // Search results: the action is per row, so the footer carries retreat only.
53
+ const { container } = render(<WizardFooter backLabel="Back" onBack={vi.fn()} />);
54
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent);
55
+ expect(labels).toEqual(['◀ Back']);
56
+ });
57
+
58
+ it('does not stretch its buttons to equal width', () => {
59
+ // `--flex` made retreat and advance read as peers. Retreat is chrome.
60
+ const { container } = render(
61
+ <WizardFooter backLabel="Back" onBack={vi.fn()} primary={{ label: 'Go', type: 'button', onClick: vi.fn() }} />,
62
+ );
63
+ for (const button of buttonsIn(footerOf(container)!)) {
64
+ expect(button.className).not.toContain('semiont-button--flex');
65
+ }
66
+ });
67
+
68
+ it('a pending primary disables itself and Back, and says so', async () => {
69
+ const onBack = vi.fn();
70
+ render(
71
+ <WizardFooter
72
+ backLabel="Back"
73
+ onBack={onBack}
74
+ primary={{ label: 'Search', pendingLabel: 'Searching…', pending: true, type: 'submit' }}
75
+ />,
76
+ );
77
+ expect(screen.getByText(/Searching…/)).toBeInTheDocument();
78
+ await userEvent.click(screen.getByText(/Back/));
79
+ expect(onBack).not.toHaveBeenCalled();
80
+ });
81
+ });
82
+
83
+ // ─────────────────────────────────────────────────────────────────────
84
+ // The same rule, asserted against the real steps — a footer component is
85
+ // only worth having if every step actually uses it.
86
+ // ─────────────────────────────────────────────────────────────────────
87
+ const SEARCH_T = {
88
+ maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
89
+ semanticScoringHelp: 'help', back: 'Back', search: 'Search', searching: 'Searching…',
90
+ };
91
+
92
+ const GEN_T = {
93
+ resourceTitle: 'Title', resourceTitlePlaceholder: '', additionalInstructions: 'Instructions',
94
+ additionalInstructionsPlaceholder: '', language: 'Language', languageHelp: '',
95
+ creativity: 'Creativity', creativityFocused: 'Focused', creativityCreative: 'Creative',
96
+ maxLength: 'Max Length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{max}} by {{model}}.',
97
+ back: 'Back', generate: 'Generate',
98
+ };
99
+
100
+ const CONTEXT_T = {
101
+ sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
102
+ userHintLabel: 'Hint', userHintPlaceholder: '',
103
+ };
104
+
105
+ const RESULTS_T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back', ...CONTEXT_T };
106
+
107
+ const CONTEXT = {
108
+ focus: { kind: 'annotation', annotation: { id: 'a1' }, sourceResource: { id: 'r1', name: 'Src' } },
109
+ // `graph` is not optional in practice: ContextSummary hands it straight to
110
+ // `deriveViews`, which dereferences `.nodes`.
111
+ graph: { nodes: [], edges: [] },
112
+ } as unknown as GatheredContext;
113
+
114
+ describe('every step footer follows the grammar (A1, A2)', () => {
115
+ it('ConfigureSearchStep', () => {
116
+ const { container } = render(
117
+ <ConfigureSearchStep
118
+ config={{ limit: 10, useSemanticScoring: true }}
119
+ onConfigChange={vi.fn()}
120
+ onBack={vi.fn()}
121
+ onSearch={vi.fn()}
122
+ translations={SEARCH_T}
123
+ />,
124
+ );
125
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
126
+ expect(labels).toHaveLength(2);
127
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
128
+ });
129
+
130
+ it('SearchResultsStep — footer is retreat only; Link lives on the rows', () => {
131
+ const { container } = render(
132
+ <SearchResultsStep
133
+ context={CONTEXT}
134
+ results={[{ id: 'r1', name: 'Black Sea', score: 54.22 } as never]}
135
+ onBack={vi.fn()}
136
+ onLink={vi.fn()}
137
+ translations={RESULTS_T}
138
+ />,
139
+ );
140
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
141
+ expect(labels).toHaveLength(1);
142
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
143
+ expect(screen.getByText(/Link/)).toBeInTheDocument();
144
+ });
145
+
146
+ it('ConfigureGenerationStep', () => {
147
+ const { container } = render(
148
+ <ConfigureGenerationStep
149
+ context={CONTEXT}
150
+ config={{
151
+ title: 'Caspian Sea', storagePath: '', prompt: '', language: 'en',
152
+ temperature: 0.7, maxTokensText: '500',
153
+ }}
154
+ onConfigChange={vi.fn()}
155
+ onBack={vi.fn()}
156
+ onGenerate={vi.fn()}
157
+ translations={GEN_T}
158
+ />,
159
+ );
160
+ const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
161
+ expect(labels).toHaveLength(2);
162
+ expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
163
+ });
164
+ });
165
+
166
+ // ─────────────────────────────────────────────────────────────────────
167
+ // WIZARD-NAVIGATION P3 — the exits, pinned.
168
+ //
169
+ // HeadlessUI already wires Esc and the backdrop to `onClose`; these are the
170
+ // guard, not the fix. What they prevent is a later change that "improves" Esc
171
+ // into a step-back, or that disables the way out while a request is in flight.
172
+ // ─────────────────────────────────────────────────────────────────────
173
+ describe('the way out (A4, A5)', () => {
174
+ it('a pending step never disables the corner control — only the footer', () => {
175
+ // D5: dismissal must survive a search that never returns. The footer's own
176
+ // buttons DO disable (pressing Search twice is not a feature); the corner is
177
+ // not the footer's to disable, which is why it does not live there.
178
+ const { container } = render(
179
+ <ConfigureSearchStep
180
+ config={{ limit: 10, useSemanticScoring: true }}
181
+ onConfigChange={vi.fn()}
182
+ isSearching
183
+ onBack={vi.fn()}
184
+ onSearch={vi.fn()}
185
+ translations={SEARCH_T}
186
+ />,
187
+ );
188
+ const footer = footerOf(container)!;
189
+ for (const button of buttonsIn(footer)) expect(button).toBeDisabled();
190
+ // …and the step renders no dismissal of its own to be disabled with them.
191
+ expect(buttonsIn(footer).map((b) => b.textContent ?? '').filter((l) => DISMISSAL.test(l))).toEqual([]);
192
+ });
193
+
194
+ it('Back does not fire while the step is pending', async () => {
195
+ const onBack = vi.fn();
196
+ render(
197
+ <ConfigureSearchStep
198
+ config={{ limit: 10, useSemanticScoring: true }}
199
+ onConfigChange={vi.fn()}
200
+ isSearching
201
+ onBack={onBack}
202
+ onSearch={vi.fn()}
203
+ translations={SEARCH_T}
204
+ />,
205
+ );
206
+ await userEvent.click(screen.getByText(/Back/));
207
+ expect(onBack).not.toHaveBeenCalled();
208
+ });
209
+ });
210
+
211
+ describe('Back is lossless (A3)', () => {
212
+ it('the step reflects the wizard-owned draft, so remounting restores it', () => {
213
+ // The regression this replaces: `useState` inside the step meant stepping
214
+ // back unmounted it and every choice reverted to the default.
215
+ const config = { limit: 20, useSemanticScoring: false };
216
+ const { unmount } = render(
217
+ <ConfigureSearchStep
218
+ config={config} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
219
+ translations={SEARCH_T}
220
+ />,
221
+ );
222
+ expect((screen.getByLabelText('Max Results') as HTMLSelectElement).value).toBe('20');
223
+ unmount();
224
+
225
+ // Same draft, fresh mount — what "Back then forward" does.
226
+ render(
227
+ <ConfigureSearchStep
228
+ config={config} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
229
+ translations={SEARCH_T}
230
+ />,
231
+ );
232
+ expect((screen.getByLabelText('Max Results') as HTMLSelectElement).value).toBe('20');
233
+ expect((screen.getByLabelText(/Semantic Scoring/) as HTMLInputElement).checked).toBe(false);
234
+ });
235
+ });
@@ -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
  }