@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.
Files changed (40) hide show
  1. package/README.md +4 -2
  2. package/package.json +7 -7
  3. package/src/lib/components/editor/EditorBackMatterPage.tsx +3 -1
  4. package/src/lib/components/editor/EditorHeader.tsx +1 -1
  5. package/src/lib/components/editor/EditorProvider.tsx +76 -0
  6. package/src/lib/components/editor/PaginationProvider.tsx +30 -45
  7. package/src/lib/components/editor/extensions/Heading/Heading.ts +4 -3
  8. package/src/lib/components/editor/extensions/InternalLink/InternalLink.ts +4 -1
  9. package/src/lib/components/editor/extensions/Mention/Mention.ts +4 -1
  10. package/src/lib/components/editor/extensions/Passage/EditorOptions.tsx +2 -2
  11. package/src/lib/components/editor/extensions/Passage/Passage.tsx +3 -3
  12. package/src/lib/components/editor/extensions/Passage/ReaderOptions.tsx +3 -3
  13. package/src/lib/components/editor/extensions/Passage/ShowAnnotations.tsx +3 -3
  14. package/src/lib/components/editor/extensions/TitlesNode.ts +1 -1
  15. package/src/lib/components/reader/ReaderBackMatterPage.tsx +1 -0
  16. package/src/lib/components/reader/ReaderBackMatterPanel.tsx +3 -0
  17. package/src/lib/components/reader/ReaderBodyPanel.tsx +1 -1
  18. package/src/lib/components/shared/BackMatterPanel.tsx +17 -10
  19. package/src/lib/components/shared/BodyPanel.tsx +6 -6
  20. package/src/lib/components/shared/LabeledElement.tsx +3 -3
  21. package/src/lib/components/shared/LeftPanel.tsx +3 -3
  22. package/src/lib/components/shared/NavigationContext.ts +1 -1
  23. package/src/lib/components/shared/NavigationProvider.tsx +33 -1
  24. package/src/lib/components/shared/ReaderSearchButton.tsx +110 -0
  25. package/src/lib/components/shared/SearchReplacePanel.tsx +259 -0
  26. package/src/lib/components/shared/SourceReader.tsx +13 -4
  27. package/src/lib/components/shared/SuggestRevisionForm.tsx +1 -1
  28. package/src/lib/components/shared/TableOfContents.tsx +40 -27
  29. package/src/lib/components/shared/ThreeColumnRenderer.tsx +16 -9
  30. package/src/lib/components/shared/TranslationHeader.tsx +2 -63
  31. package/src/lib/components/shared/bibliography/BibliographyList.tsx +2 -2
  32. package/src/lib/components/shared/glossary/GlossaryInstanceBody.tsx +15 -5
  33. package/src/lib/components/shared/glossary/GlossaryPaginationProvider.tsx +17 -56
  34. package/src/lib/components/shared/glossary/GlossaryTermList.tsx +4 -5
  35. package/src/lib/components/shared/hooks/usePaginationLoadTriggers.spec.tsx +158 -0
  36. package/src/lib/components/shared/hooks/usePaginationLoadTriggers.ts +129 -0
  37. package/src/lib/components/shared/titles/FramedCard.tsx +3 -3
  38. package/src/lib/components/shared/titles/Titles.tsx +17 -6
  39. package/src/lib/components/shared/titles/TitlesCard.tsx +6 -6
  40. 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
- This library was generated with [Nx](https://nx.dev).
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.0",
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.0",
21
- "@eightyfourthousand/data-access": "^2026.3.0",
22
- "@eightyfourthousand/design-system": "^2026.3.0",
23
- "@eightyfourthousand/lib-instr": "^2026.3.0",
24
- "@eightyfourthousand/lib-search": "^2026.3.0",
25
- "@eightyfourthousand/lib-utils": "^2026.3.0"
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 shouldLoadMoreAtStartRef = useRef(false);
107
- const shouldLoadMoreAtEndRef = useRef(false);
108
- const [loadMoreTrigger, setLoadMoreTrigger] = useState(0);
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 ? panels[panel]?.hash : undefined;
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
- loadMoreTrigger,
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
- loadMoreTrigger,
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: H5_STYLE,
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
- const href = props.mark.attrs.href || '#';
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
- const href = `/entity/${item.linkType}/${item.entity}`;
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-ochre" /> Edit Label
14
+ <PencilIcon className="text-Primary" /> Edit Label
15
15
  </DropdownMenuItem>
16
16
  <DropdownMenuSeparator />
17
17
  <DropdownMenuItem onSelect={() => onSelection('attributes')}>
18
- <BracesIcon className="text-ochre" /> View Attributes
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 -mt-0.25';
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 label={node.attrs.label} className="mt-0.5">
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-ochre" />
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-ochre" /> Copy Link
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-ochre" /> Suggest Revision
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-navy">UUID: </span>
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-navy">Label:</span>
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-navy">Type:</span>
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:font-light before:text-slate';
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',
@@ -57,6 +57,7 @@ export const ReaderBackMatterPage = async ({
57
57
  bibliography={bibliography}
58
58
  endnotes={endnotes}
59
59
  glossary={glossary}
60
+ withAttestations={withAttestations}
60
61
  />
61
62
  );
62
63
  };
@@ -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
  };
@@ -15,7 +15,7 @@ import { TranslationReader } from './TranslationReader';
15
15
  const TITLE_VARIANTS_FOR_TABS: { [key: string]: TitlesVariant } = {
16
16
  translation: 'english',
17
17
  source: 'tibetan',
18
- front: 'english',
18
+ front: 'front',
19
19
  compare: 'comparison',
20
20
  };
21
21
 
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@eightyfourthousand/design-system';
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="px-2">
86
+ <div className="flex-1 min-h-0">
81
87
  <div
82
88
  ref={scrollContainerRef}
83
- className="overflow-auto md:h-[calc(100vh-8.5rem)] h-[calc(100vh-4rem)] rounded bg-surface"
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
- (activeTab === 'translation' || activeTab === 'compare')
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 mt-8'
192
+ ? '2xl:max-w-7xl max-w-5xl'
193
193
  : 'max-w-readable',
194
194
  )}
195
195
  >