From d5eef5b342e351f1548865ca918ddf401ea6652a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 6 Mar 2026 15:57:22 -0500 Subject: [PATCH] Add footnotes! Squashed commit of the following: commit 0f22f24c5ff5ed98ab139fc609632bfef20fd280 Merge: 26876031 66f9d441 Author: Jared Pereira Date: Fri Mar 6 15:55:34 2026 -0500 Merge branch 'main' into feature/footnotes commit 268760316c02f10d545f7f81b9438cc76981bb7c Author: Jared Pereira Date: Fri Mar 6 15:38:32 2026 -0500 open footnote popover commit 5add9002932f24b2cb3e88a79415359e5bfdc1aa Author: Jared Pereira Date: Fri Mar 6 15:29:28 2026 -0500 try mask-image css commit 10f35a1361dfb4672b03391b91090b368c5abbf9 Author: Jared Pereira Date: Fri Mar 6 15:16:33 2026 -0500 make footnotes text sm commit 3ae07878e7284b1112d177c4d787d27e1f2726f4 Author: Jared Pereira Date: Fri Mar 6 14:31:46 2026 -0500 fix the type errors commit 2aa857ba975fa5df90629984550d3aa7ef47d402 Author: Jared Pereira Date: Fri Mar 6 11:45:01 2026 -0500 fix footnote rendering and counts commit 212fabd22c77d12e71530403eb6b5bbbdbef1ae3 Author: celine Date: Thu Mar 5 22:24:39 2026 -0500 autofocus editor when footnote is selecter commit ce10ee81ff666d1c5f33e927719d0c5a840627ae Author: celine Date: Thu Mar 5 21:56:21 2026 -0500 some styling updates commit 4a8480f35afe12c2703f07a19579e06ec3f509fb Author: Jared Pereira Date: Thu Mar 5 18:58:14 2026 -0500 add toolbar stuff commit bf1a2f50bb31336fefd67d47d11e670aeabf9da4 Author: Jared Pereira Date: Thu Mar 5 18:06:11 2026 -0500 fix popover number and threshold commit aae6fae776da6570a26e6de12fab119ef8510bc7 Author: Jared Pereira Date: Thu Mar 5 16:22:40 2026 -0500 share footnote item layout commit 0f843a5a899eacd98d17f11412d07ca91b74841e Author: Jared Pereira Date: Thu Mar 5 16:15:25 2026 -0500 share editor logic between footnote and textblock commit 16f8052758e6aae9157ab97014509ef010eb717f Author: Jared Pereira Date: Thu Mar 5 16:15:17 2026 -0500 render footnotes properly in published page commit 713d14b48eeb5a2ceb44a71099b3d912b9f3ba6c Author: Jared Pereira Date: Thu Mar 5 15:53:39 2026 -0500 add footnotes --- actions/publishToPublication.ts | 41 ++- app/[leaflet_id]/Footer.tsx | 12 + app/globals.css | 53 ++++ .../[rkey]/Blocks/BaseTextBlock.tsx | 3 + .../[publication]/[rkey]/Blocks/TextBlock.tsx | 3 +- .../[rkey]/Blocks/TextBlockCore.tsx | 24 ++ .../Footnotes/FootnoteCollectorContext.tsx | 56 +++++ .../Footnotes/PublishedFootnotePopover.tsx | 163 ++++++++++++ .../Footnotes/PublishedFootnoteSideColumn.tsx | 56 +++++ .../[rkey]/Footnotes/PublishedFootnotes.tsx | 120 +++++++++ .../[rkey]/LinearDocumentPage.tsx | 17 ++ .../[publication]/[rkey]/PostContent.tsx | 10 + .../[did]/[publication]/[rkey]/PostPages.tsx | 15 +- .../[rkey]/StaticPostContent.tsx | 2 +- .../Blocks/TextBlock/RenderYJSFragment.tsx | 15 ++ components/Blocks/TextBlock/index.tsx | 14 ++ components/Blocks/TextBlock/inputRules.ts | 18 ++ components/Blocks/TextBlock/insertFootnote.ts | 43 ++++ .../Blocks/TextBlock/mountProsemirror.ts | 138 ++++++++-- components/Blocks/TextBlock/schema.ts | 35 +++ components/Blocks/index.tsx | 19 +- components/DesktopFooter.tsx | 15 ++ components/Footnotes/FootnoteContext.tsx | 18 ++ components/Footnotes/FootnoteEditor.tsx | 235 ++++++++++++++++++ components/Footnotes/FootnoteItemLayout.tsx | 58 +++++ components/Footnotes/FootnotePopover.tsx | 152 +++++++++++ components/Footnotes/FootnoteSection.tsx | 38 +++ components/Footnotes/FootnoteSideColumn.tsx | 63 +++++ .../Footnotes/FootnoteSideColumnLayout.tsx | 178 +++++++++++++ .../Footnotes/deleteFootnoteFromBlock.ts | 32 +++ components/Footnotes/usePageFootnotes.ts | 61 +++++ components/Pages/Page.tsx | 84 ++++--- components/Toolbar/FootnoteButton.tsx | 57 +++++ components/Toolbar/FootnoteTextToolbar.tsx | 70 ++++++ components/Toolbar/FootnoteToolbarWrapper.tsx | 77 ++++++ components/Toolbar/TextToolbar.tsx | 2 + lexicons/api/lexicons.ts | 21 ++ .../api/types/pub/leaflet/richtext/facet.ts | 19 ++ lexicons/pub/leaflet/richtext/facet.json | 23 +- src/replicache/attributes.ts | 4 + src/replicache/mutations.ts | 33 +++ src/useUIState.ts | 2 + src/utils/deleteBlock.ts | 13 + src/utils/yjsFragmentToString.ts | 4 + 44 files changed, 2050 insertions(+), 66 deletions(-) create mode 100644 app/lish/[did]/[publication]/[rkey]/Footnotes/FootnoteCollectorContext.tsx create mode 100644 app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx create mode 100644 app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnoteSideColumn.tsx create mode 100644 app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotes.tsx create mode 100644 components/Blocks/TextBlock/insertFootnote.ts create mode 100644 components/Footnotes/FootnoteContext.tsx create mode 100644 components/Footnotes/FootnoteEditor.tsx create mode 100644 components/Footnotes/FootnoteItemLayout.tsx create mode 100644 components/Footnotes/FootnotePopover.tsx create mode 100644 components/Footnotes/FootnoteSection.tsx create mode 100644 components/Footnotes/FootnoteSideColumn.tsx create mode 100644 components/Footnotes/FootnoteSideColumnLayout.tsx create mode 100644 components/Footnotes/deleteFootnoteFromBlock.ts create mode 100644 components/Footnotes/usePageFootnotes.ts create mode 100644 components/Toolbar/FootnoteButton.tsx create mode 100644 components/Toolbar/FootnoteTextToolbar.tsx create mode 100644 components/Toolbar/FootnoteToolbarWrapper.tsx diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index 2dd62d89..fe2988dd 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -485,6 +485,17 @@ async function processBlocksToPages( ).flat(); } async function blockToRecord(b: Block, did: string) { + const footnoteContentResolver = (footnoteEntityID: string) => { + let [content] = scan.eav(footnoteEntityID, "block/text"); + if (!content) return { plaintext: "", facets: [] as PubLeafletRichtextFacet.Main[] }; + let doc = new Y.Doc(); + const update = base64.toByteArray(content.data.value); + Y.applyUpdate(doc, update); + let nodes = doc.getXmlElement("prosemirror").toArray(); + let plaintext = YJSFragmentToString(nodes[0]); + let { facets } = YJSFragmentToFacets(nodes[0]); + return { plaintext, facets }; + }; const getBlockContent = (b: string) => { let [content] = scan.eav(b, "block/text"); if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const; @@ -493,7 +504,7 @@ async function processBlocksToPages( Y.applyUpdate(doc, update); let nodes = doc.getXmlElement("prosemirror").toArray(); let stringValue = YJSFragmentToString(nodes[0]); - let { facets } = YJSFragmentToFacets(nodes[0]); + let { facets } = YJSFragmentToFacets(nodes[0], 0, footnoteContentResolver); return [stringValue, facets] as const; }; if (b.type === "card") { @@ -759,8 +770,34 @@ async function processBlocksToPages( function YJSFragmentToFacets( node: Y.XmlElement | Y.XmlText | Y.XmlHook, byteOffset: number = 0, + footnoteContentResolver?: (footnoteEntityID: string) => { plaintext: string; facets: PubLeafletRichtextFacet.Main[] }, ): { facets: PubLeafletRichtextFacet.Main[]; byteLength: number } { if (node.constructor === Y.XmlElement) { + // Handle footnote inline nodes + if (node.nodeName === "footnote") { + const footnoteEntityID = node.getAttribute("footnoteEntityID") || ""; + const placeholder = "*"; + const unicodestring = new UnicodeString(placeholder); + let footnoteContent = footnoteContentResolver?.(footnoteEntityID); + const facet: PubLeafletRichtextFacet.Main = { + index: { + byteStart: byteOffset, + byteEnd: byteOffset + unicodestring.length, + }, + features: [ + { + $type: "pub.leaflet.richtext.facet#footnote", + footnoteId: footnoteEntityID, + contentPlaintext: footnoteContent?.plaintext || "", + ...(footnoteContent?.facets?.length + ? { contentFacets: footnoteContent.facets } + : {}), + }, + ], + }; + return { facets: [facet], byteLength: unicodestring.length }; + } + // Handle inline mention nodes if (node.nodeName === "didMention") { const text = node.getAttribute("text") || ""; @@ -807,7 +844,7 @@ function YJSFragmentToFacets( let allFacets: PubLeafletRichtextFacet.Main[] = []; let currentOffset = byteOffset; for (const child of node.toArray()) { - const result = YJSFragmentToFacets(child, currentOffset); + const result = YJSFragmentToFacets(child, currentOffset, footnoteContentResolver); allFacets.push(...result.facets); currentOffset += result.byteLength; } diff --git a/app/[leaflet_id]/Footer.tsx b/app/[leaflet_id]/Footer.tsx index ad063f02..f8ff45fa 100644 --- a/app/[leaflet_id]/Footer.tsx +++ b/app/[leaflet_id]/Footer.tsx @@ -4,6 +4,7 @@ import { Footer } from "components/ActionBar/Footer"; import { Media } from "components/Media"; import { ThemePopover } from "components/ThemeManager/ThemeSetter"; import { Toolbar } from "components/Toolbar"; +import { FootnoteToolbar } from "components/Toolbar/FootnoteToolbarWrapper"; import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; import { HomeButton } from "app/[leaflet_id]/actions/HomeButton"; import { PublishButton } from "./actions/PublishButton"; @@ -56,6 +57,17 @@ export function LeafletFooter(props: { entityID: string }) { blockType={blockType} /> + ) : focusedBlock && + focusedBlock.entityType === "footnote" && + entity_set.permissions.write ? ( +
{ + if (e.currentTarget === e.target) e.preventDefault(); + }} + > + +
) : entity_set.permissions.write ? (