@eightyfourthousand/lib-editing 2026.3.0 → 2026.4.0
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/README.md +4 -2
- package/package.json +7 -7
- package/src/lib/components/editor/EditorBackMatterPage.tsx +3 -1
- package/src/lib/components/editor/EditorHeader.tsx +1 -1
- package/src/lib/components/editor/EditorProvider.tsx +76 -0
- package/src/lib/components/editor/PaginationProvider.tsx +30 -45
- package/src/lib/components/editor/extensions/Heading/Heading.ts +4 -3
- package/src/lib/components/editor/extensions/InternalLink/InternalLink.ts +4 -1
- package/src/lib/components/editor/extensions/Mention/Mention.ts +4 -1
- package/src/lib/components/editor/extensions/Passage/EditorOptions.tsx +2 -2
- package/src/lib/components/editor/extensions/Passage/Passage.tsx +3 -3
- package/src/lib/components/editor/extensions/Passage/ReaderOptions.tsx +3 -3
- package/src/lib/components/editor/extensions/Passage/ShowAnnotations.tsx +3 -3
- package/src/lib/components/editor/extensions/TitlesNode.ts +1 -1
- package/src/lib/components/reader/ReaderBackMatterPage.tsx +1 -0
- package/src/lib/components/reader/ReaderBackMatterPanel.tsx +3 -0
- package/src/lib/components/reader/ReaderBodyPanel.tsx +1 -1
- package/src/lib/components/shared/BackMatterPanel.tsx +17 -10
- package/src/lib/components/shared/BodyPanel.tsx +6 -6
- package/src/lib/components/shared/LabeledElement.tsx +3 -3
- package/src/lib/components/shared/LeftPanel.tsx +3 -3
- package/src/lib/components/shared/NavigationContext.ts +1 -1
- package/src/lib/components/shared/NavigationProvider.tsx +33 -1
- package/src/lib/components/shared/ReaderSearchButton.tsx +110 -0
- package/src/lib/components/shared/SearchReplacePanel.tsx +259 -0
- package/src/lib/components/shared/SourceReader.tsx +13 -4
- package/src/lib/components/shared/SuggestRevisionForm.tsx +1 -1
- package/src/lib/components/shared/TableOfContents.tsx +40 -27
- package/src/lib/components/shared/ThreeColumnRenderer.tsx +16 -9
- package/src/lib/components/shared/TranslationHeader.tsx +2 -63
- package/src/lib/components/shared/bibliography/BibliographyList.tsx +2 -2
- package/src/lib/components/shared/glossary/GlossaryInstanceBody.tsx +15 -5
- package/src/lib/components/shared/glossary/GlossaryPaginationProvider.tsx +17 -56
- package/src/lib/components/shared/glossary/GlossaryTermList.tsx +4 -5
- package/src/lib/components/shared/hooks/usePaginationLoadTriggers.spec.tsx +158 -0
- package/src/lib/components/shared/hooks/usePaginationLoadTriggers.ts +129 -0
- package/src/lib/components/shared/titles/FramedCard.tsx +3 -3
- package/src/lib/components/shared/titles/Titles.tsx +17 -6
- package/src/lib/components/shared/titles/TitlesCard.tsx +6 -6
- package/src/lib/transformers/has-abbreviation.ts +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# lib-editing
|
|
1
|
+
# @eightyfourthousand/lib-editing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared editing and reader components for 84000 translation workflows.
|
|
4
|
+
|
|
5
|
+
Use this package for the translation editor, reader UI, shared body-panel rendering, and related editing helpers.
|
|
4
6
|
|
|
5
7
|
## Running unit tests
|
|
6
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eightyfourthousand/lib-editing",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.4.0",
|
|
4
4
|
"description": "Shared editing and reader components for 84000 translation workflows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@eightyfourthousand/client-graphql": "^2026.3.
|
|
21
|
-
"@eightyfourthousand/data-access": "^2026.3.
|
|
22
|
-
"@eightyfourthousand/design-system": "^2026.3.
|
|
23
|
-
"@eightyfourthousand/lib-instr": "^2026.3.
|
|
24
|
-
"@eightyfourthousand/lib-search": "^2026.3.
|
|
25
|
-
"@eightyfourthousand/lib-utils": "^2026.3.
|
|
20
|
+
"@eightyfourthousand/client-graphql": "^2026.3.1",
|
|
21
|
+
"@eightyfourthousand/data-access": "^2026.3.1",
|
|
22
|
+
"@eightyfourthousand/design-system": "^2026.3.1",
|
|
23
|
+
"@eightyfourthousand/lib-instr": "^2026.3.1",
|
|
24
|
+
"@eightyfourthousand/lib-search": "^2026.3.1",
|
|
25
|
+
"@eightyfourthousand/lib-utils": "^2026.3.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"next": "^16.0.0",
|
|
@@ -18,6 +18,7 @@ import { TranslationBuilder } from '../editor';
|
|
|
18
18
|
import { isStaticFeatureEnabled } from '@eightyfourthousand/lib-instr/static';
|
|
19
19
|
|
|
20
20
|
export const EditorBackMatterPage = () => {
|
|
21
|
+
const withAttestations = isStaticFeatureEnabled('glossary-attestations');
|
|
21
22
|
const { work } = useEditorState();
|
|
22
23
|
const [endnotes, setEndnotes] = useState<TranslationEditorContent>();
|
|
23
24
|
const [abbreviations, setAbbreviations] =
|
|
@@ -29,7 +30,6 @@ export const EditorBackMatterPage = () => {
|
|
|
29
30
|
(async () => {
|
|
30
31
|
const { uuid } = work;
|
|
31
32
|
const graphqlClient = createGraphQLClient();
|
|
32
|
-
const withAttestations = isStaticFeatureEnabled('glossary-attestations');
|
|
33
33
|
|
|
34
34
|
const [
|
|
35
35
|
{ blocks: endnoteBlocks },
|
|
@@ -90,6 +90,8 @@ export const EditorBackMatterPage = () => {
|
|
|
90
90
|
bibliography={bibliography}
|
|
91
91
|
abbreviations={abbreviations}
|
|
92
92
|
renderTranslation={renderTranslation}
|
|
93
|
+
withAttestations={withAttestations}
|
|
94
|
+
isEditor={true}
|
|
93
95
|
/>
|
|
94
96
|
);
|
|
95
97
|
};
|
|
@@ -15,7 +15,7 @@ export const EditorHeader = () => {
|
|
|
15
15
|
);
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
|
-
<div className="px-4 py-3 flex justify-end h-12 z-10 gap-2">
|
|
18
|
+
<div className="px-4 py-3 flex justify-end h-12 z-10 gap-2 pointer-events-none [&>*]:pointer-events-auto">
|
|
19
19
|
{isDirty && <SaveButton size="xs" onClick={save} />}
|
|
20
20
|
</div>
|
|
21
21
|
);
|
|
@@ -7,6 +7,7 @@ import type { Passage, Work } from '@eightyfourthousand/data-access';
|
|
|
7
7
|
import {
|
|
8
8
|
createGraphQLClient,
|
|
9
9
|
hasPermission,
|
|
10
|
+
type ReplacedPassage,
|
|
10
11
|
savePassages,
|
|
11
12
|
} from '@eightyfourthousand/client-graphql';
|
|
12
13
|
import { passagesFromNodes, ensureUuids } from '../../passage';
|
|
@@ -19,6 +20,7 @@ interface EditorContextState {
|
|
|
19
20
|
work: Work;
|
|
20
21
|
dirtyStore: DirtyStore;
|
|
21
22
|
canEdit(): Promise<boolean>;
|
|
23
|
+
applyReplacedPassages: (passages: ReplacedPassage[]) => Promise<void>;
|
|
22
24
|
getFragment: (builder: string) => XmlFragment;
|
|
23
25
|
setDoc: (doc: Doc) => void;
|
|
24
26
|
getEditor: (key: string) => Editor | undefined;
|
|
@@ -57,6 +59,9 @@ export const EditorContext = createContext<EditorContextState>({
|
|
|
57
59
|
getSnapshot: () => false,
|
|
58
60
|
},
|
|
59
61
|
canEdit: async () => false,
|
|
62
|
+
applyReplacedPassages: async () => {
|
|
63
|
+
// No-op when outside provider
|
|
64
|
+
},
|
|
60
65
|
getFragment: () => {
|
|
61
66
|
throw Error('Not implemented');
|
|
62
67
|
},
|
|
@@ -373,6 +378,76 @@ export const EditorContextProvider = ({
|
|
|
373
378
|
return await hasPermission({ client, permission: 'EDITOR_EDIT' });
|
|
374
379
|
}, [client]);
|
|
375
380
|
|
|
381
|
+
const applyReplacedPassages = useCallback(
|
|
382
|
+
async (passages: ReplacedPassage[]) => {
|
|
383
|
+
if (passages.length === 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
setNavigating(true);
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const passagesByUuid = new Map(
|
|
391
|
+
passages
|
|
392
|
+
.filter(
|
|
393
|
+
(passage): passage is ReplacedPassage & { json: NonNullable<ReplacedPassage['json']> } =>
|
|
394
|
+
Boolean(passage.json),
|
|
395
|
+
)
|
|
396
|
+
.map((passage) => [passage.uuid, passage]),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
Object.values(editorCache.current).forEach((editor) => {
|
|
400
|
+
if (editor.isDestroyed || passagesByUuid.size === 0) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const replacements: Array<{
|
|
405
|
+
from: number;
|
|
406
|
+
nodeSize: number;
|
|
407
|
+
replacement: NonNullable<ReplacedPassage['json']>;
|
|
408
|
+
}> = [];
|
|
409
|
+
|
|
410
|
+
editor.state.doc.descendants((node, pos) => {
|
|
411
|
+
if (node.type.name !== 'passage' || !node.attrs.uuid) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const replacement = passagesByUuid.get(node.attrs.uuid);
|
|
416
|
+
if (replacement?.json) {
|
|
417
|
+
replacements.push({
|
|
418
|
+
from: pos,
|
|
419
|
+
nodeSize: node.nodeSize,
|
|
420
|
+
replacement: replacement.json,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return true;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (replacements.length === 0) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let tr = editor.state.tr;
|
|
432
|
+
replacements
|
|
433
|
+
.sort((a, b) => b.from - a.from)
|
|
434
|
+
.forEach(({ from, nodeSize, replacement }) => {
|
|
435
|
+
tr = tr.replaceWith(
|
|
436
|
+
from,
|
|
437
|
+
from + nodeSize,
|
|
438
|
+
editor.schema.nodeFromJSON(replacement),
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
editor.view.dispatch(tr);
|
|
443
|
+
});
|
|
444
|
+
} finally {
|
|
445
|
+
setNavigating(false);
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
[setNavigating],
|
|
449
|
+
);
|
|
450
|
+
|
|
376
451
|
return (
|
|
377
452
|
<EditorContext.Provider
|
|
378
453
|
value={{
|
|
@@ -380,6 +455,7 @@ export const EditorContextProvider = ({
|
|
|
380
455
|
doc,
|
|
381
456
|
dirtyStore,
|
|
382
457
|
canEdit,
|
|
458
|
+
applyReplacedPassages,
|
|
383
459
|
getFragment,
|
|
384
460
|
setDoc,
|
|
385
461
|
getEditor,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
9
|
useEffect,
|
|
10
|
+
useMemo,
|
|
10
11
|
} from 'react';
|
|
11
12
|
import { Editor } from '@tiptap/react';
|
|
12
13
|
import { TranslationEditorContent } from './TranslationEditor';
|
|
@@ -23,6 +24,7 @@ import { isUuid, scrollToElement, useIsMobile } from '@eightyfourthousand/lib-ut
|
|
|
23
24
|
import { PanelName, TabName, useNavigation } from '../shared';
|
|
24
25
|
import { LotusPond, SHEET_ANIMATION_DURATION } from '@eightyfourthousand/design-system';
|
|
25
26
|
import { useEditorState } from './EditorProvider';
|
|
27
|
+
import { usePaginationLoadTriggers } from '../shared/hooks/usePaginationLoadTriggers';
|
|
26
28
|
|
|
27
29
|
const LOADING_SKELETONS_COUNT = 3;
|
|
28
30
|
const CHUNK_SIZE = 25;
|
|
@@ -100,13 +102,10 @@ export const PaginationProvider = ({
|
|
|
100
102
|
const [startIsLoading, setStartIsLoading] = useState(false);
|
|
101
103
|
const [endIsLoading, setEndIsLoading] = useState(true);
|
|
102
104
|
const [isEditorReady, setIsEditorReady] = useState(false);
|
|
103
|
-
const loadMoreAtStartRef = useRef<HTMLDivElement>(null);
|
|
104
|
-
const loadMoreAtEndRef = useRef<HTMLDivElement>(null);
|
|
105
105
|
const childrenDivRef = useRef<HTMLDivElement>(null);
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const dataClient = createGraphQLClient();
|
|
106
|
+
const handledStartLoadRequestRef = useRef(0);
|
|
107
|
+
const handledEndLoadRequestRef = useRef(0);
|
|
108
|
+
const dataClient = useMemo(() => createGraphQLClient(), []);
|
|
110
109
|
|
|
111
110
|
const { panels, updatePanel, setShowOuterContent } = useNavigation();
|
|
112
111
|
const isMobile = useIsMobile();
|
|
@@ -117,7 +116,9 @@ export const PaginationProvider = ({
|
|
|
117
116
|
// matches — this prevents a PaginationProvider for one tab (e.g. endnotes)
|
|
118
117
|
// from reacting to hashes set by a different tab (e.g. glossary).
|
|
119
118
|
const panelHash =
|
|
120
|
-
!tab || panels[panel]?.tab === tab
|
|
119
|
+
!tab || panels[panel]?.tab === tab || (tab === 'translation' && panels[panel]?.tab === 'compare')
|
|
120
|
+
? panels[panel]?.hash
|
|
121
|
+
: undefined;
|
|
121
122
|
|
|
122
123
|
const { extensions } = useTranslationExtensions({
|
|
123
124
|
fragment,
|
|
@@ -133,6 +134,18 @@ export const PaginationProvider = ({
|
|
|
133
134
|
onCreate?.({ editor });
|
|
134
135
|
},
|
|
135
136
|
});
|
|
137
|
+
const {
|
|
138
|
+
loadMoreAtStartRef,
|
|
139
|
+
loadMoreAtEndRef,
|
|
140
|
+
startLoadRequest,
|
|
141
|
+
endLoadRequest,
|
|
142
|
+
} = usePaginationLoadTriggers({
|
|
143
|
+
enabled: isEditorReady,
|
|
144
|
+
startCursor,
|
|
145
|
+
endCursor,
|
|
146
|
+
startIsLoading,
|
|
147
|
+
endIsLoading,
|
|
148
|
+
});
|
|
136
149
|
|
|
137
150
|
useEffect(() => {
|
|
138
151
|
setShowOuterContent(!startCursor);
|
|
@@ -265,12 +278,6 @@ export const PaginationProvider = ({
|
|
|
265
278
|
} finally {
|
|
266
279
|
isNavigatingRef.current = false;
|
|
267
280
|
setNavigating(false);
|
|
268
|
-
// Re-trigger load-more check: the observer may have fired while
|
|
269
|
-
// navigation was in progress (sentinel entered view after content
|
|
270
|
-
// was replaced), but the load-more effect was blocked by the
|
|
271
|
-
// isNavigatingRef guard. Bumping the trigger re-evaluates now
|
|
272
|
-
// that navigation is complete.
|
|
273
|
-
setLoadMoreTrigger((c) => c + 1);
|
|
274
281
|
}
|
|
275
282
|
})();
|
|
276
283
|
}, [
|
|
@@ -294,43 +301,18 @@ export const PaginationProvider = ({
|
|
|
294
301
|
};
|
|
295
302
|
}, [editor]);
|
|
296
303
|
|
|
297
|
-
// Set up IntersectionObserver only after the editor has created and rendered
|
|
298
|
-
// initial content. isEditorReady flips to true once and stays true, so the
|
|
299
|
-
// observer is created once and never torn down/recreated during page fetches.
|
|
300
|
-
useEffect(() => {
|
|
301
|
-
if (!isEditorReady) return;
|
|
302
|
-
|
|
303
|
-
const startEl = loadMoreAtStartRef.current;
|
|
304
|
-
const endEl = loadMoreAtEndRef.current;
|
|
305
|
-
|
|
306
|
-
const observer = new IntersectionObserver((entries) => {
|
|
307
|
-
for (const entry of entries) {
|
|
308
|
-
if (entry.target === startEl) {
|
|
309
|
-
shouldLoadMoreAtStartRef.current = entry.isIntersecting;
|
|
310
|
-
} else if (entry.target === endEl) {
|
|
311
|
-
shouldLoadMoreAtEndRef.current = entry.isIntersecting;
|
|
312
|
-
}
|
|
313
|
-
if (entry.isIntersecting) {
|
|
314
|
-
setLoadMoreTrigger((c) => c + 1);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
if (startEl) observer.observe(startEl);
|
|
320
|
-
if (endEl) observer.observe(endEl);
|
|
321
|
-
|
|
322
|
-
return () => observer.disconnect();
|
|
323
|
-
}, [isEditorReady]);
|
|
324
|
-
|
|
325
304
|
useEffect(() => {
|
|
326
305
|
if (
|
|
306
|
+
endLoadRequest === 0 ||
|
|
307
|
+
handledEndLoadRequestRef.current === endLoadRequest ||
|
|
327
308
|
endIsLoading ||
|
|
328
|
-
!shouldLoadMoreAtEndRef.current ||
|
|
329
309
|
!endCursor ||
|
|
330
310
|
isNavigatingRef.current
|
|
331
311
|
) {
|
|
332
312
|
return;
|
|
333
313
|
}
|
|
314
|
+
|
|
315
|
+
handledEndLoadRequestRef.current = endLoadRequest;
|
|
334
316
|
setEndIsLoading(true);
|
|
335
317
|
|
|
336
318
|
(async () => {
|
|
@@ -363,18 +345,21 @@ export const PaginationProvider = ({
|
|
|
363
345
|
editor,
|
|
364
346
|
endCursor,
|
|
365
347
|
dataClient,
|
|
366
|
-
|
|
348
|
+
endLoadRequest,
|
|
367
349
|
]);
|
|
368
350
|
|
|
369
351
|
useEffect(() => {
|
|
370
352
|
if (
|
|
353
|
+
startLoadRequest === 0 ||
|
|
354
|
+
handledStartLoadRequestRef.current === startLoadRequest ||
|
|
371
355
|
startIsLoading ||
|
|
372
|
-
!shouldLoadMoreAtStartRef.current ||
|
|
373
356
|
!startCursor ||
|
|
374
357
|
isNavigatingRef.current
|
|
375
358
|
) {
|
|
376
359
|
return;
|
|
377
360
|
}
|
|
361
|
+
|
|
362
|
+
handledStartLoadRequestRef.current = startLoadRequest;
|
|
378
363
|
setStartIsLoading(true);
|
|
379
364
|
|
|
380
365
|
(async () => {
|
|
@@ -425,7 +410,7 @@ export const PaginationProvider = ({
|
|
|
425
410
|
editor,
|
|
426
411
|
startCursor,
|
|
427
412
|
dataClient,
|
|
428
|
-
|
|
413
|
+
startLoadRequest,
|
|
429
414
|
]);
|
|
430
415
|
|
|
431
416
|
return (
|
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
H3_STYLE,
|
|
8
8
|
H4_STYLE,
|
|
9
9
|
H5_STYLE,
|
|
10
|
+
H6_STYLE,
|
|
10
11
|
HONORIFIC_TITLE_STYLE,
|
|
11
12
|
SECTION_TITLE_STYLE,
|
|
12
13
|
} from '@eightyfourthousand/design-system';
|
|
13
14
|
import { createNodeViewDom } from '../../util';
|
|
14
15
|
import { HTMLElementType } from 'react';
|
|
16
|
+
import { cn } from '@eightyfourthousand/lib-utils';
|
|
15
17
|
|
|
16
18
|
const CLASS_FOR_LEVEL: Record<Level, string> = {
|
|
17
19
|
1: H1_STYLE,
|
|
@@ -19,7 +21,7 @@ const CLASS_FOR_LEVEL: Record<Level, string> = {
|
|
|
19
21
|
3: H3_STYLE,
|
|
20
22
|
4: H4_STYLE,
|
|
21
23
|
5: H5_STYLE,
|
|
22
|
-
6:
|
|
24
|
+
6: H6_STYLE,
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
const CLASS_FOR_CLASS: Record<string, string> = {
|
|
@@ -47,8 +49,7 @@ export const Heading = TiptapHeading.extend({
|
|
|
47
49
|
const hasLevel = this.options.levels.includes(nodeLevel);
|
|
48
50
|
const level = hasLevel ? nodeLevel : this.options.levels.at(-1);
|
|
49
51
|
const element = `h${level}` as HTMLElementType;
|
|
50
|
-
const className =
|
|
51
|
-
CLASS_FOR_CLASS[node.attrs.class] || CLASS_FOR_LEVEL[nodeLevel];
|
|
52
|
+
const className = cn(CLASS_FOR_LEVEL[nodeLevel], CLASS_FOR_CLASS[node.attrs.class]);
|
|
52
53
|
|
|
53
54
|
const { dom } = createNodeViewDom({
|
|
54
55
|
editor,
|
|
@@ -91,7 +91,10 @@ export const InternalLink = Mark.create<InternalLinkOptions>({
|
|
|
91
91
|
e.preventDefault();
|
|
92
92
|
if (!isSameWork) {
|
|
93
93
|
// Cross-work links open in a new tab
|
|
94
|
-
|
|
94
|
+
let href = props.mark.attrs.href || '#';
|
|
95
|
+
if (isEditable && href) {
|
|
96
|
+
href = `${href}?edit=true`;
|
|
97
|
+
}
|
|
95
98
|
window.open(href, '_blank');
|
|
96
99
|
return
|
|
97
100
|
}
|
|
@@ -86,7 +86,10 @@ export const Mention = Node.create<MentionOptions>({
|
|
|
86
86
|
anchor.textContent = item.text || item.displayText || item.entity;
|
|
87
87
|
|
|
88
88
|
// Compute href from linkType + entity
|
|
89
|
-
|
|
89
|
+
let href = `/entity/${item.linkType}/${item.entity}`;
|
|
90
|
+
if (isEditable) {
|
|
91
|
+
href = `${href}?edit=true`;
|
|
92
|
+
}
|
|
90
93
|
|
|
91
94
|
if (item.isSameWork) {
|
|
92
95
|
// Same-work links navigate in-place via panel system
|
|
@@ -11,11 +11,11 @@ export const EditorOptions = ({
|
|
|
11
11
|
return (
|
|
12
12
|
<>
|
|
13
13
|
<DropdownMenuItem onSelect={() => onSelection('label')}>
|
|
14
|
-
<PencilIcon className="text-
|
|
14
|
+
<PencilIcon className="text-Primary" /> Edit Label
|
|
15
15
|
</DropdownMenuItem>
|
|
16
16
|
<DropdownMenuSeparator />
|
|
17
17
|
<DropdownMenuItem onSelect={() => onSelection('attributes')}>
|
|
18
|
-
<BracesIcon className="text-
|
|
18
|
+
<BracesIcon className="text-primary" /> View Attributes
|
|
19
19
|
</DropdownMenuItem>
|
|
20
20
|
<DropdownMenuSeparator />
|
|
21
21
|
<DropdownMenuItem
|
|
@@ -56,7 +56,7 @@ const PassageComponent = (props: NodeViewProps) => {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const alignment = node.attrs.alignments?.[toh] as Alignment;
|
|
59
|
-
const source = alignment?.tibetan || '';
|
|
59
|
+
const source = alignment?.tibetan.trim() || '';
|
|
60
60
|
|
|
61
61
|
const firstChild = node.content.firstChild;
|
|
62
62
|
let compareLeadingSpace = 'md:mt-1';
|
|
@@ -93,7 +93,7 @@ const PassageComponent = (props: NodeViewProps) => {
|
|
|
93
93
|
}, [node.attrs.uuid, editor]);
|
|
94
94
|
|
|
95
95
|
const className =
|
|
96
|
-
'absolute labeled -left-16 w-16 text-end hover:cursor-pointer
|
|
96
|
+
'absolute labeled -left-16 w-16 text-end hover:cursor-pointer';
|
|
97
97
|
const borderClassName =
|
|
98
98
|
editor.storage.globalConfig.debug && node.attrs.invalid
|
|
99
99
|
? 'after:content-["⚠️"] after:absolute after:top-0 after:-right-5'
|
|
@@ -223,7 +223,7 @@ const PassageComponent = (props: NodeViewProps) => {
|
|
|
223
223
|
<div
|
|
224
224
|
className={cn('w-full', source ? '' : 'hidden', compareLeadingSpace)}
|
|
225
225
|
>
|
|
226
|
-
<LabeledElement
|
|
226
|
+
<LabeledElement className="mt-0.5">
|
|
227
227
|
<div className="leading-7 font-tibetan text-lg whitespace-normal mt-1.5 pb-4 md:pb-2">
|
|
228
228
|
{source}
|
|
229
229
|
</div>
|
|
@@ -30,12 +30,12 @@ export const ReaderOptions = (
|
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
32
|
<DropdownMenuItem onSelect={props.toggleBookmark}>
|
|
33
|
-
<BookmarkIcon className="text-
|
|
33
|
+
<BookmarkIcon className="text-primary" />
|
|
34
34
|
{props.isBookmarked ? 'Remove Bookmark' : 'Add Bookmark'}
|
|
35
35
|
</DropdownMenuItem>
|
|
36
36
|
<DropdownMenuSeparator />
|
|
37
37
|
<DropdownMenuItem onSelect={copyLink}>
|
|
38
|
-
<CopyIcon className="text-
|
|
38
|
+
<CopyIcon className="text-primary" /> Copy Link
|
|
39
39
|
</DropdownMenuItem>
|
|
40
40
|
{props.onSuggestRevision && (
|
|
41
41
|
<>
|
|
@@ -46,7 +46,7 @@ export const ReaderOptions = (
|
|
|
46
46
|
props.onSuggestRevision?.();
|
|
47
47
|
}}
|
|
48
48
|
>
|
|
49
|
-
<MessageSquareIcon className="text-
|
|
49
|
+
<MessageSquareIcon className="text-primary" /> Suggest Revision
|
|
50
50
|
</DropdownMenuItem>
|
|
51
51
|
</>
|
|
52
52
|
)}
|
|
@@ -54,13 +54,13 @@ export const ShowAnnotations = (props: NodeViewProps) => {
|
|
|
54
54
|
</DialogDescription>
|
|
55
55
|
|
|
56
56
|
<div className="text-sm flex gap-1">
|
|
57
|
-
<span className="text-
|
|
57
|
+
<span className="text-primary">UUID: </span>
|
|
58
58
|
<span>{node.attrs.uuid}</span>
|
|
59
59
|
<span className="text-muted-foreground px-2">{'|'}</span>
|
|
60
|
-
<span className="text-
|
|
60
|
+
<span className="text-primary">Label:</span>
|
|
61
61
|
<span>{node.attrs.label}</span>
|
|
62
62
|
<span className="text-muted-foreground px-2">{'|'}</span>
|
|
63
|
-
<span className="text-
|
|
63
|
+
<span className="text-primary">Type:</span>
|
|
64
64
|
<span>{node.attrs.type}</span>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
@@ -4,7 +4,7 @@ import { Node, mergeAttributes } from '@tiptap/core';
|
|
|
4
4
|
// TODO: make nice custom components for each titles type
|
|
5
5
|
|
|
6
6
|
const BASE_STYLE =
|
|
7
|
-
'relative border-t border-border/50 pb-8 pt-12 before:absolute before:top-0 before:text-lg before:
|
|
7
|
+
'relative border-t border-border/50 pb-8 pt-12 before:absolute before:top-0 before:text-lg before:text-secondary';
|
|
8
8
|
|
|
9
9
|
export const TohsNode = Node.create({
|
|
10
10
|
name: 'tohs',
|
|
@@ -15,12 +15,14 @@ export const ReaderBackMatterPanel = ({
|
|
|
15
15
|
glossary,
|
|
16
16
|
bibliography,
|
|
17
17
|
abbreviations,
|
|
18
|
+
withAttestations = false,
|
|
18
19
|
}: {
|
|
19
20
|
workUuid: string;
|
|
20
21
|
endnotes: TranslationEditorContent;
|
|
21
22
|
glossary: GlossaryTermsPage;
|
|
22
23
|
bibliography: BibliographyEntries;
|
|
23
24
|
abbreviations: TranslationEditorContent;
|
|
25
|
+
withAttestations?: boolean;
|
|
24
26
|
}) => {
|
|
25
27
|
const { hasTranslationContent } = useNavigation();
|
|
26
28
|
if (!hasTranslationContent) {
|
|
@@ -48,6 +50,7 @@ export const ReaderBackMatterPanel = ({
|
|
|
48
50
|
bibliography={bibliography}
|
|
49
51
|
abbreviations={abbreviations}
|
|
50
52
|
renderTranslation={renderTranslation}
|
|
53
|
+
withAttestations={withAttestations}
|
|
51
54
|
/>
|
|
52
55
|
);
|
|
53
56
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Tabs,
|
|
5
|
+
TabsContent,
|
|
6
|
+
TabsList,
|
|
7
|
+
TabsTrigger,
|
|
8
|
+
} from '@eightyfourthousand/design-system';
|
|
4
9
|
import { TranslationEditorContent } from '../editor';
|
|
5
10
|
import { TranslationRenderer } from './types';
|
|
6
11
|
import { BibliographyEntries } from '@eightyfourthousand/data-access';
|
|
@@ -18,6 +23,8 @@ export const BackMatterPanel = ({
|
|
|
18
23
|
glossary,
|
|
19
24
|
bibliography,
|
|
20
25
|
abbreviations,
|
|
26
|
+
isEditor = false,
|
|
27
|
+
withAttestations = false,
|
|
21
28
|
renderTranslation,
|
|
22
29
|
}: {
|
|
23
30
|
workUuid: string;
|
|
@@ -25,6 +32,8 @@ export const BackMatterPanel = ({
|
|
|
25
32
|
glossary: GlossaryTermsPage;
|
|
26
33
|
bibliography: BibliographyEntries;
|
|
27
34
|
abbreviations: TranslationEditorContent;
|
|
35
|
+
isEditor?: boolean;
|
|
36
|
+
withAttestations?: boolean;
|
|
28
37
|
renderTranslation: (
|
|
29
38
|
params: TranslationRenderer,
|
|
30
39
|
) => ReactElement<TranslationRenderer>;
|
|
@@ -39,8 +48,7 @@ export const BackMatterPanel = ({
|
|
|
39
48
|
!!panels.right.hash,
|
|
40
49
|
);
|
|
41
50
|
|
|
42
|
-
const hasGlossary =
|
|
43
|
-
glossary.terms.length > 0 || glossary.hasMoreAfter;
|
|
51
|
+
const hasGlossary = glossary.terms.length > 0 || glossary.hasMoreAfter;
|
|
44
52
|
|
|
45
53
|
return (
|
|
46
54
|
<Tabs
|
|
@@ -54,7 +62,7 @@ export const BackMatterPanel = ({
|
|
|
54
62
|
updatePanel({ name: 'right', state: { open: true, tab } });
|
|
55
63
|
}}
|
|
56
64
|
defaultValue="endnotes"
|
|
57
|
-
className="w-full gap-0 @container/sidebar"
|
|
65
|
+
className="w-full gap-0 @container/sidebar h-full flex flex-col"
|
|
58
66
|
>
|
|
59
67
|
<div className="sticky top-0 pt-1 pb-2 z-10 w-full rounded-t bg-background overflow-x-auto text-center">
|
|
60
68
|
<TabsList
|
|
@@ -66,9 +74,7 @@ export const BackMatterPanel = ({
|
|
|
66
74
|
{endnotes.length > 0 && (
|
|
67
75
|
<TabsTrigger value="endnotes">Notes</TabsTrigger>
|
|
68
76
|
)}
|
|
69
|
-
{hasGlossary &&
|
|
70
|
-
<TabsTrigger value="glossary">Glossary</TabsTrigger>
|
|
71
|
-
)}
|
|
77
|
+
{hasGlossary && <TabsTrigger value="glossary">Glossary</TabsTrigger>}
|
|
72
78
|
{bibliography.length > 0 && (
|
|
73
79
|
<TabsTrigger value="bibliography">Biblio</TabsTrigger>
|
|
74
80
|
)}
|
|
@@ -77,10 +83,10 @@ export const BackMatterPanel = ({
|
|
|
77
83
|
)}
|
|
78
84
|
</TabsList>
|
|
79
85
|
</div>
|
|
80
|
-
<div className="
|
|
86
|
+
<div className="flex-1 min-h-0">
|
|
81
87
|
<div
|
|
82
88
|
ref={scrollContainerRef}
|
|
83
|
-
className="overflow-auto
|
|
89
|
+
className="overflow-auto h-full bg-surface"
|
|
84
90
|
data-panel="right"
|
|
85
91
|
>
|
|
86
92
|
<div className="rounded ps-10 pe-4 max-w-readable mx-auto">
|
|
@@ -107,8 +113,9 @@ export const BackMatterPanel = ({
|
|
|
107
113
|
<GlossaryPaginationProvider
|
|
108
114
|
workUuid={workUuid}
|
|
109
115
|
initialPage={glossary}
|
|
116
|
+
withAttestations={withAttestations}
|
|
110
117
|
>
|
|
111
|
-
<GlossaryTermList />
|
|
118
|
+
<GlossaryTermList isEditor={isEditor} />
|
|
112
119
|
</GlossaryPaginationProvider>
|
|
113
120
|
</TabsContent>
|
|
114
121
|
)}
|
|
@@ -64,7 +64,7 @@ export const BodyPanel = ({
|
|
|
64
64
|
panels.main.tab || (hasTranslationContent ? 'translation' : 'source');
|
|
65
65
|
const safeTab =
|
|
66
66
|
!hasTranslationContent &&
|
|
67
|
-
|
|
67
|
+
(activeTab === 'translation' || activeTab === 'compare')
|
|
68
68
|
? 'source'
|
|
69
69
|
: activeTab;
|
|
70
70
|
|
|
@@ -141,8 +141,8 @@ export const BodyPanel = ({
|
|
|
141
141
|
defaultValue={hasTranslationContent ? 'translation' : 'source'}
|
|
142
142
|
className="px-12 w-full"
|
|
143
143
|
>
|
|
144
|
-
<div className="sticky top-0.75 -mt-28 z-10 w-full overflow-x-auto text-center">
|
|
145
|
-
<TabsList className="w-fit inline-flex">
|
|
144
|
+
<div className="sticky top-0.75 -mt-28 z-10 w-full overflow-x-auto text-center pointer-events-none">
|
|
145
|
+
<TabsList className="w-fit inline-flex pointer-events-auto">
|
|
146
146
|
<TabsTrigger value="front">Front</TabsTrigger>
|
|
147
147
|
{hasTranslationContent && (
|
|
148
148
|
<TabsTrigger value="translation">Translation</TabsTrigger>
|
|
@@ -153,7 +153,7 @@ export const BodyPanel = ({
|
|
|
153
153
|
<TabsTrigger value="source">Source</TabsTrigger>
|
|
154
154
|
</TabsList>
|
|
155
155
|
</div>
|
|
156
|
-
{showOuterContent ? theTitles : null}
|
|
156
|
+
{showOuterContent && safeTab === 'front' ? theTitles : null}
|
|
157
157
|
<TabsContent
|
|
158
158
|
value="front"
|
|
159
159
|
forceMount
|
|
@@ -187,9 +187,9 @@ export const BodyPanel = ({
|
|
|
187
187
|
>
|
|
188
188
|
<div
|
|
189
189
|
className={cn(
|
|
190
|
-
'w-full mx-auto',
|
|
190
|
+
'w-full mx-auto mt-8',
|
|
191
191
|
safeTab === 'compare'
|
|
192
|
-
? '2xl:max-w-7xl max-w-5xl
|
|
192
|
+
? '2xl:max-w-7xl max-w-5xl'
|
|
193
193
|
: 'max-w-readable',
|
|
194
194
|
)}
|
|
195
195
|
>
|