@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,238 @@
1
+ import { cn } from '@eightyfourthousand/lib-utils';
2
+ import {
3
+ NodeViewContent,
4
+ NodeViewWrapper,
5
+ type NodeViewProps,
6
+ } from '@tiptap/react';
7
+ import {
8
+ Button,
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuTrigger,
18
+ } from '@eightyfourthousand/design-system';
19
+ import { EditorOptions } from './EditorOptions';
20
+ import { ReaderOptions } from './ReaderOptions';
21
+ import { memo, useCallback, useMemo, useState } from 'react';
22
+ import { EditLabel } from './EditLabel';
23
+ import { ShowAnnotations } from './ShowAnnotations';
24
+ import {
25
+ LabeledElement,
26
+ useNavigation,
27
+ SuggestRevisionForm,
28
+ PANEL_FOR_SECTION,
29
+ TAB_FOR_SECTION,
30
+ } from '../../../shared';
31
+ import { Alignment, useBookmark } from '@eightyfourthousand/data-access';
32
+ import { BookmarkIcon } from 'lucide-react';
33
+ import { deleteEndnotePassageNode } from '../EndNoteLink/endnote-utils';
34
+
35
+ const PassageComponent = (props: NodeViewProps) => {
36
+ const { node, editor } = props;
37
+
38
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
39
+ const [dialogType, setDialogType] = useState<string>();
40
+ const [showRevisionForm, setShowRevisionForm] = useState(false);
41
+ const [dropdownOpen, setDropdownOpen] = useState(false);
42
+
43
+ const { isBookmarked, toggle: toggleBookmark } = useBookmark(
44
+ node.attrs.uuid,
45
+ { type: 'passage', subType: node.attrs.type, tab: node.attrs.type ?? '' },
46
+ );
47
+
48
+ const { panels, toh, updatePanel } = useNavigation();
49
+
50
+ // Compute values directly instead of using effects to avoid re-render loops
51
+ const isCompare = panels.main.open && panels.main.tab === 'compare';
52
+
53
+ const { source, compareLeadingSpace } = useMemo(() => {
54
+ if (!isCompare || !toh) {
55
+ return { source: '', compareLeadingSpace: 'md:mt-1' };
56
+ }
57
+
58
+ const alignment = node.attrs.alignments?.[toh] as Alignment;
59
+ const source = alignment?.tibetan || '';
60
+
61
+ const firstChild = node.content.firstChild;
62
+ let compareLeadingSpace = 'md:mt-1';
63
+ if (firstChild?.attrs.hasLeadingSpace) {
64
+ compareLeadingSpace = 'md:mt-5';
65
+ } else if (['lineGroup', 'list'].includes(firstChild?.type.name || '')) {
66
+ compareLeadingSpace = 'md:mt-2';
67
+ }
68
+
69
+ return { source, compareLeadingSpace };
70
+ }, [isCompare, toh, node.attrs.alignments, node.content.firstChild]);
71
+
72
+ const references = node.attrs.references as
73
+ | { uuid: string; label: string | null; sort: number; type: string }[]
74
+ | undefined;
75
+
76
+ const handleReferenceClick = useCallback(
77
+ (passage: { uuid: string; type: string }) => {
78
+ updatePanel({
79
+ name: PANEL_FOR_SECTION[passage.type] || 'main',
80
+ state: {
81
+ open: true,
82
+ tab: TAB_FOR_SECTION[passage.type] || 'translation',
83
+ hash: passage.uuid,
84
+ },
85
+ });
86
+ },
87
+ [updatePanel],
88
+ );
89
+
90
+ const handleDeletePassage = useCallback(() => {
91
+ deleteEndnotePassageNode(editor, node.attrs.uuid);
92
+ setIsDialogOpen(false);
93
+ }, [node.attrs.uuid, editor]);
94
+
95
+ const className =
96
+ 'absolute labeled -left-16 w-16 text-end hover:cursor-pointer -mt-0.25';
97
+ const borderClassName =
98
+ editor.storage.globalConfig.debug && node.attrs.invalid
99
+ ? 'after:content-["⚠️"] after:absolute after:top-0 after:-right-5'
100
+ : '';
101
+
102
+ return (
103
+ <NodeViewWrapper
104
+ id={node.attrs.uuid}
105
+ as="div"
106
+ className={cn(
107
+ 'flex md:flex-row flex-col w-full md:gap-10 gap-2 scroll-mt-20',
108
+ node.attrs.toh,
109
+ )}
110
+ >
111
+ <div className="w-full">
112
+ <div
113
+ className={cn(
114
+ 'relative scroll-m-20 w-full self-start',
115
+ borderClassName,
116
+ )}
117
+ >
118
+ <DropdownMenu
119
+ open={dropdownOpen}
120
+ onOpenChange={(open) => {
121
+ setDropdownOpen(open);
122
+ if (!open) setShowRevisionForm(false);
123
+ }}
124
+ >
125
+ {!editor.isEditable && isBookmarked && (
126
+ <div className="absolute -left-15.75 top-6 w-16 flex justify-end">
127
+ <BookmarkIcon
128
+ className="text-accent size-3"
129
+ fill="currentColor"
130
+ />
131
+ </div>
132
+ )}
133
+ <DropdownMenuTrigger className={className} contentEditable={false}>
134
+ {node.attrs.label || ''}
135
+ </DropdownMenuTrigger>
136
+ <DropdownMenuContent
137
+ align="start"
138
+ alignOffset={48}
139
+ className={cn(showRevisionForm ? 'w-80' : 'w-64')}
140
+ >
141
+ {showRevisionForm ? (
142
+ <SuggestRevisionForm
143
+ toh={toh ?? ''}
144
+ type={'passage'}
145
+ label={node.attrs.label ?? ''}
146
+ onClose={() => {
147
+ setShowRevisionForm(false);
148
+ setDropdownOpen(false);
149
+ }}
150
+ />
151
+ ) : editor.isEditable ? (
152
+ <EditorOptions
153
+ onSelection={(item) => {
154
+ setDialogType(item);
155
+ setIsDialogOpen(true);
156
+ }}
157
+ />
158
+ ) : (
159
+ <ReaderOptions
160
+ {...props}
161
+ contentType={source ? 'compare' : node.attrs.type}
162
+ isBookmarked={isBookmarked}
163
+ toggleBookmark={toggleBookmark}
164
+ onSuggestRevision={() => setShowRevisionForm(true)}
165
+ />
166
+ )}
167
+ </DropdownMenuContent>
168
+ </DropdownMenu>
169
+ <NodeViewContent className="passage is-editable pl-6 @c/sidebar:pl-4" />
170
+ {references && references.length > 0 && (
171
+ <div className="pl-6 @c/sidebar:pl-4 mt-1" contentEditable={false}>
172
+ {references.map((ref, index) => (
173
+ <span key={ref.uuid}>
174
+ {index > 0 && ', '}
175
+ <Button
176
+ variant="link"
177
+ className="p-0 h-6 font-normal hover:cursor-pointer"
178
+ onClick={() => handleReferenceClick(ref)}
179
+ >
180
+ {ref.label || ref.uuid.slice(0, 6)}
181
+ </Button>
182
+ </span>
183
+ ))}
184
+ </div>
185
+ )}
186
+ {editor.isEditable && (
187
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
188
+ {dialogType === 'label' && (
189
+ <EditLabel {...props} close={() => setIsDialogOpen(false)} />
190
+ )}
191
+ {dialogType === 'attributes' && <ShowAnnotations {...props} />}
192
+ {dialogType === 'delete' && (
193
+ <DialogContent
194
+ className="max-w-readable w-full font-serif"
195
+ showCloseButton={false}
196
+ >
197
+ <DialogHeader>
198
+ <DialogTitle>Delete Passage</DialogTitle>
199
+ <DialogDescription>
200
+ This will delete passage {node.attrs.label}
201
+ {node.attrs.type === 'endnotes' &&
202
+ ' and remove all links to it in the translation'}
203
+ . This action cannot be undone after saving.
204
+ </DialogDescription>
205
+ </DialogHeader>
206
+ <DialogFooter>
207
+ <Button
208
+ variant="outline"
209
+ onClick={() => setIsDialogOpen(false)}
210
+ >
211
+ Cancel
212
+ </Button>
213
+ <Button variant="destructive" onClick={handleDeletePassage}>
214
+ Delete
215
+ </Button>
216
+ </DialogFooter>
217
+ </DialogContent>
218
+ )}
219
+ </Dialog>
220
+ )}
221
+ </div>
222
+ </div>
223
+ <div
224
+ className={cn('w-full', source ? '' : 'hidden', compareLeadingSpace)}
225
+ >
226
+ <LabeledElement label={node.attrs.label} className="mt-0.5">
227
+ <div className="leading-7 font-tibetan text-lg whitespace-normal mt-1.5 pb-4 md:pb-2">
228
+ {source}
229
+ </div>
230
+ </LabeledElement>
231
+ </div>
232
+ </NodeViewWrapper>
233
+ );
234
+ };
235
+
236
+ export const Passage = memo(PassageComponent);
237
+
238
+ export default Passage;
@@ -0,0 +1,223 @@
1
+ import { Node } from '@tiptap/core';
2
+ import { ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react';
3
+ import { Passage } from './Passage';
4
+ import { Selection, TextSelection } from '@tiptap/pm/state';
5
+ import { ResolvedPos } from '@tiptap/pm/model';
6
+ import { incrementLabel } from './label';
7
+
8
+ declare module '@tiptap/core' {
9
+ interface Commands<ReturnType> {
10
+ passage: {
11
+ normalizeLabelsAfter: () => ReturnType;
12
+ splitPassage: () => ReturnType;
13
+ };
14
+ }
15
+ }
16
+
17
+ const currentPassageDepth = ($from: ResolvedPos) => {
18
+ let passageDepth = null;
19
+ for (let i = $from.depth; i >= 0; i--) {
20
+ if ($from.node(i).type.name === 'passage') {
21
+ passageDepth = i;
22
+ break;
23
+ }
24
+ }
25
+
26
+ return passageDepth;
27
+ };
28
+
29
+ export const PassageNode = Node.create({
30
+ name: 'passage',
31
+ group: 'block',
32
+ content: 'block+',
33
+ parseHTML() {
34
+ return [
35
+ {
36
+ tag: 'passage',
37
+ },
38
+ ];
39
+ },
40
+ addAttributes() {
41
+ return {
42
+ label: {
43
+ default: null,
44
+ parseHTML: (element) => element.getAttribute('label'),
45
+ renderHTML: (attributes) => {
46
+ return mergeAttributes(attributes, {
47
+ label: attributes.label,
48
+ });
49
+ },
50
+ },
51
+ sort: {
52
+ default: 0,
53
+ parseHTML: (element) => element.getAttribute('sort'),
54
+ renderHTML: (attributes) => {
55
+ return mergeAttributes(attributes, {
56
+ sort: attributes.sort,
57
+ });
58
+ },
59
+ },
60
+ alignments: {
61
+ default: {},
62
+ },
63
+ references: {
64
+ default: [],
65
+ },
66
+ };
67
+ },
68
+ renderHTML({ HTMLAttributes }) {
69
+ return ['passage', mergeAttributes(HTMLAttributes), 0];
70
+ },
71
+ addNodeView() {
72
+ return ReactNodeViewRenderer(Passage);
73
+ },
74
+ addCommands() {
75
+ return {
76
+ normalizeLabelsAfter:
77
+ () =>
78
+ ({ dispatch, tr }) => {
79
+ if (!dispatch) return true;
80
+
81
+ // Use tr.selection so this works correctly when chained after commands
82
+ // like joinBackward that have already modified the document.
83
+ const { $from } = tr.selection;
84
+ const passageDepth = currentPassageDepth($from);
85
+ if (passageDepth === null) return false;
86
+
87
+ const passagePos = $from.start(passageDepth) - 1;
88
+ const currentPassage = $from.node(passageDepth);
89
+ const currentLabel = currentPassage.attrs.label as string;
90
+ if (!currentLabel) return false;
91
+
92
+ const currentParts = currentLabel.split('.');
93
+ const numParts = currentParts.length;
94
+ const currentPrefixWithDot =
95
+ numParts > 1 ? currentParts.slice(0, -1).join('.') + '.' : '';
96
+
97
+ const parentDepth = passageDepth - 1;
98
+ if (parentDepth < 0) return false;
99
+
100
+ const parentNode = $from.node(parentDepth);
101
+ const startIndex = $from.index(parentDepth);
102
+
103
+ let expectedNext = incrementLabel(currentLabel);
104
+ let pos = passagePos + currentPassage.nodeSize;
105
+ let changed = false;
106
+
107
+ for (let i = startIndex + 1; i < parentNode.childCount; i++) {
108
+ const child = parentNode.child(i);
109
+
110
+ if (child.type.name !== 'passage') {
111
+ pos += child.nodeSize;
112
+ continue;
113
+ }
114
+
115
+ const targetLabel = child.attrs.label as string;
116
+ if (!targetLabel) {
117
+ pos += child.nodeSize;
118
+ continue;
119
+ }
120
+
121
+ const targetParts = targetLabel.split('.');
122
+ if (targetParts.length < numParts) break; // shallower → out of scope
123
+ if (targetParts.length > numParts) {
124
+ // deeper → skip sub-passage
125
+ pos += child.nodeSize;
126
+ continue;
127
+ }
128
+ if (!targetLabel.startsWith(currentPrefixWithDot)) break; // different prefix
129
+
130
+ if (targetLabel === expectedNext) break; // already contiguous
131
+
132
+ tr.setNodeMarkup(pos, null, { ...child.attrs, label: expectedNext });
133
+ expectedNext = incrementLabel(expectedNext);
134
+ changed = true;
135
+ pos += child.nodeSize;
136
+ }
137
+
138
+ if (changed) dispatch(tr);
139
+ return true;
140
+ },
141
+ splitPassage:
142
+ () =>
143
+ ({ state, dispatch }) => {
144
+ const { selection } = state;
145
+ const { $from } = selection;
146
+
147
+ // Find the passage node that contains the current selection
148
+ const passageDepth = currentPassageDepth($from);
149
+ if (passageDepth === null) {
150
+ return false;
151
+ }
152
+
153
+ const passageNode = $from.node(passageDepth);
154
+ const passageStart = $from.start(passageDepth) - 1; // -1 to include the passage node itself
155
+ const passageEnd = $from.end(passageDepth) + 1; // +1 to include the passage node itself
156
+
157
+ // Get the position within the passage content
158
+ const posInPassage = $from.pos - $from.start(passageDepth);
159
+
160
+ // Split the passage content
161
+ const beforeContent = passageNode.content.cut(0, posInPassage);
162
+ const afterContent = passageNode.content.cut(posInPassage);
163
+
164
+ if (!dispatch) return true;
165
+
166
+ const tr = state.tr;
167
+
168
+ // Replace current passage with first part
169
+ tr.replaceWith(
170
+ passageStart,
171
+ passageEnd,
172
+ state.schema.nodes.passage.create(passageNode.attrs, beforeContent),
173
+ );
174
+
175
+ // Calculate where to insert the new passage
176
+ const newPassagePos = passageStart + beforeContent.size + 2; // +2 for passage wrapper
177
+ const oldAttrs = passageNode.attrs;
178
+ const attrs = {
179
+ ...oldAttrs,
180
+ uuid: crypto.randomUUID(),
181
+ sort: oldAttrs.sort + 1,
182
+ label: incrementLabel(oldAttrs.label),
183
+ };
184
+ // Insert new passage with remaining content
185
+ tr.insert(
186
+ newPassagePos,
187
+ state.schema.nodes.passage.create(attrs, afterContent),
188
+ );
189
+
190
+ // move the cursor to the new paragraph
191
+ const $pos = tr.doc.resolve(newPassagePos + 1);
192
+ const newSelection =
193
+ TextSelection.findFrom($pos, 1, true) || Selection.near($pos, 1);
194
+ if (newSelection) {
195
+ tr.setSelection(newSelection);
196
+ }
197
+
198
+ dispatch(tr);
199
+ return true;
200
+ },
201
+ };
202
+ },
203
+ addKeyboardShortcuts() {
204
+ return {
205
+ Backspace: () =>
206
+ this.editor.commands.first(({ commands }) => [
207
+ () => commands.undoInputRule(),
208
+ () => {
209
+ // Only intercept at the very start of a passage's content
210
+ const { $from } = this.editor.state.selection;
211
+ const passageDepth = currentPassageDepth($from);
212
+ if (passageDepth === null) return false;
213
+ const isAtStart =
214
+ $from.parentOffset === 0 && $from.index(passageDepth) === 0;
215
+ if (!isAtStart) return false;
216
+ const joined = commands.joinBackward();
217
+ if (joined) commands.normalizeLabelsAfter();
218
+ return joined;
219
+ },
220
+ ]),
221
+ };
222
+ },
223
+ });
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { PanelContentType, urlForPanelContent } from '@eightyfourthousand/data-access';
4
+ import { DropdownMenuItem, DropdownMenuSeparator } from '@eightyfourthousand/design-system';
5
+ import { NodeViewProps } from '@tiptap/react';
6
+ import { BookmarkIcon, CopyIcon, MessageSquareIcon } from 'lucide-react';
7
+ import { useCallback } from 'react';
8
+
9
+ export const ReaderOptions = (
10
+ props: NodeViewProps & {
11
+ contentType: PanelContentType;
12
+ isBookmarked?: boolean;
13
+ toggleBookmark?: () => void;
14
+ onSuggestRevision?: () => void;
15
+ },
16
+ ) => {
17
+ const copyLink = useCallback(() => {
18
+ const hash = props.node.attrs.uuid;
19
+ const contentType = props.contentType;
20
+ const location = window.location;
21
+
22
+ const link = urlForPanelContent({
23
+ location,
24
+ hash,
25
+ contentType,
26
+ });
27
+
28
+ navigator.clipboard.writeText(link);
29
+ }, [props.node, props.contentType]);
30
+ return (
31
+ <>
32
+ <DropdownMenuItem onSelect={props.toggleBookmark}>
33
+ <BookmarkIcon className="text-ochre" />
34
+ {props.isBookmarked ? 'Remove Bookmark' : 'Add Bookmark'}
35
+ </DropdownMenuItem>
36
+ <DropdownMenuSeparator />
37
+ <DropdownMenuItem onSelect={copyLink}>
38
+ <CopyIcon className="text-ochre" /> Copy Link
39
+ </DropdownMenuItem>
40
+ {props.onSuggestRevision && (
41
+ <>
42
+ <DropdownMenuSeparator />
43
+ <DropdownMenuItem
44
+ onSelect={(e) => {
45
+ e.preventDefault();
46
+ props.onSuggestRevision?.();
47
+ }}
48
+ >
49
+ <MessageSquareIcon className="text-ochre" /> Suggest Revision
50
+ </DropdownMenuItem>
51
+ </>
52
+ )}
53
+ </>
54
+ );
55
+ };
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import {
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogTitle,
7
+ Skeleton,
8
+ ToggleGroup,
9
+ ToggleGroupItem,
10
+ } from '@eightyfourthousand/design-system';
11
+ import { NodeViewProps } from '@tiptap/react';
12
+ import { useNavigation } from '../../../shared';
13
+ import { useEffect, useState } from 'react';
14
+ import { Passage } from '@eightyfourthousand/data-access';
15
+
16
+ const CodeBlock = ({ code }: { code: string }) => {
17
+ return (
18
+ <div className="flex-1 overflow-auto rounded-md bg-muted p-4 font-mono text-sm">
19
+ <pre className="whitespace-pre-wrap">
20
+ <code className="language-json">{code}</code>
21
+ </pre>
22
+ </div>
23
+ );
24
+ };
25
+
26
+ export const ShowAnnotations = (props: NodeViewProps) => {
27
+ const [passage, setPassage] = useState<Passage>();
28
+ const [toggleValue, setToggleValue] = useState<string[]>(['db', 'editor']);
29
+
30
+ const { node } = props;
31
+ const { fetchPassage } = useNavigation();
32
+
33
+ useEffect(() => {
34
+ if (passage) {
35
+ return;
36
+ }
37
+
38
+ const uuid = node.attrs.uuid;
39
+ if (!uuid) {
40
+ return;
41
+ }
42
+
43
+ (async () => {
44
+ const fetchedPassage = await fetchPassage(uuid);
45
+ setPassage(fetchedPassage);
46
+ })();
47
+ }, [node, passage, fetchPassage]);
48
+
49
+ return (
50
+ <DialogContent className="size-full flex flex-col gap-4 max-h-[calc(100vh-2rem)]">
51
+ <DialogTitle>Passage Details</DialogTitle>
52
+ <DialogDescription className="hidden">
53
+ All annotations and metadata for this passage
54
+ </DialogDescription>
55
+
56
+ <div className="text-sm flex gap-1">
57
+ <span className="text-navy">UUID: </span>
58
+ <span>{node.attrs.uuid}</span>
59
+ <span className="text-muted-foreground px-2">{'|'}</span>
60
+ <span className="text-navy">Label:</span>
61
+ <span>{node.attrs.label}</span>
62
+ <span className="text-muted-foreground px-2">{'|'}</span>
63
+ <span className="text-navy">Type:</span>
64
+ <span>{node.attrs.type}</span>
65
+ </div>
66
+
67
+ <ToggleGroup
68
+ type="multiple"
69
+ variant="outline"
70
+ defaultValue={['db', 'editor']}
71
+ onValueChange={setToggleValue}
72
+ className="mt-4"
73
+ >
74
+ <ToggleGroupItem value="db">Annotations</ToggleGroupItem>
75
+ <ToggleGroupItem value="editor">Editor JSON</ToggleGroupItem>
76
+ </ToggleGroup>
77
+
78
+ <div className="flex gap-2 flex-1 min-h-0 overflow-auto">
79
+ {toggleValue.includes('db') &&
80
+ (passage ? (
81
+ <CodeBlock code={JSON.stringify(passage.annotations, null, 2)} />
82
+ ) : (
83
+ <Skeleton className="size-full flex-1" />
84
+ ))}
85
+
86
+ {toggleValue.includes('editor') && (
87
+ <CodeBlock code={JSON.stringify(node, null, 2)} />
88
+ )}
89
+ </div>
90
+ </DialogContent>
91
+ );
92
+ };
@@ -0,0 +1 @@
1
+ export * from './PassageNode';