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
Merge branch 'main' into feature/post-options
awarm.space
2 months ago
22a8f8ef
374179c9
+529
-194
44 changed files
expand all
collapse all
unified
split
.prettierrc
actions
publishToPublication.ts
app
(home-pages)
notifications
CommentNotication.tsx
Notification.tsx
ReplyNotification.tsx
p
[didOrHandle]
comments
CommentsContent.tsx
[leaflet_id]
actions
HelpButton.tsx
publish
PublishPost.tsx
lish
[did]
[publication]
[rkey]
Blocks
BaseTextBlock.tsx
PubCodeBlock.tsx
PublishBskyPostBlock.tsx
PublishedPageBlock.tsx
PublishedPollBlock.tsx
StaticMathBlock.tsx
TextBlock.tsx
TextBlockCore.tsx
Interactions
Comments
index.tsx
PostContent.tsx
StaticPostContent.tsx
page.tsx
components
ActionBar
ActionButton.tsx
Blocks
Block.tsx
TextBlock
RenderYJSFragment.tsx
index.tsx
keymap.ts
useHandlePaste.ts
SelectionManager
index.tsx
ThemeManager
PublicationThemeProvider.tsx
ThemeProvider.tsx
themeUtils.ts
Toolbar
BlockToolbar.tsx
HighlightToolbar.tsx
InlineLinkToolbar.tsx
ListToolbar.tsx
TextBlockTypeToolbar.tsx
TextToolbar.tsx
utils
DotLoader.tsx
lexicons
api
lexicons.ts
types
pub
leaflet
blocks
text.ts
pub
leaflet
blocks
text.json
publication.json
src
blocks.ts
src
replicache
attributes.ts
utils
getBlocksAsHTML.tsx
+1
-1
.prettierrc
···
1
1
-
{}
1
1
+
{}
+20
-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)) };
···
463
467
464
468
if (b.type == "text") {
465
469
let [stringValue, facets] = getBlockContent(b.value);
470
470
+
let [textSize] = scan.eav(b.value, "block/text-size");
466
471
let block: $Typed<PubLeafletBlocksText.Main> = {
467
472
$type: ids.PubLeafletBlocksText,
468
473
plaintext: stringValue,
469
474
facets,
475
475
+
...(textSize && { textSize: textSize.data.value }),
470
476
};
471
477
return block;
472
478
}
···
778
784
root_entity,
779
785
"theme/background-image-repeat",
780
786
)?.[0];
787
787
+
let pageWidth = scan.eav(root_entity, "theme/page-width")?.[0];
781
788
782
789
let theme: PubLeafletPublication.Theme = {
783
790
showPageBackground: showPageBackground ?? true,
784
791
};
785
792
793
793
+
if (pageWidth) theme.pageWidth = pageWidth.data.value;
786
794
if (pageBackground)
787
795
theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`));
788
796
if (cardBackground)
···
865
873
.single();
866
874
867
875
if (publication && publication.identity_did !== authorDid) {
868
868
-
mentionedPublications.set(publication.identity_did, feature.atURI);
876
876
+
mentionedPublications.set(
877
877
+
publication.identity_did,
878
878
+
feature.atURI,
879
879
+
);
869
880
}
870
881
} else if (uri.collection === "pub.leaflet.document") {
871
882
// Get the document owner's DID
···
876
887
.single();
877
888
878
889
if (document) {
879
879
-
const docRecord = document.data as PubLeafletDocument.Record;
890
890
+
const docRecord =
891
891
+
document.data as PubLeafletDocument.Record;
880
892
if (docRecord.author !== authorDid) {
881
893
mentionedDocuments.set(docRecord.author, feature.atURI);
882
894
}
+1
-1
app/(home-pages)/notifications/CommentNotication.tsx
···
1
1
-
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock";
1
1
+
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock";
2
2
import {
3
3
AppBskyActorProfile,
4
4
PubLeafletComment,
+1
-1
app/(home-pages)/notifications/Notification.tsx
···
1
1
"use client";
2
2
import { Avatar } from "components/Avatar";
3
3
-
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock";
3
3
+
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock";
4
4
import { PubLeafletPublication, PubLeafletRichtextFacet } from "lexicons/api";
5
5
import { timeAgo } from "src/utils/timeAgo";
6
6
import { useReplicache, useEntity } from "src/replicache";
+1
-1
app/(home-pages)/notifications/ReplyNotification.tsx
···
1
1
import { Avatar } from "components/Avatar";
2
2
-
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock";
2
2
+
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock";
3
3
import { ReplyTiny } from "components/Icons/ReplyTiny";
4
4
import {
5
5
CommentInNotification,
+1
-1
app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
···
6
6
import { PubLeafletComment, PubLeafletDocument } from "lexicons/api";
7
7
import { ReplyTiny } from "components/Icons/ReplyTiny";
8
8
import { Avatar } from "components/Avatar";
9
9
-
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock";
9
9
+
import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock";
10
10
import { blobRefToSrc } from "src/utils/blobRefToSrc";
11
11
import {
12
12
getProfileComments,
+24
app/[leaflet_id]/actions/HelpButton.tsx
···
58
58
keys={[metaKey(), isMac() ? "Ctrl" : "Meta", "X"]}
59
59
/>
60
60
<KeyboardShortcut name="Inline Link" keys={[metaKey(), "K"]} />
61
61
+
<KeyboardShortcut
62
62
+
name="Make Title"
63
63
+
keys={[metaKey(), isMac() ? "Opt" : "Alt", "1"]}
64
64
+
/>
65
65
+
<KeyboardShortcut
66
66
+
name="Make Heading"
67
67
+
keys={[metaKey(), isMac() ? "Opt" : "Alt", "2"]}
68
68
+
/>
69
69
+
<KeyboardShortcut
70
70
+
name="Make Subheading"
71
71
+
keys={[metaKey(), isMac() ? "Opt" : "Alt", "3"]}
72
72
+
/>
73
73
+
<KeyboardShortcut
74
74
+
name="Regular Text"
75
75
+
keys={[metaKey(), isMac() ? "Opt" : "Alt", "0"]}
76
76
+
/>
77
77
+
<KeyboardShortcut
78
78
+
name="Large Text"
79
79
+
keys={[metaKey(), isMac() ? "Opt" : "Alt", "+"]}
80
80
+
/>
81
81
+
<KeyboardShortcut
82
82
+
name="Small Text"
83
83
+
keys={[metaKey(), isMac() ? "Opt" : "Alt", "-"]}
84
84
+
/>
61
85
62
86
<Label>Block Shortcuts</Label>
63
87
{/* shift + up/down arrows (or click + drag): select multiple blocks */}
+5
-1
app/[leaflet_id]/publish/PublishPost.tsx
···
199
199
className="place-self-end h-[30px]"
200
200
disabled={charCount > 300}
201
201
>
202
202
-
{isLoading ? <DotLoader /> : "Publish this Post!"}
202
202
+
{isLoading ? (
203
203
+
<DotLoader className="h-[23px]" />
204
204
+
) : (
205
205
+
"Publish this Post!"
206
206
+
)}
203
207
</ButtonPrimary>
204
208
</div>
205
209
{oauthError && (
+6
-7
app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock.tsx
···
1
1
import { ProfilePopover } from "components/ProfilePopover";
2
2
-
import { TextBlockCore, TextBlockCoreProps, RichText } from "./TextBlockCore";
2
2
+
import {
3
3
+
TextBlockCore,
4
4
+
TextBlockCoreProps,
5
5
+
RichText,
6
6
+
} from "../Blocks/TextBlockCore";
3
7
import { ReactNode } from "react";
4
8
5
9
// Re-export RichText for backwards compatibility
6
10
export { RichText };
7
11
8
12
function DidMentionWithPopover(props: { did: string; children: ReactNode }) {
9
9
-
return (
10
10
-
<ProfilePopover
11
11
-
didOrHandle={props.did}
12
12
-
trigger={props.children}
13
13
-
/>
14
14
-
);
13
13
+
return <ProfilePopover didOrHandle={props.did} trigger={props.children} />;
15
14
}
16
15
17
16
export function BaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) {
+1
-1
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
···
5
5
import { CommentBox } from "./CommentBox";
6
6
import { Json } from "supabase/database.types";
7
7
import { PubLeafletComment } from "lexicons/api";
8
8
-
import { BaseTextBlock } from "../../BaseTextBlock";
8
8
+
import { BaseTextBlock } from "../../Blocks/BaseTextBlock";
9
9
import { useMemo, useState } from "react";
10
10
import { CommentTiny } from "components/Icons/CommentTiny";
11
11
import { Separator } from "components/Layout";
+18
-8
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
···
20
20
} from "lexicons/api";
21
21
22
22
import { blobRefToSrc } from "src/utils/blobRefToSrc";
23
23
-
import { TextBlock } from "./TextBlock";
23
23
+
import { TextBlock } from "./Blocks/TextBlock";
24
24
import { Popover } from "components/Popover";
25
25
import { theme } from "tailwind.config";
26
26
import { ImageAltSmall } from "components/Icons/ImageAlt";
27
27
-
import { StaticMathBlock } from "./StaticMathBlock";
28
28
-
import { PubCodeBlock } from "./PubCodeBlock";
27
27
+
import { StaticMathBlock } from "./Blocks/StaticMathBlock";
28
28
+
import { PubCodeBlock } from "./Blocks/PubCodeBlock";
29
29
import { AppBskyFeedDefs } from "@atproto/api";
30
30
-
import { PubBlueskyPostBlock } from "./PublishBskyPostBlock";
30
30
+
import { PubBlueskyPostBlock } from "./Blocks/PublishBskyPostBlock";
31
31
import { openPage } from "./PostPages";
32
32
import { PageLinkBlock } from "components/Blocks/PageLinkBlock";
33
33
-
import { PublishedPageLinkBlock } from "./PublishedPageBlock";
34
34
-
import { PublishedPollBlock } from "./PublishedPollBlock";
33
33
+
import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock";
34
34
+
import { PublishedPollBlock } from "./Blocks/PublishedPollBlock";
35
35
import { PollData } from "./fetchPollData";
36
36
import { ButtonPrimary } from "components/Buttons";
37
37
···
173
173
let uri = b.block.postRef.uri;
174
174
let post = bskyPostData.find((p) => p.uri === uri);
175
175
if (!post) return <div>no prefetched post rip</div>;
176
176
-
return <PubBlueskyPostBlock post={post} className={className} pageId={pageId} />;
176
176
+
return (
177
177
+
<PubBlueskyPostBlock
178
178
+
post={post}
179
179
+
className={className}
180
180
+
pageId={pageId}
181
181
+
/>
182
182
+
);
177
183
}
178
184
case PubLeafletBlocksIframe.isMain(b.block): {
179
185
return (
···
339
345
}
340
346
case PubLeafletBlocksText.isMain(b.block):
341
347
return (
342
342
-
<p className={`textBlock ${className}`} {...blockProps}>
348
348
+
<p
349
349
+
className={`textBlock ${className} ${b.block.textSize === "small" ? "text-sm text-secondary" : b.block.textSize === "large" ? "text-lg" : ""}`}
350
350
+
{...blockProps}
351
351
+
>
343
352
<TextBlock
344
353
facets={b.block.facets}
345
354
plaintext={b.block.plaintext}
···
349
358
/>
350
359
</p>
351
360
);
361
361
+
352
362
case PubLeafletBlocksHeader.isMain(b.block): {
353
363
if (b.block.level === 1)
354
364
return (
app/lish/[did]/[publication]/[rkey]/PubCodeBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/PubCodeBlock.tsx
+9
-7
app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx
···
5
5
import { BlueskyTiny } from "components/Icons/BlueskyTiny";
6
6
import { CommentTiny } from "components/Icons/CommentTiny";
7
7
import { QuoteTiny } from "components/Icons/QuoteTiny";
8
8
-
import { ThreadLink, QuotesLink } from "./PostLinks";
8
8
+
import { ThreadLink, QuotesLink } from "../PostLinks";
9
9
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
10
10
import {
11
11
BlueskyEmbed,
12
12
PostNotAvailable,
13
13
} from "components/Blocks/BlueskyPostBlock/BlueskyEmbed";
14
14
import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText";
15
15
-
import { openPage } from "./PostPages";
15
15
+
import { openPage } from "../PostPages";
16
16
17
17
export const PubBlueskyPostBlock = (props: {
18
18
post: PostView;
···
22
22
let post = props.post;
23
23
24
24
const handleOpenThread = () => {
25
25
-
openPage(
26
26
-
props.pageId ? { type: "doc", id: props.pageId } : undefined,
27
27
-
{ type: "thread", uri: post.uri },
28
28
-
);
25
25
+
openPage(props.pageId ? { type: "doc", id: props.pageId } : undefined, {
26
26
+
type: "thread",
27
27
+
uri: post.uri,
28
28
+
});
29
29
};
30
30
31
31
switch (true) {
···
51
51
let postId = post.uri.split("/")[4];
52
52
let url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`;
53
53
54
54
-
const parent = props.pageId ? { type: "doc" as const, id: props.pageId } : undefined;
54
54
+
const parent = props.pageId
55
55
+
? { type: "doc" as const, id: props.pageId }
56
56
+
: undefined;
55
57
56
58
return (
57
59
<div
+14
-10
app/lish/[did]/[publication]/[rkey]/PublishedPageBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx
···
4
4
import { useUIState } from "src/useUIState";
5
5
import { CSSProperties, useContext, useRef } from "react";
6
6
import { useCardBorderHidden } from "components/Pages/useCardBorderHidden";
7
7
-
import { PostContent, Block } from "./PostContent";
7
7
+
import { PostContent, Block } from "../PostContent";
8
8
import {
9
9
PubLeafletBlocksHeader,
10
10
PubLeafletBlocksText,
···
15
15
} from "lexicons/api";
16
16
import { AppBskyFeedDefs } from "@atproto/api";
17
17
import { TextBlock } from "./TextBlock";
18
18
-
import { PostPageContext } from "./PostPageContext";
19
19
-
import { openPage, useOpenPages } from "./PostPages";
18
18
+
import { PostPageContext } from "../PostPageContext";
19
19
+
import { openPage, useOpenPages } from "../PostPages";
20
20
import {
21
21
openInteractionDrawer,
22
22
setInteractionState,
23
23
useInteractionState,
24
24
-
} from "./Interactions/Interactions";
24
24
+
} from "../Interactions/Interactions";
25
25
import { CommentTiny } from "components/Icons/CommentTiny";
26
26
import { QuoteTiny } from "components/Icons/QuoteTiny";
27
27
import { CanvasBackgroundPattern } from "components/Canvas";
···
40
40
}) {
41
41
//switch to use actually state
42
42
let openPages = useOpenPages();
43
43
-
let isOpen = openPages.some(
44
44
-
(p) => p.type === "doc" && p.id === props.pageId,
45
45
-
);
43
43
+
let isOpen = openPages.some((p) => p.type === "doc" && p.id === props.pageId);
46
44
return (
47
45
<div
48
46
className={`w-full cursor-pointer
···
60
58
e.stopPropagation();
61
59
62
60
openPage(
63
63
-
props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined,
61
61
+
props.parentPageId
62
62
+
? { type: "doc", id: props.parentPageId }
63
63
+
: undefined,
64
64
{ type: "doc", id: props.pageId },
65
65
);
66
66
}}
···
219
219
e.preventDefault();
220
220
e.stopPropagation();
221
221
openPage(
222
222
-
props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined,
222
222
+
props.parentPageId
223
223
+
? { type: "doc", id: props.parentPageId }
224
224
+
: undefined,
223
225
{ type: "doc", id: props.pageId },
224
226
{ scrollIntoView: false },
225
227
);
···
239
241
e.preventDefault();
240
242
e.stopPropagation();
241
243
openPage(
242
242
-
props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined,
244
244
+
props.parentPageId
245
245
+
? { type: "doc", id: props.parentPageId }
246
246
+
: undefined,
243
247
{ type: "doc", id: props.pageId },
244
248
{ scrollIntoView: false },
245
249
);
+3
-3
app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPollBlock.tsx
···
9
9
import { ButtonPrimary, ButtonSecondary } from "components/Buttons";
10
10
import { useIdentityData } from "components/IdentityProvider";
11
11
import { AtpAgent } from "@atproto/api";
12
12
-
import { voteOnPublishedPoll } from "./voteOnPublishedPoll";
13
13
-
import { PollData } from "./fetchPollData";
12
12
+
import { voteOnPublishedPoll } from "../voteOnPublishedPoll";
13
13
+
import { PollData } from "../fetchPollData";
14
14
import { Popover } from "components/Popover";
15
15
import LoginForm from "app/login/LoginForm";
16
16
import { BlueskyTiny } from "components/Icons/BlueskyTiny";
17
17
-
import { getVoterIdentities, VoterIdentity } from "./getVoterIdentities";
17
17
+
import { getVoterIdentities, VoterIdentity } from "../getVoterIdentities";
18
18
import { Json } from "supabase/database.types";
19
19
import { InfoSmall } from "components/Icons/InfoSmall";
20
20
app/lish/[did]/[publication]/[rkey]/StaticMathBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/StaticMathBlock.tsx
+2
-2
app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx
···
12
12
PubLeafletPagesLinearDocument,
13
13
} from "lexicons/api";
14
14
import { blobRefToSrc } from "src/utils/blobRefToSrc";
15
15
-
import { TextBlockCore, TextBlockCoreProps } from "./TextBlockCore";
16
16
-
import { StaticMathBlock } from "./StaticMathBlock";
15
15
+
import { TextBlockCore, TextBlockCoreProps } from "./Blocks/TextBlockCore";
16
16
+
import { StaticMathBlock } from "./Blocks/StaticMathBlock";
17
17
import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki";
18
18
19
19
function StaticBaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) {
+1
-1
app/lish/[did]/[publication]/[rkey]/TextBlock.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/TextBlock.tsx
···
2
2
import { UnicodeString } from "@atproto/api";
3
3
import { PubLeafletRichtextFacet } from "lexicons/api";
4
4
import { useMemo } from "react";
5
5
-
import { useHighlight } from "./useHighlight";
5
5
+
import { useHighlight } from "../useHighlight";
6
6
import { BaseTextBlock } from "./BaseTextBlock";
7
7
8
8
type Facet = PubLeafletRichtextFacet.Main;
app/lish/[did]/[publication]/[rkey]/TextBlockCore.tsx
app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx
+7
-2
app/lish/[did]/[publication]/page.tsx
···
18
18
import { LocalizedDate } from "./LocalizedDate";
19
19
import { PublicationHomeLayout } from "./PublicationHomeLayout";
20
20
import { PublicationAuthor } from "./PublicationAuthor";
21
21
+
import { Separator } from "components/Layout";
21
22
22
23
export default async function Publication(props: {
23
24
params: Promise<{ publication: string; did: string }>;
···
147
148
</p>
148
149
</SpeedyLink>
149
150
150
150
-
<div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2">
151
151
+
<div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center">
151
152
<p className="text-sm text-tertiary ">
152
153
{doc_record.publishedAt && (
153
154
<LocalizedDate
···
160
161
/>
161
162
)}{" "}
162
163
</p>
163
163
-
{comments > 0 || quotes > 0 ? "| " : ""}
164
164
+
{comments > 0 || quotes > 0 || tags.length > 0 ? (
165
165
+
<Separator classname="h-4! mx-1" />
166
166
+
) : (
167
167
+
""
168
168
+
)}
164
169
<InteractionPreview
165
170
quotesCount={quotes}
166
171
commentsCount={comments}
+2
-2
components/ActionBar/ActionButton.tsx
···
70
70
>
71
71
<div className="shrink-0">{icon}</div>
72
72
<div
73
73
-
className={`flex flex-col pr-1 leading-snug max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`}
73
73
+
className={`flex flex-col pr-1 ${subtext && "leading-snug"} max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`}
74
74
>
75
75
-
<div className="truncate text-left pt-[1px]">{label}</div>
75
75
+
<div className="truncate text-left">{label}</div>
76
76
{subtext && (
77
77
<div className="text-xs text-tertiary font-normal text-left">
78
78
{subtext}
+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";
+1
-1
components/Blocks/TextBlock/RenderYJSFragment.tsx
···
8
8
import { Delta } from "src/utils/yjsFragmentToString";
9
9
import { ProfilePopover } from "components/ProfilePopover";
10
10
11
11
-
type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p";
11
11
+
type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p" | "small";
12
12
export function RenderYJSFragment({
13
13
value,
14
14
wrapper,
+18
-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 =
133
133
+
textSize?.data.value === "small"
134
134
+
? "text-sm"
135
135
+
: textSize?.data.value === "large"
136
136
+
? "text-lg"
137
137
+
: "";
131
138
let { permissions } = useEntitySetContext();
132
139
133
140
let content = <br />;
···
159
166
className={`
160
167
${alignmentClass}
161
168
${props.type === "blockquote" ? (props.previousBlock?.type === "blockquote" ? `blockquote pt-3 ` : "blockquote") : ""}
162
162
-
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""}
169
169
+
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle}
163
170
w-full whitespace-pre-wrap outline-hidden ${props.className} `}
164
171
>
165
172
{content}
···
169
176
170
177
export function BaseTextBlock(props: BlockProps & { className?: string }) {
171
178
let headingLevel = useEntity(props.entityID, "block/heading-level");
179
179
+
let textSize = useEntity(props.entityID, "block/text-size");
172
180
let alignment =
173
181
useEntity(props.entityID, "block/text-alignment")?.data.value || "left";
174
182
···
184
192
center: "text-center",
185
193
justify: "text-justify",
186
194
}[alignment];
195
195
+
let textStyle =
196
196
+
textSize?.data.value === "small"
197
197
+
? "text-sm text-secondary"
198
198
+
: textSize?.data.value === "large"
199
199
+
? "text-lg text-primary"
200
200
+
: "text-base text-primary";
187
201
188
202
let editorState = useEditorStates(
189
203
(s) => s.editorStates[props.entityID],
···
258
272
grow resize-none align-top whitespace-pre-wrap bg-transparent
259
273
outline-hidden
260
274
261
261
-
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""}
275
275
+
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle}
262
276
${props.className}`}
263
277
ref={mountRef}
264
278
/>
···
277
291
// if this is the only block on the page and is empty or is a canvas, show placeholder
278
292
<div
279
293
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] : ""}
294
294
+
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle}
281
295
`}
282
296
>
283
297
{props.type === "text"
···
496
510
497
511
// Find the relative positioned parent container
498
512
const editorEl = view.dom;
499
499
-
const container = editorEl.closest('.relative') as HTMLElement | null;
513
513
+
const container = editorEl.closest(".relative") as HTMLElement | null;
500
514
501
515
if (container) {
502
516
const containerRect = container.getBoundingClientRect();
+14
components/Blocks/TextBlock/keymap.ts
···
555
555
},
556
556
});
557
557
}
558
558
+
let [textSize] =
559
559
+
(await repRef.current?.query((tx) =>
560
560
+
scanIndex(tx).eav(propsRef.current.entityID, "block/text-size"),
561
561
+
)) || [];
562
562
+
if (textSize) {
563
563
+
await repRef.current?.mutate.assertFact({
564
564
+
entity: newEntityID,
565
565
+
attribute: "block/text-size",
566
566
+
data: {
567
567
+
type: "text-size-union",
568
568
+
value: textSize.data.value,
569
569
+
},
570
570
+
});
571
571
+
}
558
572
};
559
573
asyncRun().then(() => {
560
574
useUIState.getState().setSelectedBlock({
+11
components/Blocks/TextBlock/useHandlePaste.ts
···
299
299
},
300
300
});
301
301
}
302
302
+
let textSize = child.getAttribute("data-text-size");
303
303
+
if (textSize && ["default", "small", "large"].includes(textSize)) {
304
304
+
rep.mutate.assertFact({
305
305
+
entity: entityID,
306
306
+
attribute: "block/text-size",
307
307
+
data: {
308
308
+
type: "text-size-union",
309
309
+
value: textSize as "default" | "small" | "large",
310
310
+
},
311
311
+
});
312
312
+
}
302
313
if (child.tagName === "A") {
303
314
let href = child.getAttribute("href");
304
315
let dataType = child.getAttribute("data-type");
+148
-1
components/SelectionManager/index.tsx
···
89
89
},
90
90
{
91
91
metaKey: true,
92
92
+
altKey: true,
93
93
+
key: ["1", "¡"],
94
94
+
handler: async () => {
95
95
+
let [sortedBlocks] = await getSortedSelectionBound();
96
96
+
for (let block of sortedBlocks) {
97
97
+
await rep?.mutate.assertFact({
98
98
+
entity: block.value,
99
99
+
attribute: "block/heading-level",
100
100
+
data: { type: "number", value: 1 },
101
101
+
});
102
102
+
await rep?.mutate.assertFact({
103
103
+
entity: block.value,
104
104
+
attribute: "block/type",
105
105
+
data: { type: "block-type-union", value: "heading" },
106
106
+
});
107
107
+
}
108
108
+
},
109
109
+
},
110
110
+
{
111
111
+
metaKey: true,
112
112
+
altKey: true,
113
113
+
key: ["2", "™"],
114
114
+
handler: async () => {
115
115
+
let [sortedBlocks] = await getSortedSelectionBound();
116
116
+
for (let block of sortedBlocks) {
117
117
+
await rep?.mutate.assertFact({
118
118
+
entity: block.value,
119
119
+
attribute: "block/heading-level",
120
120
+
data: { type: "number", value: 2 },
121
121
+
});
122
122
+
await rep?.mutate.assertFact({
123
123
+
entity: block.value,
124
124
+
attribute: "block/type",
125
125
+
data: { type: "block-type-union", value: "heading" },
126
126
+
});
127
127
+
}
128
128
+
},
129
129
+
},
130
130
+
{
131
131
+
metaKey: true,
132
132
+
altKey: true,
133
133
+
key: ["3", "£"],
134
134
+
handler: async () => {
135
135
+
let [sortedBlocks] = await getSortedSelectionBound();
136
136
+
for (let block of sortedBlocks) {
137
137
+
await rep?.mutate.assertFact({
138
138
+
entity: block.value,
139
139
+
attribute: "block/heading-level",
140
140
+
data: { type: "number", value: 3 },
141
141
+
});
142
142
+
await rep?.mutate.assertFact({
143
143
+
entity: block.value,
144
144
+
attribute: "block/type",
145
145
+
data: { type: "block-type-union", value: "heading" },
146
146
+
});
147
147
+
}
148
148
+
},
149
149
+
},
150
150
+
{
151
151
+
metaKey: true,
152
152
+
altKey: true,
153
153
+
key: ["0", "º"],
154
154
+
handler: async () => {
155
155
+
let [sortedBlocks] = await getSortedSelectionBound();
156
156
+
for (let block of sortedBlocks) {
157
157
+
// Convert to text block
158
158
+
await rep?.mutate.assertFact({
159
159
+
entity: block.value,
160
160
+
attribute: "block/type",
161
161
+
data: { type: "block-type-union", value: "text" },
162
162
+
});
163
163
+
// Remove heading level if exists
164
164
+
let headingLevel = await rep?.query((tx) =>
165
165
+
scanIndex(tx).eav(block.value, "block/heading-level"),
166
166
+
);
167
167
+
if (headingLevel?.[0]) {
168
168
+
await rep?.mutate.retractFact({ factID: headingLevel[0].id });
169
169
+
}
170
170
+
// Remove text-size to make it default
171
171
+
let textSizeFact = await rep?.query((tx) =>
172
172
+
scanIndex(tx).eav(block.value, "block/text-size"),
173
173
+
);
174
174
+
if (textSizeFact?.[0]) {
175
175
+
await rep?.mutate.retractFact({ factID: textSizeFact[0].id });
176
176
+
}
177
177
+
}
178
178
+
},
179
179
+
},
180
180
+
{
181
181
+
metaKey: true,
182
182
+
altKey: true,
183
183
+
key: ["+", "≠"],
184
184
+
handler: async () => {
185
185
+
let [sortedBlocks] = await getSortedSelectionBound();
186
186
+
for (let block of sortedBlocks) {
187
187
+
// Convert to text block
188
188
+
await rep?.mutate.assertFact({
189
189
+
entity: block.value,
190
190
+
attribute: "block/type",
191
191
+
data: { type: "block-type-union", value: "text" },
192
192
+
});
193
193
+
// Remove heading level if exists
194
194
+
let headingLevel = await rep?.query((tx) =>
195
195
+
scanIndex(tx).eav(block.value, "block/heading-level"),
196
196
+
);
197
197
+
if (headingLevel?.[0]) {
198
198
+
await rep?.mutate.retractFact({ factID: headingLevel[0].id });
199
199
+
}
200
200
+
// Set text size to large
201
201
+
await rep?.mutate.assertFact({
202
202
+
entity: block.value,
203
203
+
attribute: "block/text-size",
204
204
+
data: { type: "text-size-union", value: "large" },
205
205
+
});
206
206
+
}
207
207
+
},
208
208
+
},
209
209
+
{
210
210
+
metaKey: true,
211
211
+
altKey: true,
212
212
+
key: ["-", "–"],
213
213
+
handler: async () => {
214
214
+
let [sortedBlocks] = await getSortedSelectionBound();
215
215
+
for (let block of sortedBlocks) {
216
216
+
// Convert to text block
217
217
+
await rep?.mutate.assertFact({
218
218
+
entity: block.value,
219
219
+
attribute: "block/type",
220
220
+
data: { type: "block-type-union", value: "text" },
221
221
+
});
222
222
+
// Remove heading level if exists
223
223
+
let headingLevel = await rep?.query((tx) =>
224
224
+
scanIndex(tx).eav(block.value, "block/heading-level"),
225
225
+
);
226
226
+
if (headingLevel?.[0]) {
227
227
+
await rep?.mutate.retractFact({ factID: headingLevel[0].id });
228
228
+
}
229
229
+
// Set text size to small
230
230
+
await rep?.mutate.assertFact({
231
231
+
entity: block.value,
232
232
+
attribute: "block/text-size",
233
233
+
data: { type: "text-size-union", value: "small" },
234
234
+
});
235
235
+
}
236
236
+
},
237
237
+
},
238
238
+
{
239
239
+
metaKey: true,
92
240
shift: true,
93
241
key: ["ArrowDown", "J"],
94
242
handler: async () => {
···
684
832
}
685
833
return null;
686
834
}
687
687
-
688
835
689
836
function toggleMarkInBlocks(blocks: string[], mark: MarkType, attrs?: any) {
690
837
let everyBlockHasMark = blocks.reduce((acc, block) => {
+7
-7
components/ThemeManager/PublicationThemeProvider.tsx
···
2
2
import { useMemo, useState } from "react";
3
3
import { parseColor } from "react-aria-components";
4
4
import { useEntity } from "src/replicache";
5
5
-
import { getColorContrast } from "./themeUtils";
5
5
+
import { getColorDifference } from "./themeUtils";
6
6
import { useColorAttribute, colorToString } from "./useColorAttribute";
7
7
import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider";
8
8
import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api";
···
174
174
let newAccentContrast;
175
175
let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => {
176
176
return (
177
177
-
getColorContrast(
177
177
+
getColorDifference(
178
178
colorToString(b, "rgb"),
179
179
colorToString(
180
180
showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
181
181
"rgb",
182
182
),
183
183
) -
184
184
-
getColorContrast(
184
184
+
getColorDifference(
185
185
colorToString(a, "rgb"),
186
186
colorToString(
187
187
showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
···
191
191
);
192
192
});
193
193
if (
194
194
-
getColorContrast(
194
194
+
getColorDifference(
195
195
colorToString(sortedAccents[0], "rgb"),
196
196
colorToString(newTheme.primary, "rgb"),
197
197
-
) < 30 &&
198
198
-
getColorContrast(
197
197
+
) < 0.15 &&
198
198
+
getColorDifference(
199
199
colorToString(sortedAccents[1], "rgb"),
200
200
colorToString(
201
201
showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet,
202
202
"rgb",
203
203
),
204
204
-
) > 12
204
204
+
) > 0.08
205
205
) {
206
206
newAccentContrast = sortedAccents[1];
207
207
} else newAccentContrast = sortedAccents[0];
+9
-9
components/ThemeManager/ThemeProvider.tsx
···
22
22
PublicationThemeProvider,
23
23
} from "./PublicationThemeProvider";
24
24
import { PubLeafletPublication } from "lexicons/api";
25
25
-
import { getColorContrast } from "./themeUtils";
25
25
+
import { getColorDifference } from "./themeUtils";
26
26
27
27
// define a function to set an Aria Color to a CSS Variable in RGB
28
28
function setCSSVariableToColor(
···
140
140
//sorting the accents by contrast on background
141
141
let sortedAccents = [accent1, accent2].sort((a, b) => {
142
142
return (
143
143
-
getColorContrast(
143
143
+
getColorDifference(
144
144
colorToString(b, "rgb"),
145
145
colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
146
146
) -
147
147
-
getColorContrast(
147
147
+
getColorDifference(
148
148
colorToString(a, "rgb"),
149
149
colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
150
150
)
···
156
156
// then use the not contrasty option
157
157
158
158
if (
159
159
-
getColorContrast(
159
159
+
getColorDifference(
160
160
colorToString(sortedAccents[0], "rgb"),
161
161
colorToString(primary, "rgb"),
162
162
-
) < 30 &&
163
163
-
getColorContrast(
162
162
+
) < 0.15 &&
163
163
+
getColorDifference(
164
164
colorToString(sortedAccents[1], "rgb"),
165
165
colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"),
166
166
-
) > 12
166
166
+
) > 0.08
167
167
) {
168
168
accentContrast = sortedAccents[1];
169
169
} else accentContrast = sortedAccents[0];
···
286
286
bgPage && accent1 && accent2
287
287
? [accent1, accent2].sort((a, b) => {
288
288
return (
289
289
-
getColorContrast(
289
289
+
getColorDifference(
290
290
colorToString(b, "rgb"),
291
291
colorToString(bgPage, "rgb"),
292
292
) -
293
293
-
getColorContrast(
293
293
+
getColorDifference(
294
294
colorToString(a, "rgb"),
295
295
colorToString(bgPage, "rgb"),
296
296
)
+4
-3
components/ThemeManager/themeUtils.ts
···
1
1
-
import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn";
1
1
+
import { parse, ColorSpace, sRGB, distance, OKLab } from "colorjs.io/fn";
2
2
3
3
// define the color defaults for everything
4
4
export const ThemeDefaults = {
···
17
17
};
18
18
19
19
// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
20
20
-
export function getColorContrast(color1: string, color2: string) {
20
20
+
export function getColorDifference(color1: string, color2: string) {
21
21
ColorSpace.register(sRGB);
22
22
+
ColorSpace.register(OKLab);
22
23
23
24
let parsedColor1 = parse(`rgb(${color1})`);
24
25
let parsedColor2 = parse(`rgb(${color2})`);
25
26
26
26
-
return contrastLstar(parsedColor1, parsedColor2);
27
27
+
return distance(parsedColor1, parsedColor2, "oklab");
27
28
}
+9
-5
components/Toolbar/BlockToolbar.tsx
···
5
5
import { useUIState } from "src/useUIState";
6
6
import { LockBlockButton } from "./LockBlockButton";
7
7
import { TextAlignmentButton } from "./TextAlignmentToolbar";
8
8
-
import { ImageFullBleedButton, ImageAltTextButton, ImageCoverButton } from "./ImageToolbar";
8
8
+
import {
9
9
+
ImageFullBleedButton,
10
10
+
ImageAltTextButton,
11
11
+
ImageCoverButton,
12
12
+
} from "./ImageToolbar";
9
13
import { DeleteSmall } from "components/Icons/DeleteSmall";
10
14
import { getSortedSelection } from "components/SelectionManager/selectionState";
11
15
···
37
41
>
38
42
<DeleteSmall />
39
43
</ToolbarButton>
40
40
-
<Separator classname="h-6" />
44
44
+
<Separator classname="h-6!" />
41
45
<MoveBlockButtons />
42
46
{blockType === "image" && (
43
47
<>
···
46
50
<ImageAltTextButton setToolbarState={props.setToolbarState} />
47
51
<ImageCoverButton />
48
52
{focusedEntityType?.data.value !== "canvas" && (
49
49
-
<Separator classname="h-6" />
53
53
+
<Separator classname="h-6!" />
50
54
)}
51
55
</>
52
56
)}
···
54
58
<>
55
59
<TextAlignmentButton setToolbarState={props.setToolbarState} />
56
60
{focusedEntityType?.data.value !== "canvas" && (
57
57
-
<Separator classname="h-6" />
61
61
+
<Separator classname="h-6!" />
58
62
)}
59
63
</>
60
64
)}
···
175
179
>
176
180
<MoveBlockDown />
177
181
</ToolbarButton>
178
178
-
<Separator classname="h-6" />
182
182
+
<Separator classname="h-6!" />
179
183
</>
180
184
);
181
185
};
+1
-1
components/Toolbar/HighlightToolbar.tsx
···
126
126
setLastUsedHightlight={props.setLastUsedHighlight}
127
127
/>
128
128
129
129
-
<Separator classname="h-6" />
129
129
+
<Separator classname="h-6!" />
130
130
<HighlightColorSettings pageID={props.pageID} />
131
131
</div>
132
132
</div>
+1
-1
components/Toolbar/InlineLinkToolbar.tsx
···
132
132
return (
133
133
<div className="w-full flex items-center gap-[6px] grow">
134
134
<LinkSmall />
135
135
-
<Separator classname="h-6" />
135
135
+
<Separator classname="h-6!" />
136
136
<Input
137
137
autoFocus
138
138
className="w-full grow bg-transparent border-none outline-hidden "
+2
-2
components/Toolbar/ListToolbar.tsx
···
131
131
>
132
132
<ListIndentIncreaseSmall />
133
133
</ToolbarButton>
134
134
-
<Separator classname="h-6" />
134
134
+
<Separator classname="h-6!" />
135
135
<ToolbarButton
136
136
disabled={!isList?.data.value}
137
137
tooltipContent=<div className="flex flex-col gap-1 justify-center">
138
138
<div className="text-center">Add a Checkbox</div>
139
139
<div className="flex gap-1 font-normal">
140
140
-
start line with <ShortcutKey>[</ShortcutKey>
140
140
+
<ShortcutKey>[</ShortcutKey>
141
141
<ShortcutKey>]</ShortcutKey>
142
142
</div>
143
143
</div>
+154
-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>
71
71
+
</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>
72
91
</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>
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>
93
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();
94
145
}
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>
114
114
-
</div>
146
146
+
}}
147
147
+
active={
148
148
+
blockType?.data.value === "text" &&
149
149
+
textSize?.data.value !== "small" &&
150
150
+
textSize?.data.value !== "large"
151
151
+
}
152
152
+
tooltipContent={<div>Normal Text</div>}
153
153
+
>
154
154
+
Text
155
155
+
</ToolbarButton>
156
156
+
<ToolbarButton
157
157
+
className={`px-[6px] text-lg ${props.className}`}
158
158
+
onClick={async () => {
159
159
+
if (!focusedBlock || !blockType) return;
160
160
+
if (blockType.data.value !== "text") {
161
161
+
// Convert to text block first if it's a heading
162
162
+
if (headingLevel)
163
163
+
await rep?.mutate.retractFact({ factID: headingLevel.id });
164
164
+
await rep?.mutate.assertFact({
165
165
+
entity: focusedBlock.entityID,
166
166
+
attribute: "block/type",
167
167
+
data: { type: "block-type-union", value: "text" },
168
168
+
});
115
169
}
116
116
-
>
117
117
-
<Header3Small />
118
118
-
</ToolbarButton>
119
119
-
<ToolbarButton
120
120
-
className={`px-[6px] ${props.className}`}
121
121
-
onClick={async () => {
170
170
+
// Set text size to large
171
171
+
await rep?.mutate.assertFact({
172
172
+
entity: focusedBlock.entityID,
173
173
+
attribute: "block/text-size",
174
174
+
data: { type: "text-size-union", value: "large" },
175
175
+
});
176
176
+
}}
177
177
+
active={
178
178
+
blockType?.data.value === "text" && textSize?.data.value === "large"
179
179
+
}
180
180
+
tooltipContent={<div>Large Text</div>}
181
181
+
>
182
182
+
<div className="leading-[1.625rem]">Large</div>
183
183
+
</ToolbarButton>
184
184
+
<ToolbarButton
185
185
+
className={`px-[6px] text-sm text-secondary ${props.className}`}
186
186
+
onClick={async () => {
187
187
+
if (!focusedBlock || !blockType) return;
188
188
+
if (blockType.data.value !== "text") {
189
189
+
// Convert to text block first if it's a heading
122
190
if (headingLevel)
123
191
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>
192
192
+
await rep?.mutate.assertFact({
193
193
+
entity: focusedBlock.entityID,
194
194
+
attribute: "block/type",
195
195
+
data: { type: "block-type-union", value: "text" },
196
196
+
});
197
197
+
}
198
198
+
// Set text size to small
199
199
+
await rep?.mutate.assertFact({
200
200
+
entity: focusedBlock.entityID,
201
201
+
attribute: "block/text-size",
202
202
+
data: { type: "text-size-union", value: "small" },
203
203
+
});
204
204
+
}}
205
205
+
active={
206
206
+
blockType?.data.value === "text" && textSize?.data.value === "small"
207
207
+
}
208
208
+
tooltipContent={<div>Small Text</div>}
209
209
+
>
210
210
+
<div className="leading-[1.625rem]">Small</div>
211
211
+
</ToolbarButton>
212
212
+
</>
154
213
);
155
214
};
156
215
+3
-3
components/Toolbar/TextToolbar.tsx
···
74
74
lastUsedHighlight={props.lastUsedHighlight}
75
75
setToolbarState={props.setToolbarState}
76
76
/>
77
77
-
<Separator classname="h-6" />
77
77
+
<Separator classname="h-6!" />
78
78
<LinkButton setToolbarState={props.setToolbarState} />
79
79
-
<Separator classname="h-6" />
79
79
+
<Separator classname="h-6!" />
80
80
<TextBlockTypeButton setToolbarState={props.setToolbarState} />
81
81
<TextAlignmentButton setToolbarState={props.setToolbarState} />
82
82
<ListButton setToolbarState={props.setToolbarState} />
83
83
-
<Separator classname="h-6" />
83
83
+
<Separator classname="h-6!" />
84
84
85
85
<LockBlockButton />
86
86
</>
+2
-2
components/utils/DotLoader.tsx
···
1
1
import { useEffect, useState } from "react";
2
2
3
3
-
export function DotLoader() {
3
3
+
export function DotLoader(props: { className?: string }) {
4
4
let [dots, setDots] = useState(1);
5
5
useEffect(() => {
6
6
let id = setInterval(() => {
···
11
11
};
12
12
}, []);
13
13
return (
14
14
-
<div className="w-[26px] h-[24px] text-center text-sm">
14
14
+
<div className={`w-[26px] h-[24px] text-center text-sm ${props.className}`}>
15
15
{".".repeat(dots) + "\u00a0".repeat(3 - dots)}
16
16
</div>
17
17
);
+5
-1
lexicons/api/lexicons.ts
···
1246
1246
plaintext: {
1247
1247
type: 'string',
1248
1248
},
1249
1249
+
textSize: {
1250
1250
+
type: 'string',
1251
1251
+
enum: ['default', 'small', 'large'],
1252
1252
+
},
1249
1253
facets: {
1250
1254
type: 'array',
1251
1255
items: {
···
1812
1816
},
1813
1817
showPrevNext: {
1814
1818
type: 'boolean',
1815
1815
-
default: true,
1819
1819
+
default: false,
1816
1820
},
1817
1821
},
1818
1822
},
+1
lexicons/api/types/pub/leaflet/blocks/text.ts
···
18
18
export interface Main {
19
19
$type?: 'pub.leaflet.blocks.text'
20
20
plaintext: string
21
21
+
textSize?: 'default' | 'small' | 'large'
21
22
facets?: PubLeafletRichtextFacet.Main[]
22
23
}
23
24
+8
lexicons/pub/leaflet/blocks/text.json
···
11
11
"plaintext": {
12
12
"type": "string"
13
13
},
14
14
+
"textSize": {
15
15
+
"type": "string",
16
16
+
"enum": [
17
17
+
"default",
18
18
+
"small",
19
19
+
"large"
20
20
+
]
21
21
+
},
14
22
"facets": {
15
23
"type": "array",
16
24
"items": {
+1
-1
lexicons/pub/leaflet/publication.json
···
58
58
},
59
59
"showPrevNext": {
60
60
"type": "boolean",
61
61
-
"default": true
61
61
+
"default": false
62
62
}
63
63
}
64
64
},
+1
lexicons/src/blocks.ts
···
10
10
required: ["plaintext"],
11
11
properties: {
12
12
plaintext: { type: "string" },
13
13
+
textSize: { type: "string", enum: ["default", "small", "large"] },
13
14
facets: {
14
15
type: "array",
15
16
items: { type: "ref", ref: PubLeafletRichTextFacet.id },
+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",
···
321
325
"text-alignment-type-union": {
322
326
type: "text-alignment-type-union";
323
327
value: "right" | "left" | "center" | "justify";
328
328
+
};
329
329
+
"text-size-union": {
330
330
+
type: "text-size-union";
331
331
+
value: "default" | "small" | "large";
324
332
};
325
333
"page-type-union": { type: "page-type-union"; value: "doc" | "canvas" };
326
334
"block-type-union": {
+3
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");
175
175
+
174
176
return (
175
177
<RenderYJSFragment
176
178
value={value?.data.value}
177
179
attrs={{
178
180
"data-alignment": a,
181
181
+
"data-text-size": textSize?.data.value,
179
182
}}
180
183
wrapper="p"
181
184
/>