@semiont/react-ui 0.5.27 → 0.5.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +31 -4
  2. package/dist/{ar-OU3DQGC6.js → ar-FABPTJVB.js} +39 -23
  3. package/dist/{bn-PHWCR6AZ.js → bn-LKP6BDMH.js} +39 -23
  4. package/dist/{chunk-PCRRBU7H.js → chunk-2GOHTTHW.js} +39 -23
  5. package/dist/{chunk-5VDTWRXJ.js → chunk-2YHEE63S.js} +31 -31
  6. package/dist/{cs-NEXYZ62I.js → cs-YQYNWHRG.js} +39 -23
  7. package/dist/{da-FAT43YCU.js → da-QHZOYKRY.js} +39 -23
  8. package/dist/{de-KRQ3WBCU.js → de-GKA7RD7J.js} +39 -23
  9. package/dist/{el-Q4IU3EKC.js → el-45T257AL.js} +39 -23
  10. package/dist/{en-2KADE7N4.js → en-RVW5EQ66.js} +2 -2
  11. package/dist/{es-2IO3R2MK.js → es-AVER7RU4.js} +39 -23
  12. package/dist/{fa-Q5GOQFXX.js → fa-K3VPK3O2.js} +39 -23
  13. package/dist/{fi-R6NMRWBE.js → fi-5NEKF7EB.js} +39 -23
  14. package/dist/{fr-QY4D5E6J.js → fr-FUMNDLBZ.js} +39 -23
  15. package/dist/{he-X6Y77XHY.js → he-3JGNA3PR.js} +39 -23
  16. package/dist/{hi-XL7IOEQM.js → hi-IJY375DF.js} +39 -23
  17. package/dist/{id-QZ4ZZS67.js → id-LWHKJ7ZD.js} +39 -23
  18. package/dist/index.css +279 -74
  19. package/dist/index.d.ts +170 -145
  20. package/dist/index.js +2743 -2365
  21. package/dist/{it-ME2Q7OB6.js → it-EBVOV4RW.js} +39 -23
  22. package/dist/{ja-HX7QASRI.js → ja-KWRIX2NF.js} +39 -23
  23. package/dist/{ko-EQO5AHAV.js → ko-L5CJMFDS.js} +39 -23
  24. package/dist/{ms-UILFSF3I.js → ms-LSNMJDR2.js} +39 -23
  25. package/dist/{nl-NNI45PMK.js → nl-ZB32MB7U.js} +39 -23
  26. package/dist/{no-MAL5F6P6.js → no-4GTNY7M5.js} +39 -23
  27. package/dist/{pl-4RMY6JAJ.js → pl-LUHBLTHB.js} +39 -23
  28. package/dist/{pt-VQH2X3QQ.js → pt-C4H3UNQ6.js} +39 -23
  29. package/dist/{ro-XXCQO2OG.js → ro-GGITS5V4.js} +39 -23
  30. package/dist/{sv-7Z6EJ6U4.js → sv-T56RTLBP.js} +39 -23
  31. package/dist/test-utils.js +2 -2
  32. package/dist/{th-L3DXXYVQ.js → th-22X2IWCB.js} +39 -23
  33. package/dist/{tr-P3DFVLFJ.js → tr-77TFG62H.js} +39 -23
  34. package/dist/{uk-7C5D3PXS.js → uk-MIIL6NXT.js} +39 -23
  35. package/dist/{vi-ZLTLEO7P.js → vi-7JYOIGNV.js} +39 -23
  36. package/dist/{zh-5FHVWDIN.js → zh-ID6DRYU4.js} +39 -23
  37. package/package.json +9 -8
  38. package/src/components/AssistProgress.tsx +36 -2
  39. package/src/components/CodeMirrorRenderer.tsx +8 -8
  40. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  41. package/src/components/modals/ComposeStep.tsx +218 -0
  42. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  43. package/src/components/modals/ConfigureGenerationStep.tsx +35 -19
  44. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  45. package/src/components/modals/ContextSummary.tsx +185 -72
  46. package/src/components/modals/CorpusPane.tsx +75 -0
  47. package/src/components/modals/DiscardPrompt.tsx +38 -0
  48. package/src/components/modals/GatherContextStep.tsx +237 -107
  49. package/src/components/modals/ReferenceWizardModal.tsx +238 -50
  50. package/src/components/modals/ResourceGenerateModal.tsx +216 -108
  51. package/src/components/modals/SearchModal.css +337 -101
  52. package/src/components/modals/SearchResultsStep.tsx +61 -68
  53. package/src/components/modals/WizardFooter.tsx +7 -1
  54. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  55. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  56. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +35 -0
  57. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  58. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  59. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +417 -38
  60. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +276 -85
  61. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  62. package/src/components/modals/__tests__/WizardFooter.test.tsx +113 -4
  63. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  64. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  65. package/src/components/resource/AnnotateView.tsx +3 -3
  66. package/src/components/resource/BrowseView.tsx +5 -5
  67. package/src/components/resource/ResourceViewer.tsx +4 -4
  68. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  69. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  70. package/src/components/resource/__tests__/BrowseView.test.tsx +9 -9
  71. package/src/components/resource/annotate-renderers.tsx +3 -3
  72. package/src/components/resource/panels/AssistSection.css +35 -0
  73. package/src/components/resource/panels/ReferenceEntry.tsx +9 -2
  74. package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
  75. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  76. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  77. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  78. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +33 -1
  79. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +148 -6
  80. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  81. package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
  82. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  83. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  84. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
  85. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +133 -59
  86. package/src/styles/core/forms.css +35 -8
  87. package/src/styles/features/resource-viewer.css +0 -22
  88. package/src/styles/features/resource.css +0 -4
  89. package/translations/ar.json +38 -22
  90. package/translations/bn.json +38 -22
  91. package/translations/cs.json +38 -22
  92. package/translations/da.json +38 -22
  93. package/translations/de.json +38 -22
  94. package/translations/el.json +38 -22
  95. package/translations/en.json +38 -22
  96. package/translations/es.json +38 -22
  97. package/translations/fa.json +38 -22
  98. package/translations/fi.json +38 -22
  99. package/translations/fr.json +38 -22
  100. package/translations/he.json +38 -22
  101. package/translations/hi.json +38 -22
  102. package/translations/id.json +38 -22
  103. package/translations/it.json +38 -22
  104. package/translations/ja.json +38 -22
  105. package/translations/ko.json +38 -22
  106. package/translations/ms.json +38 -22
  107. package/translations/nl.json +38 -22
  108. package/translations/no.json +38 -22
  109. package/translations/pl.json +38 -22
  110. package/translations/pt.json +38 -22
  111. package/translations/ro.json +38 -22
  112. package/translations/sv.json +38 -22
  113. package/translations/th.json +38 -22
  114. package/translations/tr.json +38 -22
  115. package/translations/uk.json +38 -22
  116. package/translations/vi.json +38 -22
  117. package/translations/zh.json +38 -22
  118. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  119. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  120. package/dist/ar-OU3DQGC6.js.map +0 -1
  121. package/dist/bn-PHWCR6AZ.js.map +0 -1
  122. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  123. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  124. package/dist/chunk-O23TLLXW.js.map +0 -1
  125. package/dist/chunk-PCRRBU7H.js.map +0 -1
  126. package/dist/chunk-QT7LYM72.js.map +0 -1
  127. package/dist/chunk-VXASXU4K.js.map +0 -1
  128. package/dist/cs-NEXYZ62I.js.map +0 -1
  129. package/dist/da-FAT43YCU.js.map +0 -1
  130. package/dist/de-KRQ3WBCU.js.map +0 -1
  131. package/dist/el-Q4IU3EKC.js.map +0 -1
  132. package/dist/en-2KADE7N4.js.map +0 -1
  133. package/dist/es-2IO3R2MK.js.map +0 -1
  134. package/dist/fa-Q5GOQFXX.js.map +0 -1
  135. package/dist/fi-R6NMRWBE.js.map +0 -1
  136. package/dist/fr-QY4D5E6J.js.map +0 -1
  137. package/dist/he-X6Y77XHY.js.map +0 -1
  138. package/dist/hi-XL7IOEQM.js.map +0 -1
  139. package/dist/id-QZ4ZZS67.js.map +0 -1
  140. package/dist/index.css.map +0 -1
  141. package/dist/index.js.map +0 -1
  142. package/dist/integrations/css-modules-helper.js.map +0 -1
  143. package/dist/integrations/styled-components-theme.js.map +0 -1
  144. package/dist/it-ME2Q7OB6.js.map +0 -1
  145. package/dist/ja-HX7QASRI.js.map +0 -1
  146. package/dist/ko-EQO5AHAV.js.map +0 -1
  147. package/dist/ms-UILFSF3I.js.map +0 -1
  148. package/dist/nl-NNI45PMK.js.map +0 -1
  149. package/dist/no-MAL5F6P6.js.map +0 -1
  150. package/dist/pl-4RMY6JAJ.js.map +0 -1
  151. package/dist/pt-VQH2X3QQ.js.map +0 -1
  152. package/dist/ro-XXCQO2OG.js.map +0 -1
  153. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  154. package/dist/test-utils.css.map +0 -1
  155. package/dist/test-utils.js.map +0 -1
  156. package/dist/th-L3DXXYVQ.js.map +0 -1
  157. package/dist/tr-P3DFVLFJ.js.map +0 -1
  158. package/dist/uk-7C5D3PXS.js.map +0 -1
  159. package/dist/vi-ZLTLEO7P.js.map +0 -1
  160. package/dist/zh-5FHVWDIN.js.map +0 -1
  161. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  162. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
@@ -2,15 +2,15 @@
2
2
  * ResourceComposePage Component
3
3
  *
4
4
  * Pure React component for creating and editing resources.
5
- * Supports three modes: new resource, clone, and reference completion.
5
+ * Supports two modes: new resource and clone. (Reference completion moved
6
+ * into the resolve wizard — COMPOSE-IN-MODAL.)
6
7
  * All dependencies passed as props - no Next.js hooks!
7
8
  */
8
9
 
9
10
  import React, { useState, useEffect } from 'react';
10
- import { deriveViews, type GatheredContext } from '@semiont/core';
11
11
  import { capabilitiesOf, AUTHORABLE_MEDIA_TYPES, MEDIA_TYPES, mediaTypeForExtension, isSupportedMediaType, LOCALES } from '@semiont/core';
12
12
  import type { UploadProgress } from '@semiont/sdk';
13
- import { type CloneData, type ReferenceData } from '../state/compose-page-state-unit';
13
+ import { type CloneData } from '../state/compose-page-state-unit';
14
14
  import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
15
15
  import { buttonStyles } from '../../../lib/button-styles';
16
16
  import { CodeMirrorRenderer } from '../../../components/CodeMirrorRenderer';
@@ -42,10 +42,8 @@ function detectUploadMediaType(file: File): string {
42
42
  }
43
43
 
44
44
  export interface ResourceComposePageProps {
45
- mode: 'new' | 'clone' | 'reference';
45
+ mode: 'new' | 'clone';
46
46
  cloneData?: CloneData | null;
47
- referenceData?: ReferenceData | null;
48
- gatheredContext?: GatheredContext | null;
49
47
 
50
48
  // Available options
51
49
  availableEntityTypes: string[];
@@ -74,10 +72,7 @@ export interface ResourceComposePageProps {
74
72
  translations: {
75
73
  title: string;
76
74
  titleEditClone: string;
77
- titleCompleteReference: string;
78
75
  subtitleClone: string;
79
- subtitleReference: string;
80
- linkedNoticePrefix: string;
81
76
  resourceName: string;
82
77
  resourceNamePlaceholder: string;
83
78
  entityTypes: string;
@@ -99,9 +94,7 @@ export interface ResourceComposePageProps {
99
94
  cancel: string;
100
95
  saving: string;
101
96
  creating: string;
102
- creatingAndLinking: string;
103
97
  saveClonedResource: string;
104
- createAndLinkResource: string;
105
98
  createResource: string;
106
99
  };
107
100
 
@@ -111,7 +104,7 @@ export interface ResourceComposePageProps {
111
104
  }
112
105
 
113
106
  export interface SaveResourceParams {
114
- mode: 'new' | 'clone' | 'reference';
107
+ mode: 'new' | 'clone';
115
108
  name: string;
116
109
  storageUri: string;
117
110
  content?: string;
@@ -121,15 +114,11 @@ export interface SaveResourceParams {
121
114
  entityTypes?: string[];
122
115
  language: string;
123
116
  archiveOriginal?: boolean;
124
- annotationUri?: string;
125
- sourceDocumentId?: string;
126
117
  }
127
118
 
128
119
  export function ResourceComposePage({
129
120
  mode,
130
121
  cloneData,
131
- referenceData,
132
- gatheredContext,
133
122
  availableEntityTypes,
134
123
  initialLocale,
135
124
  theme,
@@ -184,13 +173,8 @@ export function ResourceComposePage({
184
173
  if (mode === 'clone' && cloneData) {
185
174
  setNewResourceName(cloneData.sourceResource.name);
186
175
  setNewResourceContent(cloneData.sourceContent);
187
- } else if (mode === 'reference' && referenceData) {
188
- setNewResourceName(referenceData.name);
189
- if (referenceData.entityTypes.length > 0) {
190
- setSelectedEntityTypes(referenceData.entityTypes);
191
- }
192
176
  }
193
- }, [mode, cloneData, referenceData]);
177
+ }, [mode, cloneData]);
194
178
 
195
179
  // Handle file upload
196
180
  const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -266,20 +250,12 @@ export function ResourceComposePage({
266
250
  if (mode === 'clone') {
267
251
  params.archiveOriginal = archiveOriginal;
268
252
  }
269
- if (referenceData?.annotationUri) {
270
- params.annotationUri = referenceData.annotationUri;
271
- }
272
- if (referenceData?.sourceDocumentId) {
273
- params.sourceDocumentId = referenceData.sourceDocumentId;
274
- }
275
253
 
276
254
  await onSaveResource(params);
277
255
 
278
256
  // Announce success based on mode
279
257
  const successMessage = mode === 'clone'
280
258
  ? 'Resource cloned successfully'
281
- : mode === 'reference'
282
- ? 'Reference completed successfully'
283
259
  : 'Resource created successfully';
284
260
  announceFormSuccess(successMessage);
285
261
  } catch (error) {
@@ -291,13 +267,6 @@ export function ResourceComposePage({
291
267
  };
292
268
 
293
269
  const isClone = mode === 'clone';
294
- const isReferenceCompletion = mode === 'reference';
295
-
296
- // Gathered-context panel — annotation-focus only for now.
297
- const composeFocus = gatheredContext?.focus.kind === 'annotation' ? gatheredContext.focus : null;
298
- const composeConnections = gatheredContext && composeFocus
299
- ? deriveViews(gatheredContext.graph, String(composeFocus.sourceResource.id)).connections
300
- : [];
301
270
 
302
271
  return (
303
272
  <div className={`semiont-page${activePanel && COMMON_PANELS.includes(activePanel as ToolbarPanelType) ? ' semiont-page--panel-open' : ''}`}>
@@ -306,73 +275,15 @@ export function ResourceComposePage({
306
275
  {/* Page Title */}
307
276
  <div className="semiont-page__header">
308
277
  <h1 className="semiont-page__title">
309
- {isClone ? t.titleEditClone : isReferenceCompletion ? t.titleCompleteReference : t.title}
278
+ {isClone ? t.titleEditClone : t.title}
310
279
  </h1>
311
- {(isClone || isReferenceCompletion) && (
280
+ {isClone && (
312
281
  <p className="semiont-page__subtitle">
313
- {isClone ? t.subtitleClone : t.subtitleReference}
282
+ {t.subtitleClone}
314
283
  </p>
315
284
  )}
316
- {isReferenceCompletion && (
317
- <div className="semiont-page__reference-notice">
318
- <p className="semiont-page__reference-notice-text">
319
- {t.linkedNoticePrefix}
320
- </p>
321
- </div>
322
- )}
323
285
  </div>
324
286
 
325
- {/* Gathered Context Panel (from wizard) */}
326
- {gatheredContext && (
327
- <div className="semiont-form__field" style={{
328
- marginBottom: '1.5rem',
329
- padding: '1rem',
330
- backgroundColor: 'var(--semiont-bg-secondary)',
331
- borderRadius: 'var(--semiont-radius-md)',
332
- border: '1px solid var(--semiont-border-primary)',
333
- }}>
334
- <h3 className="semiont-form__label" style={{ marginBottom: '0.75rem' }}>Gathered Context</h3>
335
- {composeFocus?.selected && (
336
- <div style={{
337
- padding: '0.75rem',
338
- backgroundColor: 'var(--semiont-bg-primary)',
339
- borderRadius: 'var(--semiont-radius-md)',
340
- border: '1px solid var(--semiont-border-primary)',
341
- maxHeight: '150px',
342
- overflowY: 'auto',
343
- marginBottom: '0.5rem',
344
- }}>
345
- <div style={{ fontSize: 'var(--semiont-text-sm)', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--semiont-text-secondary)' }}>
346
- {composeFocus.selected.before && <span>{composeFocus.selected.before}</span>}
347
- <span style={{
348
- backgroundColor: 'var(--semiont-color-primary-100)',
349
- padding: '0 0.25rem',
350
- fontWeight: 600,
351
- color: 'var(--semiont-color-primary-900)',
352
- }}>
353
- {composeFocus.selected.text}
354
- </span>
355
- {composeFocus.selected.after && <span>{composeFocus.selected.after}</span>}
356
- </div>
357
- </div>
358
- )}
359
- {composeConnections.length > 0 && (
360
- <div style={{
361
- padding: '0.5rem 0',
362
- fontSize: 'var(--semiont-text-sm)',
363
- color: 'var(--semiont-text-secondary)',
364
- }}>
365
- <span style={{ fontSize: 'var(--semiont-text-xs)', fontWeight: 500, marginRight: '0.5rem' }}>Connections:</span>
366
- {composeConnections.map(conn => (
367
- <span key={conn.resourceId} className="semiont-chip" style={{ fontSize: 'var(--semiont-text-xs)', padding: '0.125rem 0.5rem', marginRight: '0.25rem' }}>
368
- {conn.resourceName}
369
- </span>
370
- ))}
371
- </div>
372
- )}
373
- </div>
374
- )}
375
-
376
287
  {/* Create Form */}
377
288
  <div className="semiont-form">
378
289
  <form onSubmit={handleSaveResource} className="semiont-form__fields semiont-compose-grid">
@@ -416,7 +327,7 @@ export function ResourceComposePage({
416
327
  </div>
417
328
 
418
329
  {/* Entity Types Selection */}
419
- {(!isReferenceCompletion || selectedEntityTypes.length === 0) && (
330
+ {(
420
331
  <div className="semiont-form__field semiont-form__entity-types">
421
332
  <div className="semiont-form__label">
422
333
  {t.entityTypes}
@@ -462,30 +373,6 @@ export function ResourceComposePage({
462
373
  </div>
463
374
  )}
464
375
 
465
- {/* Entity Types Display for reference completion */}
466
- {isReferenceCompletion && selectedEntityTypes.length > 0 && (
467
- <div className="semiont-form__field semiont-form__entity-types-display" role="region" aria-labelledby="selected-entity-types-label">
468
- <h3 id="selected-entity-types-label" className="semiont-form__label">
469
- {t.entityTypes}
470
- </h3>
471
- <div className="semiont-form__entity-type-tags" role="list">
472
- {selectedEntityTypes.map((type) => (
473
- <span
474
- key={type}
475
- role="listitem"
476
- className="semiont-form__entity-type-tag"
477
- aria-label={`Entity type: ${type}`}
478
- >
479
- {type}
480
- </span>
481
- ))}
482
- </div>
483
- <p className="semiont-form__helper-text" id="reference-entity-types-description">
484
- These entity types were selected when creating the reference
485
- </p>
486
- </div>
487
- )}
488
-
489
376
  {/* Language Selector */}
490
377
  <div className="semiont-form__field">
491
378
  <label htmlFor="language-select" className="semiont-form__label">
@@ -512,7 +399,7 @@ export function ResourceComposePage({
512
399
  <div className="semiont-compose-grid__content">
513
400
 
514
401
  {/* Content Source Toggle - only show for new resources */}
515
- {!isClone && !isReferenceCompletion && (
402
+ {!isClone && (
516
403
  <div className="semiont-form__field">
517
404
  <label className="semiont-form__label">
518
405
  {t.contentSource}
@@ -554,7 +441,7 @@ export function ResourceComposePage({
554
441
  )}
555
442
 
556
443
  {/* Upload File Section */}
557
- {!isClone && !isReferenceCompletion && inputMethod === 'upload' && (
444
+ {!isClone && inputMethod === 'upload' && (
558
445
  <div className="semiont-form__upload-section">
559
446
  <div>
560
447
  <div className="semiont-form__upload-container">
@@ -631,10 +518,10 @@ export function ResourceComposePage({
631
518
  )}
632
519
 
633
520
  {/* Write Content Section */}
634
- {(isClone || isReferenceCompletion || inputMethod === 'write') && (
521
+ {(isClone || inputMethod === 'write') && (
635
522
  <div className="semiont-form__write-section">
636
523
  {/* Format Selector - only for manual entry, not clones */}
637
- {!isClone && !isReferenceCompletion && (
524
+ {!isClone && (
638
525
  <div className="semiont-form__field">
639
526
  <label htmlFor="format-select" className="semiont-form__label">
640
527
  {t.format}
@@ -673,7 +560,7 @@ export function ResourceComposePage({
673
560
  </div>
674
561
 
675
562
  {/* Encoding Selector - only for manual text entry */}
676
- {!isClone && !isReferenceCompletion && (
563
+ {!isClone && (
677
564
  <div className="semiont-form__field">
678
565
  <label htmlFor="charset-select" className="semiont-form__label">
679
566
  {t.encoding}
@@ -731,8 +618,8 @@ export function ResourceComposePage({
731
618
  className={buttonStyles.primary.base}
732
619
  >
733
620
  {isCreating
734
- ? (isClone ? t.saving : isReferenceCompletion ? t.creatingAndLinking : t.creating)
735
- : (isClone ? t.saveClonedResource : isReferenceCompletion ? t.createAndLinkResource : t.createResource)}
621
+ ? (isClone ? t.saving : t.creating)
622
+ : (isClone ? t.saveClonedResource : t.createResource)}
736
623
  </button>
737
624
  </div>
738
625
 
@@ -64,80 +64,6 @@ describe('createComposePageStateUnit', () => {
64
64
  stateUnit.dispose();
65
65
  });
66
66
 
67
- it('detects reference mode from params', async () => {
68
- const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
69
- annotationUri: 'ann-1',
70
- sourceDocumentId: 'doc-1',
71
- name: 'Reference Doc',
72
- entityTypes: 'Person,Place',
73
- });
74
-
75
- const mode = await firstValueFrom(stateUnit.mode$);
76
- expect(mode).toBe('reference');
77
-
78
- const ref = await firstValueFrom(stateUnit.referenceData$.pipe(filter((r) => r !== null)));
79
- expect(ref!.annotationUri).toBe('ann-1');
80
- expect(ref!.entityTypes).toEqual(['Person', 'Place']);
81
-
82
- stateUnit.dispose();
83
- });
84
-
85
- it('parses storedContext in reference mode', async () => {
86
- // A real `GatheredContext` shape. The old fixture here was
87
- // `{ annotation, sourceContext }` — not a context at all — and it round-tripped
88
- // because nothing checked. That is the hole the guard closes.
89
- const context = {
90
- focus: { kind: 'annotation', annotation: { id: 'ann-1' }, sourceResource: { id: 'doc-1' } },
91
- graph: { nodes: [], edges: [] },
92
- metadata: {},
93
- };
94
- const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
95
- annotationUri: 'ann-1',
96
- sourceDocumentId: 'doc-1',
97
- name: 'Ref',
98
- storedContext: JSON.stringify(context),
99
- });
100
-
101
- const gathered = await firstValueFrom(stateUnit.gatheredContext$.pipe(filter((g) => g !== null)));
102
- expect(gathered).toEqual(context);
103
-
104
- stateUnit.dispose();
105
- });
106
-
107
- it('ignores a stash that parses but is not a GatheredContext', async () => {
108
- // The realistic case: an entry written by an older build, read back after a
109
- // deploy. It used to sail through and reach `deriveViews`, which maps
110
- // `graph.nodes` during render and throws instead of degrading.
111
- const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
112
- annotationUri: 'ann-1',
113
- sourceDocumentId: 'doc-1',
114
- name: 'Ref',
115
- storedContext: JSON.stringify({ annotation: { id: 'ann-1' }, sourceContext: 'text' }),
116
- });
117
-
118
- // Treated as NO stash: the ordinary gather path owns the outcome from here.
119
- expect(await firstValueFrom(stateUnit.gatheredContext$)).toBeNull();
120
-
121
- stateUnit.dispose();
122
- });
123
-
124
- it('ignores malformed storedContext', async () => {
125
- const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
126
- annotationUri: 'ann-1',
127
- sourceDocumentId: 'doc-1',
128
- name: 'Ref',
129
- storedContext: 'not-json{{{',
130
- });
131
-
132
- const loading = await firstValueFrom(stateUnit.loading$.pipe(filter((l) => !l)));
133
- expect(loading).toBe(false);
134
-
135
- const gathered = await firstValueFrom(stateUnit.gatheredContext$);
136
- expect(gathered).toBeNull();
137
-
138
- stateUnit.dispose();
139
- });
140
-
141
67
  it('exposes entity types', async () => {
142
68
  const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {});
143
69
 
@@ -166,31 +92,6 @@ describe('createComposePageStateUnit', () => {
166
92
  stateUnit.dispose();
167
93
  });
168
94
 
169
- it('save in reference mode calls yield.resource then bind.body', async () => {
170
- const resource = mockUpload('new-4');
171
- const body = vi.fn().mockResolvedValue(undefined);
172
- const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ resource, body })), mockBrowse(), {
173
- annotationUri: 'ann-1',
174
- sourceDocumentId: 'doc-1',
175
- name: 'Ref',
176
- });
177
-
178
- const id = await stateUnit.save({
179
- mode: 'reference',
180
- name: 'Ref Doc',
181
- storageUri: '/docs/ref.md',
182
- content: 'content',
183
- language: 'en',
184
- annotationUri: 'ann-1',
185
- sourceDocumentId: 'doc-1',
186
- });
187
-
188
- expect(id).toBe('new-4');
189
- expect(body).toHaveBeenCalledOnce();
190
-
191
- stateUnit.dispose();
192
- });
193
-
194
95
  it('save in clone mode calls yield.createFromToken', async () => {
195
96
  const createFromToken = vi.fn().mockResolvedValue({ resourceId: 'cloned-1' });
196
97
  const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ createFromToken })), mockBrowse(), {
@@ -220,7 +121,7 @@ describe('ComposePageStateUnit — StateUnit axioms', () => {
220
121
  const browse = disposeProbe();
221
122
  return { unit: createComposePageStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
222
123
  },
223
- surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.referenceData$, u.gatheredContext$, u.uploadProgress$],
124
+ surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.uploadProgress$],
224
125
  });
225
126
  });
226
127
  });
@@ -1,6 +1,6 @@
1
1
  import { BehaviorSubject, type Observable } from 'rxjs';
2
- import type { GatheredContext, AnnotationId, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
3
- import { isGatheredContext, resourceId as makeResourceId, annotationId as makeAnnotationId } from '@semiont/core';
2
+ import type { AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
3
+ import { resourceId as makeResourceId } from '@semiont/core';
4
4
  import { createDisposer } from '@semiont/sdk';
5
5
  import type { StateUnit } from '@semiont/core';
6
6
  import type { ShellStateUnit } from '../../../state/shell-state-unit';
@@ -9,16 +9,11 @@ import type { SemiontSession } from '@semiont/sdk';
9
9
  import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
10
10
  import type { UploadProgress } from '@semiont/sdk';
11
11
 
12
- export type ComposeMode = 'new' | 'clone' | 'reference';
12
+ export type ComposeMode = 'new' | 'clone';
13
13
 
14
14
  export interface ComposeParams {
15
15
  mode?: string | undefined;
16
16
  token?: string | undefined;
17
- annotationUri?: string | undefined;
18
- sourceDocumentId?: string | undefined;
19
- name?: string | undefined;
20
- entityTypes?: string | undefined;
21
- storedContext?: string | undefined;
22
17
  }
23
18
 
24
19
  export interface CloneData {
@@ -26,13 +21,6 @@ export interface CloneData {
26
21
  sourceContent: string;
27
22
  }
28
23
 
29
- export interface ReferenceData {
30
- annotationUri: string;
31
- sourceDocumentId: string;
32
- name: string;
33
- entityTypes: string[];
34
- }
35
-
36
24
  export interface SaveResourceParams {
37
25
  mode: ComposeMode;
38
26
  name: string;
@@ -44,8 +32,6 @@ export interface SaveResourceParams {
44
32
  entityTypes?: string[];
45
33
  language: string;
46
34
  archiveOriginal?: boolean;
47
- annotationUri?: string;
48
- sourceDocumentId?: string;
49
35
  }
50
36
 
51
37
  export interface ComposePageStateUnit extends StateUnit {
@@ -53,8 +39,6 @@ export interface ComposePageStateUnit extends StateUnit {
53
39
  mode$: Observable<ComposeMode>;
54
40
  loading$: Observable<boolean>;
55
41
  cloneData$: Observable<CloneData | null>;
56
- referenceData$: Observable<ReferenceData | null>;
57
- gatheredContext$: Observable<GatheredContext | null>;
58
42
  entityTypes: ListState<string[]>;
59
43
  /**
60
44
  * Live upload-progress for the in-flight `save(...)` call. Emits the
@@ -78,43 +62,19 @@ export function createComposePageStateUnit(
78
62
  // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
79
63
  // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
80
64
 
81
- const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
82
65
  const isCloneMode = params.mode === 'clone' && Boolean(params.token);
83
- const pageMode: ComposeMode = isCloneMode ? 'clone' : isReferenceMode ? 'reference' : 'new';
66
+ const pageMode: ComposeMode = isCloneMode ? 'clone' : 'new';
84
67
 
85
68
  const mode$ = new BehaviorSubject<ComposeMode>(pageMode);
86
69
  const loading$ = new BehaviorSubject<boolean>(true);
87
70
  const cloneData$ = new BehaviorSubject<CloneData | null>(null);
88
- const referenceData$ = new BehaviorSubject<ReferenceData | null>(null);
89
- const gatheredContext$ = new BehaviorSubject<GatheredContext | null>(null);
90
71
  const uploadProgress$ = new BehaviorSubject<UploadProgress | null>(null);
91
72
 
92
73
  const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
93
74
  disposer.add(entityTypes.dispose);
94
75
 
95
76
  // Initialize based on mode
96
- if (isReferenceMode) {
97
- const entityTypes = params.entityTypes ? params.entityTypes.split(',') : [];
98
- referenceData$.next({
99
- annotationUri: params.annotationUri!,
100
- sourceDocumentId: params.sourceDocumentId!,
101
- name: params.name!,
102
- entityTypes,
103
- });
104
- if (params.storedContext) {
105
- // The stash is the one place a `GatheredContext` arrives with its type
106
- // history severed (sessionStorage → JSON.parse), so it is checked here and
107
- // trusted everywhere after. A stale entry — a tab held open across a deploy
108
- // — used to reach `deriveViews`, which maps `graph.nodes` during render and
109
- // throws rather than degrading. A failed guard is treated as NO stash: the
110
- // ordinary gather path runs, with the loading and error states it already has.
111
- try {
112
- const parsed: unknown = JSON.parse(params.storedContext);
113
- if (isGatheredContext(parsed)) gatheredContext$.next(parsed);
114
- } catch { /* ignore malformed */ }
115
- }
116
- loading$.next(false);
117
- } else if (isCloneMode) {
77
+ if (isCloneMode) {
118
78
  void (async () => {
119
79
  try {
120
80
  const tokenResult = await client.yield.fromToken(params.token!);
@@ -184,14 +144,6 @@ export function createComposePageStateUnit(
184
144
  });
185
145
  });
186
146
 
187
- if (saveParams.mode === 'reference' && saveParams.annotationUri && saveParams.sourceDocumentId) {
188
- await client.bind.body(
189
- makeResourceId(saveParams.sourceDocumentId),
190
- makeAnnotationId(saveParams.annotationUri) as AnnotationId,
191
- [{ op: 'add', item: { type: 'SpecificResource' as const, source: newResourceId, purpose: 'linking' as const } }],
192
- );
193
- }
194
-
195
147
  return newResourceId;
196
148
  };
197
149
 
@@ -200,8 +152,6 @@ export function createComposePageStateUnit(
200
152
  mode$: mode$.asObservable(),
201
153
  loading$: loading$.asObservable(),
202
154
  cloneData$: cloneData$.asObservable(),
203
- referenceData$: referenceData$.asObservable(),
204
- gatheredContext$: gatheredContext$.asObservable(),
205
155
  entityTypes: entityTypes.state,
206
156
  uploadProgress$: uploadProgress$.asObservable(),
207
157
  save,
@@ -209,8 +159,6 @@ export function createComposePageStateUnit(
209
159
  mode$.complete();
210
160
  loading$.complete();
211
161
  cloneData$.complete();
212
- referenceData$.complete();
213
- gatheredContext$.complete();
214
162
  uploadProgress$.complete();
215
163
  disposer.dispose();
216
164
  },
@@ -14,7 +14,8 @@ import { ToastProvider } from '../../../components/Toast';
14
14
  import { ThemeProvider } from '../../../contexts/ThemeContext';
15
15
  import { LineNumbersProvider } from '../../../contexts/LineNumbersContext';
16
16
  import { createTestSemiontWrapper } from '../../../test-utils';
17
- import type { ResourceId } from '@semiont/core';
17
+ import type { BodyOperation, EventMap, ResourceId, UserId } from '@semiont/core';
18
+ import { annotationId as makeAnnotationId, resourceId as makeResourceId } from '@semiont/core';
18
19
 
19
20
  // jsdom doesn't implement window.matchMedia — mock it for useTheme
20
21
  Object.defineProperty(window, 'matchMedia', {
@@ -102,24 +103,22 @@ useDebouncedCallback: (fn: any) => fn,
102
103
  announceResourceLoaded: vi.fn(),
103
104
  }),
104
105
  useEventSubscriptions: vi.fn(),
105
- useResourceAnnotations: () => ({
106
- clearNewAnnotationId: vi.fn(),
107
- newAnnotationIds: new Set(),
108
- markAnnotation: vi.fn(),
109
- deleteAnnotation: vi.fn(),
110
- triggerSparkleAnimation: vi.fn(),
111
- }),
106
+ useResourceAnnotations: () => sparkleContext,
112
107
  };
113
108
  });
114
109
 
110
+ // Shared, assertable spies — the page's sparkle wiring is pinned against
111
+ // these (a per-call `vi.fn()` object would be unobservable from the tests).
112
+ const sparkleContext = vi.hoisted(() => ({
113
+ clearSparkle: vi.fn(),
114
+ sparkleAnnotationIds: new Set<string>(),
115
+ markAnnotation: vi.fn(),
116
+ deleteAnnotation: vi.fn(),
117
+ triggerSparkleAnimation: vi.fn(),
118
+ }));
119
+
115
120
  vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
116
- useResourceAnnotations: () => ({
117
- clearNewAnnotationId: vi.fn(),
118
- newAnnotationIds: new Set(),
119
- markAnnotation: vi.fn(),
120
- deleteAnnotation: vi.fn(),
121
- triggerSparkleAnimation: vi.fn(),
122
- }),
121
+ useResourceAnnotations: () => sparkleContext,
123
122
  ResourceAnnotationsProvider: ({ children }: any) => children,
124
123
  }));
125
124
 
@@ -206,6 +205,63 @@ describe('ResourceViewerPage — outcome toasts reach the user', () => {
206
205
  });
207
206
  });
208
207
 
208
+ // RESOLUTION-SPARKLE D2: a reference resolving — from ANY strategy, local or
209
+ // remote — arrives as mark:body-updated with a linking-add operation, and the
210
+ // page turns exactly that into a sparkle. The harness mocks
211
+ // useEventSubscriptions, so the pin drives the captured handler directly.
212
+ describe('ResourceViewerPage — resolution sparkles', () => {
213
+ const bodyUpdated = (operations: BodyOperation[]): EventMap['mark:body-updated'] => ({
214
+ id: 'evt-1',
215
+ timestamp: '2026-08-21T12:00:00Z',
216
+ resourceId: makeResourceId('test-123'),
217
+ userId: 'did:web:example.com:users:alice' as UserId,
218
+ version: 1,
219
+ type: 'mark:body-updated',
220
+ payload: { annotationId: makeAnnotationId('ann-7'), operations },
221
+ metadata: { sequenceNumber: 1 },
222
+ });
223
+
224
+ const renderAndGetHandler = () => {
225
+ mockUseEventSubscriptions.mockClear();
226
+ sparkleContext.triggerSparkleAnimation.mockClear();
227
+ renderWithProviders(<ResourceViewerPage {...createMockProps()} />);
228
+ const map = mockUseEventSubscriptions.mock.calls.at(-1)?.[0] as
229
+ Record<string, (event: EventMap['mark:body-updated']) => void>;
230
+ return map['mark:body-updated'];
231
+ };
232
+
233
+ it('an operation adding a linking SpecificResource sparkles the annotation', () => {
234
+ const handler = renderAndGetHandler();
235
+ expect(handler).toBeDefined();
236
+
237
+ handler!(bodyUpdated([
238
+ { op: 'add', item: { type: 'SpecificResource', source: 'res-target', purpose: 'linking' } },
239
+ ]));
240
+
241
+ expect(sparkleContext.triggerSparkleAnimation).toHaveBeenCalledWith('ann-7');
242
+ });
243
+
244
+ it('unlink (remove-only operations) stays dark — A2', () => {
245
+ const handler = renderAndGetHandler();
246
+
247
+ handler?.(bodyUpdated([
248
+ { op: 'remove', item: { type: 'SpecificResource', source: 'res-target', purpose: 'linking' } },
249
+ ]));
250
+
251
+ expect(sparkleContext.triggerSparkleAnimation).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it('a TextualBody add stays dark', () => {
255
+ const handler = renderAndGetHandler();
256
+
257
+ handler?.(bodyUpdated([
258
+ { op: 'add', item: { type: 'TextualBody', value: 'Person', purpose: 'tagging' } },
259
+ ]));
260
+
261
+ expect(sparkleContext.triggerSparkleAnimation).not.toHaveBeenCalled();
262
+ });
263
+ });
264
+
209
265
  describe('ResourceViewerPage', () => {
210
266
  beforeEach(() => {
211
267
  });