@eightyfourthousand/lib-editing 2026.3.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 (304) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +7 -0
  4. package/jest.config.ts +10 -0
  5. package/package.json +35 -0
  6. package/postcss.config.mjs +9 -0
  7. package/project.json +29 -0
  8. package/src/fixtures/basic/json.ts +396 -0
  9. package/src/fixtures/toh251/json.ts +4913 -0
  10. package/src/fixtures/toh251/passages.ts +2814 -0
  11. package/src/fixtures/types.ts +8 -0
  12. package/src/index.ts +4 -0
  13. package/src/lib/block.ts +226 -0
  14. package/src/lib/components/editor/BlockEditor.tsx +38 -0
  15. package/src/lib/components/editor/EditorBackMatterPage.tsx +95 -0
  16. package/src/lib/components/editor/EditorBodyPage.tsx +87 -0
  17. package/src/lib/components/editor/EditorHeader.tsx +22 -0
  18. package/src/lib/components/editor/EditorLayout.tsx +62 -0
  19. package/src/lib/components/editor/EditorLeftPanelPage.tsx +27 -0
  20. package/src/lib/components/editor/EditorProvider.tsx +399 -0
  21. package/src/lib/components/editor/PaginationProvider.tsx +472 -0
  22. package/src/lib/components/editor/TitlesBuilder.tsx +40 -0
  23. package/src/lib/components/editor/TranslationBuilder.tsx +171 -0
  24. package/src/lib/components/editor/TranslationEditor.tsx +32 -0
  25. package/src/lib/components/editor/extensions/Abbreviation/Abbreviation.ts +133 -0
  26. package/src/lib/components/editor/extensions/Audio/Audio.ts +69 -0
  27. package/src/lib/components/editor/extensions/Bold.ts +43 -0
  28. package/src/lib/components/editor/extensions/Document.ts +8 -0
  29. package/src/lib/components/editor/extensions/DragHandle/DragHandle.ts +429 -0
  30. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLink.tsx +39 -0
  31. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkHoverContent.tsx +139 -0
  32. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkMark.ts +236 -0
  33. package/src/lib/components/editor/extensions/EndNoteLink/endnote-utils.ts +412 -0
  34. package/src/lib/components/editor/extensions/GlobalConfig.ts +52 -0
  35. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInput.tsx +54 -0
  36. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstance.tsx +129 -0
  37. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstanceNode.ts +148 -0
  38. package/src/lib/components/editor/extensions/Heading/Heading.ts +71 -0
  39. package/src/lib/components/editor/extensions/HoverInputField.tsx +54 -0
  40. package/src/lib/components/editor/extensions/Image.ts +18 -0
  41. package/src/lib/components/editor/extensions/Indent.ts +103 -0
  42. package/src/lib/components/editor/extensions/InternalLink/InternalLink.ts +173 -0
  43. package/src/lib/components/editor/extensions/InternalLink/InternalLinkHoverContent.tsx +137 -0
  44. package/src/lib/components/editor/extensions/InternalLink/InternalLinkInput.tsx +71 -0
  45. package/src/lib/components/editor/extensions/InternalLink/index.ts +1 -0
  46. package/src/lib/components/editor/extensions/Italic.ts +50 -0
  47. package/src/lib/components/editor/extensions/LeadingSpace.ts +106 -0
  48. package/src/lib/components/editor/extensions/Line/LineNode.ts +41 -0
  49. package/src/lib/components/editor/extensions/LineGroup/LineGroupNode.ts +124 -0
  50. package/src/lib/components/editor/extensions/Link/Link.ts +65 -0
  51. package/src/lib/components/editor/extensions/Link/LinkHoverContent.tsx +124 -0
  52. package/src/lib/components/editor/extensions/Link/index.ts +1 -0
  53. package/src/lib/components/editor/extensions/List.ts +74 -0
  54. package/src/lib/components/editor/extensions/Mantra/Mantra.ts +88 -0
  55. package/src/lib/components/editor/extensions/Mention/Mention.ts +184 -0
  56. package/src/lib/components/editor/extensions/Mention/MentionHoverContent.tsx +158 -0
  57. package/src/lib/components/editor/extensions/NodeWrapper.tsx +57 -0
  58. package/src/lib/components/editor/extensions/Paragraph/Paragraph.ts +25 -0
  59. package/src/lib/components/editor/extensions/ParagraphIndent.ts +87 -0
  60. package/src/lib/components/editor/extensions/Passage/EditLabel.tsx +57 -0
  61. package/src/lib/components/editor/extensions/Passage/EditorOptions.tsx +29 -0
  62. package/src/lib/components/editor/extensions/Passage/Passage.tsx +238 -0
  63. package/src/lib/components/editor/extensions/Passage/PassageNode.ts +223 -0
  64. package/src/lib/components/editor/extensions/Passage/ReaderOptions.tsx +55 -0
  65. package/src/lib/components/editor/extensions/Passage/ShowAnnotations.tsx +92 -0
  66. package/src/lib/components/editor/extensions/Passage/index.ts +1 -0
  67. package/src/lib/components/editor/extensions/Passage/label.spec.ts +118 -0
  68. package/src/lib/components/editor/extensions/Passage/label.ts +39 -0
  69. package/src/lib/components/editor/extensions/Placeholder.ts +9 -0
  70. package/src/lib/components/editor/extensions/SlashCommand/SlashCommand.ts +65 -0
  71. package/src/lib/components/editor/extensions/SlashCommand/SuggestionList.tsx +109 -0
  72. package/src/lib/components/editor/extensions/SlashCommand/Suggestions.ts +185 -0
  73. package/src/lib/components/editor/extensions/SmallCaps.ts +110 -0
  74. package/src/lib/components/editor/extensions/StarterKit.ts +36 -0
  75. package/src/lib/components/editor/extensions/Subscript.ts +43 -0
  76. package/src/lib/components/editor/extensions/Superscript.ts +43 -0
  77. package/src/lib/components/editor/extensions/Table.ts +32 -0
  78. package/src/lib/components/editor/extensions/TextAlign.ts +5 -0
  79. package/src/lib/components/editor/extensions/TitleMetadata.ts +40 -0
  80. package/src/lib/components/editor/extensions/TitleNode.ts +133 -0
  81. package/src/lib/components/editor/extensions/TitlesNode.ts +102 -0
  82. package/src/lib/components/editor/extensions/Trailer.ts +57 -0
  83. package/src/lib/components/editor/extensions/TranslationDocument.ts +7 -0
  84. package/src/lib/components/editor/extensions/TranslationMetadata.ts +58 -0
  85. package/src/lib/components/editor/extensions/Underline.ts +43 -0
  86. package/src/lib/components/editor/hooks/index.ts +4 -0
  87. package/src/lib/components/editor/hooks/useBlockEditor.ts +53 -0
  88. package/src/lib/components/editor/hooks/useDefaultExtensions.ts +39 -0
  89. package/src/lib/components/editor/hooks/useDirtyStore.ts +33 -0
  90. package/src/lib/components/editor/hooks/useTranslationExtensions.ts +148 -0
  91. package/src/lib/components/editor/index.ts +10 -0
  92. package/src/lib/components/editor/menus/EmptyBubbleMenu.tsx +42 -0
  93. package/src/lib/components/editor/menus/MainBubbleMenu.tsx +51 -0
  94. package/src/lib/components/editor/menus/TranslationBubbleMenu.tsx +57 -0
  95. package/src/lib/components/editor/menus/index.ts +3 -0
  96. package/src/lib/components/editor/menus/selectors/EndNoteSelector.tsx +388 -0
  97. package/src/lib/components/editor/menus/selectors/GlossarySelector.tsx +63 -0
  98. package/src/lib/components/editor/menus/selectors/LinkSelector.tsx +68 -0
  99. package/src/lib/components/editor/menus/selectors/MantraSelector.tsx +119 -0
  100. package/src/lib/components/editor/menus/selectors/NodeSelector.tsx +144 -0
  101. package/src/lib/components/editor/menus/selectors/ParagraphButtons.tsx +68 -0
  102. package/src/lib/components/editor/menus/selectors/SelectorInputField.tsx +68 -0
  103. package/src/lib/components/editor/menus/selectors/TextAlignSelector.tsx +89 -0
  104. package/src/lib/components/editor/menus/selectors/TextButtons.tsx +89 -0
  105. package/src/lib/components/editor/menus/selectors/TranslationNodeSelector.tsx +143 -0
  106. package/src/lib/components/editor/menus/selectors/TranslationTextButtons.tsx +125 -0
  107. package/src/lib/components/editor/menus/selectors/index.ts +5 -0
  108. package/src/lib/components/editor/save-filter.spec.ts +94 -0
  109. package/src/lib/components/editor/save-filter.ts +27 -0
  110. package/src/lib/components/editor/util.ts +304 -0
  111. package/src/lib/components/index.ts +3 -0
  112. package/src/lib/components/reader/ReaderBackMatterPage.tsx +62 -0
  113. package/src/lib/components/reader/ReaderBackMatterPanel.tsx +53 -0
  114. package/src/lib/components/reader/ReaderBodyPage.tsx +46 -0
  115. package/src/lib/components/reader/ReaderBodyPanel.tsx +68 -0
  116. package/src/lib/components/reader/ReaderLayout.tsx +39 -0
  117. package/src/lib/components/reader/ReaderLeftPanel.tsx +8 -0
  118. package/src/lib/components/reader/ReaderLeftPanelPage.tsx +31 -0
  119. package/src/lib/components/reader/TranslationReader.tsx +28 -0
  120. package/src/lib/components/reader/index.ts +2 -0
  121. package/src/lib/components/reader/ssr.ts +3 -0
  122. package/src/lib/components/shared/AiSummarizerPage.tsx +12 -0
  123. package/src/lib/components/shared/BackMatterPanel.tsx +143 -0
  124. package/src/lib/components/shared/BodyPanel.tsx +214 -0
  125. package/src/lib/components/shared/HoverCardProvider.tsx +407 -0
  126. package/src/lib/components/shared/Imprint.tsx +24 -0
  127. package/src/lib/components/shared/LabeledElement.tsx +133 -0
  128. package/src/lib/components/shared/LeftPanel.tsx +65 -0
  129. package/src/lib/components/shared/NavigationContext.ts +64 -0
  130. package/src/lib/components/shared/NavigationProvider.tsx +368 -0
  131. package/src/lib/components/shared/OpenGraphImage.tsx +75 -0
  132. package/src/lib/components/shared/PassageSkeleton.tsx +10 -0
  133. package/src/lib/components/shared/RestrictionWarning.tsx +177 -0
  134. package/src/lib/components/shared/SourceReader.tsx +83 -0
  135. package/src/lib/components/shared/SuggestRevisionForm.tsx +99 -0
  136. package/src/lib/components/shared/TableOfContents.tsx +280 -0
  137. package/src/lib/components/shared/ThreeColumnRenderer.tsx +54 -0
  138. package/src/lib/components/shared/TranslationHeader.tsx +86 -0
  139. package/src/lib/components/shared/TranslationHoverCard.tsx +84 -0
  140. package/src/lib/components/shared/TranslationSkeleton.tsx +16 -0
  141. package/src/lib/components/shared/TranslationTable.tsx +155 -0
  142. package/src/lib/components/shared/bibliography/BibliographyBody.tsx +28 -0
  143. package/src/lib/components/shared/bibliography/BibliographyList.tsx +63 -0
  144. package/src/lib/components/shared/bibliography/index.ts +1 -0
  145. package/src/lib/components/shared/generate-metadata.ts +44 -0
  146. package/src/lib/components/shared/glossary/GlossaryInstanceBody.tsx +144 -0
  147. package/src/lib/components/shared/glossary/GlossaryPaginationProvider.tsx +317 -0
  148. package/src/lib/components/shared/glossary/GlossarySkeleton.tsx +19 -0
  149. package/src/lib/components/shared/glossary/GlossaryTermList.tsx +58 -0
  150. package/src/lib/components/shared/glossary/index.ts +3 -0
  151. package/src/lib/components/shared/hooks/useGlossaryInstanceListener.tsx +42 -0
  152. package/src/lib/components/shared/hooks/useScrollInTab.tsx +43 -0
  153. package/src/lib/components/shared/hooks/useScrollPositionRestore.ts +274 -0
  154. package/src/lib/components/shared/hooks/useTohToggle.tsx +52 -0
  155. package/src/lib/components/shared/index.ts +11 -0
  156. package/src/lib/components/shared/ssr.ts +2 -0
  157. package/src/lib/components/shared/titles/FramedCard.tsx +132 -0
  158. package/src/lib/components/shared/titles/LongTitle.tsx +20 -0
  159. package/src/lib/components/shared/titles/LongTitles.tsx +28 -0
  160. package/src/lib/components/shared/titles/Title.tsx +54 -0
  161. package/src/lib/components/shared/titles/TitleDetails.tsx +47 -0
  162. package/src/lib/components/shared/titles/TitleForm.tsx +37 -0
  163. package/src/lib/components/shared/titles/Titles.tsx +114 -0
  164. package/src/lib/components/shared/titles/TitlesCard.tsx +113 -0
  165. package/src/lib/components/shared/titles/index.ts +8 -0
  166. package/src/lib/components/shared/types.ts +79 -0
  167. package/src/lib/components/ssr.ts +2 -0
  168. package/src/lib/exporters/abbreviation.spec.ts +31 -0
  169. package/src/lib/exporters/abbreviation.ts +22 -0
  170. package/src/lib/exporters/annotation.ts +193 -0
  171. package/src/lib/exporters/audio.spec.ts +77 -0
  172. package/src/lib/exporters/audio.ts +27 -0
  173. package/src/lib/exporters/blockquote.spec.ts +48 -0
  174. package/src/lib/exporters/blockquote.ts +24 -0
  175. package/src/lib/exporters/code.spec.ts +93 -0
  176. package/src/lib/exporters/code.ts +26 -0
  177. package/src/lib/exporters/end-note-link.spec.ts +104 -0
  178. package/src/lib/exporters/end-note-link.ts +35 -0
  179. package/src/lib/exporters/export.ts +12 -0
  180. package/src/lib/exporters/glossary-instance.spec.ts +85 -0
  181. package/src/lib/exporters/glossary-instance.ts +31 -0
  182. package/src/lib/exporters/has-abbreviation.spec.ts +31 -0
  183. package/src/lib/exporters/has-abbreviation.ts +21 -0
  184. package/src/lib/exporters/heading.spec.ts +80 -0
  185. package/src/lib/exporters/heading.ts +28 -0
  186. package/src/lib/exporters/image.spec.ts +48 -0
  187. package/src/lib/exporters/image.ts +25 -0
  188. package/src/lib/exporters/indent.spec.ts +58 -0
  189. package/src/lib/exporters/indent.ts +18 -0
  190. package/src/lib/exporters/index.ts +1 -0
  191. package/src/lib/exporters/internal-link.spec.ts +90 -0
  192. package/src/lib/exporters/internal-link.ts +35 -0
  193. package/src/lib/exporters/italic.spec.ts +84 -0
  194. package/src/lib/exporters/italic.ts +55 -0
  195. package/src/lib/exporters/leading-space.spec.ts +28 -0
  196. package/src/lib/exporters/leading-space.ts +16 -0
  197. package/src/lib/exporters/line-group.spec.ts +48 -0
  198. package/src/lib/exporters/line-group.ts +24 -0
  199. package/src/lib/exporters/line.spec.ts +48 -0
  200. package/src/lib/exporters/line.ts +24 -0
  201. package/src/lib/exporters/link.spec.ts +123 -0
  202. package/src/lib/exporters/link.ts +67 -0
  203. package/src/lib/exporters/list-item.spec.ts +48 -0
  204. package/src/lib/exporters/list-item.ts +24 -0
  205. package/src/lib/exporters/list.spec.ts +82 -0
  206. package/src/lib/exporters/list.ts +31 -0
  207. package/src/lib/exporters/mantra.spec.ts +51 -0
  208. package/src/lib/exporters/mantra.ts +25 -0
  209. package/src/lib/exporters/mention.ts +41 -0
  210. package/src/lib/exporters/paragraph.spec.ts +173 -0
  211. package/src/lib/exporters/paragraph.ts +32 -0
  212. package/src/lib/exporters/quote.spec.ts +56 -0
  213. package/src/lib/exporters/quote.ts +25 -0
  214. package/src/lib/exporters/span.spec.ts +118 -0
  215. package/src/lib/exporters/span.ts +44 -0
  216. package/src/lib/exporters/table-body-data.spec.ts +48 -0
  217. package/src/lib/exporters/table-body-data.ts +24 -0
  218. package/src/lib/exporters/table-body-header.spec.ts +48 -0
  219. package/src/lib/exporters/table-body-header.ts +24 -0
  220. package/src/lib/exporters/table-body-row.spec.ts +48 -0
  221. package/src/lib/exporters/table-body-row.ts +24 -0
  222. package/src/lib/exporters/table.spec.ts +48 -0
  223. package/src/lib/exporters/table.ts +24 -0
  224. package/src/lib/exporters/trailer.spec.ts +48 -0
  225. package/src/lib/exporters/trailer.ts +24 -0
  226. package/src/lib/exporters/util.ts +62 -0
  227. package/src/lib/passage.ts +182 -0
  228. package/src/lib/titles.ts +80 -0
  229. package/src/lib/transformers/abbreviation.spec.ts +87 -0
  230. package/src/lib/transformers/abbreviation.ts +30 -0
  231. package/src/lib/transformers/annotate.ts +146 -0
  232. package/src/lib/transformers/audio.spec.ts +55 -0
  233. package/src/lib/transformers/audio.ts +29 -0
  234. package/src/lib/transformers/blockquote.spec.ts +48 -0
  235. package/src/lib/transformers/blockquote.ts +41 -0
  236. package/src/lib/transformers/code.spec.ts +52 -0
  237. package/src/lib/transformers/code.ts +22 -0
  238. package/src/lib/transformers/deprecated.ts +7 -0
  239. package/src/lib/transformers/end-note-link.spec.ts +56 -0
  240. package/src/lib/transformers/end-note-link.ts +76 -0
  241. package/src/lib/transformers/glossary-instance.spec.ts +55 -0
  242. package/src/lib/transformers/glossary-instance.ts +40 -0
  243. package/src/lib/transformers/has-abbreviation.spec.ts +50 -0
  244. package/src/lib/transformers/has-abbreviation.ts +30 -0
  245. package/src/lib/transformers/heading.spec.ts +62 -0
  246. package/src/lib/transformers/heading.ts +30 -0
  247. package/src/lib/transformers/image.spec.ts +51 -0
  248. package/src/lib/transformers/image.ts +28 -0
  249. package/src/lib/transformers/indent.spec.ts +53 -0
  250. package/src/lib/transformers/indent.ts +17 -0
  251. package/src/lib/transformers/index.ts +33 -0
  252. package/src/lib/transformers/inline-title.spec.ts +59 -0
  253. package/src/lib/transformers/inline-title.ts +34 -0
  254. package/src/lib/transformers/internal-link.spec.ts +67 -0
  255. package/src/lib/transformers/internal-link.ts +65 -0
  256. package/src/lib/transformers/italic.ts +22 -0
  257. package/src/lib/transformers/leading-space.spec.ts +55 -0
  258. package/src/lib/transformers/leading-space.ts +17 -0
  259. package/src/lib/transformers/line-group.spec.ts +48 -0
  260. package/src/lib/transformers/line-group.ts +37 -0
  261. package/src/lib/transformers/line.spec.ts +54 -0
  262. package/src/lib/transformers/line.ts +27 -0
  263. package/src/lib/transformers/link.spec.ts +61 -0
  264. package/src/lib/transformers/link.ts +27 -0
  265. package/src/lib/transformers/list-item.spec.ts +48 -0
  266. package/src/lib/transformers/list-item.ts +37 -0
  267. package/src/lib/transformers/list.spec.ts +58 -0
  268. package/src/lib/transformers/list.ts +42 -0
  269. package/src/lib/transformers/mantra.spec.ts +58 -0
  270. package/src/lib/transformers/mantra.ts +28 -0
  271. package/src/lib/transformers/mention.ts +70 -0
  272. package/src/lib/transformers/paragraph.spec.ts +46 -0
  273. package/src/lib/transformers/paragraph.ts +26 -0
  274. package/src/lib/transformers/quote.spec.ts +42 -0
  275. package/src/lib/transformers/quote.ts +3 -0
  276. package/src/lib/transformers/quoted.ts +3 -0
  277. package/src/lib/transformers/recurse.ts +76 -0
  278. package/src/lib/transformers/reference.ts +3 -0
  279. package/src/lib/transformers/span.spec.ts +68 -0
  280. package/src/lib/transformers/span.ts +78 -0
  281. package/src/lib/transformers/split-at.ts +58 -0
  282. package/src/lib/transformers/split-block.ts +110 -0
  283. package/src/lib/transformers/split-content.ts +67 -0
  284. package/src/lib/transformers/split-insert.ts +76 -0
  285. package/src/lib/transformers/split-marks.ts +42 -0
  286. package/src/lib/transformers/split-node.ts +138 -0
  287. package/src/lib/transformers/table-body-data.spec.ts +44 -0
  288. package/src/lib/transformers/table-body-data.ts +29 -0
  289. package/src/lib/transformers/table-body-header.spec.ts +44 -0
  290. package/src/lib/transformers/table-body-header.ts +29 -0
  291. package/src/lib/transformers/table-body-row.spec.ts +44 -0
  292. package/src/lib/transformers/table-body-row.ts +29 -0
  293. package/src/lib/transformers/table.spec.ts +47 -0
  294. package/src/lib/transformers/table.ts +29 -0
  295. package/src/lib/transformers/trailer.spec.ts +43 -0
  296. package/src/lib/transformers/trailer.ts +26 -0
  297. package/src/lib/transformers/transformer.ts +25 -0
  298. package/src/lib/transformers/unknown.ts +8 -0
  299. package/src/lib/transformers/util.ts +20 -0
  300. package/src/lib/types.ts +10 -0
  301. package/src/ssr.ts +1 -0
  302. package/tsconfig.json +20 -0
  303. package/tsconfig.lib.json +29 -0
  304. package/tsconfig.spec.json +22 -0
@@ -0,0 +1,368 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createGraphQLClient,
5
+ getBibliographyEntry,
6
+ getGlossaryInstance,
7
+ getPassage,
8
+ getTranslationImprint,
9
+ getTranslationMetadataByUuid,
10
+ } from '@eightyfourthousand/client-graphql';
11
+ import type {
12
+ BibliographyEntryItem,
13
+ GlossaryTermInstance,
14
+ Imprint,
15
+ Passage,
16
+ TohokuCatalogEntry,
17
+ Work,
18
+ } from '@eightyfourthousand/data-access';
19
+ import {
20
+ ReactNode,
21
+ useCallback,
22
+ useEffect,
23
+ useMemo,
24
+ useRef,
25
+ useState,
26
+ } from 'react';
27
+ import { ReadonlyURLSearchParams, useSearchParams } from 'next/navigation';
28
+ import {
29
+ PANEL_NAMES,
30
+ PanelName,
31
+ PanelsState,
32
+ PanelState,
33
+ TabName,
34
+ } from './types';
35
+ import { HoverCardProvider } from './HoverCardProvider';
36
+ import { GatedFeature, useFeatureFlagEnabled } from '@eightyfourthousand/lib-instr';
37
+ import { useIsMobile } from '@eightyfourthousand/lib-utils';
38
+ import { RestrictionWarning } from './RestrictionWarning';
39
+ import { NavigationContext, DEFAULT_PANELS } from './NavigationContext';
40
+
41
+ export { NavigationContext, useNavigation } from './NavigationContext';
42
+ export type { NavigationState } from './NavigationContext';
43
+
44
+ const parsePanelParams = (
45
+ params: ReadonlyURLSearchParams,
46
+ ): {
47
+ toh?: TohokuCatalogEntry;
48
+ panels: PanelsState;
49
+ } => {
50
+ const panels: PanelsState = { ...DEFAULT_PANELS };
51
+
52
+ for (const [key, value] of params.entries()) {
53
+ const match = value.match(/^(open|closed)(?::(.+))?$/);
54
+ if (match) {
55
+ const [state, tab, hash] = value.split(':');
56
+ const panelKey = key as PanelName;
57
+ if (!PANEL_NAMES.includes(panelKey)) {
58
+ continue;
59
+ }
60
+ panels[panelKey] = {
61
+ open: state === 'open',
62
+ tab: tab as TabName | undefined,
63
+ hash: hash || undefined,
64
+ };
65
+ }
66
+ }
67
+
68
+ const toh = (params.get('toh') as TohokuCatalogEntry) || undefined;
69
+
70
+ return { toh, panels };
71
+ };
72
+
73
+ export const NavigationProvider = ({
74
+ uuid,
75
+ children,
76
+ }: {
77
+ uuid: string;
78
+ children: ReactNode;
79
+ }) => {
80
+ const graphqlClient = createGraphQLClient();
81
+ const query = useSearchParams();
82
+ const isMobile = useIsMobile();
83
+ const [panels, setPanels] = useState<PanelsState>(
84
+ parsePanelParams(query).panels || DEFAULT_PANELS,
85
+ );
86
+ const isPanelTransitioning = useRef(false);
87
+ const [toh, setToh] = useState<TohokuCatalogEntry | undefined>();
88
+ const [showOuterContent, setShowOuterContent] = useState(true);
89
+ const [hasTranslationContent, setHasTranslationContent] = useState(true);
90
+ const [imprint, setImprint] = useState<Imprint | undefined>();
91
+ const bibliographyCache = useRef<{ [uuid: string]: BibliographyEntryItem }>(
92
+ {},
93
+ );
94
+ const endnoteCache = useRef<{ [uuid: string]: Passage }>({});
95
+ const glossaryCache = useRef<{ [uuid: string]: GlossaryTermInstance }>({});
96
+ const passageCache = useRef<{ [uuid: string]: Passage }>({});
97
+ const workCache = useRef<{ [uuid: string]: Work }>({});
98
+
99
+ const fetchBibliographyEntry = useCallback(
100
+ async (uuid: string): Promise<BibliographyEntryItem | undefined> => {
101
+ if (!bibliographyCache.current) {
102
+ bibliographyCache.current = {};
103
+ }
104
+
105
+ if (bibliographyCache.current[uuid]) {
106
+ return bibliographyCache.current[uuid];
107
+ }
108
+
109
+ const entry = await getBibliographyEntry({
110
+ client: graphqlClient,
111
+ uuid,
112
+ });
113
+ if (!entry) {
114
+ return undefined;
115
+ }
116
+
117
+ bibliographyCache.current[uuid] = entry;
118
+ return entry;
119
+ },
120
+ [graphqlClient],
121
+ );
122
+
123
+ const fetchEndNote = useCallback(
124
+ async (uuid: string): Promise<Passage | undefined> => {
125
+ if (!endnoteCache.current) {
126
+ endnoteCache.current = {};
127
+ }
128
+
129
+ if (endnoteCache.current[uuid]) {
130
+ return endnoteCache.current[uuid];
131
+ }
132
+
133
+ const endnote = await getPassage({ client: graphqlClient, uuid });
134
+ if (!endnote) {
135
+ return undefined;
136
+ }
137
+
138
+ endnoteCache.current[uuid] = endnote;
139
+ return endnote;
140
+ },
141
+ [graphqlClient],
142
+ );
143
+
144
+ const fetchGlossaryTerm = useCallback(
145
+ async (uuid: string) => {
146
+ if (!glossaryCache.current) {
147
+ glossaryCache.current = {};
148
+ }
149
+
150
+ if (glossaryCache.current[uuid]) {
151
+ return glossaryCache.current[uuid];
152
+ }
153
+
154
+ const term = await getGlossaryInstance({ client: graphqlClient, uuid });
155
+ if (!term) {
156
+ return undefined;
157
+ }
158
+
159
+ glossaryCache.current[uuid] = term;
160
+ return term;
161
+ },
162
+ [graphqlClient],
163
+ );
164
+
165
+ const fetchPassage = useCallback(
166
+ async (uuid: string): Promise<Passage | undefined> => {
167
+ if (!passageCache.current) {
168
+ passageCache.current = {};
169
+ }
170
+
171
+ if (passageCache.current[uuid]) {
172
+ return passageCache.current[uuid];
173
+ }
174
+
175
+ const passage = await getPassage({ client: graphqlClient, uuid });
176
+ if (!passage) {
177
+ return undefined;
178
+ }
179
+
180
+ passageCache.current[uuid] = passage;
181
+ return passage;
182
+ },
183
+ [graphqlClient],
184
+ );
185
+
186
+ const fetchWork = useCallback(
187
+ async (uuid: string): Promise<Work | undefined> => {
188
+ if (!workCache.current) {
189
+ workCache.current = {};
190
+ }
191
+
192
+ if (workCache.current[uuid]) {
193
+ return workCache.current[uuid];
194
+ }
195
+
196
+ const work = await getTranslationMetadataByUuid({
197
+ client: graphqlClient,
198
+ uuid,
199
+ });
200
+ if (!work) {
201
+ return undefined;
202
+ }
203
+
204
+ workCache.current[uuid] = work;
205
+ return work;
206
+ },
207
+ [graphqlClient],
208
+ );
209
+
210
+ const updatePanel = useCallback(
211
+ ({ name, state }: { name: PanelName; state: PanelState }) => {
212
+ const { open } = state;
213
+ isPanelTransitioning.current = true;
214
+ setPanels((prev) => {
215
+ const newPanels = {
216
+ ...prev,
217
+ [name]: state,
218
+ };
219
+
220
+ // On mobile, auto-close sidebars when navigating to other panels
221
+ if (isMobile && open) {
222
+ // If opening left panel with navigation, close right panel
223
+ if (name === 'left') {
224
+ newPanels.right = { ...prev.right, open: false };
225
+ }
226
+ // If opening right panel with navigation, close left panel
227
+ else if (name === 'right') {
228
+ newPanels.left = { ...prev.left, open: false };
229
+ }
230
+ // If opening main panel with navigation, close both sidebars
231
+ else if (name === 'main') {
232
+ newPanels.left = { ...prev.left, open: false };
233
+ newPanels.right = { ...prev.right, open: false };
234
+ }
235
+ }
236
+
237
+ return newPanels;
238
+ });
239
+ },
240
+ [isMobile, hasTranslationContent],
241
+ );
242
+
243
+ useEffect(() => {
244
+ if (!toh && !panels) {
245
+ return;
246
+ }
247
+
248
+ isPanelTransitioning.current = true;
249
+ const params = new URLSearchParams(window.location.search);
250
+
251
+ if (toh) {
252
+ params.set('toh', toh);
253
+ }
254
+
255
+ if (panels) {
256
+ Object.entries(panels).forEach(([panelName, panelState]) => {
257
+ const { open, tab, hash } = panelState;
258
+ const openness = open ? 'open' : 'closed';
259
+ params.set(
260
+ panelName,
261
+ `${openness}${tab ? `:${tab}` : ''}${hash ? `:${hash}` : ''}`,
262
+ );
263
+ });
264
+ }
265
+
266
+ const newUrl = `?${params.toString()}${window.location.hash}`;
267
+ window.history.replaceState(null, '', newUrl);
268
+ }, [toh, panels]);
269
+
270
+ useEffect(() => {
271
+ if (!uuid || !toh) {
272
+ return;
273
+ }
274
+
275
+ (async () => {
276
+ const imprint = await getTranslationImprint({
277
+ client: graphqlClient,
278
+ uuid,
279
+ toh,
280
+ });
281
+ setImprint(imprint);
282
+ })();
283
+ }, [uuid, toh, graphqlClient]);
284
+
285
+ useEffect(() => {
286
+ if (isPanelTransitioning.current) {
287
+ isPanelTransitioning.current = false;
288
+ return;
289
+ }
290
+
291
+ const { panels: newPanels, toh: newToh } = parsePanelParams(query);
292
+
293
+ if (newPanels) {
294
+ setPanels(newPanels);
295
+ }
296
+
297
+ if (newToh) {
298
+ setToh(newToh);
299
+ }
300
+ }, [query, parsePanelParams]);
301
+
302
+ const hasHoverCards = useFeatureFlagEnabled('translation-hover-cards');
303
+
304
+ useEffect(() => {
305
+ if (!hasTranslationContent) {
306
+ setPanels((prev) => {
307
+ if (!prev.right.open) {
308
+ return prev;
309
+ }
310
+ return {
311
+ ...prev,
312
+ right: { ...prev.right, open: false },
313
+ };
314
+ });
315
+ }
316
+ }, [hasTranslationContent]);
317
+
318
+ const contextValue = useMemo(
319
+ () => ({
320
+ uuid,
321
+ imprint,
322
+ panels,
323
+ toh,
324
+ showOuterContent,
325
+ hasTranslationContent,
326
+ setToh,
327
+ setShowOuterContent,
328
+ setHasTranslationContent,
329
+ updatePanel,
330
+ fetchBibliographyEntry,
331
+ fetchEndNote,
332
+ fetchGlossaryTerm,
333
+ fetchPassage,
334
+ fetchWork,
335
+ }),
336
+ [
337
+ uuid,
338
+ imprint,
339
+ panels,
340
+ toh,
341
+ showOuterContent,
342
+ hasTranslationContent,
343
+ setToh,
344
+ setShowOuterContent,
345
+ setHasTranslationContent,
346
+ updatePanel,
347
+ fetchBibliographyEntry,
348
+ fetchEndNote,
349
+ fetchGlossaryTerm,
350
+ fetchPassage,
351
+ fetchWork,
352
+ ],
353
+ );
354
+
355
+ return (
356
+ <NavigationContext.Provider value={contextValue}>
357
+ {hasHoverCards ? (
358
+ <HoverCardProvider>{children}</HoverCardProvider>
359
+ ) : (
360
+ children
361
+ )}
362
+ <GatedFeature flag="show-restriction-warning">
363
+ <RestrictionWarning imprint={imprint} />
364
+ </GatedFeature>
365
+ </NavigationContext.Provider>
366
+ );
367
+ };
368
+
@@ -0,0 +1,75 @@
1
+ import { ImageResponse } from 'next/og';
2
+ import {
3
+ createServerGraphQLClient,
4
+ getTranslationMetadataByUuid,
5
+ } from '@eightyfourthousand/client-graphql/ssr';
6
+ import { MainLogoSvg } from '@eightyfourthousand/design-system/ssr';
7
+ import { isUuid, parseToh } from '@eightyfourthousand/lib-utils';
8
+ import { cache } from 'react';
9
+
10
+ export const runtime = 'nodejs';
11
+ export const alt = '84000 Translation';
12
+ export const size = { width: 1200, height: 630 };
13
+ export const contentType = 'image/png';
14
+
15
+ export const OpenGraphImage = async ({
16
+ params,
17
+ }: {
18
+ params: Promise<{ slug: string }>;
19
+ }) => {
20
+ const { slug } = await params;
21
+
22
+ // Only fetch work metadata for valid UUIDs
23
+ let work = null;
24
+ if (isUuid(slug)) {
25
+ const client = await createServerGraphQLClient();
26
+ work = await cache(getTranslationMetadataByUuid)({
27
+ client,
28
+ uuid: slug,
29
+ });
30
+ }
31
+
32
+ const title = work?.title || '84000 Translation';
33
+ const toh = work?.toh.map(parseToh).join(', ') || '';
34
+
35
+ return new ImageResponse(
36
+ <div
37
+ style={{
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ alignItems: 'center',
41
+ justifyContent: 'center',
42
+ width: '100%',
43
+ height: '100%',
44
+ backgroundColor: '#004570',
45
+ padding: '60px',
46
+ }}
47
+ >
48
+ <MainLogoSvg width={332} height={200} />
49
+ <div
50
+ style={{
51
+ color: 'white',
52
+ fontSize: 48,
53
+ fontWeight: 'bold',
54
+ textAlign: 'center',
55
+ marginTop: '40px',
56
+ maxWidth: '1000px',
57
+ }}
58
+ >
59
+ {title}
60
+ </div>
61
+ {toh && (
62
+ <div
63
+ style={{
64
+ color: '#F18903',
65
+ fontSize: 32,
66
+ marginTop: '20px',
67
+ }}
68
+ >
69
+ {toh}
70
+ </div>
71
+ )}
72
+ </div>,
73
+ { ...size },
74
+ );
75
+ };
@@ -0,0 +1,10 @@
1
+ import { Skeleton } from '@eightyfourthousand/design-system';
2
+
3
+ export const PassageSkeleton = () => {
4
+ return (
5
+ <div className="flex gap-5 -ms-5 @c/sidebar:-ms-8">
6
+ <Skeleton className="h-4 w-6" />
7
+ <Skeleton className="h-32 grow" />
8
+ </div>
9
+ );
10
+ };
@@ -0,0 +1,177 @@
1
+ 'use client';
2
+
3
+ import { Imprint } from '@eightyfourthousand/data-access';
4
+ import {
5
+ Accordion,
6
+ AccordionContent,
7
+ AccordionItem,
8
+ AccordionTrigger,
9
+ Button,
10
+ Dialog,
11
+ DialogClose,
12
+ DialogContent,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from '@eightyfourthousand/design-system';
17
+ import { CircleAlertIcon, XIcon } from 'lucide-react';
18
+ import { useEffect, useState } from 'react';
19
+
20
+ const RESTRICTION_COOKIE_NAME = 'eft-restriction-warning-ignored';
21
+
22
+ export const getCookie = (name: string): string | undefined => {
23
+ const cookieValue = document.cookie
24
+ .split('; ')
25
+ .find((row) => row.startsWith(`${name}=`))
26
+ ?.split('=')[1];
27
+
28
+ return decodeURIComponent(cookieValue || '');
29
+ };
30
+
31
+ export const setCookie = (name: string, value: string, days = 365) => {
32
+ const date = new Date();
33
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
34
+ const expires = `expires=${date.toUTCString()}`;
35
+ document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}; path=/`;
36
+ };
37
+
38
+ export const useRestrictionWarning = ({ imprint }: { imprint?: Imprint }) => {
39
+ const [isOpen, setIsOpen] = useState(false);
40
+ const [shouldIgnore, setShouldIgnore] = useState(true);
41
+ const [toIgnore, setToIgnore] = useState<string[]>([]);
42
+
43
+ useEffect(() => {
44
+ const toIgnoreStr = getCookie(RESTRICTION_COOKIE_NAME) || '[]';
45
+ const toIgnore = JSON.parse(toIgnoreStr) || [];
46
+ setToIgnore(toIgnore);
47
+ }, []);
48
+
49
+ useEffect(() => {
50
+ if (!imprint?.uuid) {
51
+ return;
52
+ }
53
+
54
+ setShouldIgnore(toIgnore.includes(imprint.uuid));
55
+ }, [toIgnore, imprint]);
56
+
57
+ useEffect(() => {
58
+ if (!shouldIgnore && imprint?.restriction) {
59
+ setIsOpen(true);
60
+ }
61
+ }, [shouldIgnore, imprint]);
62
+
63
+ const ignore = () => {
64
+ if (!imprint?.uuid) {
65
+ return;
66
+ }
67
+
68
+ const newToIgnore = [...toIgnore, imprint.uuid];
69
+ setToIgnore(newToIgnore);
70
+ setCookie(RESTRICTION_COOKIE_NAME, JSON.stringify(newToIgnore));
71
+ };
72
+
73
+ return {
74
+ isOpen,
75
+ setIsOpen,
76
+ ignore,
77
+ };
78
+ };
79
+
80
+ export const RestrictionWarning = ({ imprint }: { imprint?: Imprint }) => {
81
+ const { isOpen, setIsOpen, ignore } = useRestrictionWarning({ imprint });
82
+ return (
83
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
84
+ <DialogContent
85
+ className="max-w-readable w-full font-serif"
86
+ showCloseButton={false}
87
+ >
88
+ <DialogHeader>
89
+ <div className="flex justify-between pb-4 text-destructive">
90
+ <CircleAlertIcon className="my-auto" />
91
+ <DialogTitle className="my-auto text-2xl">
92
+ Tantra Text Warning
93
+ </DialogTitle>
94
+ <DialogClose className="my-auto">
95
+ <XIcon />
96
+ </DialogClose>
97
+ </div>
98
+ </DialogHeader>
99
+ <p>
100
+ Readers are reminded that according to Vajrayāna Buddhist tradition
101
+ there are restrictions and commitments concerning tantra.
102
+ </p>
103
+ <p>
104
+ Practitioners who are not sure if they should read translations in
105
+ this section are advised to consult the authorities of their lineage.
106
+ </p>
107
+ <p>
108
+ The responsibility for reading these texts or sharing them with
109
+ others—and hence the consequences—lies in the hands of readers.
110
+ </p>
111
+ <Accordion type="single" collapsible>
112
+ <AccordionItem value="more-info">
113
+ <AccordionTrigger className="text-base">
114
+ About unrestricted access
115
+ </AccordionTrigger>
116
+ <AccordionContent className="space-y-2 leading-6">
117
+ <p>
118
+ The decision to publish tantra texts without restricted access
119
+ has been considered carefully. First of all, it should be noted
120
+ that all the original Tibetan texts of the Kangyur, including
121
+ those in this Tantra section, are in the public domain. Some of
122
+ the texts in this section (but by no means all of them) are
123
+ nevertheless, according to some traditions, only studied with
124
+ authorization and after suitable preliminaries.
125
+ </p>
126
+ <p>
127
+ It is true, of course, that a translation makes the content
128
+ accessible to a far greater number of people; 84000 has
129
+ therefore consulted many senior Buddhist teachers on this
130
+ question, and most of them felt that to publish the texts openly
131
+ is, on balance, the best solution. The alternatives would be not
132
+ to translate them at all (which would defeat the purposes of the
133
+ whole project), or to place some sort of restriction on their
134
+ access. Restricted access has been tried by some Buddhist book
135
+ publishers, and of course needs a system of administration,
136
+ judgment, and policing that is either a mere formality, or is
137
+ very difficult to implement. It would be even harder to
138
+ implement in the case of electronic texts—and even easier to
139
+ circumvent. Indeed, nowadays practically the whole range of
140
+ traditionally restricted Tibetan Buddhist material is already
141
+ available to anyone who looks for it, and is all too often
142
+ misrepresented, taken out of context, or its secret and esoteric
143
+ nature deliberately vaunted.
144
+ </p>
145
+ <p>
146
+ 84000’s policy is to present carefully authenticated
147
+ translations in their proper setting of the whole body of
148
+ Buddhist sacred literature, and to trust the good sense of the
149
+ vast majority of readers not to misuse or misunderstand them.
150
+ Readers are reminded that according to Vajrayāna Buddhist
151
+ tradition there are restrictions and commitments concerning
152
+ tantra. Practitioners who are not sure if they should read
153
+ translations in this section are advised to consult the
154
+ authorities of their lineage. The responsibility, and hence
155
+ consequences, of reading these texts and/or sharing them with
156
+ others who may or may not fulfill the requirements lie in the
157
+ hands of readers.
158
+ </p>
159
+ </AccordionContent>
160
+ </AccordionItem>
161
+ </Accordion>
162
+ <DialogFooter>
163
+ <Button
164
+ className="font-sans"
165
+ variant="destructive"
166
+ onClick={() => {
167
+ setIsOpen(false);
168
+ ignore();
169
+ }}
170
+ >
171
+ Don't show again for this text
172
+ </Button>
173
+ </DialogFooter>
174
+ </DialogContent>
175
+ </Dialog>
176
+ );
177
+ };
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import { useInView } from 'motion/react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { useNavigation } from './NavigationProvider';
6
+ import { createGraphQLClient, getWorkFolios } from '@eightyfourthousand/client-graphql';
7
+ import type { Folio } from '@eightyfourthousand/data-access';
8
+ import { LabeledElement } from './LabeledElement';
9
+ import { PassageSkeleton } from './PassageSkeleton';
10
+ import { LotusPond } from '@eightyfourthousand/design-system';
11
+
12
+ const PAGE_SIZE = 10;
13
+
14
+ export const SourceReader = () => {
15
+ const [page, setPage] = useState(0);
16
+ const [hasMore, setHasMore] = useState(true);
17
+ const [folios, setFolios] = useState<Folio[]>([]);
18
+
19
+ const { toh, uuid } = useNavigation();
20
+ const loadMoreRef = useRef<HTMLDivElement>(null);
21
+ const shouldLoadMore = useInView(loadMoreRef);
22
+ const graphqlClient = createGraphQLClient();
23
+
24
+ const fetchMore = useCallback(async () => {
25
+ if (!toh || !uuid) return;
26
+
27
+ const folios = await getWorkFolios({
28
+ client: graphqlClient,
29
+ uuid,
30
+ toh,
31
+ page,
32
+ size: PAGE_SIZE,
33
+ });
34
+ if (folios.length) {
35
+ setPage((prevPage) => prevPage + 1);
36
+ setFolios((prevFolios) => [...prevFolios, ...folios]);
37
+ setHasMore(folios.length >= PAGE_SIZE);
38
+ }
39
+ }, [toh, uuid, page, graphqlClient]);
40
+
41
+ useEffect(() => {
42
+ if (shouldLoadMore && hasMore) {
43
+ fetchMore();
44
+ }
45
+ }, [shouldLoadMore, hasMore, fetchMore]);
46
+
47
+ useEffect(() => {
48
+ // Reset when uuid or toh changes
49
+ setFolios([]);
50
+ setPage(0);
51
+ setHasMore(true);
52
+ }, [toh, uuid]);
53
+
54
+ return (
55
+ <div className="pt-12 flex flex-col gap-5 mx-auto max-w-readable 2xl:max-w-380">
56
+ {folios.map((folio, index) => (
57
+ <LabeledElement
58
+ key={index}
59
+ id={folio.uuid}
60
+ label={`f.${folio.folio}.${folio.side}\nvol.${folio.volume}`}
61
+ className="mt-0.5"
62
+ contentType="source"
63
+ >
64
+ <div className="leading-7 font-tibetan text-lg 2xl:whitespace-pre-wrap whitespace-normal">
65
+ {folio.content}
66
+ </div>
67
+ </LabeledElement>
68
+ ))}
69
+ <div ref={loadMoreRef} className="h-0" />
70
+ {hasMore ? (
71
+ <>
72
+ {Array.from({ length: 3 }).map((_, i) => (
73
+ <PassageSkeleton key={i} />
74
+ ))}
75
+ </>
76
+ ) : (
77
+ <div className="w-full pt-16 pb-6">
78
+ <LotusPond className="mx-auto w-96" />
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ };