@semiont/react-ui 0.5.22 → 0.5.24
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.
- package/dist/{ar-ZHKO4XDR.js → ar-JRTKCMTF.js} +11 -4
- package/dist/ar-JRTKCMTF.js.map +1 -0
- package/dist/{bn-3BUDKAK5.js → bn-LO6EDSCC.js} +11 -4
- package/dist/bn-LO6EDSCC.js.map +1 -0
- package/dist/{chunk-PFUXQPWJ.js → chunk-4QMTEPQ3.js} +92 -60
- package/dist/chunk-4QMTEPQ3.js.map +1 -0
- package/dist/{chunk-R52YAGHK.js → chunk-5Y5JDCKC.js} +11 -4
- package/dist/chunk-5Y5JDCKC.js.map +1 -0
- package/dist/{cs-2EFEUKTW.js → cs-LCHHOR4O.js} +11 -4
- package/dist/cs-LCHHOR4O.js.map +1 -0
- package/dist/{da-SUD3Q44Q.js → da-TARGPMJR.js} +11 -4
- package/dist/da-TARGPMJR.js.map +1 -0
- package/dist/{de-6IC5QRX7.js → de-SBH4QBPJ.js} +11 -4
- package/dist/de-SBH4QBPJ.js.map +1 -0
- package/dist/{el-Q4NAXFRC.js → el-2D2OLGWB.js} +11 -4
- package/dist/el-2D2OLGWB.js.map +1 -0
- package/dist/{en-P2L4X27N.js → en-QFIEG3CA.js} +2 -2
- package/dist/{es-YUKZBD4W.js → es-U6YK2LX6.js} +11 -4
- package/dist/es-U6YK2LX6.js.map +1 -0
- package/dist/{fa-OJ47LILE.js → fa-S7SZE37M.js} +11 -4
- package/dist/fa-S7SZE37M.js.map +1 -0
- package/dist/{fi-QFFL5CUB.js → fi-CBL3NIXN.js} +11 -4
- package/dist/fi-CBL3NIXN.js.map +1 -0
- package/dist/{fr-BOQZZWP4.js → fr-EABUHV6S.js} +11 -4
- package/dist/fr-EABUHV6S.js.map +1 -0
- package/dist/{he-LXDY3TFA.js → he-THWD244Y.js} +11 -4
- package/dist/he-THWD244Y.js.map +1 -0
- package/dist/{hi-HOJER77V.js → hi-GHGFZJ44.js} +11 -4
- package/dist/hi-GHGFZJ44.js.map +1 -0
- package/dist/{id-I5WQ6EWD.js → id-POZOUMDW.js} +11 -4
- package/dist/id-POZOUMDW.js.map +1 -0
- package/dist/index.d.ts +269 -69
- package/dist/index.js +2080 -1861
- package/dist/index.js.map +1 -1
- package/dist/{it-UYH642IJ.js → it-KDYJADSH.js} +11 -4
- package/dist/it-KDYJADSH.js.map +1 -0
- package/dist/{ja-JTVWAC4L.js → ja-Z5Z6ZF3P.js} +11 -4
- package/dist/ja-Z5Z6ZF3P.js.map +1 -0
- package/dist/{ko-IHXFMOIZ.js → ko-7X3XUXRN.js} +11 -4
- package/dist/ko-7X3XUXRN.js.map +1 -0
- package/dist/{ms-JH25VFLO.js → ms-BTWEIW43.js} +11 -4
- package/dist/ms-BTWEIW43.js.map +1 -0
- package/dist/{nl-SURXW7IQ.js → nl-IS2CGF5I.js} +11 -4
- package/dist/nl-IS2CGF5I.js.map +1 -0
- package/dist/{no-HXIKQFDZ.js → no-AAMEVA26.js} +11 -4
- package/dist/no-AAMEVA26.js.map +1 -0
- package/dist/{pl-7A4KU6XK.js → pl-PVXUQYFT.js} +11 -4
- package/dist/pl-PVXUQYFT.js.map +1 -0
- package/dist/{pt-6MGYP7Q5.js → pt-3ITQ76TL.js} +11 -4
- package/dist/pt-3ITQ76TL.js.map +1 -0
- package/dist/{ro-IROCHNIA.js → ro-FRB4IIVS.js} +11 -4
- package/dist/ro-FRB4IIVS.js.map +1 -0
- package/dist/{sv-WDJD75CV.js → sv-DS3RO24M.js} +11 -4
- package/dist/sv-DS3RO24M.js.map +1 -0
- package/dist/test-utils.js +9 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-RIO2E6B3.js → th-AX66MWWB.js} +11 -4
- package/dist/th-AX66MWWB.js.map +1 -0
- package/dist/{tr-TZ7ERCIN.js → tr-7KTB67TT.js} +11 -4
- package/dist/tr-7KTB67TT.js.map +1 -0
- package/dist/{uk-ZFWDKLUE.js → uk-SMQS2MAQ.js} +11 -4
- package/dist/uk-SMQS2MAQ.js.map +1 -0
- package/dist/{vi-UIOSP2JG.js → vi-D3JXL2EZ.js} +11 -4
- package/dist/vi-D3JXL2EZ.js.map +1 -0
- package/dist/{zh-6R6YUY46.js → zh-S754AWFS.js} +11 -4
- package/dist/zh-S754AWFS.js.map +1 -0
- package/package.json +4 -4
- package/src/components/annotation-popups/JsonLdView.tsx +1 -1
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +8 -2
- package/src/components/modals/ReferenceWizardModal.tsx +2 -1
- package/src/components/modals/ResourceGenerateModal.tsx +10 -4
- package/src/components/modals/ResourceSearchModal.tsx +3 -1
- package/src/components/modals/SearchModal.tsx +3 -1
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +5 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +2 -1
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +2 -1
- package/src/components/resource/AnnotateView.tsx +70 -138
- package/src/components/resource/AnnotationHistory.tsx +53 -17
- package/src/components/resource/BrowseView.tsx +3 -2
- package/src/components/resource/ResourceViewer.tsx +39 -22
- package/src/components/resource/__tests__/AnnotateView.registry.test.tsx +114 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +63 -41
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +2 -1
- package/src/components/resource/__tests__/ResourceViewer.multi-mount-invalidation.test.tsx +140 -0
- package/src/components/resource/__tests__/ResourceViewer.registry.test.tsx +89 -0
- package/src/components/resource/annotate-renderers.tsx +155 -0
- package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +38 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -1
- package/src/components/resource/panels/TaggingPanel.tsx +6 -1
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +41 -0
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +8 -2
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +90 -0
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/UnifiedAnnotationsPanel.failure.test.tsx +78 -0
- package/src/components/settings/SettingsPanel.tsx +6 -2
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +28 -3
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +3 -11
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -3
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -3
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +1 -1
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -8
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -3
- package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +10 -9
- package/src/features/admin-security/state/admin-security-state-unit.ts +3 -2
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -3
- package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +7 -6
- package/src/features/admin-users/state/admin-users-state-unit.ts +3 -2
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +36 -26
- package/src/features/auth-welcome/state/welcome-state-unit.ts +7 -2
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -2
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +0 -3
- package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +37 -12
- package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +9 -10
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +3 -5
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +0 -3
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -5
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +0 -3
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -3
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +17 -7
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -3
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +33 -12
- package/src/features/resource-compose/state/compose-page-state-unit.ts +9 -8
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +3 -2
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +25 -3
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +90 -25
- package/src/features/resource-discovery/state/discover-state-unit.ts +30 -30
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +9 -5
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +70 -40
- package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +111 -8
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +167 -27
- package/src/features/resource-viewer/state/resource-loader-state-unit.ts +74 -9
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +29 -28
- package/translations/ar.json +10 -3
- package/translations/bn.json +10 -3
- package/translations/cs.json +10 -3
- package/translations/da.json +10 -3
- package/translations/de.json +10 -3
- package/translations/el.json +10 -3
- package/translations/en.json +10 -3
- package/translations/es.json +10 -3
- package/translations/fa.json +10 -3
- package/translations/fi.json +10 -3
- package/translations/fr.json +10 -3
- package/translations/he.json +10 -3
- package/translations/hi.json +10 -3
- package/translations/id.json +10 -3
- package/translations/it.json +10 -3
- package/translations/ja.json +10 -3
- package/translations/ko.json +10 -3
- package/translations/ms.json +10 -3
- package/translations/nl.json +10 -3
- package/translations/no.json +10 -3
- package/translations/pl.json +10 -3
- package/translations/pt.json +10 -3
- package/translations/ro.json +10 -3
- package/translations/sv.json +10 -3
- package/translations/th.json +10 -3
- package/translations/tr.json +10 -3
- package/translations/uk.json +10 -3
- package/translations/vi.json +10 -3
- package/translations/zh.json +10 -3
- package/dist/ar-ZHKO4XDR.js.map +0 -1
- package/dist/bn-3BUDKAK5.js.map +0 -1
- package/dist/chunk-PFUXQPWJ.js.map +0 -1
- package/dist/chunk-R52YAGHK.js.map +0 -1
- package/dist/cs-2EFEUKTW.js.map +0 -1
- package/dist/da-SUD3Q44Q.js.map +0 -1
- package/dist/de-6IC5QRX7.js.map +0 -1
- package/dist/el-Q4NAXFRC.js.map +0 -1
- package/dist/es-YUKZBD4W.js.map +0 -1
- package/dist/fa-OJ47LILE.js.map +0 -1
- package/dist/fi-QFFL5CUB.js.map +0 -1
- package/dist/fr-BOQZZWP4.js.map +0 -1
- package/dist/he-LXDY3TFA.js.map +0 -1
- package/dist/hi-HOJER77V.js.map +0 -1
- package/dist/id-I5WQ6EWD.js.map +0 -1
- package/dist/it-UYH642IJ.js.map +0 -1
- package/dist/ja-JTVWAC4L.js.map +0 -1
- package/dist/ko-IHXFMOIZ.js.map +0 -1
- package/dist/ms-JH25VFLO.js.map +0 -1
- package/dist/nl-SURXW7IQ.js.map +0 -1
- package/dist/no-HXIKQFDZ.js.map +0 -1
- package/dist/pl-7A4KU6XK.js.map +0 -1
- package/dist/pt-6MGYP7Q5.js.map +0 -1
- package/dist/ro-IROCHNIA.js.map +0 -1
- package/dist/sv-WDJD75CV.js.map +0 -1
- package/dist/th-RIO2E6B3.js.map +0 -1
- package/dist/tr-TZ7ERCIN.js.map +0 -1
- package/dist/uk-ZFWDKLUE.js.map +0 -1
- package/dist/vi-UIOSP2JG.js.map +0 -1
- package/dist/zh-6R6YUY46.js.map +0 -1
- /package/dist/{en-P2L4X27N.js.map → en-QFIEG3CA.js.map} +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { lazy, Suspense, type ComponentType } from 'react';
|
|
4
|
+
import type { Annotation } from '@semiont/core';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { SvgDrawingCanvas } from '../image-annotation/SvgDrawingCanvas';
|
|
7
|
+
import { CodeMirrorRenderer } from '../CodeMirrorRenderer';
|
|
8
|
+
import { segmentTextWithAnnotations } from '../../lib/text-segmentation';
|
|
9
|
+
import type { SelectionMotivation, ShapeType } from '../annotation/AnnotateToolbar';
|
|
10
|
+
|
|
11
|
+
// Lazy-load the PDF component to avoid SSR issues with browser PDF.js loading.
|
|
12
|
+
// Kept lazy here deliberately: hoisting it to a static import would make it
|
|
13
|
+
// eager again and reintroduce the SSR problem this indirection exists for.
|
|
14
|
+
const PdfAnnotationCanvas = lazy(() =>
|
|
15
|
+
import('../pdf-annotation/PdfAnnotationCanvas.client').then((mod) => ({ default: mod.PdfAnnotationCanvas })),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Common props for an ANNOTATING media renderer. The read-only sibling is
|
|
20
|
+
* `MediaRendererProps` in `browse-renderers.tsx`; this one additionally
|
|
21
|
+
* carries the active tool, because annotating is an interaction, not a paint.
|
|
22
|
+
*
|
|
23
|
+
* `content` is decoded text for the text renderer, or a media URL for the
|
|
24
|
+
* image / PDF renderers. `annotations` is the FLAT list — a renderer derives
|
|
25
|
+
* whatever shape it needs (the text one segments it), so a custom renderer is
|
|
26
|
+
* never handed a projection only one implementation can use.
|
|
27
|
+
* See .plans/ANNOTATE-RENDERER-REGISTRY.md (D1)
|
|
28
|
+
*/
|
|
29
|
+
export interface AnnotateMediaRendererProps {
|
|
30
|
+
content: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
resourceUri: string;
|
|
33
|
+
annotations: Annotation[];
|
|
34
|
+
/** Session for interaction routing — the canvases emit via `session.client`. */
|
|
35
|
+
session: SemiontSession | null;
|
|
36
|
+
|
|
37
|
+
/** Active drawing tool, or null when nothing is selected (read-only paint). */
|
|
38
|
+
drawingMode: ShapeType | null;
|
|
39
|
+
selectedMotivation: SelectionMotivation | null;
|
|
40
|
+
hoveredAnnotationId?: string | null;
|
|
41
|
+
hoverDelayMs?: number;
|
|
42
|
+
|
|
43
|
+
// Text-path extras. Optional on the shared interface: a custom image or PDF
|
|
44
|
+
// renderer simply ignores them, the same way `MediaRendererProps.session` is
|
|
45
|
+
// documented to be ignorable.
|
|
46
|
+
newAnnotationIds?: Set<string>;
|
|
47
|
+
scrollToAnnotationId?: string | null;
|
|
48
|
+
showLineNumbers?: boolean;
|
|
49
|
+
enableWidgets?: boolean;
|
|
50
|
+
getTargetResourceName?: (resourceId: string) => string | undefined;
|
|
51
|
+
generatingReferenceId?: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Annotating media dispatch, keyed by the registry render mode. */
|
|
55
|
+
export type AnnotateMediaRenderers = Partial<Record<string, ComponentType<AnnotateMediaRendererProps>>>;
|
|
56
|
+
|
|
57
|
+
export function TextAnnotateRenderer({
|
|
58
|
+
content,
|
|
59
|
+
annotations,
|
|
60
|
+
session,
|
|
61
|
+
newAnnotationIds,
|
|
62
|
+
hoveredAnnotationId,
|
|
63
|
+
scrollToAnnotationId,
|
|
64
|
+
showLineNumbers = false,
|
|
65
|
+
hoverDelayMs = 150,
|
|
66
|
+
enableWidgets = false,
|
|
67
|
+
getTargetResourceName,
|
|
68
|
+
generatingReferenceId,
|
|
69
|
+
}: AnnotateMediaRendererProps) {
|
|
70
|
+
// D1: segmentation is a text-rendering concern, derived here rather than
|
|
71
|
+
// threaded through the shared interface.
|
|
72
|
+
const segments = segmentTextWithAnnotations(content, annotations);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<CodeMirrorRenderer
|
|
76
|
+
content={content}
|
|
77
|
+
segments={segments}
|
|
78
|
+
editable={false}
|
|
79
|
+
newAnnotationIds={newAnnotationIds}
|
|
80
|
+
{...(hoveredAnnotationId !== undefined && { hoveredAnnotationId })}
|
|
81
|
+
{...(scrollToAnnotationId !== undefined && { scrollToAnnotationId })}
|
|
82
|
+
sourceView={true}
|
|
83
|
+
showLineNumbers={showLineNumbers}
|
|
84
|
+
hoverDelayMs={hoverDelayMs}
|
|
85
|
+
enableWidgets={enableWidgets}
|
|
86
|
+
session={session}
|
|
87
|
+
{...(getTargetResourceName && { getTargetResourceName })}
|
|
88
|
+
{...(generatingReferenceId !== undefined && { generatingReferenceId })}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ImageAnnotateRenderer({
|
|
94
|
+
content,
|
|
95
|
+
resourceUri,
|
|
96
|
+
annotations,
|
|
97
|
+
session,
|
|
98
|
+
drawingMode,
|
|
99
|
+
selectedMotivation,
|
|
100
|
+
hoveredAnnotationId,
|
|
101
|
+
hoverDelayMs = 150,
|
|
102
|
+
}: AnnotateMediaRendererProps) {
|
|
103
|
+
if (!content) return null;
|
|
104
|
+
return (
|
|
105
|
+
<SvgDrawingCanvas
|
|
106
|
+
imageUrl={content}
|
|
107
|
+
resourceUri={resourceUri}
|
|
108
|
+
existingAnnotations={annotations}
|
|
109
|
+
drawingMode={drawingMode}
|
|
110
|
+
selectedMotivation={selectedMotivation}
|
|
111
|
+
session={session}
|
|
112
|
+
hoveredAnnotationId={hoveredAnnotationId || null}
|
|
113
|
+
hoverDelayMs={hoverDelayMs}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function PdfAnnotateRenderer({
|
|
119
|
+
content,
|
|
120
|
+
resourceUri,
|
|
121
|
+
annotations,
|
|
122
|
+
session,
|
|
123
|
+
drawingMode,
|
|
124
|
+
selectedMotivation,
|
|
125
|
+
hoveredAnnotationId,
|
|
126
|
+
hoverDelayMs = 150,
|
|
127
|
+
}: AnnotateMediaRendererProps) {
|
|
128
|
+
if (!content) return null;
|
|
129
|
+
return (
|
|
130
|
+
<Suspense fallback={<div className="semiont-annotate-view__loading">Loading PDF viewer...</div>}>
|
|
131
|
+
<PdfAnnotationCanvas
|
|
132
|
+
pdfUrl={content}
|
|
133
|
+
resourceUri={resourceUri}
|
|
134
|
+
existingAnnotations={annotations}
|
|
135
|
+
drawingMode={drawingMode}
|
|
136
|
+
selectedMotivation={selectedMotivation}
|
|
137
|
+
session={session}
|
|
138
|
+
hoveredAnnotationId={hoveredAnnotationId || null}
|
|
139
|
+
hoverDelayMs={hoverDelayMs}
|
|
140
|
+
/>
|
|
141
|
+
</Suspense>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Default annotating media renderers. `AnnotateView` merges a caller's
|
|
147
|
+
* `renderers` override on top of these, so a host can swap one renderer (its
|
|
148
|
+
* own PDF viewer, say) or add a mode without forking the view — the same seam
|
|
149
|
+
* `defaultBrowseRenderers` gives the read-only path.
|
|
150
|
+
*/
|
|
151
|
+
export const defaultAnnotateRenderers: Record<'text' | 'image' | 'pdf', ComponentType<AnnotateMediaRendererProps>> = {
|
|
152
|
+
text: TextAnnotateRenderer,
|
|
153
|
+
image: ImageAnnotateRenderer,
|
|
154
|
+
pdf: PdfAnnotateRenderer,
|
|
155
|
+
};
|
|
@@ -7,7 +7,7 @@ import { json } from '@codemirror/lang-json';
|
|
|
7
7
|
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
|
-
import { useLineNumbers } from '../../../
|
|
10
|
+
import { useLineNumbers } from '../../../contexts/LineNumbersContext';
|
|
11
11
|
import { useResourceGraph } from '../../../hooks/useResourceGraph';
|
|
12
12
|
import type { ResourceId } from '@semiont/core';
|
|
13
13
|
import './JsonLdPanel.css';
|
|
@@ -58,9 +58,25 @@ interface Props {
|
|
|
58
58
|
|
|
59
59
|
// Reference-specific props
|
|
60
60
|
allEntityTypes: string[];
|
|
61
|
+
/**
|
|
62
|
+
* The entity-type list failed to load. Without it the picker's empty branch
|
|
63
|
+
* asserts "no entity types available" — a claim about the knowledge base
|
|
64
|
+
* that a failed load cannot support.
|
|
65
|
+
* See .plans/PANEL-FAILURE-STATES.md
|
|
66
|
+
*/
|
|
67
|
+
entityTypesError?: Error | null;
|
|
61
68
|
generatingReferenceId?: string | null;
|
|
62
69
|
referencedBy?: ReferencedBy[];
|
|
63
70
|
referencedByLoading?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The incoming-references load failed terminally (B15). Distinct from
|
|
73
|
+
* `referencedByLoading` — without it a dead request is indistinguishable
|
|
74
|
+
* from one still in flight, and the panel says "Loading..." forever.
|
|
75
|
+
* See .plans/PANEL-FAILURE-STATES.md
|
|
76
|
+
*/
|
|
77
|
+
referencedByError?: Error | null;
|
|
78
|
+
/** Retry the failed incoming-references load. */
|
|
79
|
+
onRetryReferencedBy?: () => void;
|
|
64
80
|
pendingAnnotation: PendingAnnotation | null;
|
|
65
81
|
scrollToAnnotationId?: string | null;
|
|
66
82
|
onScrollCompleted?: () => void;
|
|
@@ -91,9 +107,12 @@ export function ReferencesPanel({
|
|
|
91
107
|
Link,
|
|
92
108
|
routes,
|
|
93
109
|
allEntityTypes,
|
|
110
|
+
entityTypesError = null,
|
|
94
111
|
generatingReferenceId,
|
|
95
112
|
referencedBy = [],
|
|
96
113
|
referencedByLoading = false,
|
|
114
|
+
referencedByError = null,
|
|
115
|
+
onRetryReferencedBy,
|
|
97
116
|
pendingAnnotation,
|
|
98
117
|
scrollToAnnotationId,
|
|
99
118
|
onScrollCompleted,
|
|
@@ -403,7 +422,7 @@ export function ReferencesPanel({
|
|
|
403
422
|
))
|
|
404
423
|
) : (
|
|
405
424
|
<p className="semiont-assist-widget__no-types">
|
|
406
|
-
{t('noEntityTypes')}
|
|
425
|
+
{entityTypesError ? t('entityTypesFailed') : t('noEntityTypes')}
|
|
407
426
|
</p>
|
|
408
427
|
)}
|
|
409
428
|
</div>
|
|
@@ -485,13 +504,29 @@ export function ReferencesPanel({
|
|
|
485
504
|
<div className="semiont-panel__divider">
|
|
486
505
|
<h3 className="semiont-panel__subtitle">
|
|
487
506
|
{t('incomingReferences')} ({referencedBy.length})
|
|
488
|
-
{referencedByLoading && (
|
|
507
|
+
{referencedByLoading && !referencedByError && (
|
|
489
508
|
<span className="semiont-panel__loading-indicator">({t('loading')})</span>
|
|
490
509
|
)}
|
|
491
510
|
</h3>
|
|
492
511
|
</div>
|
|
493
512
|
|
|
494
|
-
{
|
|
513
|
+
{referencedByError ? (
|
|
514
|
+
<p className="semiont-panel__empty-message semiont-panel__empty-message--small">
|
|
515
|
+
{t('incomingReferencesFailed')}
|
|
516
|
+
{onRetryReferencedBy && (
|
|
517
|
+
<>
|
|
518
|
+
{' '}
|
|
519
|
+
<button
|
|
520
|
+
type="button"
|
|
521
|
+
onClick={onRetryReferencedBy}
|
|
522
|
+
className="semiont-panel__inline-action"
|
|
523
|
+
>
|
|
524
|
+
{t('retry')}
|
|
525
|
+
</button>
|
|
526
|
+
</>
|
|
527
|
+
)}
|
|
528
|
+
</p>
|
|
529
|
+
) : referencedBy.length > 0 ? (
|
|
495
530
|
<div className="semiont-panel__list">
|
|
496
531
|
{referencedBy.map((ref) => {
|
|
497
532
|
const resourceId = ref.target.source;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
+
import { isReady } from '@semiont/sdk';
|
|
4
5
|
import type { Ref } from 'react';
|
|
5
6
|
import type { Annotation } from '@semiont/core';
|
|
6
7
|
import { getAnnotationExactText } from '@semiont/core';
|
|
@@ -42,7 +43,10 @@ export function TagEntry({
|
|
|
42
43
|
[session],
|
|
43
44
|
);
|
|
44
45
|
const schemas = useObservable(tagSchemas$);
|
|
45
|
-
const schema =
|
|
46
|
+
const schema =
|
|
47
|
+
schemaId && schemas && isReady(schemas)
|
|
48
|
+
? schemas.value.find((s) => s.id === schemaId) ?? null
|
|
49
|
+
: null;
|
|
46
50
|
|
|
47
51
|
return (
|
|
48
52
|
<div
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
|
+
import { isReady } from '@semiont/sdk';
|
|
4
5
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
6
|
import { useObservable } from '../../../hooks/useObservable';
|
|
6
7
|
import type { SemiontSession } from '@semiont/sdk';
|
|
@@ -91,7 +92,11 @@ export function TaggingPanel({
|
|
|
91
92
|
() => session?.client.browse.tagSchemas() ?? null,
|
|
92
93
|
[session],
|
|
93
94
|
);
|
|
94
|
-
const
|
|
95
|
+
const schemasState = useObservable(tagSchemas$);
|
|
96
|
+
// D1 unwrap: value-or-undefined, with the third outcome explicit — a
|
|
97
|
+
// failed registry read renders like still-loading here (the panel's
|
|
98
|
+
// failure UI is PANEL-FAILURE-STATES follow-up work, not this migration).
|
|
99
|
+
const schemasObserved = schemasState && isReady(schemasState) ? schemasState.value : undefined;
|
|
95
100
|
const schemas = schemasObserved ?? [];
|
|
96
101
|
// True only AFTER the registry has resolved AND it's empty — distinct
|
|
97
102
|
// from the initial-loading state (`schemasObserved === undefined`),
|
|
@@ -46,6 +46,14 @@ interface UnifiedAnnotationsPanelProps {
|
|
|
46
46
|
|
|
47
47
|
// All annotations (grouped internally by motivation)
|
|
48
48
|
annotations: Annotation[];
|
|
49
|
+
/**
|
|
50
|
+
* The annotations load failed terminally (B15). Every tab here derives from
|
|
51
|
+
* the one `annotations` array, so without this the panel presents an empty
|
|
52
|
+
* array as fact — "no highlights" for a resource that may be full of them.
|
|
53
|
+
* See .plans/PANEL-FAILURE-STATES.md
|
|
54
|
+
*/
|
|
55
|
+
annotationsError?: Error | null;
|
|
56
|
+
onRetryAnnotations?: () => void;
|
|
49
57
|
|
|
50
58
|
// Annotators (pure static data - no handlers)
|
|
51
59
|
annotators: Record<string, Annotator>;
|
|
@@ -65,6 +73,9 @@ interface UnifiedAnnotationsPanelProps {
|
|
|
65
73
|
generatingReferenceId?: string | null;
|
|
66
74
|
referencedBy?: any[];
|
|
67
75
|
referencedByLoading?: boolean;
|
|
76
|
+
entityTypesError?: Error | null;
|
|
77
|
+
referencedByError?: Error | null;
|
|
78
|
+
onRetryReferencedBy?: () => void;
|
|
68
79
|
|
|
69
80
|
// Resource context — threaded to every per-motivation panel, which stamps it
|
|
70
81
|
// as `source` on mark:submit (multi-viewer routing).
|
|
@@ -174,6 +185,33 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
174
185
|
return `semiont-unified-panel__tab-button${isActive ? ' semiont-unified-panel__tab-button--active' : ''}`;
|
|
175
186
|
};
|
|
176
187
|
|
|
188
|
+
if (props.annotationsError) {
|
|
189
|
+
// The tabs below all read one array; an empty one after a failed load is
|
|
190
|
+
// not "no annotations", it is "we do not know".
|
|
191
|
+
return (
|
|
192
|
+
<div className="semiont-unified-panel">
|
|
193
|
+
<h3 className="semiont-unified-panel__title">
|
|
194
|
+
{t('title')}
|
|
195
|
+
</h3>
|
|
196
|
+
<p className="semiont-unified-panel__error">
|
|
197
|
+
{t('annotationsFailed')}
|
|
198
|
+
{props.onRetryAnnotations && (
|
|
199
|
+
<>
|
|
200
|
+
{' '}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={props.onRetryAnnotations}
|
|
204
|
+
className="semiont-panel__inline-action"
|
|
205
|
+
>
|
|
206
|
+
{t('retry')}
|
|
207
|
+
</button>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
177
215
|
return (
|
|
178
216
|
<div className="semiont-unified-panel">
|
|
179
217
|
{/* Panel Title */}
|
|
@@ -284,9 +322,12 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
284
322
|
locale={commonProps.locale}
|
|
285
323
|
sourceLanguage={commonProps.sourceLanguage}
|
|
286
324
|
allEntityTypes={props.allEntityTypes || []}
|
|
325
|
+
entityTypesError={props.entityTypesError}
|
|
287
326
|
generatingReferenceId={props.generatingReferenceId}
|
|
288
327
|
referencedBy={props.referencedBy}
|
|
289
328
|
referencedByLoading={props.referencedByLoading}
|
|
329
|
+
referencedByError={props.referencedByError}
|
|
330
|
+
onRetryReferencedBy={props.onRetryReferencedBy}
|
|
290
331
|
Link={props.Link}
|
|
291
332
|
routes={props.routes}
|
|
292
333
|
/>
|
|
@@ -32,9 +32,15 @@ vi.mock('../../../lib/codemirror-json-theme', () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
// Mock the hooks the panel consumes — NOT the session/client stack.
|
|
35
|
-
vi.mock('@/
|
|
35
|
+
vi.mock('@/contexts/LineNumbersContext', async () => {
|
|
36
|
+
const actual = await vi.importActual('@/contexts/LineNumbersContext');
|
|
37
|
+
// Mock ONLY the hook: renderWithProviders mounts the real
|
|
38
|
+
// LineNumbersProvider, which an auto-mock would swallow (rendering
|
|
39
|
+
// nothing), while these tests want a fixed showLineNumbers value.
|
|
40
|
+
return { ...actual, useLineNumbers: vi.fn() };
|
|
41
|
+
});
|
|
36
42
|
vi.mock('@/hooks/useResourceGraph');
|
|
37
|
-
import { useLineNumbers } from '@/
|
|
43
|
+
import { useLineNumbers } from '@/contexts/LineNumbersContext';
|
|
38
44
|
import { useResourceGraph } from '@/hooks/useResourceGraph';
|
|
39
45
|
|
|
40
46
|
const RID = resourceId('test-resource-1');
|
|
@@ -84,4 +84,94 @@ describe('ReferencesPanel — headless (session prop, no providers)', () => {
|
|
|
84
84
|
fireEvent.click(entryText);
|
|
85
85
|
expect(client.browse.click).toHaveBeenCalledWith('ref-1', 'linking');
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
describe('incoming references — terminal load failure', () => {
|
|
89
|
+
// `referencedByLoading` alone cannot distinguish "still in flight" from
|
|
90
|
+
// "dead" (B15), so the panel used to render "Loading..." for ever.
|
|
91
|
+
// See .plans/PANEL-FAILURE-STATES.md
|
|
92
|
+
|
|
93
|
+
type PanelProps = React.ComponentProps<typeof ReferencesPanel>;
|
|
94
|
+
const base = (): PanelProps => {
|
|
95
|
+
const { session } = fakeSession();
|
|
96
|
+
return {
|
|
97
|
+
session,
|
|
98
|
+
resourceId: 'res-1' as const,
|
|
99
|
+
annotations: [],
|
|
100
|
+
isAssisting: false,
|
|
101
|
+
progress: null,
|
|
102
|
+
pendingAnnotation: null,
|
|
103
|
+
allEntityTypes: [],
|
|
104
|
+
Link: TestLink,
|
|
105
|
+
routes: testRoutes,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
it('reports the failure instead of an endless loading line', () => {
|
|
110
|
+
render(
|
|
111
|
+
<ReferencesPanel
|
|
112
|
+
{...base()}
|
|
113
|
+
referencedBy={[]}
|
|
114
|
+
referencedByLoading
|
|
115
|
+
referencedByError={new Error('Resource not found')}
|
|
116
|
+
/>,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
120
|
+
expect(screen.getByText(/Could not load incoming references/)).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('offers a retry that calls back', () => {
|
|
124
|
+
const onRetryReferencedBy = vi.fn();
|
|
125
|
+
render(
|
|
126
|
+
<ReferencesPanel
|
|
127
|
+
{...base()}
|
|
128
|
+
referencedBy={[]}
|
|
129
|
+
referencedByError={new Error('boom')}
|
|
130
|
+
onRetryReferencedBy={onRetryReferencedBy}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
|
|
135
|
+
expect(onRetryReferencedBy).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('with no error, a still-loading list keeps its loading affordance', () => {
|
|
139
|
+
render(
|
|
140
|
+
<ReferencesPanel
|
|
141
|
+
{...base()}
|
|
142
|
+
referencedBy={[]}
|
|
143
|
+
referencedByLoading
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(screen.queryByText(/Could not load incoming references/)).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('entity types — terminal load failure', () => {
|
|
152
|
+
// The picker's empty branch says "no entity types", which is a claim about
|
|
153
|
+
// the knowledge base. On a failed load it is false — the KB may have
|
|
154
|
+
// plenty. See .plans/PANEL-FAILURE-STATES.md
|
|
155
|
+
|
|
156
|
+
it('does not present a failed entity-type load as "none exist"', () => {
|
|
157
|
+
const { session } = fakeSession();
|
|
158
|
+
render(
|
|
159
|
+
<ReferencesPanel
|
|
160
|
+
session={session}
|
|
161
|
+
resourceId="res-1"
|
|
162
|
+
annotations={[]}
|
|
163
|
+
isAssisting={false}
|
|
164
|
+
progress={null}
|
|
165
|
+
pendingAnnotation={null}
|
|
166
|
+
allEntityTypes={[]}
|
|
167
|
+
entityTypesError={new Error('boom')}
|
|
168
|
+
Link={TestLink}
|
|
169
|
+
routes={testRoutes}
|
|
170
|
+
/>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(screen.queryByText('No entity types available')).not.toBeInTheDocument();
|
|
174
|
+
expect(screen.getByText(/Could not load entity types/)).toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
87
177
|
});
|
|
@@ -135,7 +135,7 @@ describe('TagEntry', () => {
|
|
|
135
135
|
// transport's HTTP plumbing.
|
|
136
136
|
const { SemiontWrapper, client, session } = createTestSemiontWrapper();
|
|
137
137
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
138
|
-
CacheObservable.from(of([NER_SCHEMA]))
|
|
138
|
+
CacheObservable.from(of({ status: 'ready' as const, value: [NER_SCHEMA] }))
|
|
139
139
|
);
|
|
140
140
|
render(<TagEntry {...defaultProps} session={session} />, { wrapper: SemiontWrapper });
|
|
141
141
|
|
|
@@ -149,7 +149,7 @@ describe('TagEntry', () => {
|
|
|
149
149
|
// misses, the schema-name `<span>` is not rendered.
|
|
150
150
|
const { SemiontWrapper, client, session } = createTestSemiontWrapper();
|
|
151
151
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
152
|
-
CacheObservable.from(of([]))
|
|
152
|
+
CacheObservable.from(of({ status: 'ready' as const, value: [] }))
|
|
153
153
|
);
|
|
154
154
|
const { container } = render(<TagEntry {...defaultProps} session={session} />, { wrapper: SemiontWrapper });
|
|
155
155
|
|
|
@@ -56,7 +56,7 @@ const TEST_TAG_SCHEMAS: TagSchema[] = [
|
|
|
56
56
|
const renderWithEventBus = (component: React.ReactElement<{ session: SemiontSession | null }>, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
57
57
|
const { SemiontWrapper, eventBus, client, session } = createTestSemiontWrapper();
|
|
58
58
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
59
|
-
CacheObservable.from(of(TEST_TAG_SCHEMAS))
|
|
59
|
+
CacheObservable.from(of({ status: 'ready' as const, value: TEST_TAG_SCHEMAS }))
|
|
60
60
|
);
|
|
61
61
|
if (tracker) tracker._attach(eventBus);
|
|
62
62
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
@@ -74,7 +74,7 @@ const renderWithEventBus = (component: React.ReactElement<{ session: SemiontSess
|
|
|
74
74
|
const renderWithEmptyRegistry = (component: React.ReactElement<{ session: SemiontSession | null }>) => {
|
|
75
75
|
const { SemiontWrapper, client, session } = createTestSemiontWrapper();
|
|
76
76
|
vi.spyOn(client.browse, 'tagSchemas').mockReturnValue(
|
|
77
|
-
CacheObservable.from(of([]))
|
|
77
|
+
CacheObservable.from(of({ status: 'ready' as const, value: [] }))
|
|
78
78
|
);
|
|
79
79
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
80
80
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The annotations panel must be able to say the annotations failed to load.
|
|
3
|
+
*
|
|
4
|
+
* Every tab in this panel derives from one `annotations` array. When that load
|
|
5
|
+
* fails terminally (B15) the array is empty — indistinguishable from a
|
|
6
|
+
* resource that genuinely has no annotations, so the panel cheerfully reports
|
|
7
|
+
* "no highlights" for a resource full of them. That is the same
|
|
8
|
+
* apparent-data-loss shape as the PDF-annotations bug, arrived at from a
|
|
9
|
+
* different direction.
|
|
10
|
+
*
|
|
11
|
+
* See .plans/PANEL-FAILURE-STATES.md
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
15
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
16
|
+
import '@testing-library/jest-dom';
|
|
17
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
18
|
+
import type { RouteBuilder } from '../../../../contexts/RoutingContext';
|
|
19
|
+
import { UnifiedAnnotationsPanel } from '../UnifiedAnnotationsPanel';
|
|
20
|
+
import { ANNOTATORS } from '../../../../lib/annotation-registry';
|
|
21
|
+
|
|
22
|
+
const TestLink = ({ href, children, ...rest }: any) => <a href={href} {...rest}>{children}</a>;
|
|
23
|
+
const testRoutes = {
|
|
24
|
+
resourceDetail: (id: string) => `/r/${id}`,
|
|
25
|
+
} as unknown as RouteBuilder;
|
|
26
|
+
|
|
27
|
+
function fakeSession(): SemiontSession {
|
|
28
|
+
return {
|
|
29
|
+
client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
|
|
30
|
+
subscribe: () => () => {},
|
|
31
|
+
} as unknown as SemiontSession;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type PanelProps = React.ComponentProps<typeof UnifiedAnnotationsPanel>;
|
|
35
|
+
|
|
36
|
+
const base = (): PanelProps => ({
|
|
37
|
+
session: fakeSession(),
|
|
38
|
+
annotations: [],
|
|
39
|
+
annotators: ANNOTATORS,
|
|
40
|
+
allEntityTypes: [],
|
|
41
|
+
pendingAnnotation: null,
|
|
42
|
+
resourceId: 'res-1',
|
|
43
|
+
Link: TestLink,
|
|
44
|
+
routes: testRoutes,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('UnifiedAnnotationsPanel — annotations load failure', () => {
|
|
48
|
+
it('reports the failure rather than presenting an empty annotation set as fact', () => {
|
|
49
|
+
render(
|
|
50
|
+
<UnifiedAnnotationsPanel
|
|
51
|
+
{...base()}
|
|
52
|
+
annotationsError={new Error('Resource not found')}
|
|
53
|
+
/>,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText(/Could not load annotations/)).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('offers a retry that calls back', () => {
|
|
60
|
+
const onRetryAnnotations = vi.fn();
|
|
61
|
+
render(
|
|
62
|
+
<UnifiedAnnotationsPanel
|
|
63
|
+
{...base()}
|
|
64
|
+
annotationsError={new Error('boom')}
|
|
65
|
+
onRetryAnnotations={onRetryAnnotations}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
|
|
70
|
+
expect(onRetryAnnotations).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('says nothing about failure when the annotations are merely empty', () => {
|
|
74
|
+
render(<UnifiedAnnotationsPanel {...base()} />);
|
|
75
|
+
|
|
76
|
+
expect(screen.queryByText(/Could not load annotations/)).not.toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -5,10 +5,10 @@ import { LOCALES } from '@semiont/core';
|
|
|
5
5
|
import { useTranslations } from '../../contexts/TranslationContext';
|
|
6
6
|
import { useLanguageChangeAnnouncements } from '../LiveRegion';
|
|
7
7
|
import { useSemiont } from '../../session/SemiontProvider';
|
|
8
|
+
import { useLineNumbers } from '../../contexts/LineNumbersContext';
|
|
8
9
|
import './SettingsPanel.css';
|
|
9
10
|
|
|
10
11
|
interface SettingsPanelProps {
|
|
11
|
-
showLineNumbers: boolean;
|
|
12
12
|
theme: 'light' | 'dark' | 'system';
|
|
13
13
|
locale: string;
|
|
14
14
|
isPendingLocaleChange?: boolean;
|
|
@@ -24,7 +24,6 @@ interface SettingsPanelProps {
|
|
|
24
24
|
* @emits settings:hover-delay-changed - Hover delay changed by user. Payload: { hoverDelayMs: number }
|
|
25
25
|
*/
|
|
26
26
|
export function SettingsPanel({
|
|
27
|
-
showLineNumbers,
|
|
28
27
|
theme,
|
|
29
28
|
locale,
|
|
30
29
|
isPendingLocaleChange = false,
|
|
@@ -32,6 +31,11 @@ export function SettingsPanel({
|
|
|
32
31
|
}: SettingsPanelProps) {
|
|
33
32
|
const t = useTranslations('Settings');
|
|
34
33
|
const semiont = useSemiont();
|
|
34
|
+
// The switch renders the SHARED line-numbers state, not a prop snapshot:
|
|
35
|
+
// its toggle is applied by a bus subscriber in ToolbarPanels, so a prop
|
|
36
|
+
// fed from any per-route source is a second copy that never updates.
|
|
37
|
+
// See .plans/bugs/line-numbers-toggle-desynced-by-hoist.md
|
|
38
|
+
const { showLineNumbers } = useLineNumbers();
|
|
35
39
|
const { announceLanguageChanging, announceLanguageChanged } = useLanguageChangeAnnouncements();
|
|
36
40
|
|
|
37
41
|
// Track previous locale to detect changes
|