@semiont/react-ui 0.5.6 → 0.5.7

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 (155) hide show
  1. package/README.md +1 -1
  2. package/dist/{ar-U2EXWUMQ.js → ar-SONK6MON.js} +3 -7
  3. package/dist/ar-SONK6MON.js.map +1 -0
  4. package/dist/{bn-DRJGV772.js → bn-ZKPRITNG.js} +3 -7
  5. package/dist/bn-ZKPRITNG.js.map +1 -0
  6. package/dist/{chunk-3Q3TUKWP.js → chunk-Y2EEAOMZ.js} +29 -29
  7. package/dist/{cs-PTWDM23V.js → cs-LPXQ7NHQ.js} +3 -7
  8. package/dist/cs-LPXQ7NHQ.js.map +1 -0
  9. package/dist/{da-KSNIKYSS.js → da-6TKY7MCY.js} +6 -10
  10. package/dist/da-6TKY7MCY.js.map +1 -0
  11. package/dist/{de-F2XBEWFY.js → de-C3GNII74.js} +3 -7
  12. package/dist/de-C3GNII74.js.map +1 -0
  13. package/dist/{el-DLD2GWAP.js → el-UBCXQDJ7.js} +3 -7
  14. package/dist/el-UBCXQDJ7.js.map +1 -0
  15. package/dist/{es-WLPYWGB5.js → es-BQ23TRI7.js} +11 -15
  16. package/dist/es-BQ23TRI7.js.map +1 -0
  17. package/dist/{fa-BAXHSDZG.js → fa-AFTBZB77.js} +3 -7
  18. package/dist/fa-AFTBZB77.js.map +1 -0
  19. package/dist/{fi-FCHSYVOT.js → fi-WOYNLZC2.js} +3 -7
  20. package/dist/fi-WOYNLZC2.js.map +1 -0
  21. package/dist/{fr-3UERBSL6.js → fr-NDSMIFJM.js} +3 -7
  22. package/dist/fr-NDSMIFJM.js.map +1 -0
  23. package/dist/{he-F6F3FV2K.js → he-VJXVRDOY.js} +3 -7
  24. package/dist/he-VJXVRDOY.js.map +1 -0
  25. package/dist/{hi-4BK6IK7Q.js → hi-BF6PHIE2.js} +3 -7
  26. package/dist/hi-BF6PHIE2.js.map +1 -0
  27. package/dist/{id-7ECCWP3J.js → id-GXG5QCZY.js} +3 -7
  28. package/dist/id-GXG5QCZY.js.map +1 -0
  29. package/dist/index.css +97 -0
  30. package/dist/index.css.map +1 -1
  31. package/dist/index.d.ts +29 -22
  32. package/dist/index.js +346 -301
  33. package/dist/index.js.map +1 -1
  34. package/dist/{it-234Z6XK6.js → it-XKHHCBAF.js} +3 -7
  35. package/dist/it-XKHHCBAF.js.map +1 -0
  36. package/dist/{ja-PJWQI4OQ.js → ja-TX7VM4XD.js} +3 -7
  37. package/dist/ja-TX7VM4XD.js.map +1 -0
  38. package/dist/{ko-APUEW2RS.js → ko-DNC7EQ7J.js} +3 -7
  39. package/dist/ko-DNC7EQ7J.js.map +1 -0
  40. package/dist/{ms-PJBZWZWD.js → ms-POZGBKPH.js} +3 -7
  41. package/dist/ms-POZGBKPH.js.map +1 -0
  42. package/dist/{nl-L4C3ZBCU.js → nl-IRMTKI7Z.js} +4 -11
  43. package/dist/nl-IRMTKI7Z.js.map +1 -0
  44. package/dist/{no-QE5N5KNG.js → no-ZUDJA4S6.js} +20 -24
  45. package/dist/no-ZUDJA4S6.js.map +1 -0
  46. package/dist/{pl-5Q2D23PD.js → pl-2NGAXL5U.js} +3 -7
  47. package/dist/pl-2NGAXL5U.js.map +1 -0
  48. package/dist/{pt-AIGUOIOC.js → pt-ABMCXZUM.js} +118 -122
  49. package/dist/pt-ABMCXZUM.js.map +1 -0
  50. package/dist/{ro-T56CSHTY.js → ro-VOJP6O5X.js} +3 -7
  51. package/dist/ro-VOJP6O5X.js.map +1 -0
  52. package/dist/{sv-L4TJQ2UH.js → sv-4HVFIIE5.js} +43 -47
  53. package/dist/sv-4HVFIIE5.js.map +1 -0
  54. package/dist/test-utils.js +2 -2
  55. package/dist/test-utils.js.map +1 -1
  56. package/dist/{th-6O7Y6O2Q.js → th-IFPZP3HQ.js} +3 -7
  57. package/dist/th-IFPZP3HQ.js.map +1 -0
  58. package/dist/{tr-D4CQCSNO.js → tr-2GYEAMJ4.js} +3 -7
  59. package/dist/tr-2GYEAMJ4.js.map +1 -0
  60. package/dist/{uk-2HMQG6ND.js → uk-XCJBVLLD.js} +3 -7
  61. package/dist/uk-XCJBVLLD.js.map +1 -0
  62. package/dist/{vi-XVJ4RUEJ.js → vi-4FR7CB2F.js} +3 -7
  63. package/dist/vi-4FR7CB2F.js.map +1 -0
  64. package/dist/{zh-K2KDPGHK.js → zh-NSKFOINB.js} +3 -7
  65. package/dist/zh-NSKFOINB.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/components/ProtectedErrorBoundary.css +119 -0
  68. package/src/components/ProtectedErrorBoundary.tsx +18 -13
  69. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +1 -1
  70. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +1 -1
  71. package/src/components/resource/AnnotateView.tsx +35 -37
  72. package/src/components/resource/BrowseView.tsx +31 -31
  73. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +0 -1
  74. package/src/components/resource/__tests__/BrowseView.test.tsx +4 -8
  75. package/src/components/resource/__tests__/HistoryEvent.test.tsx +0 -1
  76. package/src/components/resource/__tests__/event-formatting.test.ts +1 -1
  77. package/src/components/resource/panels/CollaborationPanel.tsx +1 -1
  78. package/src/components/resource/panels/JsonLdPanel.tsx +33 -16
  79. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +1 -1
  80. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +1 -1
  81. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -1
  82. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +1 -1
  83. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +1 -1
  84. package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +95 -424
  85. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +1 -1
  87. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -1
  88. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +7 -10
  89. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +38 -27
  90. package/src/features/admin-exchange/components/ImportProgress.tsx +28 -34
  91. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +11 -9
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +36 -9
  93. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -8
  94. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +7 -5
  95. package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +37 -0
  96. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +9 -5
  97. package/src/styles/features/exchange.css +0 -30
  98. package/src/styles/index.css +1 -0
  99. package/translations/ar.json +1 -3
  100. package/translations/bn.json +1 -3
  101. package/translations/cs.json +1 -3
  102. package/translations/da.json +4 -6
  103. package/translations/de.json +1 -3
  104. package/translations/el.json +1 -3
  105. package/translations/es.json +9 -11
  106. package/translations/fa.json +1 -3
  107. package/translations/fi.json +1 -3
  108. package/translations/fr.json +1 -3
  109. package/translations/he.json +1 -3
  110. package/translations/hi.json +1 -3
  111. package/translations/id.json +1 -3
  112. package/translations/it.json +1 -3
  113. package/translations/ja.json +1 -3
  114. package/translations/ko.json +1 -3
  115. package/translations/ms.json +1 -3
  116. package/translations/nl.json +2 -7
  117. package/translations/no.json +18 -20
  118. package/translations/pl.json +1 -3
  119. package/translations/pt.json +116 -118
  120. package/translations/ro.json +1 -3
  121. package/translations/sv.json +41 -43
  122. package/translations/th.json +1 -3
  123. package/translations/tr.json +1 -3
  124. package/translations/uk.json +1 -3
  125. package/translations/vi.json +1 -3
  126. package/translations/zh.json +1 -3
  127. package/dist/ar-U2EXWUMQ.js.map +0 -1
  128. package/dist/bn-DRJGV772.js.map +0 -1
  129. package/dist/cs-PTWDM23V.js.map +0 -1
  130. package/dist/da-KSNIKYSS.js.map +0 -1
  131. package/dist/de-F2XBEWFY.js.map +0 -1
  132. package/dist/el-DLD2GWAP.js.map +0 -1
  133. package/dist/es-WLPYWGB5.js.map +0 -1
  134. package/dist/fa-BAXHSDZG.js.map +0 -1
  135. package/dist/fi-FCHSYVOT.js.map +0 -1
  136. package/dist/fr-3UERBSL6.js.map +0 -1
  137. package/dist/he-F6F3FV2K.js.map +0 -1
  138. package/dist/hi-4BK6IK7Q.js.map +0 -1
  139. package/dist/id-7ECCWP3J.js.map +0 -1
  140. package/dist/it-234Z6XK6.js.map +0 -1
  141. package/dist/ja-PJWQI4OQ.js.map +0 -1
  142. package/dist/ko-APUEW2RS.js.map +0 -1
  143. package/dist/ms-PJBZWZWD.js.map +0 -1
  144. package/dist/nl-L4C3ZBCU.js.map +0 -1
  145. package/dist/no-QE5N5KNG.js.map +0 -1
  146. package/dist/pl-5Q2D23PD.js.map +0 -1
  147. package/dist/pt-AIGUOIOC.js.map +0 -1
  148. package/dist/ro-T56CSHTY.js.map +0 -1
  149. package/dist/sv-L4TJQ2UH.js.map +0 -1
  150. package/dist/th-6O7Y6O2Q.js.map +0 -1
  151. package/dist/tr-D4CQCSNO.js.map +0 -1
  152. package/dist/uk-2HMQG6ND.js.map +0 -1
  153. package/dist/vi-XVJ4RUEJ.js.map +0 -1
  154. package/dist/zh-K2KDPGHK.js.map +0 -1
  155. /package/dist/{chunk-3Q3TUKWP.js.map → chunk-Y2EEAOMZ.js.map} +0 -0
@@ -0,0 +1,119 @@
1
+ /* ============================================
2
+ ProtectedErrorBoundary Component Styles
3
+ ============================================ */
4
+
5
+ .semiont-protected-error-boundary-container {
6
+ min-height: 400px;
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ padding: var(--semiont-spacing-md);
11
+ }
12
+
13
+ .semiont-protected-error-boundary-card {
14
+ max-width: 28rem;
15
+ width: 100%;
16
+ background-color: var(--semiont-color-white);
17
+ border-radius: var(--semiont-radius-lg);
18
+ box-shadow: var(--semiont-shadow-lg);
19
+ padding: var(--semiont-spacing-lg);
20
+ }
21
+
22
+ [data-theme="dark"] .semiont-protected-error-boundary-card {
23
+ background-color: var(--semiont-color-gray-800);
24
+ }
25
+
26
+ .semiont-protected-error-boundary-header {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 0.75rem;
30
+ margin-bottom: var(--semiont-spacing-md);
31
+ }
32
+
33
+ .semiont-protected-error-boundary-icon-wrapper {
34
+ flex-shrink: 0;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ width: 2.5rem;
39
+ height: 2.5rem;
40
+ border-radius: var(--semiont-radius-full);
41
+ background-color: var(--semiont-color-red-100);
42
+ }
43
+
44
+ [data-theme="dark"] .semiont-protected-error-boundary-icon-wrapper {
45
+ background-color: rgba(127, 29, 29, 0.3);
46
+ }
47
+
48
+ .semiont-protected-error-boundary-icon {
49
+ width: 1.5rem;
50
+ height: 1.5rem;
51
+ color: var(--semiont-color-red-600);
52
+ }
53
+
54
+ [data-theme="dark"] .semiont-protected-error-boundary-icon {
55
+ color: var(--semiont-color-red-400);
56
+ }
57
+
58
+ .semiont-protected-error-boundary-title {
59
+ font-size: var(--semiont-text-xl);
60
+ font-weight: var(--semiont-font-semibold);
61
+ color: var(--semiont-color-gray-900);
62
+ }
63
+
64
+ [data-theme="dark"] .semiont-protected-error-boundary-title {
65
+ color: var(--semiont-color-white);
66
+ }
67
+
68
+ .semiont-protected-error-boundary-message {
69
+ color: var(--semiont-color-gray-600);
70
+ margin-bottom: var(--semiont-spacing-lg);
71
+ }
72
+
73
+ [data-theme="dark"] .semiont-protected-error-boundary-message {
74
+ color: var(--semiont-color-gray-300);
75
+ }
76
+
77
+ .semiont-protected-error-boundary-details {
78
+ margin-bottom: var(--semiont-spacing-md);
79
+ }
80
+
81
+ .semiont-protected-error-boundary-summary {
82
+ font-size: var(--semiont-text-sm);
83
+ color: var(--semiont-color-gray-500);
84
+ cursor: pointer;
85
+ }
86
+
87
+ .semiont-protected-error-boundary-summary:hover {
88
+ color: var(--semiont-color-gray-700);
89
+ }
90
+
91
+ [data-theme="dark"] .semiont-protected-error-boundary-summary {
92
+ color: var(--semiont-color-gray-500);
93
+ }
94
+
95
+ [data-theme="dark"] .semiont-protected-error-boundary-summary:hover {
96
+ color: var(--semiont-color-gray-300);
97
+ }
98
+
99
+ .semiont-protected-error-boundary-stack {
100
+ margin-top: var(--semiont-spacing-sm);
101
+ font-size: var(--semiont-text-xs);
102
+ background-color: var(--semiont-color-gray-100);
103
+ padding: var(--semiont-spacing-sm);
104
+ border-radius: var(--semiont-radius-sm);
105
+ overflow: auto;
106
+ }
107
+
108
+ [data-theme="dark"] .semiont-protected-error-boundary-stack {
109
+ background-color: var(--semiont-color-gray-900);
110
+ }
111
+
112
+ .semiont-protected-error-boundary-actions {
113
+ display: flex;
114
+ gap: 0.75rem;
115
+ }
116
+
117
+ .semiont-protected-error-boundary-actions .semiont-button {
118
+ flex: 1 1 0;
119
+ }
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { ErrorBoundary, type FallbackProps } from 'react-error-boundary';
3
+ import './ProtectedErrorBoundary.css';
3
4
 
4
5
  interface ProtectedErrorBoundaryProps {
5
6
  children: React.ReactNode;
@@ -50,45 +51,49 @@ function ProtectedErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
50
51
  const message = error instanceof Error ? error.message : String(error);
51
52
  const stack = error instanceof Error ? error.stack : undefined;
52
53
  return (
53
- <div className="min-h-[400px] flex items-center justify-center p-4">
54
- <div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
55
- <div className="flex items-center gap-3 mb-4">
56
- <div className="flex-shrink-0 w-10 h-10 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
57
- <svg className="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <div className="semiont-protected-error-boundary-container">
55
+ <div className="semiont-protected-error-boundary-card">
56
+ <div className="semiont-protected-error-boundary-header">
57
+ <div className="semiont-protected-error-boundary-icon-wrapper">
58
+ <svg className="semiont-protected-error-boundary-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
59
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
59
60
  </svg>
60
61
  </div>
61
- <h2 className="text-xl font-semibold text-gray-900 dark:text-white">
62
+ <h2 className="semiont-protected-error-boundary-title">
62
63
  Something went wrong
63
64
  </h2>
64
65
  </div>
65
66
 
66
- <p className="text-gray-600 dark:text-gray-300 mb-6">
67
+ <p className="semiont-protected-error-boundary-message">
67
68
  An unexpected error occurred. Try again, or refresh the page.
68
69
  </p>
69
70
 
70
71
  {process.env.NODE_ENV === 'development' && (
71
- <details className="mb-4">
72
- <summary className="text-sm text-gray-500 cursor-pointer hover:text-gray-700 dark:hover:text-gray-300">
72
+ <details className="semiont-protected-error-boundary-details">
73
+ <summary className="semiont-protected-error-boundary-summary">
73
74
  Error details (development only)
74
75
  </summary>
75
- <pre className="mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded-sm overflow-auto">
76
+ <pre className="semiont-protected-error-boundary-stack">
76
77
  {message}
77
78
  {stack}
78
79
  </pre>
79
80
  </details>
80
81
  )}
81
82
 
82
- <div className="flex gap-3">
83
+ <div className="semiont-protected-error-boundary-actions">
83
84
  <button
84
85
  onClick={resetErrorBoundary}
85
- className="flex-1 px-4 py-2 text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
86
+ className="semiont-button"
87
+ data-variant="secondary"
88
+ data-size="md"
86
89
  >
87
90
  Try Again
88
91
  </button>
89
92
  <button
90
93
  onClick={() => window.location.reload()}
91
- className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
94
+ className="semiont-button"
95
+ data-variant="primary"
96
+ data-size="md"
92
97
  >
93
98
  Refresh Page
94
99
  </button>
@@ -14,7 +14,7 @@ vi.mock('@headlessui/react', () => ({
14
14
  TransitionChild: ({ children }: any) => <>{children}</>,
15
15
  }));
16
16
 
17
- // Mock the api-client Observable surface.
17
+ // Mock the http-transport Observable surface.
18
18
  // The session-based useSemiont path: useObservable(useSemiont().activeSession$)?.client
19
19
  // We mock useSemiont to return a stable browser whose activeSession$ emits a
20
20
  // session-shaped object that carries the mock client.
@@ -31,7 +31,7 @@ vi.mock('@headlessui/react', () => ({
31
31
  TransitionChild: ({ children }: any) => <>{children}</>,
32
32
  }));
33
33
 
34
- // Mock the api-client Observable surface
34
+ // Mock the http-transport Observable surface
35
35
  const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
36
36
  const browseResourcesMock = vi.fn(() => browseResourcesSubject.asObservable());
37
37
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect, useCallback, lazy, Suspense } from 'react';
4
- import { getMimeCategory, isPdfMimeType } from '@semiont/core';
4
+ import { capabilitiesOf } from '@semiont/core';
5
5
  import { ANNOTATORS } from '../../lib/annotation-registry';
6
6
  import { segmentTextWithAnnotations } from '../../lib/text-segmentation';
7
7
  import { buildTextSelectors, fallbackTextPosition } from '../../lib/text-selection-handler';
@@ -71,7 +71,7 @@ export function AnnotateView({
71
71
  const containerRef = useRef<HTMLDivElement>(null);
72
72
  const session = useObservable(useSemiont().activeSession$);
73
73
 
74
- const category = getMimeCategory(mimeType);
74
+ const render = capabilitiesOf(mimeType)?.render ?? 'none';
75
75
 
76
76
  const { highlights, references, assessments, comments, tags } = annotations;
77
77
 
@@ -192,8 +192,8 @@ export function AnnotateView({
192
192
  };
193
193
  }, [selectedMotivation, content]);
194
194
 
195
- // Route to appropriate viewer based on MIME type category
196
- switch (category) {
195
+ // Route to the annotation viewer for this media type's render mode.
196
+ switch (render) {
197
197
  case 'text':
198
198
  return (
199
199
  <div className="semiont-annotate-view" data-mime-type="text" ref={containerRef}>
@@ -225,40 +225,38 @@ export function AnnotateView({
225
225
  </div>
226
226
  );
227
227
 
228
- case 'image':
229
- // MIME-specific viewer selection within spatial annotation category
230
- if (isPdfMimeType(mimeType)) {
231
- // Phase 2: PDF annotation support
232
- return (
233
- <div className="semiont-annotate-view" data-mime-type="pdf" ref={containerRef}>
234
- <AnnotateToolbar
235
- selectedMotivation={selectedMotivation}
236
- selectedClick={selectedClick}
237
- showShapeGroup={true}
238
- selectedShape={selectedShape}
239
- mediaType={mimeType}
240
- annotateMode={annotateMode}
241
- annotators={ANNOTATORS}
242
- />
243
- <div className="semiont-annotate-view__content">
244
- {content && (
245
- <Suspense fallback={<div className="semiont-annotate-view__loading">Loading PDF viewer...</div>}>
246
- <PdfAnnotationCanvas
247
- pdfUrl={content}
248
- existingAnnotations={allAnnotations}
249
- drawingMode={selectedMotivation ? selectedShape : null}
250
- selectedMotivation={selectedMotivation}
251
- session={session}
252
- hoveredAnnotationId={hoveredAnnotationId || null}
253
- hoverDelayMs={hoverDelayMs}
254
- />
255
- </Suspense>
256
- )}
257
- </div>
228
+ case 'pdf':
229
+ // PDF annotation support (spatial, FragmentSelector)
230
+ return (
231
+ <div className="semiont-annotate-view" data-mime-type="pdf" ref={containerRef}>
232
+ <AnnotateToolbar
233
+ selectedMotivation={selectedMotivation}
234
+ selectedClick={selectedClick}
235
+ showShapeGroup={true}
236
+ selectedShape={selectedShape}
237
+ mediaType={mimeType}
238
+ annotateMode={annotateMode}
239
+ annotators={ANNOTATORS}
240
+ />
241
+ <div className="semiont-annotate-view__content">
242
+ {content && (
243
+ <Suspense fallback={<div className="semiont-annotate-view__loading">Loading PDF viewer...</div>}>
244
+ <PdfAnnotationCanvas
245
+ pdfUrl={content}
246
+ existingAnnotations={allAnnotations}
247
+ drawingMode={selectedMotivation ? selectedShape : null}
248
+ selectedMotivation={selectedMotivation}
249
+ session={session}
250
+ hoveredAnnotationId={hoveredAnnotationId || null}
251
+ hoverDelayMs={hoverDelayMs}
252
+ />
253
+ </Suspense>
254
+ )}
258
255
  </div>
259
- );
260
- }
256
+ </div>
257
+ );
261
258
 
259
+ case 'image':
262
260
  // PNG, JPEG, etc. - full annotation support
263
261
  return (
264
262
  <div className="semiont-annotate-view" data-mime-type="image" ref={containerRef}>
@@ -287,7 +285,7 @@ export function AnnotateView({
287
285
  </div>
288
286
  );
289
287
 
290
- case 'unsupported':
288
+ case 'none':
291
289
  default:
292
290
  return (
293
291
  <div ref={containerRef} className="semiont-annotate-view semiont-annotate-view--unsupported" data-mime-type="unsupported">
@@ -4,7 +4,7 @@ import { useEffect, useRef, useCallback, useMemo, memo, lazy, Suspense } from 'r
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
6
  import { annotationId as toAnnotationId } from '@semiont/core';
7
- import { getMimeCategory, isPdfMimeType } from '@semiont/core';
7
+ import { capabilitiesOf } from '@semiont/core';
8
8
  import { createHoverHandlers } from '@semiont/sdk';
9
9
  import { ANNOTATORS } from '../../lib/annotation-registry';
10
10
  import { scrollAnnotationIntoView } from '../../lib/scroll-utils';
@@ -83,7 +83,7 @@ export const BrowseView = memo(function BrowseView({
83
83
  const session = useObservable(useSemiont().activeSession$);
84
84
  const containerRef = useRef<HTMLDivElement>(null);
85
85
 
86
- const category = getMimeCategory(mimeType);
86
+ const render = capabilitiesOf(mimeType)?.render ?? 'none';
87
87
 
88
88
  const { highlights, references, assessments, comments, tags } = annotations;
89
89
 
@@ -207,8 +207,9 @@ export const BrowseView = memo(function BrowseView({
207
207
  'beckon:focus': handleAnnotationFocus,
208
208
  });
209
209
 
210
- // Route to appropriate viewer based on MIME type category
211
- switch (category) {
210
+ // Route to the viewer for this media type's render mode. The switch is
211
+ // exhaustive over RenderMode, so every path returns.
212
+ switch (render) {
212
213
  case 'text':
213
214
  return (
214
215
  <div className="semiont-browse-view" data-mime-type="text">
@@ -226,34 +227,31 @@ export const BrowseView = memo(function BrowseView({
226
227
  </div>
227
228
  );
228
229
 
229
- case 'image':
230
- // Check if it's actually a PDF (categorized as 'image' for spatial annotations)
231
- if (isPdfMimeType(mimeType)) {
232
- return (
233
- <div className="semiont-browse-view" data-mime-type="pdf">
234
- <AnnotateToolbar
235
- selectedMotivation={null}
236
- selectedClick={selectedClick}
237
- showSelectionGroup={false}
238
- showDeleteButton={false}
239
- annotateMode={annotateMode}
240
- annotators={ANNOTATORS}
241
- />
242
- <div ref={containerRef} className="semiont-browse-view__content">
243
- <Suspense fallback={<div className="semiont-browse-view__loading">Loading PDF viewer...</div>}>
244
- <PdfAnnotationCanvas
245
- pdfUrl={content}
246
- existingAnnotations={allAnnotations}
247
- drawingMode={null}
248
- selectedMotivation={null}
249
- />
250
- </Suspense>
251
- </div>
230
+ case 'pdf':
231
+ return (
232
+ <div className="semiont-browse-view" data-mime-type="pdf">
233
+ <AnnotateToolbar
234
+ selectedMotivation={null}
235
+ selectedClick={selectedClick}
236
+ showSelectionGroup={false}
237
+ showDeleteButton={false}
238
+ annotateMode={annotateMode}
239
+ annotators={ANNOTATORS}
240
+ />
241
+ <div ref={containerRef} className="semiont-browse-view__content">
242
+ <Suspense fallback={<div className="semiont-browse-view__loading">Loading PDF viewer...</div>}>
243
+ <PdfAnnotationCanvas
244
+ pdfUrl={content}
245
+ existingAnnotations={allAnnotations}
246
+ drawingMode={null}
247
+ selectedMotivation={null}
248
+ />
249
+ </Suspense>
252
250
  </div>
253
- );
254
- }
251
+ </div>
252
+ );
255
253
 
256
- // Regular image
254
+ case 'image':
257
255
  return (
258
256
  <div className="semiont-browse-view" data-mime-type="image">
259
257
  <AnnotateToolbar
@@ -274,7 +272,9 @@ export const BrowseView = memo(function BrowseView({
274
272
  </div>
275
273
  );
276
274
 
277
- case 'unsupported':
275
+ case 'none':
276
+ // Catalogued type with no preview (render: 'none') or an imported
277
+ // foreign type the registry doesn't know — same UI: metadata + download.
278
278
  return (
279
279
  <div ref={containerRef} className="semiont-browse-view semiont-browse-view--unsupported" data-mime-type="unsupported">
280
280
  <div className="semiont-browse-view__empty">
@@ -78,7 +78,6 @@ function makeStoredEvent(id: string, type: string, seq: number, overrides: Recor
78
78
  ...overrides,
79
79
  metadata: {
80
80
  sequenceNumber: seq,
81
- streamPosition: 0,
82
81
  },
83
82
  };
84
83
  }
@@ -15,18 +15,14 @@ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
15
15
  })),
16
16
  }));
17
17
 
18
- // Mock @semiont/core utilities (these helpers and `resourceId` all live in core).
18
+ // Mock @semiont/core utilities. The media-type registry (`capabilitiesOf`)
19
+ // is NOT mocked — BrowseView dispatches on the real registry's render mode,
20
+ // so the tested types (text/markdown, image/png, application/octet-stream)
21
+ // resolve through the real source of truth.
19
22
  vi.mock('@semiont/core', async () => {
20
23
  const actual = await vi.importActual('@semiont/core');
21
24
  return {
22
25
  ...actual,
23
- getMimeCategory: vi.fn((mimeType: string) => {
24
- if (mimeType.startsWith('text/')) return 'text';
25
- if (mimeType.startsWith('image/')) return 'image';
26
- if (mimeType === 'application/pdf') return 'image';
27
- return 'unsupported';
28
- }),
29
- isPdfMimeType: vi.fn((mimeType: string) => mimeType === 'application/pdf'),
30
26
  resourceId: vi.fn((id: string) => id),
31
27
  getExactText: vi.fn(() => 'exact text'),
32
28
  getTextPositionSelector: vi.fn(() => ({ start: 0, end: 10 })),
@@ -58,7 +58,6 @@ function makeStoredEvent(overrides: Record<string, any> = {}): any {
58
58
  ...overrides,
59
59
  metadata: {
60
60
  sequenceNumber: 1,
61
- streamPosition: 0,
62
61
  },
63
62
  };
64
63
  }
@@ -8,7 +8,7 @@ import {
8
8
  getResourceCreationDetails,
9
9
  } from '../event-formatting';
10
10
 
11
- // Mock api-client functions
11
+ // Mock http-transport functions
12
12
  vi.mock('@semiont/core', async (importOriginal) => {
13
13
  const actual = await importOriginal<typeof import('@semiont/core')>();
14
14
  return {
@@ -7,7 +7,7 @@ import './CollaborationPanel.css';
7
7
  interface Props {
8
8
  /**
9
9
  * Connection state from `client.actor.state$`. See
10
- * `packages/api-client/src/state/domain/actor-state-unit.ts`.
10
+ * `packages/http-transport/src/state/domain/actor-state-unit.ts`.
11
11
  *
12
12
  * UI mapping:
13
13
  * `open` | `reconnecting` | `initial` | `connecting`
@@ -8,42 +8,46 @@ import { oneDark } from '@codemirror/theme-one-dark';
8
8
  import { syntaxHighlighting } from '@codemirror/language';
9
9
  import { jsonLightTheme, jsonLightHighlightStyle } from '../../../lib/codemirror-json-theme';
10
10
  import { useLineNumbers } from '../../../hooks/useLineNumbers';
11
- import type { components } from '@semiont/core';
11
+ import { useResourceGraph } from '../../../hooks/useResourceGraph';
12
+ import type { ResourceId } from '@semiont/core';
12
13
  import './JsonLdPanel.css';
13
14
 
14
- type SemiontResource = components['schemas']['ResourceDescriptor'];
15
-
16
15
  interface Props {
17
- resource: SemiontResource;
16
+ resourceId: ResourceId;
18
17
  }
19
18
 
20
- export function JsonLdPanel({ resource: semiontResource }: Props) {
19
+ /**
20
+ * Dereferences the resource's LD face (`GET /resources/:id/jsonld` via
21
+ * `browse.resourceGraph`) and pretty-prints the full graph — descriptor +
22
+ * annotations + inbound entity references — read-only. This is exactly what
23
+ * an external linked-data client gets when dereferencing the resource's
24
+ * `describedby` URI, so the panel doubles as a living end-to-end test of the
25
+ * LD face. See `.plans/SIMPLER-JSON-LD.md` §5.
26
+ */
27
+ export function JsonLdPanel({ resourceId }: Props) {
21
28
  const editorRef = useRef<HTMLDivElement>(null);
22
29
  const viewRef = useRef<EditorView | null>(null);
23
30
  const { showLineNumbers } = useLineNumbers();
31
+ const { graph, loading, error } = useResourceGraph(resourceId);
32
+
33
+ const documentText = graph ? JSON.stringify(graph, null, 2) : '';
24
34
 
25
- // Initialize CodeMirror
35
+ // Initialize CodeMirror once the graph has loaded.
26
36
  useEffect(() => {
27
- if (!editorRef.current) return;
37
+ if (!editorRef.current || !documentText) return;
28
38
 
29
- // Check if dark mode is active
30
39
  const isDarkMode = document.documentElement?.classList.contains('dark') ?? false;
31
40
 
32
- // Convert resource to JSON-LD format
33
- const jsonLdContent = JSON.stringify(semiontResource, null, 2);
34
-
35
41
  const extensions = [
36
42
  json(),
37
43
  EditorView.editable.of(false),
38
44
  EditorState.readOnly.of(true),
39
45
  ];
40
46
 
41
- // Add line numbers if enabled
42
47
  if (showLineNumbers) {
43
48
  extensions.push(lineNumbers());
44
49
  }
45
50
 
46
- // Add theme based on dark/light mode
47
51
  if (isDarkMode) {
48
52
  extensions.push(oneDark);
49
53
  } else {
@@ -52,7 +56,7 @@ export function JsonLdPanel({ resource: semiontResource }: Props) {
52
56
  }
53
57
 
54
58
  const state = EditorState.create({
55
- doc: jsonLdContent,
59
+ doc: documentText,
56
60
  extensions,
57
61
  });
58
62
 
@@ -67,11 +71,12 @@ export function JsonLdPanel({ resource: semiontResource }: Props) {
67
71
  view.destroy();
68
72
  viewRef.current = null;
69
73
  };
70
- }, [semiontResource, showLineNumbers]);
74
+ }, [documentText, showLineNumbers]);
71
75
 
72
76
  const handleCopyToClipboard = async () => {
77
+ if (!documentText) return;
73
78
  try {
74
- await navigator.clipboard.writeText(JSON.stringify(semiontResource, null, 2));
79
+ await navigator.clipboard.writeText(documentText);
75
80
  } catch (err) {
76
81
  console.error('Failed to copy JSON-LD:', err);
77
82
  }
@@ -88,11 +93,23 @@ export function JsonLdPanel({ resource: semiontResource }: Props) {
88
93
  onClick={handleCopyToClipboard}
89
94
  className="semiont-button semiont-button--icon"
90
95
  title="Copy to clipboard"
96
+ disabled={!graph}
91
97
  >
92
98
  📋 Copy
93
99
  </button>
94
100
  </div>
95
101
 
102
+ {loading && (
103
+ <p className="semiont-jsonld-panel__status" role="status">
104
+ Loading JSON-LD…
105
+ </p>
106
+ )}
107
+ {error && !loading && (
108
+ <p className="semiont-jsonld-panel__status semiont-jsonld-panel__status--error" role="alert">
109
+ Failed to load JSON-LD.
110
+ </p>
111
+ )}
112
+
96
113
  {/* JSON-LD content rendered with CodeMirror */}
97
114
  <div
98
115
  ref={editorRef}
@@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event';
6
6
 
7
7
  import type { Annotation, AnnotationId } from '@semiont/core';
8
8
 
9
- // Mock @semiont/api-client
9
+ // Mock @semiont/http-transport
10
10
  vi.mock('@semiont/core', async () => {
11
11
  const actual = await vi.importActual('@semiont/core');
12
12
  return {
@@ -57,7 +57,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
57
57
  TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
58
58
  }));
59
59
 
60
- // Mock @semiont/api-client utilities
60
+ // Mock @semiont/http-transport utilities
61
61
  vi.mock('@semiont/core', async () => {
62
62
  const actual = await vi.importActual('@semiont/core');
63
63
  return {
@@ -21,7 +21,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
21
21
  TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
22
22
  }));
23
23
 
24
- // Mock @semiont/api-client utilities
24
+ // Mock @semiont/http-transport utilities
25
25
  vi.mock('@semiont/core', async () => {
26
26
  const actual = await vi.importActual('@semiont/core');
27
27
  return {
@@ -56,7 +56,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
56
56
  TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
57
57
  }));
58
58
 
59
- // Mock @semiont/api-client utilities
59
+ // Mock @semiont/http-transport utilities
60
60
  vi.mock('@semiont/core', async () => {
61
61
  const actual = await vi.importActual('@semiont/core');
62
62
  return {
@@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event';
6
6
 
7
7
  import type { Annotation, AnnotationId } from '@semiont/core';
8
8
 
9
- // Mock @semiont/api-client
9
+ // Mock @semiont/http-transport
10
10
  vi.mock('@semiont/core', async () => {
11
11
  const actual = await vi.importActual('@semiont/core');
12
12
  return {