tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
share footnote item layout
awarm.space
6 days ago
aae6fae7
0f843a5a
+160
-108
5 changed files
expand all
collapse all
unified
split
app
lish
[did]
[publication]
[rkey]
Footnotes
PublishedFootnoteSideColumn.tsx
PublishedFootnotes.tsx
components
Footnotes
FootnoteEditor.tsx
FootnoteItemLayout.tsx
FootnoteSection.tsx
+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
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
27
-
<>
28
28
-
<a
29
29
-
href={`#fnref-${item.footnoteId}`}
30
30
-
className="text-accent-contrast font-medium text-xs no-underline hover:underline"
31
31
-
>
32
32
-
{item.index}.
33
33
-
</a>{" "}
34
34
-
<span className="text-secondary">
35
35
-
{item.contentPlaintext ? (
36
36
-
<TextBlockCore
37
37
-
plaintext={item.contentPlaintext}
38
38
-
facets={item.contentFacets}
39
39
-
index={[]}
40
40
-
/>
41
41
-
) : (
42
42
-
<span className="italic text-tertiary">Empty footnote</span>
43
43
-
)}
44
44
-
</span>
45
45
-
</>
28
28
+
<FootnoteItemLayout
29
29
+
index={item.index}
30
30
+
indexHref={`#fnref-${item.footnoteId}`}
31
31
+
>
32
32
+
{item.contentPlaintext ? (
33
33
+
<TextBlockCore
34
34
+
plaintext={item.contentPlaintext}
35
35
+
facets={item.contentFacets}
36
36
+
index={[]}
37
37
+
/>
38
38
+
) : (
39
39
+
<span className="italic text-tertiary">Empty footnote</span>
40
40
+
)}
41
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
11
+
import {
12
12
+
FootnoteItemLayout,
13
13
+
FootnoteSectionLayout,
14
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
77
-
<div className="footnote-section px-3 sm:px-4 pb-2 mt-4">
78
78
-
<hr className="border-border-light mb-3" />
79
79
-
<div className="flex flex-col gap-2">
80
80
-
{props.footnotes.map((fn) => (
81
81
-
<div
82
82
-
key={fn.footnoteId}
83
83
-
id={`fn-${fn.footnoteId}`}
84
84
-
className="flex items-start gap-2 text-xs"
85
85
-
>
86
86
-
<a
87
87
-
href={`#fnref-${fn.footnoteId}`}
88
88
-
className="text-accent-contrast font-medium shrink-0 mt-0.5 text-xs no-underline hover:underline"
89
89
-
>
90
90
-
{fn.index}.
91
91
-
</a>
92
92
-
<div className="text-secondary min-w-0">
93
93
-
{fn.contentPlaintext ? (
94
94
-
<TextBlockCore
95
95
-
plaintext={fn.contentPlaintext}
96
96
-
facets={fn.contentFacets}
97
97
-
index={[]}
98
98
-
/>
99
99
-
) : (
100
100
-
<span className="italic text-tertiary">Empty footnote</span>
101
101
-
)}
102
102
-
</div>
103
103
-
<a
104
104
-
href={`#fnref-${fn.footnoteId}`}
105
105
-
className="text-accent-contrast shrink-0 mt-0.5 text-xs no-underline hover:underline"
106
106
-
title="Back to text"
107
107
-
aria-label={`Back to footnote ${fn.index} in text`}
108
108
-
>
109
109
-
↩
110
110
-
</a>
111
111
-
</div>
112
112
-
))}
113
113
-
</div>
114
114
-
</div>
81
81
+
<FootnoteSectionLayout className="mt-4">
82
82
+
{props.footnotes.map((fn) => (
83
83
+
<PublishedFootnoteItem key={fn.footnoteId} footnote={fn} />
84
84
+
))}
85
85
+
</FootnoteSectionLayout>
86
86
+
);
87
87
+
}
88
88
+
89
89
+
export function PublishedFootnoteItem(props: {
90
90
+
footnote: PublishedFootnote;
91
91
+
}) {
92
92
+
let fn = props.footnote;
93
93
+
return (
94
94
+
<FootnoteItemLayout
95
95
+
index={fn.index}
96
96
+
indexHref={`#fnref-${fn.footnoteId}`}
97
97
+
id={`fn-${fn.footnoteId}`}
98
98
+
trailing={
99
99
+
<a
100
100
+
href={`#fnref-${fn.footnoteId}`}
101
101
+
className="text-accent-contrast shrink-0 mt-0.5 text-xs no-underline hover:underline"
102
102
+
title="Back to text"
103
103
+
aria-label={`Back to footnote ${fn.index} in text`}
104
104
+
>
105
105
+
↩
106
106
+
</a>
107
107
+
}
108
108
+
>
109
109
+
{fn.contentPlaintext ? (
110
110
+
<TextBlockCore
111
111
+
plaintext={fn.contentPlaintext}
112
112
+
facets={fn.contentFacets}
113
113
+
index={[]}
114
114
+
/>
115
115
+
) : (
116
116
+
<span className="italic text-tertiary">Empty footnote</span>
117
117
+
)}
118
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
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
100
-
<div className="footnote-editor flex items-start gap-2 text-xs group/footnote" data-footnote-editor={props.footnoteEntityID}>
101
101
-
<button
102
102
-
className="text-accent-contrast font-medium shrink-0 text-xs leading-normal hover:underline cursor-pointer"
103
103
-
onClick={() => {
104
104
-
let ref = document.querySelector(
105
105
-
`.footnote-ref[data-footnote-id="${props.footnoteEntityID}"]`,
106
106
-
);
107
107
-
if (ref) {
108
108
-
ref.scrollIntoView({ behavior: "smooth", block: "center" });
109
109
-
}
110
110
-
}}
111
111
-
title="Jump to footnote in text"
112
112
-
>
113
113
-
{props.index}.
114
114
-
</button>
101
101
+
<FootnoteItemLayout
102
102
+
index={props.index}
103
103
+
indexAction={() => {
104
104
+
let ref = document.querySelector(
105
105
+
`.footnote-ref[data-footnote-id="${props.footnoteEntityID}"]`,
106
106
+
);
107
107
+
if (ref) {
108
108
+
ref.scrollIntoView({ behavior: "smooth", block: "center" });
109
109
+
}
110
110
+
}}
111
111
+
trailing={
112
112
+
props.editable && props.onDelete ? (
113
113
+
<button
114
114
+
className="shrink-0 mt-0.5 text-tertiary hover:text-primary opacity-0 group-hover/footnote:opacity-100 transition-opacity"
115
115
+
onClick={props.onDelete}
116
116
+
title="Delete footnote"
117
117
+
>
118
118
+
<CloseTiny />
119
119
+
</button>
120
120
+
) : undefined
121
121
+
}
122
122
+
>
115
123
<div
116
124
ref={mountRef}
117
117
-
className="grow outline-hidden min-w-0 text-secondary [&_.ProseMirror]:outline-hidden"
118
118
-
style={{ wordBreak: "break-word" }}
125
125
+
className="outline-hidden"
119
126
/>
120
120
-
{props.editable && props.onDelete && (
121
121
-
<button
122
122
-
className="shrink-0 mt-0.5 text-tertiary hover:text-primary opacity-0 group-hover/footnote:opacity-100 transition-opacity"
123
123
-
onClick={props.onDelete}
124
124
-
title="Delete footnote"
125
125
-
>
126
126
-
<CloseTiny />
127
127
-
</button>
128
128
-
)}
129
129
-
</div>
127
127
+
</FootnoteItemLayout>
130
128
);
131
129
}
132
130
+56
components/Footnotes/FootnoteItemLayout.tsx
···
1
1
+
import { ReactNode } from "react";
2
2
+
3
3
+
export function FootnoteItemLayout(props: {
4
4
+
index: number;
5
5
+
indexAction?: () => void;
6
6
+
indexHref?: string;
7
7
+
children: ReactNode;
8
8
+
trailing?: ReactNode;
9
9
+
id?: string;
10
10
+
className?: string;
11
11
+
}) {
12
12
+
let indexClassName =
13
13
+
"text-accent-contrast font-medium shrink-0 text-xs leading-normal no-underline hover:underline cursor-pointer";
14
14
+
15
15
+
let indexContent = <>{props.index}.</>;
16
16
+
17
17
+
return (
18
18
+
<div
19
19
+
id={props.id}
20
20
+
className={`footnote-item flex items-start gap-2 text-xs group/footnote ${props.className ?? ""}`}
21
21
+
>
22
22
+
{props.indexHref ? (
23
23
+
<a href={props.indexHref} className={indexClassName}>
24
24
+
{indexContent}
25
25
+
</a>
26
26
+
) : (
27
27
+
<button
28
28
+
className={indexClassName}
29
29
+
onClick={props.indexAction}
30
30
+
title="Jump to footnote in text"
31
31
+
>
32
32
+
{indexContent}
33
33
+
</button>
34
34
+
)}
35
35
+
<div
36
36
+
className="grow min-w-0 text-secondary [&_.ProseMirror]:outline-hidden"
37
37
+
style={{ wordBreak: "break-word" }}
38
38
+
>
39
39
+
{props.children}
40
40
+
</div>
41
41
+
{props.trailing}
42
42
+
</div>
43
43
+
);
44
44
+
}
45
45
+
46
46
+
export function FootnoteSectionLayout(props: {
47
47
+
children: ReactNode;
48
48
+
className?: string;
49
49
+
}) {
50
50
+
return (
51
51
+
<div className={`footnote-section px-3 sm:px-4 pb-2 ${props.className ?? ""}`}>
52
52
+
<hr className="border-border-light mb-3" />
53
53
+
<div className="flex flex-col gap-2">{props.children}</div>
54
54
+
</div>
55
55
+
);
56
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
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
15
-
<div className={`footnote-section px-3 sm:px-4 pb-2 ${props.hiddenOnDesktop ? "lg:hidden" : ""}`}>
16
16
-
<hr className="border-border-light mb-3" />
17
17
-
<div className="flex flex-col gap-2">
18
18
-
{footnotes.map((fn) => (
19
19
-
<FootnoteEditor
20
20
-
key={fn.footnoteEntityID}
21
21
-
footnoteEntityID={fn.footnoteEntityID}
22
22
-
index={fn.index}
23
23
-
editable={permissions.write}
24
24
-
onDelete={
25
25
-
permissions.write
26
26
-
? () => {
27
27
-
deleteFootnoteFromBlock(
28
28
-
fn.footnoteEntityID,
29
29
-
fn.blockID,
30
30
-
rep.rep,
31
31
-
);
32
32
-
}
33
33
-
: undefined
34
34
-
}
35
35
-
/>
36
36
-
))}
37
37
-
</div>
38
38
-
</div>
16
16
+
<FootnoteSectionLayout className={props.hiddenOnDesktop ? "lg:hidden" : ""}>
17
17
+
{footnotes.map((fn) => (
18
18
+
<FootnoteEditor
19
19
+
key={fn.footnoteEntityID}
20
20
+
footnoteEntityID={fn.footnoteEntityID}
21
21
+
index={fn.index}
22
22
+
editable={permissions.write}
23
23
+
onDelete={
24
24
+
permissions.write
25
25
+
? () => {
26
26
+
deleteFootnoteFromBlock(
27
27
+
fn.footnoteEntityID,
28
28
+
fn.blockID,
29
29
+
rep.rep,
30
30
+
);
31
31
+
}
32
32
+
: undefined
33
33
+
}
34
34
+
/>
35
35
+
))}
36
36
+
</FootnoteSectionLayout>
39
37
);
40
38
}