@semiont/react-ui 0.5.27 → 0.5.29

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 (192) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
  3. package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
  4. package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
  8. package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
  9. package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
  10. package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
  11. package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
  12. package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
  14. package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
  15. package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
  16. package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
  17. package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
  18. package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
  19. package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
  20. package/dist/index.css +279 -74
  21. package/dist/index.d.ts +318 -393
  22. package/dist/index.js +2619 -2687
  23. package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
  24. package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
  25. package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
  26. package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
  27. package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
  28. package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
  29. package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
  30. package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
  31. package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
  32. package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
  33. package/dist/test-utils.js +16 -7
  34. package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
  35. package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
  36. package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
  37. package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
  38. package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
  39. package/package.json +16 -15
  40. package/src/components/AssistProgress.tsx +36 -2
  41. package/src/components/CodeMirrorRenderer.tsx +8 -8
  42. package/src/components/StatusDisplay.tsx +13 -13
  43. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  44. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  45. package/src/components/modals/ComposeStep.tsx +233 -0
  46. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  47. package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
  48. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  49. package/src/components/modals/ContextSummary.tsx +185 -72
  50. package/src/components/modals/CorpusPane.tsx +75 -0
  51. package/src/components/modals/DiscardPrompt.tsx +38 -0
  52. package/src/components/modals/GatherContextStep.tsx +237 -107
  53. package/src/components/modals/ReferenceWizardModal.tsx +252 -59
  54. package/src/components/modals/ResourceGenerateModal.tsx +225 -112
  55. package/src/components/modals/SearchModal.css +337 -101
  56. package/src/components/modals/SearchResultsStep.tsx +61 -68
  57. package/src/components/modals/WizardFooter.tsx +7 -1
  58. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  59. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  60. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
  61. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  62. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  63. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
  64. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
  65. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  66. package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
  67. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  68. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  69. package/src/components/resource/AnnotateView.tsx +3 -3
  70. package/src/components/resource/BrowseView.tsx +8 -8
  71. package/src/components/resource/DownloadFileLink.tsx +1 -1
  72. package/src/components/resource/ResourceViewer.tsx +4 -4
  73. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  74. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  75. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  76. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
  77. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  78. package/src/components/resource/annotate-renderers.tsx +3 -3
  79. package/src/components/resource/panels/AssistSection.css +35 -0
  80. package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
  81. package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  83. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  84. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  85. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
  87. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
  88. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  89. package/src/features/auth/components/SignInForm.tsx +53 -53
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  91. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  95. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  96. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  97. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  98. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
  99. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  100. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
  101. package/src/features/resource-viewer/generation-options.ts +29 -0
  102. package/src/styles/core/forms.css +35 -8
  103. package/src/styles/features/resource-viewer.css +0 -22
  104. package/src/styles/features/resource.css +0 -4
  105. package/src/styles/index.css +0 -1
  106. package/translations/ar.json +42 -22
  107. package/translations/bn.json +42 -22
  108. package/translations/cs.json +42 -22
  109. package/translations/da.json +42 -22
  110. package/translations/de.json +42 -22
  111. package/translations/el.json +42 -22
  112. package/translations/en.json +42 -22
  113. package/translations/es.json +42 -22
  114. package/translations/fa.json +42 -22
  115. package/translations/fi.json +42 -22
  116. package/translations/fr.json +42 -22
  117. package/translations/he.json +42 -22
  118. package/translations/hi.json +42 -22
  119. package/translations/id.json +42 -22
  120. package/translations/it.json +42 -22
  121. package/translations/ja.json +42 -22
  122. package/translations/ko.json +42 -22
  123. package/translations/ms.json +42 -22
  124. package/translations/nl.json +42 -22
  125. package/translations/no.json +42 -22
  126. package/translations/pl.json +42 -22
  127. package/translations/pt.json +42 -22
  128. package/translations/ro.json +42 -22
  129. package/translations/sv.json +42 -22
  130. package/translations/th.json +42 -22
  131. package/translations/tr.json +42 -22
  132. package/translations/uk.json +42 -22
  133. package/translations/vi.json +42 -22
  134. package/translations/zh.json +42 -22
  135. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  136. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  137. package/dist/ar-OU3DQGC6.js.map +0 -1
  138. package/dist/bn-PHWCR6AZ.js.map +0 -1
  139. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  140. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  141. package/dist/chunk-O23TLLXW.js.map +0 -1
  142. package/dist/chunk-PCRRBU7H.js.map +0 -1
  143. package/dist/chunk-QT7LYM72.js.map +0 -1
  144. package/dist/chunk-VXASXU4K.js.map +0 -1
  145. package/dist/cs-NEXYZ62I.js.map +0 -1
  146. package/dist/da-FAT43YCU.js.map +0 -1
  147. package/dist/de-KRQ3WBCU.js.map +0 -1
  148. package/dist/el-Q4IU3EKC.js.map +0 -1
  149. package/dist/en-2KADE7N4.js.map +0 -1
  150. package/dist/es-2IO3R2MK.js.map +0 -1
  151. package/dist/fa-Q5GOQFXX.js.map +0 -1
  152. package/dist/fi-R6NMRWBE.js.map +0 -1
  153. package/dist/fr-QY4D5E6J.js.map +0 -1
  154. package/dist/he-X6Y77XHY.js.map +0 -1
  155. package/dist/hi-XL7IOEQM.js.map +0 -1
  156. package/dist/id-QZ4ZZS67.js.map +0 -1
  157. package/dist/index.css.map +0 -1
  158. package/dist/index.js.map +0 -1
  159. package/dist/integrations/css-modules-helper.js.map +0 -1
  160. package/dist/integrations/styled-components-theme.js.map +0 -1
  161. package/dist/it-ME2Q7OB6.js.map +0 -1
  162. package/dist/ja-HX7QASRI.js.map +0 -1
  163. package/dist/ko-EQO5AHAV.js.map +0 -1
  164. package/dist/ms-UILFSF3I.js.map +0 -1
  165. package/dist/nl-NNI45PMK.js.map +0 -1
  166. package/dist/no-MAL5F6P6.js.map +0 -1
  167. package/dist/pl-4RMY6JAJ.js.map +0 -1
  168. package/dist/pt-VQH2X3QQ.js.map +0 -1
  169. package/dist/ro-XXCQO2OG.js.map +0 -1
  170. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  171. package/dist/test-utils.css.map +0 -1
  172. package/dist/test-utils.js.map +0 -1
  173. package/dist/th-L3DXXYVQ.js.map +0 -1
  174. package/dist/tr-P3DFVLFJ.js.map +0 -1
  175. package/dist/uk-7C5D3PXS.js.map +0 -1
  176. package/dist/vi-ZLTLEO7P.js.map +0 -1
  177. package/dist/zh-5FHVWDIN.js.map +0 -1
  178. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  179. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
  180. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  181. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  182. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  183. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  184. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  185. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  186. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  187. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  188. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  189. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  190. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  191. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  192. package/src/styles/features/exchange.css +0 -374
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import type { GatheredContext } from '@semiont/core';
4
+
5
+ export interface CorpusPaneTranslations {
6
+ corpusPaneTitle: string;
7
+ /** Strategy-relevant empty state — emptiness is evidence (GEP D1). */
8
+ corpusEmpty: string;
9
+ /** Label for the match's cosine score. */
10
+ score: string;
11
+ /** The filter receipt; interpolates `{{types}}`. */
12
+ excludedReceipt: string;
13
+ /** The trust marker for machine-read (OCR) sources. */
14
+ machineRead: string;
15
+ }
16
+
17
+ export interface CorpusPaneProps {
18
+ semanticContext: GatheredContext['semanticContext'];
19
+ translations: CorpusPaneTranslations;
20
+ }
21
+
22
+ /**
23
+ * The corpus pane (GEP D4): latent knowledge — ranked `SemanticMatch` cards,
24
+ * read-only. THE datum for Search-vs-Generate ("does the KB already discuss
25
+ * this?"). This is recall-at-gather evidence, not a preview of Search: the
26
+ * matcher re-runs fresh (selected text + hint, scoring) and may rank
27
+ * differently.
28
+ *
29
+ * Read-only for now by decision, not forever — when per-match actions arrive
30
+ * they route through the footer's fork or a designed extension of it, never
31
+ * ad-hoc buttons here.
32
+ */
33
+ export function CorpusPane({ semanticContext, translations: t }: CorpusPaneProps) {
34
+ // Descending by score, whatever order the wire delivered.
35
+ const matches = [...(semanticContext?.similar ?? [])].sort((a, b) => b.score - a.score);
36
+ const excluded = semanticContext?.excludedEntityTypes ?? [];
37
+
38
+ return (
39
+ <div className="semiont-gather-pane semiont-gather-pane--corpus">
40
+ <div className="semiont-gather-pane__title">{t.corpusPaneTitle}</div>
41
+ {matches.length === 0 ? (
42
+ <p className="semiont-gather-pane__empty">{t.corpusEmpty}</p>
43
+ ) : (
44
+ <ul className="semiont-corpus__list">
45
+ {matches.map((m, i) => (
46
+ <li key={`${m.resourceId}-${i}`} className="semiont-corpus__card">
47
+ <div className="semiont-corpus__card-head">
48
+ <span className="semiont-corpus__source">{m.resourceName}</span>
49
+ {(m.entityTypes ?? []).map((et) => (
50
+ <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
51
+ {et}
52
+ </span>
53
+ ))}
54
+ {m.machineRead && (
55
+ <span className="semiont-chip semiont-corpus__trust" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
56
+ {t.machineRead}
57
+ </span>
58
+ )}
59
+ {/* 0–1 cosine to two places — deliberately NOT the search step's
60
+ raw render, which is a different scale (logged deviation). */}
61
+ <span className="semiont-corpus__score">{t.score}: {m.score.toFixed(2)}</span>
62
+ </div>
63
+ <p className="semiont-corpus__snippet">{m.text}</p>
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ )}
68
+ {excluded.length > 0 && (
69
+ <p className="semiont-corpus__receipt">
70
+ {t.excludedReceipt.replace('{{types}}', excluded.join(', '))}
71
+ </p>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The inline discard band (COMPOSE-IN-MODAL D4, shared by GATHER-AT-THE-TOP
5
+ * P1): a modal dies on ✕/Escape/backdrop, but typed work must not die with
6
+ * it. Hosts route ALL dismissal through one handler; when the draft is dirty
7
+ * that handler shows this band instead of closing. Success paths never come
8
+ * here — the guard protects dismissal, not completion.
9
+ */
10
+ interface DiscardPromptProps {
11
+ prompt: string;
12
+ discardLabel: string;
13
+ keepLabel: string;
14
+ onDiscard: () => void;
15
+ onKeepEditing: () => void;
16
+ }
17
+
18
+ export function DiscardPrompt({ prompt, discardLabel, keepLabel, onDiscard, onKeepEditing }: DiscardPromptProps) {
19
+ return (
20
+ <div className="semiont-wizard__discard-prompt" role="alert">
21
+ <span className="semiont-wizard__discard-prompt-text">{prompt}</span>
22
+ <button
23
+ type="button"
24
+ className="semiont-button--danger"
25
+ onClick={onDiscard}
26
+ >
27
+ {discardLabel}
28
+ </button>
29
+ <button
30
+ type="button"
31
+ className="semiont-button--secondary"
32
+ onClick={onKeepEditing}
33
+ >
34
+ {keepLabel}
35
+ </button>
36
+ </div>
37
+ );
38
+ }
@@ -4,43 +4,81 @@ import { useState, useEffect, useRef } from 'react';
4
4
  import type { GatheredContext } from '@semiont/core';
5
5
  import { ContextSummary } from './ContextSummary';
6
6
  import type { ContextSummaryTranslations } from './ContextSummary';
7
+ import { CorpusPane } from './CorpusPane';
8
+ import type { CorpusPaneTranslations } from './CorpusPane';
9
+ import { ANNOTATORS, annotatorKeyForMotivation } from '../../lib/annotation-registry';
10
+
11
+ /**
12
+ * The annotation-wizard resolution controls, as ONE optional group: a caller
13
+ * either serves the whole surface (hint + Bind/Generate/Compose, with their
14
+ * labels) or none of it. This is what keeps a display-only caller from having
15
+ * to pass `''` for keys it will never render (GENERATE-FROM-RESOURCE D2).
16
+ */
17
+ export interface GatherContextStepAnnotate {
18
+ userHint: string;
19
+ onUserHintChange: (value: string) => void;
20
+ onBind: () => void;
21
+ onGenerate: () => void;
22
+ onCompose: () => void;
23
+ translations: {
24
+ search: string;
25
+ generate: string;
26
+ compose: string;
27
+ resolutionStrategyLabel: string;
28
+ userHintLabel: string;
29
+ /** The label states its effect — "steers Search and Generate" (GEP D5). */
30
+ userHintEffect: string;
31
+ userHintPlaceholder: string;
32
+ };
33
+ }
7
34
 
8
35
  export interface GatherContextStepProps {
9
36
  context: GatheredContext | null;
10
37
  contextLoading: boolean;
11
38
  contextError: Error | null;
12
- /** Annotation-wizard controls. Omit for a display-only (e.g. resource-focus) render. */
13
- userHint?: string;
14
- onUserHintChange?: (value: string) => void;
15
- onBind?: () => void;
16
- onGenerate?: () => void;
17
- onCompose?: () => void;
39
+ /**
40
+ * What the host already knows about the annotation BEFORE the gather
41
+ * answers its exact text and entity types are wizard props, no wire
42
+ * round-trip needed. When present, the loading state renders them with
43
+ * skeleton panes so the loading screen is the loaded screen minus the
44
+ * data, not a blank modal with dots. Omit (the resource-gather path) for
45
+ * the plain loading block.
46
+ */
47
+ pending?: { exact: string; entityTypes: string[]; resourceName?: string };
48
+ /** Omit for a display-only (e.g. resource-focus) render. */
49
+ annotate?: GatherContextStepAnnotate;
50
+ /**
51
+ * Display-only callers on a STRATEGY step (the chooser already clicked):
52
+ * keep the RESOLUTION STRATEGY band for continuity, collapsed from three
53
+ * choices to the chosen one — a passive echo, never a second chooser.
54
+ * Ignored when `annotate` is present (the live chooser owns the band).
55
+ */
56
+ chosenStrategy?: { label: string; value: string };
18
57
  translations: {
19
- title: string;
20
58
  loadingContext: string;
21
59
  failedContext: string;
22
- search: string;
23
- generate: string;
24
- compose: string;
25
- resolutionStrategyLabel: string;
26
- } & ContextSummaryTranslations;
60
+ } & ContextSummaryTranslations & CorpusPaneTranslations;
27
61
  }
28
62
 
29
63
  export function GatherContextStep({
30
64
  context,
31
65
  contextLoading,
32
66
  contextError,
33
- userHint = '',
34
- onUserHintChange,
35
- onBind,
36
- onGenerate,
37
- onCompose,
67
+ pending,
68
+ annotate,
69
+ chosenStrategy,
38
70
  translations: t,
39
71
  }: GatherContextStepProps) {
40
72
  const [sourceExpanded, setSourceExpanded] = useState(false);
41
73
  const contextReady = !contextLoading && !contextError && !!context;
42
74
  const focus = context?.focus.kind === 'annotation' ? context.focus : null;
43
75
  const resourceFocus = context?.focus.kind === 'resource' ? context.focus : null;
76
+ // GEP D6: the focal span WEARS its motivation — the viewer's own registry
77
+ // class (the same one the document applies), never a copy or a chip.
78
+ const focalKey = focus?.annotation.motivation
79
+ ? annotatorKeyForMotivation(focus.annotation.motivation)
80
+ : undefined;
81
+ const focalMotivationClass = focalKey ? ANNOTATORS[focalKey].className : undefined;
44
82
  const highlightRef = useRef<HTMLSpanElement>(null);
45
83
 
46
84
  // Scroll the highlighted term into view when context loads
@@ -50,10 +88,116 @@ export function GatherContextStep({
50
88
  }
51
89
  }, [context]);
52
90
 
91
+ // Shared by the loaded view and the loading skeleton (the Hint is wizard
92
+ // state — safely typed while the gather runs — and the chooser's buttons
93
+ // already gate on contextReady). One JSX, two hosts: never two copies.
94
+ const hintRow = annotate && (
95
+ <div className="semiont-gather__hint-row">
96
+ <div className="semiont-form__field">
97
+ <label className="semiont-form__label">
98
+ {annotate.translations.userHintLabel}
99
+ <span className="semiont-gather__hint-effect"> — {annotate.translations.userHintEffect}</span>
100
+ </label>
101
+ <textarea
102
+ value={annotate.userHint}
103
+ onChange={(e) => annotate.onUserHintChange(e.target.value)}
104
+ placeholder={annotate.translations.userHintPlaceholder}
105
+ className="semiont-search-modal__search-input semiont-gather__hint-textarea"
106
+ style={{ resize: 'vertical', fontFamily: 'inherit' }}
107
+ />
108
+ </div>
109
+ </div>
110
+ );
111
+
112
+ const strategyFooter = annotate && (
113
+ <div className="semiont-gather__footer">
114
+ <div className="semiont-gather__footer-label">{annotate.translations.resolutionStrategyLabel}</div>
115
+ <div className="semiont-gather__actions">
116
+ <button
117
+ type="button"
118
+ onClick={annotate.onBind}
119
+ disabled={!contextReady}
120
+ className="semiont-button--primary semiont-button--flex"
121
+ >
122
+ 🔍 {annotate.translations.search}…
123
+ </button>
124
+ <button
125
+ type="button"
126
+ onClick={annotate.onGenerate}
127
+ disabled={!contextReady}
128
+ className="semiont-button--primary semiont-button--flex"
129
+ >
130
+ ✨ {annotate.translations.generate}…
131
+ </button>
132
+ <button
133
+ type="button"
134
+ onClick={annotate.onCompose}
135
+ disabled={!contextReady}
136
+ className="semiont-button--secondary semiont-button--flex"
137
+ >
138
+ ✍️ {annotate.translations.compose}
139
+ </button>
140
+ </div>
141
+ </div>
142
+ );
143
+
53
144
  return (
54
145
  <div className="semiont-gather__outer">
55
- {/* Loading / error states */}
56
- {contextLoading && (
146
+ {/* Loading / error states. With a `pending` preview the loading screen
147
+ is the loaded screen minus the data: the annotation's own facts and
148
+ the pane headers hold the layout, and one small dots animation sits
149
+ in EACH zone — source excerpt, Neighborhood, Similar passages —
150
+ exactly where its content will land. No central block, no dead
151
+ space; the sentence survives for screen readers. */}
152
+ {contextLoading && pending && (
153
+ <div className="semiont-gather__skeleton" role="status">
154
+ <span className="semiont-sr-only">{t.loadingContext}</span>
155
+ {pending.resourceName && (
156
+ <div className="semiont-gather__skeleton-source">
157
+ {t.sourceContextLabel}{` "${pending.resourceName}"`}
158
+ </div>
159
+ )}
160
+ <div className="semiont-gather__skeleton-dots" aria-hidden="true">
161
+ <span className="semiont-gather__loading-dot" />
162
+ <span className="semiont-gather__loading-dot" />
163
+ <span className="semiont-gather__loading-dot" />
164
+ </div>
165
+ <div className="semiont-gather__skeleton-header">
166
+ {/* Straight quotes — the same presentation the panel entries use. */}
167
+ <span className="semiont-gather__skeleton-exact">"{pending.exact}"</span>
168
+ {pending.entityTypes.length > 0 && (
169
+ <span style={{ display: 'flex', gap: '0.25rem', flexWrap: 'wrap' }}>
170
+ {pending.entityTypes.map(et => (
171
+ <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
172
+ {et}
173
+ </span>
174
+ ))}
175
+ </span>
176
+ )}
177
+ </div>
178
+ <div className="semiont-gather__skeleton-panes">
179
+ <div className="semiont-gather__skeleton-pane">
180
+ <div className="semiont-gather-pane__title">{t.graphPaneTitle}</div>
181
+ <div className="semiont-gather__skeleton-dots" aria-hidden="true">
182
+ <span className="semiont-gather__loading-dot" />
183
+ <span className="semiont-gather__loading-dot" />
184
+ <span className="semiont-gather__loading-dot" />
185
+ </div>
186
+ </div>
187
+ <div className="semiont-gather__skeleton-pane">
188
+ <div className="semiont-gather-pane__title">{t.corpusPaneTitle}</div>
189
+ <div className="semiont-gather__skeleton-dots" aria-hidden="true">
190
+ <span className="semiont-gather__loading-dot" />
191
+ <span className="semiont-gather__loading-dot" />
192
+ <span className="semiont-gather__loading-dot" />
193
+ </div>
194
+ </div>
195
+ </div>
196
+ {hintRow}
197
+ {strategyFooter}
198
+ </div>
199
+ )}
200
+ {contextLoading && !pending && (
57
201
  <div className="semiont-gather__loading">
58
202
  <div style={{ display: 'flex', gap: '0.5rem' }}>
59
203
  <span className="semiont-gather__loading-dot" />
@@ -71,37 +215,36 @@ export function GatherContextStep({
71
215
 
72
216
  {context && (
73
217
  <>
74
- {/* Full-width source context strip — annotation focus */}
218
+ {/* Full-width source context strip — annotation focus.
219
+ GEP P1 (D6/D7): the quotation is contiguous prose — anchor
220
+ metadata never interrupts it. Entity chips are tokens and live on
221
+ the label row; the motivation is never labeled, the focal span
222
+ WEARS it via the viewer's own registry class (same class the
223
+ document applies, themes included). Only annotations the context
224
+ can PLACE render here — today exactly `focus.annotation`. */}
75
225
  {focus?.selected && (
76
226
  <div className="semiont-gather__source-strip">
77
- <label className="semiont-form__label" style={{ marginBottom: '0.375rem' }}>
78
- {t.sourceContextLabel}{focus.sourceResource.name ? ` "${focus.sourceResource.name}"` : ''}
79
- </label>
227
+ <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '0.5rem', marginBottom: '0.375rem' }}>
228
+ <label className="semiont-form__label" style={{ marginBottom: 0 }}>
229
+ {t.sourceContextLabel}{focus.sourceResource.name ? ` "${focus.sourceResource.name}"` : ''}
230
+ </label>
231
+ {(context.metadata?.entityTypes ?? []).length > 0 && (
232
+ <span style={{ display: 'flex', gap: '0.25rem', flexWrap: 'wrap' }}>
233
+ {(context.metadata?.entityTypes ?? []).map(et => (
234
+ <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
235
+ {et}
236
+ </span>
237
+ ))}
238
+ </span>
239
+ )}
240
+ </div>
80
241
  <div className={`semiont-gather__source-box${sourceExpanded ? ' semiont-gather__source-box--expanded' : ''}`}>
81
242
  <div className="semiont-gather__source-context">
82
- <div style={{ fontSize: 'var(--semiont-text-sm)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
243
+ <div style={{ fontSize: 'var(--semiont-text-sm)', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
83
244
  {focus.selected.before && <span>{focus.selected.before}</span>}
84
- <span
85
- ref={highlightRef}
86
- style={{
87
- backgroundColor: 'var(--semiont-color-primary-100)',
88
- padding: '0 0.25rem',
89
- fontWeight: 600,
90
- color: 'var(--semiont-color-primary-900)',
91
- }}
92
- >
245
+ <span ref={highlightRef} {...(focalMotivationClass ? { className: focalMotivationClass } : {})}>
93
246
  {focus.selected.text}
94
247
  </span>
95
- {(context.metadata?.entityTypes ?? []).map(et => (
96
- <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400, verticalAlign: 'middle', marginLeft: '0.25rem' }}>
97
- {et}
98
- </span>
99
- ))}
100
- {focus.annotation.motivation && (
101
- <span className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400, verticalAlign: 'middle', marginLeft: '0.25rem' }}>
102
- {focus.annotation.motivation}
103
- </span>
104
- )}
105
248
  {focus.selected.after && <span>{focus.selected.after}</span>}
106
249
  </div>
107
250
  </div>
@@ -116,22 +259,30 @@ export function GatherContextStep({
116
259
  </div>
117
260
  )}
118
261
 
119
- {/* Full-width source context strip — resource focus */}
262
+ {/* Full-width source context strip — resource focus. Same D6 rule as
263
+ the annotation strip: chips are metadata about the anchor and
264
+ live on the label row, never inside the prose. */}
120
265
  {resourceFocus && (
121
266
  <div className="semiont-gather__source-strip">
122
- <label className="semiont-form__label" style={{ marginBottom: '0.375rem' }}>
123
- {t.sourceContextLabel}{resourceFocus.resource.name ? ` "${resourceFocus.resource.name}"` : ''}
124
- </label>
267
+ <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '0.5rem', marginBottom: '0.375rem' }}>
268
+ <label className="semiont-form__label" style={{ marginBottom: 0 }}>
269
+ {t.sourceContextLabel}{resourceFocus.resource.name ? ` "${resourceFocus.resource.name}"` : ''}
270
+ </label>
271
+ {(context.metadata?.entityTypes ?? []).length > 0 && (
272
+ <span style={{ display: 'flex', gap: '0.25rem', flexWrap: 'wrap' }}>
273
+ {(context.metadata?.entityTypes ?? []).map(et => (
274
+ <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400 }}>
275
+ {et}
276
+ </span>
277
+ ))}
278
+ </span>
279
+ )}
280
+ </div>
125
281
  {(resourceFocus.summary || resourceFocus.content?.main) && (
126
282
  <div className={`semiont-gather__source-box${sourceExpanded ? ' semiont-gather__source-box--expanded' : ''}`}>
127
283
  <div className="semiont-gather__source-context">
128
284
  <div style={{ fontSize: 'var(--semiont-text-sm)', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
129
285
  {resourceFocus.summary ?? resourceFocus.content?.main}
130
- {(context.metadata?.entityTypes ?? []).map(et => (
131
- <span key={et} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', fontWeight: 400, verticalAlign: 'middle', marginLeft: '0.25rem' }}>
132
- {et}
133
- </span>
134
- ))}
135
286
  </div>
136
287
  </div>
137
288
  <button
@@ -144,73 +295,52 @@ export function GatherContextStep({
144
295
  </div>
145
296
  )}
146
297
  {resourceFocus.suggestedReferences && resourceFocus.suggestedReferences.length > 0 && (
147
- <div style={{ marginTop: '0.5rem' }}>
298
+ // Prose, not chips (D3): the live values are full research
299
+ // prompts — sentences. The chip vocabulary stays for tokens.
300
+ <ul className="semiont-gather__suggested">
148
301
  {resourceFocus.suggestedReferences.map(ref => (
149
- <span key={ref} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.375rem', marginRight: '0.25rem' }}>
150
- {ref}
151
- </span>
302
+ <li key={ref}>{ref}</li>
152
303
  ))}
153
- </div>
304
+ </ul>
154
305
  )}
155
306
  </div>
156
307
  )}
157
308
 
158
- {/* Two-column body */}
309
+ {/* The evidence panes (GEP D1): curated knowledge beside latent
310
+ knowledge — the fork reads off them. Both render for every
311
+ caller; emptiness is evidence, never blankness. */}
159
312
  <div className="semiont-gather__body">
160
- {/* Left: context summary (graph views) */}
161
313
  <div className="semiont-gather__left">
162
314
  <ContextSummary context={context} translations={t} />
163
315
  </div>
164
-
165
- {/* Right: hint textarea (annotation focus only) */}
166
- {focus && (
167
- <div className="semiont-gather__right">
168
- <div className="semiont-form__field">
169
- <label className="semiont-form__label">
170
- {t.userHintLabel}
171
- </label>
172
- <textarea
173
- value={userHint}
174
- onChange={(e) => onUserHintChange?.(e.target.value)}
175
- placeholder={t.userHintPlaceholder}
176
- className="semiont-search-modal__search-input semiont-gather__hint-textarea"
177
- style={{ resize: 'vertical', fontFamily: 'inherit' }}
178
- />
179
- </div>
180
- </div>
181
- )}
316
+ <div className="semiont-gather__right">
317
+ <CorpusPane semanticContext={context.semanticContext} translations={t} />
318
+ </div>
182
319
  </div>
183
320
 
184
- {/* Full-width footer: resolution strategy (annotation focus only) */}
185
- {focus && (
186
- <div className="semiont-gather__footer">
187
- <div className="semiont-gather__footer-label">{t.resolutionStrategyLabel}</div>
188
- <div className="semiont-gather__actions">
189
- <button
190
- type="button"
191
- onClick={onBind}
192
- disabled={!contextReady}
193
- className="semiont-button--primary semiont-button--flex"
194
- >
195
- 🔍 {t.search}…
196
- </button>
197
- <button
198
- type="button"
199
- onClick={onGenerate}
200
- disabled={!contextReady}
201
- className="semiont-button--primary semiont-button--flex"
202
- >
203
- {t.generate}…
204
- </button>
205
- <button
206
- type="button"
207
- onClick={onCompose}
208
- disabled={!contextReady}
209
- className="semiont-button--secondary semiont-button--flex"
210
- >
211
- ✍️ {t.compose}
212
- </button>
213
- </div>
321
+ {/* Hint: full width, input-then-act adjacency above the footer
322
+ (GEP D5). The label states its effect. Typing does NOT re-run
323
+ recall — the panes are the at-gather evidence. */}
324
+ {focus && hintRow}
325
+
326
+ {/* Full-width footer: resolution strategy (annotation-wizard callers only).
327
+ The THIRD footer species — a CHOOSER — and the deliberate exception to
328
+ the WizardFooter grammar, which is one-retreat-plus-one-advance and
329
+ cannot express a fork. Its own grammar, pinned in WizardFooter.test:
330
+ three mutually exclusive strategies, equal width (`--flex` is honest
331
+ here: these ARE peers — the same class the shared footer banned for
332
+ falsely making retreat a peer of advance);
333
+ • no dismissal (corner ✕/Esc, as everywhere);
334
+ • all gated on contextReady;
335
+ • the AI paths are primary, the manual path secondary — Compose's
336
+ demotion is the recorded convention, not drift;
337
+ • ellipses are component-owned and mark step-vs-act: Search…/Generate…
338
+ lead to another step; Compose acts immediately (navigates away). */}
339
+ {focus && strategyFooter}
340
+ {!annotate && chosenStrategy && (
341
+ <div className="semiont-gather__footer semiont-gather__footer--echo">
342
+ <div className="semiont-gather__footer-label">{chosenStrategy.label}</div>
343
+ <span className="semiont-gather__chosen-strategy">{chosenStrategy.value}</span>
214
344
  </div>
215
345
  )}
216
346
  </>