a tool for shared writing and social publishing

share footnote item layout

+160 -108
+15 -19
app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnoteSideColumn.tsx
··· 4 4 import { PublishedFootnote } from "./PublishedFootnotes"; 5 5 import { TextBlockCore } from "../Blocks/TextBlockCore"; 6 6 import { FootnoteSideColumnLayout } from "components/Footnotes/FootnoteSideColumnLayout"; 7 + import { FootnoteItemLayout } from "components/Footnotes/FootnoteItemLayout"; 7 8 8 9 type PublishedFootnoteItem = PublishedFootnote & { 9 10 id: string; ··· 24 25 25 26 let renderItem = useCallback( 26 27 (item: PublishedFootnoteItem & { top: number }) => ( 27 - <> 28 - <a 29 - href={`#fnref-${item.footnoteId}`} 30 - className="text-accent-contrast font-medium text-xs no-underline hover:underline" 31 - > 32 - {item.index}. 33 - </a>{" "} 34 - <span className="text-secondary"> 35 - {item.contentPlaintext ? ( 36 - <TextBlockCore 37 - plaintext={item.contentPlaintext} 38 - facets={item.contentFacets} 39 - index={[]} 40 - /> 41 - ) : ( 42 - <span className="italic text-tertiary">Empty footnote</span> 43 - )} 44 - </span> 45 - </> 28 + <FootnoteItemLayout 29 + index={item.index} 30 + indexHref={`#fnref-${item.footnoteId}`} 31 + > 32 + {item.contentPlaintext ? ( 33 + <TextBlockCore 34 + plaintext={item.contentPlaintext} 35 + facets={item.contentFacets} 36 + index={[]} 37 + /> 38 + ) : ( 39 + <span className="italic text-tertiary">Empty footnote</span> 40 + )} 41 + </FootnoteItemLayout> 46 42 ), 47 43 [], 48 44 );
+42 -38
app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotes.tsx
··· 8 8 PubLeafletPagesLinearDocument, 9 9 } from "lexicons/api"; 10 10 import { TextBlockCore } from "../Blocks/TextBlockCore"; 11 + import { 12 + FootnoteItemLayout, 13 + FootnoteSectionLayout, 14 + } from "components/Footnotes/FootnoteItemLayout"; 11 15 12 16 export type PublishedFootnote = { 13 17 footnoteId: string; ··· 74 78 if (props.footnotes.length === 0) return null; 75 79 76 80 return ( 77 - <div className="footnote-section px-3 sm:px-4 pb-2 mt-4"> 78 - <hr className="border-border-light mb-3" /> 79 - <div className="flex flex-col gap-2"> 80 - {props.footnotes.map((fn) => ( 81 - <div 82 - key={fn.footnoteId} 83 - id={`fn-${fn.footnoteId}`} 84 - className="flex items-start gap-2 text-xs" 85 - > 86 - <a 87 - href={`#fnref-${fn.footnoteId}`} 88 - className="text-accent-contrast font-medium shrink-0 mt-0.5 text-xs no-underline hover:underline" 89 - > 90 - {fn.index}. 91 - </a> 92 - <div className="text-secondary min-w-0"> 93 - {fn.contentPlaintext ? ( 94 - <TextBlockCore 95 - plaintext={fn.contentPlaintext} 96 - facets={fn.contentFacets} 97 - index={[]} 98 - /> 99 - ) : ( 100 - <span className="italic text-tertiary">Empty footnote</span> 101 - )} 102 - </div> 103 - <a 104 - href={`#fnref-${fn.footnoteId}`} 105 - className="text-accent-contrast shrink-0 mt-0.5 text-xs no-underline hover:underline" 106 - title="Back to text" 107 - aria-label={`Back to footnote ${fn.index} in text`} 108 - > 109 - 110 - </a> 111 - </div> 112 - ))} 113 - </div> 114 - </div> 81 + <FootnoteSectionLayout className="mt-4"> 82 + {props.footnotes.map((fn) => ( 83 + <PublishedFootnoteItem key={fn.footnoteId} footnote={fn} /> 84 + ))} 85 + </FootnoteSectionLayout> 86 + ); 87 + } 88 + 89 + export function PublishedFootnoteItem(props: { 90 + footnote: PublishedFootnote; 91 + }) { 92 + let fn = props.footnote; 93 + return ( 94 + <FootnoteItemLayout 95 + index={fn.index} 96 + indexHref={`#fnref-${fn.footnoteId}`} 97 + id={`fn-${fn.footnoteId}`} 98 + trailing={ 99 + <a 100 + href={`#fnref-${fn.footnoteId}`} 101 + className="text-accent-contrast shrink-0 mt-0.5 text-xs no-underline hover:underline" 102 + title="Back to text" 103 + aria-label={`Back to footnote ${fn.index} in text`} 104 + > 105 + 106 + </a> 107 + } 108 + > 109 + {fn.contentPlaintext ? ( 110 + <TextBlockCore 111 + plaintext={fn.contentPlaintext} 112 + facets={fn.contentFacets} 113 + index={[]} 114 + /> 115 + ) : ( 116 + <span className="italic text-tertiary">Empty footnote</span> 117 + )} 118 + </FootnoteItemLayout> 115 119 ); 116 120 }
+25 -27
components/Footnotes/FootnoteEditor.tsx
··· 12 12 trackUndoRedo, 13 13 } from "components/Blocks/TextBlock/mountProsemirror"; 14 14 import { CloseTiny } from "components/Icons/CloseTiny"; 15 + import { FootnoteItemLayout } from "./FootnoteItemLayout"; 15 16 16 17 export function FootnoteEditor(props: { 17 18 footnoteEntityID: string; ··· 97 98 }, [props.footnoteEntityID, value, props.editable, props.autoFocus, rep.undoManager]); 98 99 99 100 return ( 100 - <div className="footnote-editor flex items-start gap-2 text-xs group/footnote" data-footnote-editor={props.footnoteEntityID}> 101 - <button 102 - className="text-accent-contrast font-medium shrink-0 text-xs leading-normal hover:underline cursor-pointer" 103 - onClick={() => { 104 - let ref = document.querySelector( 105 - `.footnote-ref[data-footnote-id="${props.footnoteEntityID}"]`, 106 - ); 107 - if (ref) { 108 - ref.scrollIntoView({ behavior: "smooth", block: "center" }); 109 - } 110 - }} 111 - title="Jump to footnote in text" 112 - > 113 - {props.index}. 114 - </button> 101 + <FootnoteItemLayout 102 + index={props.index} 103 + indexAction={() => { 104 + let ref = document.querySelector( 105 + `.footnote-ref[data-footnote-id="${props.footnoteEntityID}"]`, 106 + ); 107 + if (ref) { 108 + ref.scrollIntoView({ behavior: "smooth", block: "center" }); 109 + } 110 + }} 111 + trailing={ 112 + props.editable && props.onDelete ? ( 113 + <button 114 + className="shrink-0 mt-0.5 text-tertiary hover:text-primary opacity-0 group-hover/footnote:opacity-100 transition-opacity" 115 + onClick={props.onDelete} 116 + title="Delete footnote" 117 + > 118 + <CloseTiny /> 119 + </button> 120 + ) : undefined 121 + } 122 + > 115 123 <div 116 124 ref={mountRef} 117 - className="grow outline-hidden min-w-0 text-secondary [&_.ProseMirror]:outline-hidden" 118 - style={{ wordBreak: "break-word" }} 125 + className="outline-hidden" 119 126 /> 120 - {props.editable && props.onDelete && ( 121 - <button 122 - className="shrink-0 mt-0.5 text-tertiary hover:text-primary opacity-0 group-hover/footnote:opacity-100 transition-opacity" 123 - onClick={props.onDelete} 124 - title="Delete footnote" 125 - > 126 - <CloseTiny /> 127 - </button> 128 - )} 129 - </div> 127 + </FootnoteItemLayout> 130 128 ); 131 129 } 132 130
+56
components/Footnotes/FootnoteItemLayout.tsx
··· 1 + import { ReactNode } from "react"; 2 + 3 + export function FootnoteItemLayout(props: { 4 + index: number; 5 + indexAction?: () => void; 6 + indexHref?: string; 7 + children: ReactNode; 8 + trailing?: ReactNode; 9 + id?: string; 10 + className?: string; 11 + }) { 12 + let indexClassName = 13 + "text-accent-contrast font-medium shrink-0 text-xs leading-normal no-underline hover:underline cursor-pointer"; 14 + 15 + let indexContent = <>{props.index}.</>; 16 + 17 + return ( 18 + <div 19 + id={props.id} 20 + className={`footnote-item flex items-start gap-2 text-xs group/footnote ${props.className ?? ""}`} 21 + > 22 + {props.indexHref ? ( 23 + <a href={props.indexHref} className={indexClassName}> 24 + {indexContent} 25 + </a> 26 + ) : ( 27 + <button 28 + className={indexClassName} 29 + onClick={props.indexAction} 30 + title="Jump to footnote in text" 31 + > 32 + {indexContent} 33 + </button> 34 + )} 35 + <div 36 + className="grow min-w-0 text-secondary [&_.ProseMirror]:outline-hidden" 37 + style={{ wordBreak: "break-word" }} 38 + > 39 + {props.children} 40 + </div> 41 + {props.trailing} 42 + </div> 43 + ); 44 + } 45 + 46 + export function FootnoteSectionLayout(props: { 47 + children: ReactNode; 48 + className?: string; 49 + }) { 50 + return ( 51 + <div className={`footnote-section px-3 sm:px-4 pb-2 ${props.className ?? ""}`}> 52 + <hr className="border-border-light mb-3" /> 53 + <div className="flex flex-col gap-2">{props.children}</div> 54 + </div> 55 + ); 56 + }
+22 -24
components/Footnotes/FootnoteSection.tsx
··· 3 3 import { useReplicache } from "src/replicache"; 4 4 import { useEntitySetContext } from "components/EntitySetProvider"; 5 5 import { deleteFootnoteFromBlock } from "./deleteFootnoteFromBlock"; 6 + import { FootnoteSectionLayout } from "./FootnoteItemLayout"; 6 7 7 8 export function FootnoteSection(props: { hiddenOnDesktop?: boolean }) { 8 9 let { footnotes } = useFootnoteContext(); ··· 12 13 if (footnotes.length === 0) return null; 13 14 14 15 return ( 15 - <div className={`footnote-section px-3 sm:px-4 pb-2 ${props.hiddenOnDesktop ? "lg:hidden" : ""}`}> 16 - <hr className="border-border-light mb-3" /> 17 - <div className="flex flex-col gap-2"> 18 - {footnotes.map((fn) => ( 19 - <FootnoteEditor 20 - key={fn.footnoteEntityID} 21 - footnoteEntityID={fn.footnoteEntityID} 22 - index={fn.index} 23 - editable={permissions.write} 24 - onDelete={ 25 - permissions.write 26 - ? () => { 27 - deleteFootnoteFromBlock( 28 - fn.footnoteEntityID, 29 - fn.blockID, 30 - rep.rep, 31 - ); 32 - } 33 - : undefined 34 - } 35 - /> 36 - ))} 37 - </div> 38 - </div> 16 + <FootnoteSectionLayout className={props.hiddenOnDesktop ? "lg:hidden" : ""}> 17 + {footnotes.map((fn) => ( 18 + <FootnoteEditor 19 + key={fn.footnoteEntityID} 20 + footnoteEntityID={fn.footnoteEntityID} 21 + index={fn.index} 22 + editable={permissions.write} 23 + onDelete={ 24 + permissions.write 25 + ? () => { 26 + deleteFootnoteFromBlock( 27 + fn.footnoteEntityID, 28 + fn.blockID, 29 + rep.rep, 30 + ); 31 + } 32 + : undefined 33 + } 34 + /> 35 + ))} 36 + </FootnoteSectionLayout> 39 37 ); 40 38 }