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
28
pulls
pipelines
added small text attribute and button to leaflet
cozylittle.house
2 months ago
0e8a0557
d290f340
+162
-108
6 changed files
expand all
collapse all
unified
split
actions
publishToPublication.ts
components
Blocks
Block.tsx
TextBlock
index.tsx
Toolbar
TextBlockTypeToolbar.tsx
src
replicache
attributes.ts
utils
getBlocksAsHTML.tsx
+16
-8
actions/publishToPublication.ts
···
2
2
3
3
import * as Y from "yjs";
4
4
import * as base64 from "base64-js";
5
5
-
import {
6
6
-
restoreOAuthSession,
7
7
-
OAuthSessionError,
8
8
-
} from "src/atproto-oauth";
5
5
+
import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth";
9
6
import { getIdentityData } from "actions/getIdentityData";
10
7
import {
11
8
AtpBaseClient,
···
50
47
ColorToRGBA,
51
48
} from "components/ThemeManager/colorToLexicons";
52
49
import { parseColor } from "@react-stately/color";
53
53
-
import { Notification, pingIdentityToUpdateNotification } from "src/notifications";
50
50
+
import {
51
51
+
Notification,
52
52
+
pingIdentityToUpdateNotification,
53
53
+
} from "src/notifications";
54
54
import { v7 } from "uuid";
55
55
56
56
type PublishResult =
···
253
253
254
254
// Create notifications for mentions (only on first publish)
255
255
if (!existingDocUri) {
256
256
-
await createMentionNotifications(result.uri, record, credentialSession.did!);
256
256
+
await createMentionNotifications(
257
257
+
result.uri,
258
258
+
record,
259
259
+
credentialSession.did!,
260
260
+
);
257
261
}
258
262
259
263
return { success: true, rkey, record: JSON.parse(JSON.stringify(record)) };
···
865
869
.single();
866
870
867
871
if (publication && publication.identity_did !== authorDid) {
868
868
-
mentionedPublications.set(publication.identity_did, feature.atURI);
872
872
+
mentionedPublications.set(
873
873
+
publication.identity_did,
874
874
+
feature.atURI,
875
875
+
);
869
876
}
870
877
} else if (uri.collection === "pub.leaflet.document") {
871
878
// Get the document owner's DID
···
876
883
.single();
877
884
878
885
if (document) {
879
879
-
const docRecord = document.data as PubLeafletDocument.Record;
886
886
+
const docRecord =
887
887
+
document.data as PubLeafletDocument.Record;
880
888
if (docRecord.author !== authorDid) {
881
889
mentionedDocuments.set(docRecord.author, feature.atURI);
882
890
}
+1
-1
components/Blocks/Block.tsx
···
10
10
import { useHandleDrop } from "./useHandleDrop";
11
11
import { useEntitySetContext } from "components/EntitySetProvider";
12
12
13
13
-
import { TextBlock } from "components/Blocks/TextBlock";
13
13
+
import { TextBlock } from "./TextBlock/index";
14
14
import { ImageBlock } from "./ImageBlock";
15
15
import { PageLinkBlock } from "./PageLinkBlock";
16
16
import { ExternalLinkBlock } from "./ExternalLinkBlock";
+11
-4
components/Blocks/TextBlock/index.tsx
···
120
120
}) {
121
121
let initialFact = useEntity(props.entityID, "block/text");
122
122
let headingLevel = useEntity(props.entityID, "block/heading-level");
123
123
+
let textSize = useEntity(props.entityID, "block/text-size");
123
124
let alignment =
124
125
useEntity(props.entityID, "block/text-alignment")?.data.value || "left";
125
126
let alignmentClass = {
···
128
129
center: "text-center",
129
130
justify: "text-justify",
130
131
}[alignment];
132
132
+
let textStyle = textSize?.data.value === "small" ? "text-sm" : "";
131
133
let { permissions } = useEntitySetContext();
132
134
133
135
let content = <br />;
···
159
161
className={`
160
162
${alignmentClass}
161
163
${props.type === "blockquote" ? (props.previousBlock?.type === "blockquote" ? `blockquote pt-3 ` : "blockquote") : ""}
162
162
-
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""}
164
164
+
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle}
163
165
w-full whitespace-pre-wrap outline-hidden ${props.className} `}
164
166
>
165
167
{content}
···
169
171
170
172
export function BaseTextBlock(props: BlockProps & { className?: string }) {
171
173
let headingLevel = useEntity(props.entityID, "block/heading-level");
174
174
+
let textSize = useEntity(props.entityID, "block/text-size");
172
175
let alignment =
173
176
useEntity(props.entityID, "block/text-alignment")?.data.value || "left";
174
177
···
184
187
center: "text-center",
185
188
justify: "text-justify",
186
189
}[alignment];
190
190
+
let textStyle =
191
191
+
textSize?.data.value === "small"
192
192
+
? "text-sm text-secondary"
193
193
+
: "text-base text-primary";
187
194
188
195
let editorState = useEditorStates(
189
196
(s) => s.editorStates[props.entityID],
···
258
265
grow resize-none align-top whitespace-pre-wrap bg-transparent
259
266
outline-hidden
260
267
261
261
-
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""}
268
268
+
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle}
262
269
${props.className}`}
263
270
ref={mountRef}
264
271
/>
···
277
284
// if this is the only block on the page and is empty or is a canvas, show placeholder
278
285
<div
279
286
className={`${props.className} ${alignmentClass} w-full pointer-events-none absolute top-0 left-0 italic text-tertiary flex flex-col
280
280
-
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""}
287
287
+
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle}
281
288
`}
282
289
>
283
290
{props.type === "text"
···
496
503
497
504
// Find the relative positioned parent container
498
505
const editorEl = view.dom;
499
499
-
const container = editorEl.closest('.relative') as HTMLElement | null;
506
506
+
const container = editorEl.closest(".relative") as HTMLElement | null;
500
507
501
508
if (container) {
502
509
const containerRect = container.getBoundingClientRect();
+124
-95
components/Toolbar/TextBlockTypeToolbar.tsx
···
4
4
Header3Small,
5
5
} from "components/Icons/BlockTextSmall";
6
6
import { Props } from "components/Icons/Props";
7
7
-
import { ShortcutKey } from "components/Layout";
7
7
+
import { ShortcutKey, Separator } from "components/Layout";
8
8
import { ToolbarButton } from "components/Toolbar";
9
9
import { TextSelection } from "prosemirror-state";
10
10
import { useCallback } from "react";
···
22
22
focusedBlock?.entityID || null,
23
23
"block/heading-level",
24
24
);
25
25
+
26
26
+
let textSize = useEntity(focusedBlock?.entityID || null, "block/text-size");
25
27
let { rep } = useReplicache();
26
28
27
29
let setLevel = useCallback(
···
51
53
);
52
54
return (
53
55
// This Toolbar should close once the user starts typing again
54
54
-
<div className="flex w-full justify-between items-center gap-4">
55
55
-
<div className="flex items-center gap-[6px]">
56
56
-
<ToolbarButton
57
57
-
className={props.className}
58
58
-
onClick={() => {
59
59
-
setLevel(1);
60
60
-
}}
61
61
-
active={
62
62
-
blockType?.data.value === "heading" &&
63
63
-
headingLevel?.data.value === 1
64
64
-
}
65
65
-
tooltipContent={
66
66
-
<div className="flex flex-col justify-center">
67
67
-
<div className="font-bold text-center">Title</div>
68
68
-
<div className="flex gap-1 font-normal">
69
69
-
start line with
70
70
-
<ShortcutKey>#</ShortcutKey>
71
71
-
</div>
56
56
+
<>
57
57
+
<ToolbarButton
58
58
+
className={props.className}
59
59
+
onClick={() => {
60
60
+
setLevel(1);
61
61
+
}}
62
62
+
active={
63
63
+
blockType?.data.value === "heading" && headingLevel?.data.value === 1
64
64
+
}
65
65
+
tooltipContent={
66
66
+
<div className="flex flex-col justify-center">
67
67
+
<div className="font-bold text-center">Title</div>
68
68
+
<div className="flex gap-1 font-normal">
69
69
+
start line with
70
70
+
<ShortcutKey>#</ShortcutKey>
72
71
</div>
73
73
-
}
74
74
-
>
75
75
-
<Header1Small />
76
76
-
</ToolbarButton>
77
77
-
<ToolbarButton
78
78
-
className={props.className}
79
79
-
onClick={() => {
80
80
-
setLevel(2);
81
81
-
}}
82
82
-
active={
83
83
-
blockType?.data.value === "heading" &&
84
84
-
headingLevel?.data.value === 2
85
85
-
}
86
86
-
tooltipContent={
87
87
-
<div className="flex flex-col justify-center">
88
88
-
<div className="font-bold text-center">Heading</div>
89
89
-
<div className="flex gap-1 font-normal">
90
90
-
start line with
91
91
-
<ShortcutKey>##</ShortcutKey>
92
92
-
</div>
72
72
+
</div>
73
73
+
}
74
74
+
>
75
75
+
<Header1Small />
76
76
+
</ToolbarButton>
77
77
+
<ToolbarButton
78
78
+
className={props.className}
79
79
+
onClick={() => {
80
80
+
setLevel(2);
81
81
+
}}
82
82
+
active={
83
83
+
blockType?.data.value === "heading" && headingLevel?.data.value === 2
84
84
+
}
85
85
+
tooltipContent={
86
86
+
<div className="flex flex-col justify-center">
87
87
+
<div className="font-bold text-center">Heading</div>
88
88
+
<div className="flex gap-1 font-normal">
89
89
+
start line with
90
90
+
<ShortcutKey>##</ShortcutKey>
93
91
</div>
94
94
-
}
95
95
-
>
96
96
-
<Header2Small />
97
97
-
</ToolbarButton>
98
98
-
<ToolbarButton
99
99
-
className={props.className}
100
100
-
onClick={() => {
101
101
-
setLevel(3);
102
102
-
}}
103
103
-
active={
104
104
-
blockType?.data.value === "heading" &&
105
105
-
headingLevel?.data.value === 3
106
106
-
}
107
107
-
tooltipContent={
108
108
-
<div className="flex flex-col justify-center">
109
109
-
<div className="font-bold text-center">Subheading</div>
110
110
-
<div className="flex gap-1 font-normal">
111
111
-
start line with
112
112
-
<ShortcutKey>###</ShortcutKey>
113
113
-
</div>
92
92
+
</div>
93
93
+
}
94
94
+
>
95
95
+
<Header2Small />
96
96
+
</ToolbarButton>
97
97
+
<ToolbarButton
98
98
+
className={props.className}
99
99
+
onClick={() => {
100
100
+
setLevel(3);
101
101
+
}}
102
102
+
active={
103
103
+
blockType?.data.value === "heading" && headingLevel?.data.value === 3
104
104
+
}
105
105
+
tooltipContent={
106
106
+
<div className="flex flex-col justify-center">
107
107
+
<div className="font-bold text-center">Subheading</div>
108
108
+
<div className="flex gap-1 font-normal">
109
109
+
start line with
110
110
+
<ShortcutKey>###</ShortcutKey>
114
111
</div>
112
112
+
</div>
113
113
+
}
114
114
+
>
115
115
+
<Header3Small />
116
116
+
</ToolbarButton>
117
117
+
<Separator classname="h-6!!" />
118
118
+
<ToolbarButton
119
119
+
className={`px-[6px] ${props.className}`}
120
120
+
onClick={async () => {
121
121
+
if (headingLevel)
122
122
+
await rep?.mutate.retractFact({ factID: headingLevel.id });
123
123
+
if (textSize) await rep?.mutate.retractFact({ factID: textSize.id });
124
124
+
if (!focusedBlock || !blockType) return;
125
125
+
if (blockType.data.value !== "text") {
126
126
+
let existingEditor =
127
127
+
useEditorStates.getState().editorStates[focusedBlock.entityID];
128
128
+
let selection = existingEditor?.editor.selection;
129
129
+
await rep?.mutate.assertFact({
130
130
+
entity: focusedBlock?.entityID,
131
131
+
attribute: "block/type",
132
132
+
data: { type: "block-type-union", value: "text" },
133
133
+
});
134
134
+
135
135
+
let newEditor =
136
136
+
useEditorStates.getState().editorStates[focusedBlock.entityID];
137
137
+
if (!newEditor || !selection) return;
138
138
+
newEditor.view?.dispatch(
139
139
+
newEditor.editor.tr.setSelection(
140
140
+
TextSelection.create(newEditor.editor.doc, selection.anchor),
141
141
+
),
142
142
+
);
143
143
+
144
144
+
newEditor.view?.focus();
115
145
}
116
116
-
>
117
117
-
<Header3Small />
118
118
-
</ToolbarButton>
119
119
-
<ToolbarButton
120
120
-
className={`px-[6px] ${props.className}`}
121
121
-
onClick={async () => {
146
146
+
}}
147
147
+
active={
148
148
+
blockType?.data.value === "text" && textSize?.data.value !== "small"
149
149
+
}
150
150
+
tooltipContent={<div>Normal Text</div>}
151
151
+
>
152
152
+
Text
153
153
+
</ToolbarButton>
154
154
+
<ToolbarButton
155
155
+
className={`px-[6px] text-sm text-secondary ${props.className}`}
156
156
+
onClick={async () => {
157
157
+
if (!focusedBlock || !blockType) return;
158
158
+
if (blockType.data.value !== "text") {
159
159
+
// Convert to text block first if it's a heading
122
160
if (headingLevel)
123
161
await rep?.mutate.retractFact({ factID: headingLevel.id });
124
124
-
if (!focusedBlock || !blockType) return;
125
125
-
if (blockType.data.value !== "text") {
126
126
-
let existingEditor =
127
127
-
useEditorStates.getState().editorStates[focusedBlock.entityID];
128
128
-
let selection = existingEditor?.editor.selection;
129
129
-
await rep?.mutate.assertFact({
130
130
-
entity: focusedBlock?.entityID,
131
131
-
attribute: "block/type",
132
132
-
data: { type: "block-type-union", value: "text" },
133
133
-
});
134
134
-
135
135
-
let newEditor =
136
136
-
useEditorStates.getState().editorStates[focusedBlock.entityID];
137
137
-
if (!newEditor || !selection) return;
138
138
-
newEditor.view?.dispatch(
139
139
-
newEditor.editor.tr.setSelection(
140
140
-
TextSelection.create(newEditor.editor.doc, selection.anchor),
141
141
-
),
142
142
-
);
143
143
-
144
144
-
newEditor.view?.focus();
145
145
-
}
146
146
-
}}
147
147
-
active={blockType?.data.value === "text"}
148
148
-
tooltipContent={<div>Paragraph</div>}
149
149
-
>
150
150
-
Paragraph
151
151
-
</ToolbarButton>
152
152
-
</div>
153
153
-
</div>
162
162
+
await rep?.mutate.assertFact({
163
163
+
entity: focusedBlock.entityID,
164
164
+
attribute: "block/type",
165
165
+
data: { type: "block-type-union", value: "text" },
166
166
+
});
167
167
+
}
168
168
+
// Set text size to small
169
169
+
await rep?.mutate.assertFact({
170
170
+
entity: focusedBlock.entityID,
171
171
+
attribute: "block/text-size",
172
172
+
data: { type: "text-size-union", value: "small" },
173
173
+
});
174
174
+
}}
175
175
+
active={
176
176
+
blockType?.data.value === "text" && textSize?.data.value === "small"
177
177
+
}
178
178
+
tooltipContent={<div>Small Text</div>}
179
179
+
>
180
180
+
Small
181
181
+
</ToolbarButton>
182
182
+
</>
154
183
);
155
184
};
156
185
+8
src/replicache/attributes.ts
···
71
71
type: "number",
72
72
cardinality: "one",
73
73
},
74
74
+
"block/text-size": {
75
75
+
type: "text-size-union",
76
76
+
cardinality: "one",
77
77
+
},
74
78
"block/image": {
75
79
type: "image",
76
80
cardinality: "one",
···
317
321
"text-alignment-type-union": {
318
322
type: "text-alignment-type-union";
319
323
value: "right" | "left" | "center" | "justify";
324
324
+
};
325
325
+
"text-size-union": {
326
326
+
type: "text-size-union";
327
327
+
value: "default" | "small";
320
328
};
321
329
"page-type-union": { type: "page-type-union"; value: "doc" | "canvas" };
322
330
"block-type-union": {
+2
src/utils/getBlocksAsHTML.tsx
···
171
171
},
172
172
text: async (b, tx, a) => {
173
173
let [value] = await scanIndex(tx).eav(b.value, "block/text");
174
174
+
let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size");
174
175
return (
175
176
<RenderYJSFragment
176
177
value={value?.data.value}
177
178
attrs={{
178
179
"data-alignment": a,
180
180
+
"data-text-size": textSize?.data.value,
179
181
}}
180
182
wrapper="p"
181
183
/>