a tool for shared writing and social publishing

Merge branch 'main' into feature/post-options

+529 -194
+1 -1
.prettierrc
··· 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 - import { 6 - restoreOAuthSession, 7 - OAuthSessionError, 8 - } from "src/atproto-oauth"; 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 - import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 50 + import { 51 + Notification, 52 + pingIdentityToUpdateNotification, 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 - await createMentionNotifications(result.uri, record, credentialSession.did!); 256 + await createMentionNotifications( 257 + result.uri, 258 + record, 259 + credentialSession.did!, 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 + 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 + ...(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 + 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 + 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 - mentionedPublications.set(publication.identity_did, feature.atURI); 876 + mentionedPublications.set( 877 + publication.identity_did, 878 + feature.atURI, 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 - const docRecord = document.data as PubLeafletDocument.Record; 890 + const docRecord = 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 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 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 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 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 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 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 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 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 + <KeyboardShortcut 62 + name="Make Title" 63 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "1"]} 64 + /> 65 + <KeyboardShortcut 66 + name="Make Heading" 67 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "2"]} 68 + /> 69 + <KeyboardShortcut 70 + name="Make Subheading" 71 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "3"]} 72 + /> 73 + <KeyboardShortcut 74 + name="Regular Text" 75 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "0"]} 76 + /> 77 + <KeyboardShortcut 78 + name="Large Text" 79 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "+"]} 80 + /> 81 + <KeyboardShortcut 82 + name="Small Text" 83 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "-"]} 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 - {isLoading ? <DotLoader /> : "Publish this Post!"} 202 + {isLoading ? ( 203 + <DotLoader className="h-[23px]" /> 204 + ) : ( 205 + "Publish this Post!" 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 - import { TextBlockCore, TextBlockCoreProps, RichText } from "./TextBlockCore"; 2 + import { 3 + TextBlockCore, 4 + TextBlockCoreProps, 5 + RichText, 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 - return ( 10 - <ProfilePopover 11 - didOrHandle={props.did} 12 - trigger={props.children} 13 - /> 14 - ); 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 - import { BaseTextBlock } from "../../BaseTextBlock"; 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 - import { TextBlock } from "./TextBlock"; 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 - import { StaticMathBlock } from "./StaticMathBlock"; 28 - import { PubCodeBlock } from "./PubCodeBlock"; 27 + import { StaticMathBlock } from "./Blocks/StaticMathBlock"; 28 + import { PubCodeBlock } from "./Blocks/PubCodeBlock"; 29 29 import { AppBskyFeedDefs } from "@atproto/api"; 30 - import { PubBlueskyPostBlock } from "./PublishBskyPostBlock"; 30 + import { PubBlueskyPostBlock } from "./Blocks/PublishBskyPostBlock"; 31 31 import { openPage } from "./PostPages"; 32 32 import { PageLinkBlock } from "components/Blocks/PageLinkBlock"; 33 - import { PublishedPageLinkBlock } from "./PublishedPageBlock"; 34 - import { PublishedPollBlock } from "./PublishedPollBlock"; 33 + import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock"; 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 - return <PubBlueskyPostBlock post={post} className={className} pageId={pageId} />; 176 + return ( 177 + <PubBlueskyPostBlock 178 + post={post} 179 + className={className} 180 + pageId={pageId} 181 + /> 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 - <p className={`textBlock ${className}`} {...blockProps}> 348 + <p 349 + className={`textBlock ${className} ${b.block.textSize === "small" ? "text-sm text-secondary" : b.block.textSize === "large" ? "text-lg" : ""}`} 350 + {...blockProps} 351 + > 343 352 <TextBlock 344 353 facets={b.block.facets} 345 354 plaintext={b.block.plaintext} ··· 349 358 /> 350 359 </p> 351 360 ); 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 - import { ThreadLink, QuotesLink } from "./PostLinks"; 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 - import { openPage } from "./PostPages"; 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 - openPage( 26 - props.pageId ? { type: "doc", id: props.pageId } : undefined, 27 - { type: "thread", uri: post.uri }, 28 - ); 25 + openPage(props.pageId ? { type: "doc", id: props.pageId } : undefined, { 26 + type: "thread", 27 + uri: post.uri, 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 - const parent = props.pageId ? { type: "doc" as const, id: props.pageId } : undefined; 54 + const parent = props.pageId 55 + ? { type: "doc" as const, id: props.pageId } 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 - import { PostContent, Block } from "./PostContent"; 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 - import { PostPageContext } from "./PostPageContext"; 19 - import { openPage, useOpenPages } from "./PostPages"; 18 + import { PostPageContext } from "../PostPageContext"; 19 + import { openPage, useOpenPages } from "../PostPages"; 20 20 import { 21 21 openInteractionDrawer, 22 22 setInteractionState, 23 23 useInteractionState, 24 - } from "./Interactions/Interactions"; 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 - let isOpen = openPages.some( 44 - (p) => p.type === "doc" && p.id === props.pageId, 45 - ); 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 - props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined, 61 + props.parentPageId 62 + ? { type: "doc", id: props.parentPageId } 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 - props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined, 222 + props.parentPageId 223 + ? { type: "doc", id: props.parentPageId } 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 - props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined, 244 + props.parentPageId 245 + ? { type: "doc", id: props.parentPageId } 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 - import { voteOnPublishedPoll } from "./voteOnPublishedPoll"; 13 - import { PollData } from "./fetchPollData"; 12 + import { voteOnPublishedPoll } from "../voteOnPublishedPoll"; 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 - import { getVoterIdentities, VoterIdentity } from "./getVoterIdentities"; 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 - import { TextBlockCore, TextBlockCoreProps } from "./TextBlockCore"; 16 - import { StaticMathBlock } from "./StaticMathBlock"; 15 + import { TextBlockCore, TextBlockCoreProps } from "./Blocks/TextBlockCore"; 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 - import { useHighlight } from "./useHighlight"; 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 + 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 - <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2"> 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 - {comments > 0 || quotes > 0 ? "| " : ""} 164 + {comments > 0 || quotes > 0 || tags.length > 0 ? ( 165 + <Separator classname="h-4! mx-1" /> 166 + ) : ( 167 + "" 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 - className={`flex flex-col pr-1 leading-snug max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`} 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 - <div className="truncate text-left pt-[1px]">{label}</div> 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 - import { TextBlock } from "components/Blocks/TextBlock"; 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 - type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p"; 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 + 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 + let textStyle = 133 + textSize?.data.value === "small" 134 + ? "text-sm" 135 + : textSize?.data.value === "large" 136 + ? "text-lg" 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 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 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 + 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 + let textStyle = 196 + textSize?.data.value === "small" 197 + ? "text-sm text-secondary" 198 + : textSize?.data.value === "large" 199 + ? "text-lg text-primary" 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 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 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 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 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 - const container = editorEl.closest('.relative') as HTMLElement | null; 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 + let [textSize] = 559 + (await repRef.current?.query((tx) => 560 + scanIndex(tx).eav(propsRef.current.entityID, "block/text-size"), 561 + )) || []; 562 + if (textSize) { 563 + await repRef.current?.mutate.assertFact({ 564 + entity: newEntityID, 565 + attribute: "block/text-size", 566 + data: { 567 + type: "text-size-union", 568 + value: textSize.data.value, 569 + }, 570 + }); 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 + let textSize = child.getAttribute("data-text-size"); 303 + if (textSize && ["default", "small", "large"].includes(textSize)) { 304 + rep.mutate.assertFact({ 305 + entity: entityID, 306 + attribute: "block/text-size", 307 + data: { 308 + type: "text-size-union", 309 + value: textSize as "default" | "small" | "large", 310 + }, 311 + }); 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 + altKey: true, 93 + key: ["1", "¡"], 94 + handler: async () => { 95 + let [sortedBlocks] = await getSortedSelectionBound(); 96 + for (let block of sortedBlocks) { 97 + await rep?.mutate.assertFact({ 98 + entity: block.value, 99 + attribute: "block/heading-level", 100 + data: { type: "number", value: 1 }, 101 + }); 102 + await rep?.mutate.assertFact({ 103 + entity: block.value, 104 + attribute: "block/type", 105 + data: { type: "block-type-union", value: "heading" }, 106 + }); 107 + } 108 + }, 109 + }, 110 + { 111 + metaKey: true, 112 + altKey: true, 113 + key: ["2", "™"], 114 + handler: async () => { 115 + let [sortedBlocks] = await getSortedSelectionBound(); 116 + for (let block of sortedBlocks) { 117 + await rep?.mutate.assertFact({ 118 + entity: block.value, 119 + attribute: "block/heading-level", 120 + data: { type: "number", value: 2 }, 121 + }); 122 + await rep?.mutate.assertFact({ 123 + entity: block.value, 124 + attribute: "block/type", 125 + data: { type: "block-type-union", value: "heading" }, 126 + }); 127 + } 128 + }, 129 + }, 130 + { 131 + metaKey: true, 132 + altKey: true, 133 + key: ["3", "£"], 134 + handler: async () => { 135 + let [sortedBlocks] = await getSortedSelectionBound(); 136 + for (let block of sortedBlocks) { 137 + await rep?.mutate.assertFact({ 138 + entity: block.value, 139 + attribute: "block/heading-level", 140 + data: { type: "number", value: 3 }, 141 + }); 142 + await rep?.mutate.assertFact({ 143 + entity: block.value, 144 + attribute: "block/type", 145 + data: { type: "block-type-union", value: "heading" }, 146 + }); 147 + } 148 + }, 149 + }, 150 + { 151 + metaKey: true, 152 + altKey: true, 153 + key: ["0", "º"], 154 + handler: async () => { 155 + let [sortedBlocks] = await getSortedSelectionBound(); 156 + for (let block of sortedBlocks) { 157 + // Convert to text block 158 + await rep?.mutate.assertFact({ 159 + entity: block.value, 160 + attribute: "block/type", 161 + data: { type: "block-type-union", value: "text" }, 162 + }); 163 + // Remove heading level if exists 164 + let headingLevel = await rep?.query((tx) => 165 + scanIndex(tx).eav(block.value, "block/heading-level"), 166 + ); 167 + if (headingLevel?.[0]) { 168 + await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 169 + } 170 + // Remove text-size to make it default 171 + let textSizeFact = await rep?.query((tx) => 172 + scanIndex(tx).eav(block.value, "block/text-size"), 173 + ); 174 + if (textSizeFact?.[0]) { 175 + await rep?.mutate.retractFact({ factID: textSizeFact[0].id }); 176 + } 177 + } 178 + }, 179 + }, 180 + { 181 + metaKey: true, 182 + altKey: true, 183 + key: ["+", "≠"], 184 + handler: async () => { 185 + let [sortedBlocks] = await getSortedSelectionBound(); 186 + for (let block of sortedBlocks) { 187 + // Convert to text block 188 + await rep?.mutate.assertFact({ 189 + entity: block.value, 190 + attribute: "block/type", 191 + data: { type: "block-type-union", value: "text" }, 192 + }); 193 + // Remove heading level if exists 194 + let headingLevel = await rep?.query((tx) => 195 + scanIndex(tx).eav(block.value, "block/heading-level"), 196 + ); 197 + if (headingLevel?.[0]) { 198 + await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 199 + } 200 + // Set text size to large 201 + await rep?.mutate.assertFact({ 202 + entity: block.value, 203 + attribute: "block/text-size", 204 + data: { type: "text-size-union", value: "large" }, 205 + }); 206 + } 207 + }, 208 + }, 209 + { 210 + metaKey: true, 211 + altKey: true, 212 + key: ["-", "–"], 213 + handler: async () => { 214 + let [sortedBlocks] = await getSortedSelectionBound(); 215 + for (let block of sortedBlocks) { 216 + // Convert to text block 217 + await rep?.mutate.assertFact({ 218 + entity: block.value, 219 + attribute: "block/type", 220 + data: { type: "block-type-union", value: "text" }, 221 + }); 222 + // Remove heading level if exists 223 + let headingLevel = await rep?.query((tx) => 224 + scanIndex(tx).eav(block.value, "block/heading-level"), 225 + ); 226 + if (headingLevel?.[0]) { 227 + await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 228 + } 229 + // Set text size to small 230 + await rep?.mutate.assertFact({ 231 + entity: block.value, 232 + attribute: "block/text-size", 233 + data: { type: "text-size-union", value: "small" }, 234 + }); 235 + } 236 + }, 237 + }, 238 + { 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 - 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 - import { getColorContrast } from "./themeUtils"; 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 - getColorContrast( 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 - getColorContrast( 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 - getColorContrast( 194 + getColorDifference( 195 195 colorToString(sortedAccents[0], "rgb"), 196 196 colorToString(newTheme.primary, "rgb"), 197 - ) < 30 && 198 - getColorContrast( 197 + ) < 0.15 && 198 + getColorDifference( 199 199 colorToString(sortedAccents[1], "rgb"), 200 200 colorToString( 201 201 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 202 202 "rgb", 203 203 ), 204 - ) > 12 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 - import { getColorContrast } from "./themeUtils"; 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 - getColorContrast( 143 + getColorDifference( 144 144 colorToString(b, "rgb"), 145 145 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 146 146 ) - 147 - getColorContrast( 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 - getColorContrast( 159 + getColorDifference( 160 160 colorToString(sortedAccents[0], "rgb"), 161 161 colorToString(primary, "rgb"), 162 - ) < 30 && 163 - getColorContrast( 162 + ) < 0.15 && 163 + getColorDifference( 164 164 colorToString(sortedAccents[1], "rgb"), 165 165 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 166 - ) > 12 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 - getColorContrast( 289 + getColorDifference( 290 290 colorToString(b, "rgb"), 291 291 colorToString(bgPage, "rgb"), 292 292 ) - 293 - getColorContrast( 293 + getColorDifference( 294 294 colorToString(a, "rgb"), 295 295 colorToString(bgPage, "rgb"), 296 296 )
+4 -3
components/ThemeManager/themeUtils.ts
··· 1 - import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn"; 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 - export function getColorContrast(color1: string, color2: string) { 20 + export function getColorDifference(color1: string, color2: string) { 21 21 ColorSpace.register(sRGB); 22 + ColorSpace.register(OKLab); 22 23 23 24 let parsedColor1 = parse(`rgb(${color1})`); 24 25 let parsedColor2 = parse(`rgb(${color2})`); 25 26 26 - return contrastLstar(parsedColor1, parsedColor2); 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 - import { ImageFullBleedButton, ImageAltTextButton, ImageCoverButton } from "./ImageToolbar"; 8 + import { 9 + ImageFullBleedButton, 10 + ImageAltTextButton, 11 + ImageCoverButton, 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 - <Separator classname="h-6" /> 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 - <Separator classname="h-6" /> 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 - <Separator classname="h-6" /> 61 + <Separator classname="h-6!" /> 58 62 )} 59 63 </> 60 64 )} ··· 175 179 > 176 180 <MoveBlockDown /> 177 181 </ToolbarButton> 178 - <Separator classname="h-6" /> 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 - <Separator classname="h-6" /> 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 - <Separator classname="h-6" /> 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 - <Separator classname="h-6" /> 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 - start line with <ShortcutKey>[</ShortcutKey> 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 - import { ShortcutKey } from "components/Layout"; 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 + 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 - <div className="flex w-full justify-between items-center gap-4"> 55 - <div className="flex items-center gap-[6px]"> 56 - <ToolbarButton 57 - className={props.className} 58 - onClick={() => { 59 - setLevel(1); 60 - }} 61 - active={ 62 - blockType?.data.value === "heading" && 63 - headingLevel?.data.value === 1 64 - } 65 - tooltipContent={ 66 - <div className="flex flex-col justify-center"> 67 - <div className="font-bold text-center">Title</div> 68 - <div className="flex gap-1 font-normal"> 69 - start line with 70 - <ShortcutKey>#</ShortcutKey> 71 - </div> 56 + <> 57 + <ToolbarButton 58 + className={props.className} 59 + onClick={() => { 60 + setLevel(1); 61 + }} 62 + active={ 63 + blockType?.data.value === "heading" && headingLevel?.data.value === 1 64 + } 65 + tooltipContent={ 66 + <div className="flex flex-col justify-center"> 67 + <div className="font-bold text-center">Title</div> 68 + <div className="flex gap-1 font-normal"> 69 + start line with 70 + <ShortcutKey>#</ShortcutKey> 71 + </div> 72 + </div> 73 + } 74 + > 75 + <Header1Small /> 76 + </ToolbarButton> 77 + <ToolbarButton 78 + className={props.className} 79 + onClick={() => { 80 + setLevel(2); 81 + }} 82 + active={ 83 + blockType?.data.value === "heading" && headingLevel?.data.value === 2 84 + } 85 + tooltipContent={ 86 + <div className="flex flex-col justify-center"> 87 + <div className="font-bold text-center">Heading</div> 88 + <div className="flex gap-1 font-normal"> 89 + start line with 90 + <ShortcutKey>##</ShortcutKey> 72 91 </div> 73 - } 74 - > 75 - <Header1Small /> 76 - </ToolbarButton> 77 - <ToolbarButton 78 - className={props.className} 79 - onClick={() => { 80 - setLevel(2); 81 - }} 82 - active={ 83 - blockType?.data.value === "heading" && 84 - headingLevel?.data.value === 2 85 - } 86 - tooltipContent={ 87 - <div className="flex flex-col justify-center"> 88 - <div className="font-bold text-center">Heading</div> 89 - <div className="flex gap-1 font-normal"> 90 - start line with 91 - <ShortcutKey>##</ShortcutKey> 92 - </div> 92 + </div> 93 + } 94 + > 95 + <Header2Small /> 96 + </ToolbarButton> 97 + <ToolbarButton 98 + className={props.className} 99 + onClick={() => { 100 + setLevel(3); 101 + }} 102 + active={ 103 + blockType?.data.value === "heading" && headingLevel?.data.value === 3 104 + } 105 + tooltipContent={ 106 + <div className="flex flex-col justify-center"> 107 + <div className="font-bold text-center">Subheading</div> 108 + <div className="flex gap-1 font-normal"> 109 + start line with 110 + <ShortcutKey>###</ShortcutKey> 93 111 </div> 112 + </div> 113 + } 114 + > 115 + <Header3Small /> 116 + </ToolbarButton> 117 + <Separator classname="h-6!" /> 118 + <ToolbarButton 119 + className={`px-[6px] ${props.className}`} 120 + onClick={async () => { 121 + if (headingLevel) 122 + await rep?.mutate.retractFact({ factID: headingLevel.id }); 123 + if (textSize) await rep?.mutate.retractFact({ factID: textSize.id }); 124 + if (!focusedBlock || !blockType) return; 125 + if (blockType.data.value !== "text") { 126 + let existingEditor = 127 + useEditorStates.getState().editorStates[focusedBlock.entityID]; 128 + let selection = existingEditor?.editor.selection; 129 + await rep?.mutate.assertFact({ 130 + entity: focusedBlock?.entityID, 131 + attribute: "block/type", 132 + data: { type: "block-type-union", value: "text" }, 133 + }); 134 + 135 + let newEditor = 136 + useEditorStates.getState().editorStates[focusedBlock.entityID]; 137 + if (!newEditor || !selection) return; 138 + newEditor.view?.dispatch( 139 + newEditor.editor.tr.setSelection( 140 + TextSelection.create(newEditor.editor.doc, selection.anchor), 141 + ), 142 + ); 143 + 144 + newEditor.view?.focus(); 94 145 } 95 - > 96 - <Header2Small /> 97 - </ToolbarButton> 98 - <ToolbarButton 99 - className={props.className} 100 - onClick={() => { 101 - setLevel(3); 102 - }} 103 - active={ 104 - blockType?.data.value === "heading" && 105 - headingLevel?.data.value === 3 106 - } 107 - tooltipContent={ 108 - <div className="flex flex-col justify-center"> 109 - <div className="font-bold text-center">Subheading</div> 110 - <div className="flex gap-1 font-normal"> 111 - start line with 112 - <ShortcutKey>###</ShortcutKey> 113 - </div> 114 - </div> 146 + }} 147 + active={ 148 + blockType?.data.value === "text" && 149 + textSize?.data.value !== "small" && 150 + textSize?.data.value !== "large" 151 + } 152 + tooltipContent={<div>Normal Text</div>} 153 + > 154 + Text 155 + </ToolbarButton> 156 + <ToolbarButton 157 + className={`px-[6px] text-lg ${props.className}`} 158 + onClick={async () => { 159 + if (!focusedBlock || !blockType) return; 160 + if (blockType.data.value !== "text") { 161 + // Convert to text block first if it's a heading 162 + if (headingLevel) 163 + await rep?.mutate.retractFact({ factID: headingLevel.id }); 164 + await rep?.mutate.assertFact({ 165 + entity: focusedBlock.entityID, 166 + attribute: "block/type", 167 + data: { type: "block-type-union", value: "text" }, 168 + }); 115 169 } 116 - > 117 - <Header3Small /> 118 - </ToolbarButton> 119 - <ToolbarButton 120 - className={`px-[6px] ${props.className}`} 121 - onClick={async () => { 170 + // Set text size to large 171 + await rep?.mutate.assertFact({ 172 + entity: focusedBlock.entityID, 173 + attribute: "block/text-size", 174 + data: { type: "text-size-union", value: "large" }, 175 + }); 176 + }} 177 + active={ 178 + blockType?.data.value === "text" && textSize?.data.value === "large" 179 + } 180 + tooltipContent={<div>Large Text</div>} 181 + > 182 + <div className="leading-[1.625rem]">Large</div> 183 + </ToolbarButton> 184 + <ToolbarButton 185 + className={`px-[6px] text-sm text-secondary ${props.className}`} 186 + onClick={async () => { 187 + if (!focusedBlock || !blockType) return; 188 + if (blockType.data.value !== "text") { 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 - if (!focusedBlock || !blockType) return; 125 - if (blockType.data.value !== "text") { 126 - let existingEditor = 127 - useEditorStates.getState().editorStates[focusedBlock.entityID]; 128 - let selection = existingEditor?.editor.selection; 129 - await rep?.mutate.assertFact({ 130 - entity: focusedBlock?.entityID, 131 - attribute: "block/type", 132 - data: { type: "block-type-union", value: "text" }, 133 - }); 134 - 135 - let newEditor = 136 - useEditorStates.getState().editorStates[focusedBlock.entityID]; 137 - if (!newEditor || !selection) return; 138 - newEditor.view?.dispatch( 139 - newEditor.editor.tr.setSelection( 140 - TextSelection.create(newEditor.editor.doc, selection.anchor), 141 - ), 142 - ); 143 - 144 - newEditor.view?.focus(); 145 - } 146 - }} 147 - active={blockType?.data.value === "text"} 148 - tooltipContent={<div>Paragraph</div>} 149 - > 150 - Paragraph 151 - </ToolbarButton> 152 - </div> 153 - </div> 192 + await rep?.mutate.assertFact({ 193 + entity: focusedBlock.entityID, 194 + attribute: "block/type", 195 + data: { type: "block-type-union", value: "text" }, 196 + }); 197 + } 198 + // Set text size to small 199 + await rep?.mutate.assertFact({ 200 + entity: focusedBlock.entityID, 201 + attribute: "block/text-size", 202 + data: { type: "text-size-union", value: "small" }, 203 + }); 204 + }} 205 + active={ 206 + blockType?.data.value === "text" && textSize?.data.value === "small" 207 + } 208 + tooltipContent={<div>Small Text</div>} 209 + > 210 + <div className="leading-[1.625rem]">Small</div> 211 + </ToolbarButton> 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 - <Separator classname="h-6" /> 77 + <Separator classname="h-6!" /> 78 78 <LinkButton setToolbarState={props.setToolbarState} /> 79 - <Separator classname="h-6" /> 79 + <Separator classname="h-6!" /> 80 80 <TextBlockTypeButton setToolbarState={props.setToolbarState} /> 81 81 <TextAlignmentButton setToolbarState={props.setToolbarState} /> 82 82 <ListButton setToolbarState={props.setToolbarState} /> 83 - <Separator classname="h-6" /> 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 - export function DotLoader() { 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 - <div className="w-[26px] h-[24px] text-center text-sm"> 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 + textSize: { 1250 + type: 'string', 1251 + enum: ['default', 'small', 'large'], 1252 + }, 1249 1253 facets: { 1250 1254 type: 'array', 1251 1255 items: { ··· 1812 1816 }, 1813 1817 showPrevNext: { 1814 1818 type: 'boolean', 1815 - default: true, 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 + 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 + "textSize": { 15 + "type": "string", 16 + "enum": [ 17 + "default", 18 + "small", 19 + "large" 20 + ] 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 - "default": true 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 + 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 + "block/text-size": { 75 + type: "text-size-union", 76 + cardinality: "one", 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 + }; 329 + "text-size-union": { 330 + type: "text-size-union"; 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 + let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size"); 175 + 174 176 return ( 175 177 <RenderYJSFragment 176 178 value={value?.data.value} 177 179 attrs={{ 178 180 "data-alignment": a, 181 + "data-text-size": textSize?.data.value, 179 182 }} 180 183 wrapper="p" 181 184 />