a tool for shared writing and social publishing

Feature/atp canvas blocks (#227)

* add page block type to lexicon and basic component

* add page block generated files

* set up leaflet and post to use the same layout component and adjusted
the interaction panel to work within that

* unified page, page wrapper, and post layouts, made subpage look nice in post

* added a bit of padding under the bottom of posts, deleted unused code

* remove extra merge conflict

* add published page block preview

* always render interaction drawer

* factor out useDrawerOpen

* loosen base_path to string

* scroll pages and comments into view properly

* fixed a weird scrolling issue, also squished the interaction drawer and
page together

* make quotes have a page component

* open sub-page if quoted

* change scroll into view threshold

* scroll into view comments panel

* render quote content from subpages

* implement subpage interaction drawers and buttons

* give quote popup a z-index

* fix actually posting comments on subpages

* convert comment to json before returning

* add interactions preview for subpages

* count top level quotes and comments correctly

* ensure layout doesn't break with canvases

* prev fix broke doc pages, fixed that lol

* specify post pages are docs

* absolutely position comments on subpage preview

* add canvas lexicon

* extract out page component to lineardocumentpage

* add canvas pages and previews

* handle narrow widths for the canvas

* hook rules hooks rule

* remove small canvas size

* added interactions and metadata to leaflet and posted canvases

* some fixes

* make quotehandler work on canvas

---------

Co-authored-by: celine <celine@hyperlink.academy>

authored by awarm.space

celine and committed by
GitHub
6f515d5a aa798706

+1128 -185
+87 -12
actions/publishToPublication.ts
··· 12 PubLeafletBlocksUnorderedList, 13 PubLeafletDocument, 14 PubLeafletPagesLinearDocument, 15 PubLeafletRichtextFacet, 16 PubLeafletBlocksWebsite, 17 PubLeafletBlocksCode, ··· 95 $type: "pub.leaflet.pages.linearDocument", 96 blocks: firstPageBlocks, 97 }, 98 - ...pages.map((p) => ({ 99 - $type: "pub.leaflet.pages.linearDocument", 100 - id: p.id, 101 - blocks: p.blocks, 102 - })), 103 ], 104 }; 105 let rkey = draft?.doc ? new AtUri(draft.doc).rkey : TID.nextStr(); ··· 139 root_entity: string, 140 ) { 141 let scan = scanIndexLocal(facts); 142 - let pages: { id: string; blocks: PubLeafletPagesLinearDocument.Block[] }[] = 143 - []; 144 145 let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 146 if (!firstEntity) throw new Error("No root page"); ··· 228 if (b.type === "card") { 229 let [page] = scan.eav(b.value, "block/card"); 230 if (!page) return; 231 - let blocks = getBlocksWithTypeLocal(facts, page.data.value); 232 - pages.push({ 233 - id: page.data.value, 234 - blocks: await blocksToRecord(blocks), 235 - }); 236 let block: $Typed<PubLeafletBlocksPage.Main> = { 237 $type: "pub.leaflet.blocks.page", 238 id: page.data.value, ··· 358 return block; 359 } 360 return; 361 } 362 } 363
··· 12 PubLeafletBlocksUnorderedList, 13 PubLeafletDocument, 14 PubLeafletPagesLinearDocument, 15 + PubLeafletPagesCanvas, 16 PubLeafletRichtextFacet, 17 PubLeafletBlocksWebsite, 18 PubLeafletBlocksCode, ··· 96 $type: "pub.leaflet.pages.linearDocument", 97 blocks: firstPageBlocks, 98 }, 99 + ...pages.map((p) => { 100 + if (p.type === "canvas") { 101 + return { 102 + $type: "pub.leaflet.pages.canvas" as const, 103 + id: p.id, 104 + blocks: p.blocks as PubLeafletPagesCanvas.Block[], 105 + }; 106 + } else { 107 + return { 108 + $type: "pub.leaflet.pages.linearDocument" as const, 109 + id: p.id, 110 + blocks: p.blocks as PubLeafletPagesLinearDocument.Block[], 111 + }; 112 + } 113 + }), 114 ], 115 }; 116 let rkey = draft?.doc ? new AtUri(draft.doc).rkey : TID.nextStr(); ··· 150 root_entity: string, 151 ) { 152 let scan = scanIndexLocal(facts); 153 + let pages: { 154 + id: string; 155 + blocks: 156 + | PubLeafletPagesLinearDocument.Block[] 157 + | PubLeafletPagesCanvas.Block[]; 158 + type: "doc" | "canvas"; 159 + }[] = []; 160 161 let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 162 if (!firstEntity) throw new Error("No root page"); ··· 244 if (b.type === "card") { 245 let [page] = scan.eav(b.value, "block/card"); 246 if (!page) return; 247 + let [pageType] = scan.eav(page.data.value, "page/type"); 248 + 249 + if (pageType?.data.value === "canvas") { 250 + let canvasBlocks = await canvasBlocksToRecord(page.data.value); 251 + pages.push({ 252 + id: page.data.value, 253 + blocks: canvasBlocks, 254 + type: "canvas", 255 + }); 256 + } else { 257 + let blocks = getBlocksWithTypeLocal(facts, page.data.value); 258 + pages.push({ 259 + id: page.data.value, 260 + blocks: await blocksToRecord(blocks), 261 + type: "doc", 262 + }); 263 + } 264 + 265 let block: $Typed<PubLeafletBlocksPage.Main> = { 266 $type: "pub.leaflet.blocks.page", 267 id: page.data.value, ··· 387 return block; 388 } 389 return; 390 + } 391 + 392 + async function canvasBlocksToRecord( 393 + pageID: string, 394 + ): Promise<PubLeafletPagesCanvas.Block[]> { 395 + let canvasBlocks = scan.eav(pageID, "canvas/block"); 396 + return ( 397 + await Promise.all( 398 + canvasBlocks.map(async (canvasBlock) => { 399 + let blockEntity = canvasBlock.data.value; 400 + let position = canvasBlock.data.position; 401 + 402 + // Get the block content 403 + let blockType = scan.eav(blockEntity, "block/type")?.[0]; 404 + if (!blockType) return null; 405 + 406 + let block: Block = { 407 + type: blockType.data.value, 408 + value: blockEntity, 409 + parent: pageID, 410 + position: "", 411 + factID: canvasBlock.id, 412 + }; 413 + 414 + let content = await blockToRecord(block); 415 + if (!content) return null; 416 + 417 + // Get canvas-specific properties 418 + let width = 419 + scan.eav(blockEntity, "canvas/block/width")?.[0]?.data.value || 360; 420 + let rotation = scan.eav(blockEntity, "canvas/block/rotation")?.[0] 421 + ?.data.value; 422 + 423 + let canvasBlockRecord: PubLeafletPagesCanvas.Block = { 424 + $type: "pub.leaflet.pages.canvas#block", 425 + block: content, 426 + x: position.x, 427 + y: position.y, 428 + width, 429 + ...(rotation !== undefined && { rotation }), 430 + }; 431 + 432 + return canvasBlockRecord; 433 + }), 434 + ) 435 + ).filter((b): b is PubLeafletPagesCanvas.Block => b !== null); 436 } 437 } 438
+240
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
···
··· 1 + "use client"; 2 + import { 3 + PubLeafletPagesCanvas, 4 + PubLeafletPagesLinearDocument, 5 + PubLeafletPublication, 6 + } from "lexicons/api"; 7 + import { PostPageData } from "./getPostPageData"; 8 + import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 9 + import { AppBskyFeedDefs } from "@atproto/api"; 10 + import { PageWrapper } from "components/Pages/Page"; 11 + import { Block } from "./PostContent"; 12 + import { CanvasBackgroundPattern } from "components/Canvas"; 13 + import { 14 + getCommentCount, 15 + getQuoteCount, 16 + Interactions, 17 + } from "./Interactions/Interactions"; 18 + import { Separator } from "components/Layout"; 19 + import { Popover } from "components/Popover"; 20 + import { InfoSmall } from "components/Icons/InfoSmall"; 21 + import { PostHeader } from "./PostHeader/PostHeader"; 22 + import { useDrawerOpen } from "./Interactions/InteractionDrawer"; 23 + 24 + export function CanvasPage({ 25 + document, 26 + blocks, 27 + did, 28 + profile, 29 + preferences, 30 + pubRecord, 31 + prerenderedCodeBlocks, 32 + bskyPostData, 33 + document_uri, 34 + pageId, 35 + pageOptions, 36 + fullPageScroll, 37 + pages, 38 + }: { 39 + document_uri: string; 40 + document: PostPageData; 41 + blocks: PubLeafletPagesCanvas.Block[]; 42 + profile: ProfileViewDetailed; 43 + pubRecord: PubLeafletPublication.Record; 44 + did: string; 45 + prerenderedCodeBlocks?: Map<string, string>; 46 + bskyPostData: AppBskyFeedDefs.PostView[]; 47 + preferences: { showComments?: boolean }; 48 + pageId?: string; 49 + pageOptions?: React.ReactNode; 50 + fullPageScroll: boolean; 51 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 52 + }) { 53 + let hasPageBackground = !!pubRecord.theme?.showPageBackground; 54 + let isSubpage = !!pageId; 55 + let drawer = useDrawerOpen(document_uri); 56 + 57 + return ( 58 + <PageWrapper 59 + pageType="canvas" 60 + fullPageScroll={fullPageScroll} 61 + cardBorderHidden={!hasPageBackground} 62 + id={pageId ? `post-page-${pageId}` : "post-page"} 63 + drawerOpen={ 64 + !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId) 65 + } 66 + pageOptions={pageOptions} 67 + > 68 + <CanvasMetadata 69 + pageId={pageId} 70 + isSubpage={isSubpage} 71 + data={document} 72 + profile={profile} 73 + preferences={preferences} 74 + commentsCount={getCommentCount(document, pageId)} 75 + quotesCount={getQuoteCount(document, pageId)} 76 + /> 77 + <CanvasContent 78 + blocks={blocks} 79 + did={did} 80 + prerenderedCodeBlocks={prerenderedCodeBlocks} 81 + bskyPostData={bskyPostData} 82 + pageId={pageId} 83 + pages={pages} 84 + /> 85 + </PageWrapper> 86 + ); 87 + } 88 + 89 + function CanvasContent({ 90 + blocks, 91 + did, 92 + prerenderedCodeBlocks, 93 + bskyPostData, 94 + pageId, 95 + pages, 96 + }: { 97 + blocks: PubLeafletPagesCanvas.Block[]; 98 + did: string; 99 + prerenderedCodeBlocks?: Map<string, string>; 100 + bskyPostData: AppBskyFeedDefs.PostView[]; 101 + pageId?: string; 102 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 103 + }) { 104 + let height = blocks.length > 0 ? Math.max(...blocks.map((b) => b.y), 0) : 0; 105 + 106 + return ( 107 + <div className="canvasWrapper h-full w-fit overflow-y-scroll postContent"> 108 + <div 109 + style={{ 110 + minHeight: height + 512, 111 + contain: "size layout paint", 112 + }} 113 + className="relative h-full w-[1272px]" 114 + > 115 + <CanvasBackground /> 116 + 117 + {blocks 118 + .sort((a, b) => { 119 + if (a.y === b.y) { 120 + return a.x - b.x; 121 + } 122 + return a.y - b.y; 123 + }) 124 + .map((canvasBlock, index) => { 125 + return ( 126 + <CanvasBlock 127 + key={index} 128 + canvasBlock={canvasBlock} 129 + did={did} 130 + prerenderedCodeBlocks={prerenderedCodeBlocks} 131 + bskyPostData={bskyPostData} 132 + pageId={pageId} 133 + pages={pages} 134 + index={index} 135 + /> 136 + ); 137 + })} 138 + </div> 139 + </div> 140 + ); 141 + } 142 + 143 + function CanvasBlock({ 144 + canvasBlock, 145 + did, 146 + prerenderedCodeBlocks, 147 + bskyPostData, 148 + pageId, 149 + pages, 150 + index, 151 + }: { 152 + canvasBlock: PubLeafletPagesCanvas.Block; 153 + did: string; 154 + prerenderedCodeBlocks?: Map<string, string>; 155 + bskyPostData: AppBskyFeedDefs.PostView[]; 156 + pageId?: string; 157 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 158 + index: number; 159 + }) { 160 + let { x, y, width, rotation } = canvasBlock; 161 + let transform = `translate(${x}px, ${y}px)${rotation ? ` rotate(${rotation}deg)` : ""}`; 162 + 163 + // Wrap the block in a LinearDocument.Block structure for compatibility 164 + let linearBlock: PubLeafletPagesLinearDocument.Block = { 165 + $type: "pub.leaflet.pages.linearDocument#block", 166 + block: canvasBlock.block, 167 + }; 168 + 169 + return ( 170 + <div 171 + className="absolute rounded-lg flex items-stretch origin-center p-3" 172 + style={{ 173 + top: 0, 174 + left: 0, 175 + width, 176 + transform, 177 + }} 178 + > 179 + <div className="contents"> 180 + <Block 181 + pageId={pageId} 182 + pages={pages} 183 + bskyPostData={bskyPostData} 184 + block={linearBlock} 185 + did={did} 186 + index={[index]} 187 + preview={false} 188 + prerenderedCodeBlocks={prerenderedCodeBlocks} 189 + /> 190 + </div> 191 + </div> 192 + ); 193 + } 194 + 195 + const CanvasMetadata = (props: { 196 + pageId: string | undefined; 197 + isSubpage: boolean | undefined; 198 + data: PostPageData; 199 + profile: ProfileViewDetailed; 200 + preferences: { showComments?: boolean }; 201 + quotesCount: number | undefined; 202 + commentsCount: number | undefined; 203 + }) => { 204 + return ( 205 + <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> 206 + <Interactions 207 + quotesCount={props.quotesCount || 0} 208 + commentsCount={props.commentsCount || 0} 209 + compact 210 + showComments={props.preferences.showComments} 211 + pageId={props.pageId} 212 + /> 213 + {!props.isSubpage && ( 214 + <> 215 + <Separator classname="h-5" /> 216 + <Popover 217 + side="left" 218 + align="start" 219 + className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" 220 + trigger={<InfoSmall />} 221 + > 222 + <PostHeader 223 + data={props.data} 224 + profile={props.profile} 225 + preferences={props.preferences} 226 + /> 227 + </Popover> 228 + </> 229 + )} 230 + </div> 231 + ); 232 + }; 233 + 234 + const CanvasBackground = () => { 235 + return ( 236 + <div className="w-full h-full pointer-events-none"> 237 + <CanvasBackgroundPattern pattern="grid" /> 238 + </div> 239 + ); 240 + };
+32 -1
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 5 import type { Json } from "supabase/database.types"; 6 import { create } from "zustand"; 7 import type { Comment } from "./Comments"; 8 - import { QuotePosition } from "../quotePosition"; 9 import { useContext } from "react"; 10 import { PostPageContext } from "../PostPageContext"; 11 import { scrollIntoView } from "src/utils/scrollIntoView"; 12 13 export type InteractionState = { 14 drawerOpen: undefined | boolean; ··· 149 </div> 150 ); 151 };
··· 5 import type { Json } from "supabase/database.types"; 6 import { create } from "zustand"; 7 import type { Comment } from "./Comments"; 8 + import { decodeQuotePosition, QuotePosition } from "../quotePosition"; 9 import { useContext } from "react"; 10 import { PostPageContext } from "../PostPageContext"; 11 import { scrollIntoView } from "src/utils/scrollIntoView"; 12 + import { PostPageData } from "../getPostPageData"; 13 + import { PubLeafletComment } from "lexicons/api"; 14 15 export type InteractionState = { 16 drawerOpen: undefined | boolean; ··· 151 </div> 152 ); 153 }; 154 + 155 + export function getCommentCount(document: PostPageData, pageId?: string) { 156 + if (!document) return; 157 + 158 + if (pageId) 159 + return document.document_mentions_in_bsky.filter((q) => 160 + q.link.includes(pageId), 161 + ).length; 162 + else 163 + return document.document_mentions_in_bsky.filter((q) => { 164 + const url = new URL(q.link); 165 + const quoteParam = url.pathname.split("/l-quote/")[1]; 166 + if (!quoteParam) return null; 167 + const quotePosition = decodeQuotePosition(quoteParam); 168 + return !quotePosition?.pageId; 169 + }).length; 170 + } 171 + 172 + export function getQuoteCount(document: PostPageData, pageId?: string) { 173 + if (!document) return; 174 + if (pageId) 175 + return document.comments_on_documents.filter( 176 + (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId, 177 + ).length; 178 + else 179 + return document.comments_on_documents.filter( 180 + (c) => !(c.record as PubLeafletComment.Record)?.onPage, 181 + ).length; 182 + }
+135
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
···
··· 1 + "use client"; 2 + import { 3 + PubLeafletComment, 4 + PubLeafletDocument, 5 + PubLeafletPagesLinearDocument, 6 + PubLeafletPublication, 7 + } from "lexicons/api"; 8 + import { PostPageData } from "./getPostPageData"; 9 + import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 11 + import { SubscribeWithBluesky } from "app/lish/Subscribe"; 12 + import { EditTiny } from "components/Icons/EditTiny"; 13 + import { 14 + getCommentCount, 15 + getQuoteCount, 16 + Interactions, 17 + } from "./Interactions/Interactions"; 18 + import { PostContent } from "./PostContent"; 19 + import { PostHeader } from "./PostHeader/PostHeader"; 20 + import { useIdentityData } from "components/IdentityProvider"; 21 + import { AppBskyFeedDefs } from "@atproto/api"; 22 + import { useDrawerOpen } from "./Interactions/InteractionDrawer"; 23 + import { PageWrapper } from "components/Pages/Page"; 24 + import { decodeQuotePosition } from "./quotePosition"; 25 + 26 + export function LinearDocumentPage({ 27 + document, 28 + blocks, 29 + did, 30 + profile, 31 + preferences, 32 + pubRecord, 33 + prerenderedCodeBlocks, 34 + bskyPostData, 35 + document_uri, 36 + pageId, 37 + pageOptions, 38 + fullPageScroll, 39 + }: { 40 + document_uri: string; 41 + document: PostPageData; 42 + blocks: PubLeafletPagesLinearDocument.Block[]; 43 + profile?: ProfileViewDetailed; 44 + pubRecord: PubLeafletPublication.Record; 45 + did: string; 46 + prerenderedCodeBlocks?: Map<string, string>; 47 + bskyPostData: AppBskyFeedDefs.PostView[]; 48 + preferences: { showComments?: boolean }; 49 + pageId?: string; 50 + pageOptions?: React.ReactNode; 51 + fullPageScroll: boolean; 52 + }) { 53 + let { identity } = useIdentityData(); 54 + let drawer = useDrawerOpen(document_uri); 55 + 56 + if (!document || !document.documents_in_publications[0].publications) 57 + return null; 58 + 59 + let hasPageBackground = !!pubRecord.theme?.showPageBackground; 60 + let record = document.data as PubLeafletDocument.Record; 61 + 62 + const isSubpage = !!pageId; 63 + 64 + return ( 65 + <> 66 + <PageWrapper 67 + pageType="doc" 68 + fullPageScroll={fullPageScroll} 69 + cardBorderHidden={!hasPageBackground} 70 + id={pageId ? `post-page-${pageId}` : "post-page"} 71 + drawerOpen={ 72 + !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId) 73 + } 74 + pageOptions={pageOptions} 75 + > 76 + {!isSubpage && profile && ( 77 + <PostHeader 78 + data={document} 79 + profile={profile} 80 + preferences={preferences} 81 + /> 82 + )} 83 + <PostContent 84 + pages={record.pages as PubLeafletPagesLinearDocument.Main[]} 85 + pageId={pageId} 86 + bskyPostData={bskyPostData} 87 + blocks={blocks} 88 + did={did} 89 + prerenderedCodeBlocks={prerenderedCodeBlocks} 90 + /> 91 + <Interactions 92 + pageId={pageId} 93 + showComments={preferences.showComments} 94 + commentsCount={getCommentCount(document, pageId) || 0} 95 + quotesCount={getQuoteCount(document, pageId) || 0} 96 + /> 97 + {!isSubpage && ( 98 + <> 99 + <hr className="border-border-light mb-4 mt-4 sm:mx-4 mx-3" /> 100 + <div className="sm:px-4 px-3"> 101 + {identity && 102 + identity.atp_did === 103 + document.documents_in_publications[0]?.publications 104 + ?.identity_did ? ( 105 + <a 106 + href={`https://leaflet.pub/${document.leaflets_in_publications[0]?.leaflet}`} 107 + className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1 mx-auto" 108 + > 109 + <EditTiny /> Edit Post 110 + </a> 111 + ) : ( 112 + <SubscribeWithBluesky 113 + isPost 114 + base_url={getPublicationURL( 115 + document.documents_in_publications[0].publications, 116 + )} 117 + pub_uri={ 118 + document.documents_in_publications[0].publications.uri 119 + } 120 + subscribers={ 121 + document.documents_in_publications[0].publications 122 + .publication_subscriptions 123 + } 124 + pubName={ 125 + document.documents_in_publications[0].publications.name 126 + } 127 + /> 128 + )} 129 + </div> 130 + </> 131 + )} 132 + </PageWrapper> 133 + </> 134 + ); 135 + }
+10 -4
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 9 PubLeafletBlocksWebsite, 10 PubLeafletDocument, 11 PubLeafletPagesLinearDocument, 12 PubLeafletBlocksHorizontalRule, 13 PubLeafletBlocksBlockquote, 14 PubLeafletBlocksBskyPost, ··· 46 className?: string; 47 prerenderedCodeBlocks?: Map<string, string>; 48 bskyPostData: AppBskyFeedDefs.PostView[]; 49 - pages: PubLeafletPagesLinearDocument.Main[]; 50 }) { 51 return ( 52 <div ··· 73 ); 74 } 75 76 - let Block = ({ 77 block, 78 did, 79 isList, ··· 91 block: PubLeafletPagesLinearDocument.Block; 92 did: string; 93 isList?: boolean; 94 - pages: PubLeafletPagesLinearDocument.Main[]; 95 previousBlock?: PubLeafletPagesLinearDocument.Block; 96 prerenderedCodeBlocks?: Map<string, string>; 97 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 136 let id = b.block.id; 137 let page = pages.find((p) => p.id === id); 138 if (!page) return; 139 return ( 140 <PublishedPageLinkBlock 141 blocks={page.blocks} ··· 143 parentPageId={pageId} 144 did={did} 145 bskyPostData={bskyPostData} 146 className={className} 147 /> 148 ); ··· 354 355 function ListItem(props: { 356 index: number[]; 357 - pages: PubLeafletPagesLinearDocument.Main[]; 358 item: PubLeafletBlocksUnorderedList.ListItem; 359 did: string; 360 className?: string;
··· 9 PubLeafletBlocksWebsite, 10 PubLeafletDocument, 11 PubLeafletPagesLinearDocument, 12 + PubLeafletPagesCanvas, 13 PubLeafletBlocksHorizontalRule, 14 PubLeafletBlocksBlockquote, 15 PubLeafletBlocksBskyPost, ··· 47 className?: string; 48 prerenderedCodeBlocks?: Map<string, string>; 49 bskyPostData: AppBskyFeedDefs.PostView[]; 50 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 51 }) { 52 return ( 53 <div ··· 74 ); 75 } 76 77 + export let Block = ({ 78 block, 79 did, 80 isList, ··· 92 block: PubLeafletPagesLinearDocument.Block; 93 did: string; 94 isList?: boolean; 95 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 96 previousBlock?: PubLeafletPagesLinearDocument.Block; 97 prerenderedCodeBlocks?: Map<string, string>; 98 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 137 let id = b.block.id; 138 let page = pages.find((p) => p.id === id); 139 if (!page) return; 140 + 141 + const isCanvas = PubLeafletPagesCanvas.isMain(page); 142 + 143 return ( 144 <PublishedPageLinkBlock 145 blocks={page.blocks} ··· 147 parentPageId={pageId} 148 did={did} 149 bskyPostData={bskyPostData} 150 + isCanvas={isCanvas} 151 + pages={pages} 152 className={className} 153 /> 154 ); ··· 360 361 function ListItem(props: { 362 index: number[]; 363 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 364 item: PubLeafletBlocksUnorderedList.ListItem; 365 did: string; 366 className?: string;
+64 -109
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 1 "use client"; 2 import { 3 - PubLeafletComment, 4 PubLeafletDocument, 5 PubLeafletPagesLinearDocument, 6 PubLeafletPublication, 7 } from "lexicons/api"; 8 import { PostPageData } from "./getPostPageData"; 9 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 11 - import { SubscribeWithBluesky } from "app/lish/Subscribe"; 12 - import { EditTiny } from "components/Icons/EditTiny"; 13 - import { Interactions } from "./Interactions/Interactions"; 14 - import { PostContent } from "./PostContent"; 15 - import { PostHeader } from "./PostHeader/PostHeader"; 16 - import { useIdentityData } from "components/IdentityProvider"; 17 import { AppBskyFeedDefs } from "@atproto/api"; 18 import { create } from "zustand/react"; 19 import { ··· 23 import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; 24 import { PageOptionButton } from "components/Pages/PageOptions"; 25 import { CloseTiny } from "components/Icons/CloseTiny"; 26 - import { PageWrapper } from "components/Pages/Page"; 27 import { Fragment, useEffect } from "react"; 28 import { flushSync } from "react-dom"; 29 import { scrollIntoView } from "src/utils/scrollIntoView"; 30 import { useParams } from "next/navigation"; 31 import { decodeQuotePosition } from "./quotePosition"; 32 33 const usePostPageUIState = create(() => ({ 34 pages: [] as string[], ··· 124 bskyPostData: AppBskyFeedDefs.PostView[]; 125 preferences: { showComments?: boolean }; 126 }) { 127 - let { identity } = useIdentityData(); 128 let drawer = useDrawerOpen(document_uri); 129 useInitializeOpenPages(); 130 let pages = useOpenPages(); ··· 132 return null; 133 134 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 135 let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; 136 - let record = document.data as PubLeafletDocument.Record; 137 return ( 138 <> 139 {!fullPageScroll && <BookendSpacer />} 140 - <PageWrapper 141 - pageType="doc" 142 fullPageScroll={fullPageScroll} 143 - cardBorderHidden={!hasPageBackground} 144 - id={"post-page"} 145 - drawerOpen={!!drawer && !drawer.pageId} 146 - > 147 - <PostHeader 148 - data={document} 149 - profile={profile} 150 - preferences={preferences} 151 - /> 152 - <PostContent 153 - pages={record.pages as PubLeafletPagesLinearDocument.Main[]} 154 - bskyPostData={bskyPostData} 155 - blocks={blocks} 156 - did={did} 157 - prerenderedCodeBlocks={prerenderedCodeBlocks} 158 - /> 159 - <Interactions 160 - showComments={preferences.showComments} 161 - quotesCount={ 162 - document.document_mentions_in_bsky.filter((q) => { 163 - const url = new URL(q.link); 164 - const quoteParam = url.pathname.split("/l-quote/")[1]; 165 - if (!quoteParam) return null; 166 - const quotePosition = decodeQuotePosition(quoteParam); 167 - return !quotePosition?.pageId; 168 - }).length 169 - } 170 - commentsCount={ 171 - document.comments_on_documents.filter( 172 - (c) => !(c.record as PubLeafletComment.Record)?.onPage, 173 - ).length 174 - } 175 - /> 176 - <hr className="border-border-light mb-4 mt-4 sm:mx-4 mx-3" /> 177 - <div className="sm:px-4 px-3"> 178 - {identity && 179 - identity.atp_did === 180 - document.documents_in_publications[0]?.publications 181 - ?.identity_did ? ( 182 - <a 183 - href={`https://leaflet.pub/${document.leaflets_in_publications[0]?.leaflet}`} 184 - className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1 mx-auto" 185 - > 186 - <EditTiny /> Edit Post 187 - </a> 188 - ) : ( 189 - <SubscribeWithBluesky 190 - isPost 191 - base_url={getPublicationURL( 192 - document.documents_in_publications[0].publications, 193 - )} 194 - pub_uri={document.documents_in_publications[0].publications.uri} 195 - subscribers={ 196 - document.documents_in_publications[0].publications 197 - .publication_subscriptions 198 - } 199 - pubName={document.documents_in_publications[0].publications.name} 200 - /> 201 - )} 202 - </div> 203 - </PageWrapper> 204 205 {drawer && !drawer.pageId && ( 206 <InteractionDrawer ··· 217 218 {pages.map((p) => { 219 let page = record.pages.find( 220 - (page) => (page as PubLeafletPagesLinearDocument.Main).id === p, 221 - ) as PubLeafletPagesLinearDocument.Main | undefined; 222 if (!page) return null; 223 return ( 224 <Fragment key={p}> 225 <SandwichSpacer /> 226 - {/*JARED TODO : drawerOpen here is checking whether the drawer is open on the first page, rather than if it's open on this page. Please rewire this when you add drawers per page!*/} 227 - <PageWrapper 228 - pageType="doc" 229 - cardBorderHidden={!hasPageBackground} 230 - id={`post-page-${p}`} 231 - fullPageScroll={false} 232 - drawerOpen={!!drawer && drawer.pageId === page.id} 233 - pageOptions={ 234 - <PageOptions 235 - onClick={() => closePage(page?.id!)} 236 - hasPageBackground={hasPageBackground} 237 - /> 238 - } 239 - > 240 - <PostContent 241 - pages={record.pages as PubLeafletPagesLinearDocument.Main[]} 242 - pageId={page.id} 243 - bskyPostData={bskyPostData} 244 - blocks={page.blocks} 245 did={did} 246 prerenderedCodeBlocks={prerenderedCodeBlocks} 247 /> 248 - <Interactions 249 pageId={page.id} 250 - showComments={preferences.showComments} 251 - quotesCount={ 252 - document.document_mentions_in_bsky.filter((q) => 253 - q.link.includes(page.id!), 254 - ).length 255 - } 256 - commentsCount={ 257 - document.comments_on_documents.filter( 258 - (c) => 259 - (c.record as PubLeafletComment.Record)?.onPage === 260 - page.id, 261 - ).length 262 } 263 /> 264 - </PageWrapper> 265 {drawer && drawer.pageId === page.id && ( 266 <InteractionDrawer 267 pageId={page.id}
··· 1 "use client"; 2 import { 3 PubLeafletDocument, 4 PubLeafletPagesLinearDocument, 5 + PubLeafletPagesCanvas, 6 PubLeafletPublication, 7 } from "lexicons/api"; 8 import { PostPageData } from "./getPostPageData"; 9 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 import { AppBskyFeedDefs } from "@atproto/api"; 11 import { create } from "zustand/react"; 12 import { ··· 16 import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; 17 import { PageOptionButton } from "components/Pages/PageOptions"; 18 import { CloseTiny } from "components/Icons/CloseTiny"; 19 import { Fragment, useEffect } from "react"; 20 import { flushSync } from "react-dom"; 21 import { scrollIntoView } from "src/utils/scrollIntoView"; 22 import { useParams } from "next/navigation"; 23 import { decodeQuotePosition } from "./quotePosition"; 24 + import { LinearDocumentPage } from "./LinearDocumentPage"; 25 + import { CanvasPage } from "./CanvasPage"; 26 27 const usePostPageUIState = create(() => ({ 28 pages: [] as string[], ··· 118 bskyPostData: AppBskyFeedDefs.PostView[]; 119 preferences: { showComments?: boolean }; 120 }) { 121 let drawer = useDrawerOpen(document_uri); 122 useInitializeOpenPages(); 123 let pages = useOpenPages(); ··· 125 return null; 126 127 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 128 + let record = document.data as PubLeafletDocument.Record; 129 + 130 let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; 131 return ( 132 <> 133 {!fullPageScroll && <BookendSpacer />} 134 + <LinearDocumentPage 135 + document={document} 136 + blocks={blocks} 137 + did={did} 138 + profile={profile} 139 fullPageScroll={fullPageScroll} 140 + preferences={preferences} 141 + pubRecord={pubRecord} 142 + prerenderedCodeBlocks={prerenderedCodeBlocks} 143 + bskyPostData={bskyPostData} 144 + document_uri={document_uri} 145 + /> 146 147 {drawer && !drawer.pageId && ( 148 <InteractionDrawer ··· 159 160 {pages.map((p) => { 161 let page = record.pages.find( 162 + (page) => 163 + ( 164 + page as 165 + | PubLeafletPagesLinearDocument.Main 166 + | PubLeafletPagesCanvas.Main 167 + ).id === p, 168 + ) as 169 + | PubLeafletPagesLinearDocument.Main 170 + | PubLeafletPagesCanvas.Main 171 + | undefined; 172 if (!page) return null; 173 + 174 + const isCanvas = PubLeafletPagesCanvas.isMain(page); 175 + 176 return ( 177 <Fragment key={p}> 178 <SandwichSpacer /> 179 + {isCanvas ? ( 180 + <CanvasPage 181 + fullPageScroll={false} 182 + document={document} 183 + blocks={(page as PubLeafletPagesCanvas.Main).blocks} 184 did={did} 185 + preferences={preferences} 186 + profile={profile} 187 + pubRecord={pubRecord} 188 prerenderedCodeBlocks={prerenderedCodeBlocks} 189 + bskyPostData={bskyPostData} 190 + document_uri={document_uri} 191 + pageId={page.id} 192 + pages={record.pages as PubLeafletPagesLinearDocument.Main[]} 193 + pageOptions={ 194 + <PageOptions 195 + onClick={() => closePage(page?.id!)} 196 + hasPageBackground={hasPageBackground} 197 + /> 198 + } 199 /> 200 + ) : ( 201 + <LinearDocumentPage 202 + fullPageScroll={false} 203 + document={document} 204 + blocks={(page as PubLeafletPagesLinearDocument.Main).blocks} 205 + did={did} 206 + preferences={preferences} 207 + pubRecord={pubRecord} 208 + prerenderedCodeBlocks={prerenderedCodeBlocks} 209 + bskyPostData={bskyPostData} 210 + document_uri={document_uri} 211 pageId={page.id} 212 + pageOptions={ 213 + <PageOptions 214 + onClick={() => closePage(page?.id!)} 215 + hasPageBackground={hasPageBackground} 216 + /> 217 } 218 /> 219 + )} 220 {drawer && drawer.pageId === page.id && ( 221 <InteractionDrawer 222 pageId={page.id}
+104 -5
app/lish/[did]/[publication]/[rkey]/PublishedPageBlock.tsx
··· 4 import { useUIState } from "src/useUIState"; 5 import { CSSProperties, useContext, useRef } from "react"; 6 import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 7 - import { PostContent } from "./PostContent"; 8 import { 9 PubLeafletBlocksHeader, 10 PubLeafletBlocksText, 11 PubLeafletComment, 12 PubLeafletPagesLinearDocument, 13 PubLeafletPublication, 14 } from "lexicons/api"; 15 import { AppBskyFeedDefs } from "@atproto/api"; ··· 23 } from "./Interactions/Interactions"; 24 import { CommentTiny } from "components/Icons/CommentTiny"; 25 import { QuoteTiny } from "components/Icons/QuoteTiny"; 26 27 export function PublishedPageLinkBlock(props: { 28 - blocks: PubLeafletPagesLinearDocument.Block[]; 29 parentPageId: string | undefined; 30 pageId: string; 31 did: string; ··· 33 className?: string; 34 prerenderedCodeBlocks?: Map<string, string>; 35 bskyPostData: AppBskyFeedDefs.PostView[]; 36 }) { 37 //switch to use actually state 38 let openPages = useOpenPages(); ··· 56 openPage(props.parentPageId, props.pageId); 57 }} 58 > 59 - <DocLinkBlock {...props} /> 60 </div> 61 ); 62 } ··· 203 openInteractionDrawer("quotes", document_uri, props.pageId); 204 else setInteractionState(document_uri, { drawerOpen: false }); 205 }} 206 - aria-label="Page quotes" 207 > 208 <QuoteTiny aria-hidden /> {quotes}{" "} 209 </button> 210 )} ··· 221 openInteractionDrawer("comments", document_uri, props.pageId); 222 else setInteractionState(document_uri, { drawerOpen: false }); 223 }} 224 - aria-label="Page comments" 225 > 226 <CommentTiny aria-hidden /> {comments}{" "} 227 </button> 228 )} 229 </div> 230 ); 231 };
··· 4 import { useUIState } from "src/useUIState"; 5 import { CSSProperties, useContext, useRef } from "react"; 6 import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 7 + import { PostContent, Block } from "./PostContent"; 8 import { 9 PubLeafletBlocksHeader, 10 PubLeafletBlocksText, 11 PubLeafletComment, 12 PubLeafletPagesLinearDocument, 13 + PubLeafletPagesCanvas, 14 PubLeafletPublication, 15 } from "lexicons/api"; 16 import { AppBskyFeedDefs } from "@atproto/api"; ··· 24 } from "./Interactions/Interactions"; 25 import { CommentTiny } from "components/Icons/CommentTiny"; 26 import { QuoteTiny } from "components/Icons/QuoteTiny"; 27 + import { CanvasBackgroundPattern } from "components/Canvas"; 28 29 export function PublishedPageLinkBlock(props: { 30 + blocks: PubLeafletPagesLinearDocument.Block[] | PubLeafletPagesCanvas.Block[]; 31 parentPageId: string | undefined; 32 pageId: string; 33 did: string; ··· 35 className?: string; 36 prerenderedCodeBlocks?: Map<string, string>; 37 bskyPostData: AppBskyFeedDefs.PostView[]; 38 + isCanvas?: boolean; 39 + pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 40 }) { 41 //switch to use actually state 42 let openPages = useOpenPages(); ··· 60 openPage(props.parentPageId, props.pageId); 61 }} 62 > 63 + {props.isCanvas ? ( 64 + <CanvasLinkBlock 65 + blocks={props.blocks as PubLeafletPagesCanvas.Block[]} 66 + did={props.did} 67 + pageId={props.pageId} 68 + bskyPostData={props.bskyPostData} 69 + pages={props.pages || []} 70 + /> 71 + ) : ( 72 + <DocLinkBlock 73 + {...props} 74 + blocks={props.blocks as PubLeafletPagesLinearDocument.Block[]} 75 + /> 76 + )} 77 </div> 78 ); 79 } ··· 220 openInteractionDrawer("quotes", document_uri, props.pageId); 221 else setInteractionState(document_uri, { drawerOpen: false }); 222 }} 223 > 224 + <span className="sr-only">Page quotes</span> 225 <QuoteTiny aria-hidden /> {quotes}{" "} 226 </button> 227 )} ··· 238 openInteractionDrawer("comments", document_uri, props.pageId); 239 else setInteractionState(document_uri, { drawerOpen: false }); 240 }} 241 > 242 + <span className="sr-only">Page comments</span> 243 <CommentTiny aria-hidden /> {comments}{" "} 244 </button> 245 )} 246 </div> 247 ); 248 }; 249 + 250 + const CanvasLinkBlock = (props: { 251 + blocks: PubLeafletPagesCanvas.Block[]; 252 + did: string; 253 + pageId: string; 254 + bskyPostData: AppBskyFeedDefs.PostView[]; 255 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 256 + }) => { 257 + let pageWidth = `var(--page-width-unitless)`; 258 + let height = 259 + props.blocks.length > 0 ? Math.max(...props.blocks.map((b) => b.y), 0) : 0; 260 + 261 + return ( 262 + <div 263 + style={{ contain: "size layout paint" }} 264 + className={`pageLinkBlockPreview shrink-0 h-[200px] w-full overflow-clip relative`} 265 + > 266 + <div 267 + className={`absolute top-0 left-0 origin-top-left pointer-events-none w-full`} 268 + style={{ 269 + width: `calc(1px * ${pageWidth})`, 270 + height: "calc(1150px * 2)", 271 + transform: `scale(calc(((${pageWidth} - 36) / 1272 )))`, 272 + }} 273 + > 274 + <div 275 + style={{ 276 + minHeight: height + 512, 277 + contain: "size layout paint", 278 + }} 279 + className="relative h-full w-[1272px]" 280 + > 281 + <div className="w-full h-full pointer-events-none"> 282 + <CanvasBackgroundPattern pattern="grid" /> 283 + </div> 284 + {props.blocks 285 + .sort((a, b) => { 286 + if (a.y === b.y) { 287 + return a.x - b.x; 288 + } 289 + return a.y - b.y; 290 + }) 291 + .map((canvasBlock, index) => { 292 + let { x, y, width, rotation } = canvasBlock; 293 + let transform = `translate(${x}px, ${y}px)${rotation ? ` rotate(${rotation}deg)` : ""}`; 294 + 295 + // Wrap the block in a LinearDocument.Block structure for compatibility 296 + let linearBlock: PubLeafletPagesLinearDocument.Block = { 297 + $type: "pub.leaflet.pages.linearDocument#block", 298 + block: canvasBlock.block, 299 + }; 300 + 301 + return ( 302 + <div 303 + key={index} 304 + className="absolute rounded-lg flex items-stretch origin-center p-3" 305 + style={{ 306 + top: 0, 307 + left: 0, 308 + width, 309 + transform, 310 + }} 311 + > 312 + <div className="contents"> 313 + <Block 314 + pageId={props.pageId} 315 + pages={props.pages} 316 + bskyPostData={props.bskyPostData} 317 + block={linearBlock} 318 + did={props.did} 319 + index={[index]} 320 + preview={true} 321 + /> 322 + </div> 323 + </div> 324 + ); 325 + })} 326 + </div> 327 + </div> 328 + </div> 329 + ); 330 + };
-1
components/Blocks/BlockCommands.tsx
··· 369 name: "New Canvas", 370 icon: <BlockCanvasPageSmall />, 371 type: "page", 372 - hiddenInPublication: true, 373 onSelect: async (rep, props, um) => { 374 props.entityID && clearCommandSearchText(props.entityID); 375 let entity = await createBlockWithType(rep, props, "card");
··· 369 name: "New Canvas", 370 icon: <BlockCanvasPageSmall />, 371 type: "page", 372 onSelect: async (rep, props, um) => { 373 props.entityID && clearCommandSearchText(props.entityID); 374 let entity = await createBlockWithType(rep, props, "card");
+52 -30
components/Canvas.tsx
··· 14 import { TooltipButton } from "./Buttons"; 15 import { useBlockKeyboardHandlers } from "./Blocks/useBlockKeyboardHandlers"; 16 import { AddSmall } from "./Icons/AddSmall"; 17 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 18 19 - export function Canvas(props: { entityID: string; preview?: boolean }) { 20 let entity_set = useEntitySetContext(); 21 let ref = useRef<HTMLDivElement>(null); 22 useEffect(() => { ··· 45 return () => abort.abort(); 46 }); 47 48 - let narrowWidth = useEntity(props.entityID, "canvas/narrow-width")?.data 49 - .value; 50 - 51 return ( 52 <div 53 ref={ref} ··· 59 `} 60 > 61 <AddCanvasBlockButton entityID={props.entityID} entity_set={entity_set} /> 62 <CanvasContent {...props} /> 63 - <CanvasWidthHandle entityID={props.entityID} /> 64 </div> 65 ); 66 } ··· 150 ); 151 } 152 153 - function CanvasWidthHandle(props: { entityID: string }) { 154 - let canvasFocused = useUIState((s) => s.focusedEntity?.entityType === "page"); 155 - let { rep } = useReplicache(); 156 - let narrowWidth = useEntity(props.entityID, "canvas/narrow-width")?.data 157 - .value; 158 return ( 159 - <button 160 - onClick={() => { 161 - rep?.mutate.assertFact({ 162 - entity: props.entityID, 163 - attribute: "canvas/narrow-width", 164 - data: { 165 - type: "boolean", 166 - value: !narrowWidth, 167 - }, 168 - }); 169 - }} 170 - className={`resizeHandle 171 - ${narrowWidth ? "cursor-e-resize" : "cursor-w-resize"} shrink-0 z-10 172 - ${canvasFocused ? "sm:block hidden" : "hidden"} 173 - w-[8px] h-12 174 - absolute top-1/2 right-0 -translate-y-1/2 translate-x-[3px] 175 - rounded-full bg-white border-2 border-[#8C8C8C] shadow-[0_0_0_1px_white,inset_0_0_0_1px_white]`} 176 - /> 177 ); 178 - } 179 180 const AddCanvasBlockButton = (props: { 181 entityID: string; ··· 187 188 if (!permissions.write) return null; 189 return ( 190 - <div className="absolute right-2 sm:top-4 sm:right-4 bottom-2 sm:bottom-auto z-10 flex flex-col gap-1 justify-center"> 191 <TooltipButton 192 side="left" 193 open={blocks.length === 0 ? true : undefined}
··· 14 import { TooltipButton } from "./Buttons"; 15 import { useBlockKeyboardHandlers } from "./Blocks/useBlockKeyboardHandlers"; 16 import { AddSmall } from "./Icons/AddSmall"; 17 + import { InfoSmall } from "./Icons/InfoSmall"; 18 + import { Popover } from "./Popover"; 19 + import { Separator } from "./Layout"; 20 + import { CommentTiny } from "./Icons/CommentTiny"; 21 + import { QuoteTiny } from "./Icons/QuoteTiny"; 22 + import { PublicationMetadata } from "./Pages/PublicationMetadata"; 23 + import { useLeafletPublicationData } from "./PageSWRDataProvider"; 24 + import { 25 + PubLeafletPublication, 26 + PubLeafletPublicationRecord, 27 + } from "lexicons/api"; 28 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 29 30 + export function Canvas(props: { 31 + entityID: string; 32 + preview?: boolean; 33 + first?: boolean; 34 + }) { 35 let entity_set = useEntitySetContext(); 36 let ref = useRef<HTMLDivElement>(null); 37 useEffect(() => { ··· 60 return () => abort.abort(); 61 }); 62 63 return ( 64 <div 65 ref={ref} ··· 71 `} 72 > 73 <AddCanvasBlockButton entityID={props.entityID} entity_set={entity_set} /> 74 + 75 + <CanvasMetadata isSubpage={!props.first} /> 76 + 77 <CanvasContent {...props} /> 78 </div> 79 ); 80 } ··· 164 ); 165 } 166 167 + const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { 168 + let { data: pub } = useLeafletPublicationData(); 169 + if (!pub || !pub.publications) return null; 170 + 171 + let pubRecord = pub.publications.record as PubLeafletPublication.Record; 172 + let showComments = pubRecord.preferences?.showComments; 173 + 174 return ( 175 + <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> 176 + {showComments && ( 177 + <div className="flex gap-1 text-tertiary items-center"> 178 + <CommentTiny className="text-border" /> — 179 + </div> 180 + )} 181 + <div className="flex gap-1 text-tertiary items-center"> 182 + <QuoteTiny className="text-border" /> — 183 + </div> 184 + 185 + {!props.isSubpage && ( 186 + <> 187 + <Separator classname="h-5" /> 188 + <Popover 189 + side="left" 190 + align="start" 191 + className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" 192 + trigger={<InfoSmall />} 193 + > 194 + <PublicationMetadata /> 195 + </Popover> 196 + </> 197 + )} 198 + </div> 199 ); 200 + }; 201 202 const AddCanvasBlockButton = (props: { 203 entityID: string; ··· 209 210 if (!permissions.write) return null; 211 return ( 212 + <div className="absolute right-2 sm:bottom-4 sm:right-4 bottom-2 sm:top-auto z-10 flex flex-col gap-1 justify-center"> 213 <TooltipButton 214 side="left" 215 open={blocks.length === 0 ? true : undefined}
+1 -1
components/Input.tsx
··· 100 JSX.IntrinsicElements["textarea"], 101 ) => { 102 let { label, textarea, ...inputProps } = props; 103 - let style = `appearance-none w-full font-normal bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none`; 104 return ( 105 <label className=" input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]!"> 106 {props.label}
··· 100 JSX.IntrinsicElements["textarea"], 101 ) => { 102 let { label, textarea, ...inputProps } = props; 103 + let style = `appearance-none w-full font-normal not-italic bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none`; 104 return ( 105 <label className=" input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]!"> 106 {props.label}
+8 -13
components/Pages/Page.tsx
··· 33 return focusedPageID === props.entityID; 34 }); 35 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 36 - let canvasNarrow = useEntity(props.entityID, "canvas/narrow-width")?.data 37 - .value; 38 let cardBorderHidden = useCardBorderHidden(props.entityID); 39 let drawerOpen = useDrawerOpen(props.entityID); 40 return ( 41 <CardThemeProvider entityID={props.entityID}> ··· 53 isFocused={isFocused} 54 fullPageScroll={props.fullPageScroll} 55 pageType={pageType} 56 - canvasNarrow={canvasNarrow} 57 pageOptions={ 58 <PageOptions 59 entityID={props.entityID} ··· 64 > 65 {props.first && ( 66 <> 67 - <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 68 </> 69 )} 70 - <PageContent entityID={props.entityID} /> 71 </PageWrapper> 72 <DesktopPageFooter pageID={props.entityID} /> 73 </CardThemeProvider> ··· 83 isFocused?: boolean; 84 onClickAction?: (e: React.MouseEvent) => void; 85 pageType: "canvas" | "doc"; 86 - canvasNarrow?: boolean | undefined; 87 drawerOpen: boolean | undefined; 88 }) => { 89 return ( ··· 103 className={` 104 pageScrollWrapper 105 grow 106 - 107 shrink-0 snap-center 108 overflow-y-scroll 109 ${ ··· 119 ${ 120 props.pageType === "canvas" && 121 !props.fullPageScroll && 122 - (props.canvasNarrow 123 - ? "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]" 124 - : "sm:max-w-[calc(100vw-128px)] lg:max-w-fit lg:w-[calc(var(--page-width-units)*2 + 24px))]") 125 } 126 127 `} ··· 139 </div> 140 ); 141 }; 142 - // ${narrowWidth ? " sm:max-w-(--page-width-units)" : } 143 - const PageContent = (props: { entityID: string }) => { 144 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 145 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 146 - return <Canvas entityID={props.entityID} />; 147 }; 148 149 const DocContent = (props: { entityID: string }) => { ··· 209 /> 210 ) : null} 211 <Blocks entityID={props.entityID} /> 212 {/* we handle page bg in this sepate div so that 213 we can apply an opacity the background image 214 without affecting the opacity of the rest of the page */}
··· 33 return focusedPageID === props.entityID; 34 }); 35 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 36 let cardBorderHidden = useCardBorderHidden(props.entityID); 37 + 38 let drawerOpen = useDrawerOpen(props.entityID); 39 return ( 40 <CardThemeProvider entityID={props.entityID}> ··· 52 isFocused={isFocused} 53 fullPageScroll={props.fullPageScroll} 54 pageType={pageType} 55 pageOptions={ 56 <PageOptions 57 entityID={props.entityID} ··· 62 > 63 {props.first && ( 64 <> 65 + <PublicationMetadata /> 66 </> 67 )} 68 + <PageContent entityID={props.entityID} first={props.first} /> 69 </PageWrapper> 70 <DesktopPageFooter pageID={props.entityID} /> 71 </CardThemeProvider> ··· 81 isFocused?: boolean; 82 onClickAction?: (e: React.MouseEvent) => void; 83 pageType: "canvas" | "doc"; 84 drawerOpen: boolean | undefined; 85 }) => { 86 return ( ··· 100 className={` 101 pageScrollWrapper 102 grow 103 shrink-0 snap-center 104 overflow-y-scroll 105 ${ ··· 115 ${ 116 props.pageType === "canvas" && 117 !props.fullPageScroll && 118 + "max-w-[var(--page-width-units)] sm:max-w-[calc(100vw-128px)] lg:max-w-fit lg:w-[calc(var(--page-width-units)*2 + 24px))]" 119 } 120 121 `} ··· 133 </div> 134 ); 135 }; 136 + 137 + const PageContent = (props: { entityID: string; first?: boolean }) => { 138 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 139 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 140 + return <Canvas entityID={props.entityID} first={props.first} />; 141 }; 142 143 const DocContent = (props: { entityID: string }) => { ··· 203 /> 204 ) : null} 205 <Blocks entityID={props.entityID} /> 206 + <div className="h-4 sm:h-6 w-full" /> 207 {/* we handle page bg in this sepate div so that 208 we can apply an opacity the background image 209 without affecting the opacity of the rest of the page */}
+1 -5
components/Pages/PublicationMetadata.tsx
··· 13 import { useSubscribe } from "src/replicache/useSubscribe"; 14 import { useEntitySetContext } from "components/EntitySetProvider"; 15 import { timeAgo } from "src/utils/timeAgo"; 16 - export const PublicationMetadata = ({ 17 - cardBorderHidden, 18 - }: { 19 - cardBorderHidden: boolean; 20 - }) => { 21 let { rep } = useReplicache(); 22 let { data: pub } = useLeafletPublicationData(); 23 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title"));
··· 13 import { useSubscribe } from "src/replicache/useSubscribe"; 14 import { useEntitySetContext } from "components/EntitySetProvider"; 15 import { timeAgo } from "src/utils/timeAgo"; 16 + export const PublicationMetadata = () => { 17 let { rep } = useReplicache(); 18 let { data: pub } = useLeafletPublicationData(); 19 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title"));
+5
lexicons/api/index.ts
··· 38 import * as PubLeafletComment from './types/pub/leaflet/comment' 39 import * as PubLeafletDocument from './types/pub/leaflet/document' 40 import * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' 41 import * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 42 import * as PubLeafletPublication from './types/pub/leaflet/publication' 43 import * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' ··· 73 export * as PubLeafletComment from './types/pub/leaflet/comment' 74 export * as PubLeafletDocument from './types/pub/leaflet/document' 75 export * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' 76 export * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 77 export * as PubLeafletPublication from './types/pub/leaflet/publication' 78 export * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' ··· 80 export * as PubLeafletThemeColor from './types/pub/leaflet/theme/color' 81 82 export const PUB_LEAFLET_PAGES = { 83 LinearDocumentTextAlignLeft: 'pub.leaflet.pages.linearDocument#textAlignLeft', 84 LinearDocumentTextAlignCenter: 85 'pub.leaflet.pages.linearDocument#textAlignCenter',
··· 38 import * as PubLeafletComment from './types/pub/leaflet/comment' 39 import * as PubLeafletDocument from './types/pub/leaflet/document' 40 import * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' 41 + import * as PubLeafletPagesCanvas from './types/pub/leaflet/pages/canvas' 42 import * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 43 import * as PubLeafletPublication from './types/pub/leaflet/publication' 44 import * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' ··· 74 export * as PubLeafletComment from './types/pub/leaflet/comment' 75 export * as PubLeafletDocument from './types/pub/leaflet/document' 76 export * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' 77 + export * as PubLeafletPagesCanvas from './types/pub/leaflet/pages/canvas' 78 export * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 79 export * as PubLeafletPublication from './types/pub/leaflet/publication' 80 export * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' ··· 82 export * as PubLeafletThemeColor from './types/pub/leaflet/theme/color' 83 84 export const PUB_LEAFLET_PAGES = { 85 + CanvasTextAlignLeft: 'pub.leaflet.pages.canvas#textAlignLeft', 86 + CanvasTextAlignCenter: 'pub.leaflet.pages.canvas#textAlignCenter', 87 + CanvasTextAlignRight: 'pub.leaflet.pages.canvas#textAlignRight', 88 LinearDocumentTextAlignLeft: 'pub.leaflet.pages.linearDocument#textAlignLeft', 89 LinearDocumentTextAlignCenter: 90 'pub.leaflet.pages.linearDocument#textAlignCenter',
+103 -1
lexicons/api/lexicons.ts
··· 1405 type: 'array', 1406 items: { 1407 type: 'union', 1408 - refs: ['lex:pub.leaflet.pages.linearDocument'], 1409 }, 1410 }, 1411 }, ··· 1429 type: 'string', 1430 format: 'at-uri', 1431 }, 1432 }, 1433 }, 1434 }, ··· 1873 PubLeafletComment: 'pub.leaflet.comment', 1874 PubLeafletDocument: 'pub.leaflet.document', 1875 PubLeafletGraphSubscription: 'pub.leaflet.graph.subscription', 1876 PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', 1877 PubLeafletPublication: 'pub.leaflet.publication', 1878 PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet',
··· 1405 type: 'array', 1406 items: { 1407 type: 'union', 1408 + refs: [ 1409 + 'lex:pub.leaflet.pages.linearDocument', 1410 + 'lex:pub.leaflet.pages.canvas', 1411 + ], 1412 }, 1413 }, 1414 }, ··· 1432 type: 'string', 1433 format: 'at-uri', 1434 }, 1435 + }, 1436 + }, 1437 + }, 1438 + }, 1439 + }, 1440 + PubLeafletPagesCanvas: { 1441 + lexicon: 1, 1442 + id: 'pub.leaflet.pages.canvas', 1443 + defs: { 1444 + main: { 1445 + type: 'object', 1446 + required: ['blocks'], 1447 + properties: { 1448 + id: { 1449 + type: 'string', 1450 + }, 1451 + blocks: { 1452 + type: 'array', 1453 + items: { 1454 + type: 'ref', 1455 + ref: 'lex:pub.leaflet.pages.canvas#block', 1456 + }, 1457 + }, 1458 + }, 1459 + }, 1460 + block: { 1461 + type: 'object', 1462 + required: ['block', 'x', 'y', 'width'], 1463 + properties: { 1464 + block: { 1465 + type: 'union', 1466 + refs: [ 1467 + 'lex:pub.leaflet.blocks.iframe', 1468 + 'lex:pub.leaflet.blocks.text', 1469 + 'lex:pub.leaflet.blocks.blockquote', 1470 + 'lex:pub.leaflet.blocks.header', 1471 + 'lex:pub.leaflet.blocks.image', 1472 + 'lex:pub.leaflet.blocks.unorderedList', 1473 + 'lex:pub.leaflet.blocks.website', 1474 + 'lex:pub.leaflet.blocks.math', 1475 + 'lex:pub.leaflet.blocks.code', 1476 + 'lex:pub.leaflet.blocks.horizontalRule', 1477 + 'lex:pub.leaflet.blocks.bskyPost', 1478 + 'lex:pub.leaflet.blocks.page', 1479 + ], 1480 + }, 1481 + x: { 1482 + type: 'integer', 1483 + }, 1484 + y: { 1485 + type: 'integer', 1486 + }, 1487 + width: { 1488 + type: 'integer', 1489 + }, 1490 + height: { 1491 + type: 'integer', 1492 + }, 1493 + rotation: { 1494 + type: 'integer', 1495 + }, 1496 + }, 1497 + }, 1498 + textAlignLeft: { 1499 + type: 'token', 1500 + }, 1501 + textAlignCenter: { 1502 + type: 'token', 1503 + }, 1504 + textAlignRight: { 1505 + type: 'token', 1506 + }, 1507 + quote: { 1508 + type: 'object', 1509 + required: ['start', 'end'], 1510 + properties: { 1511 + start: { 1512 + type: 'ref', 1513 + ref: 'lex:pub.leaflet.pages.canvas#position', 1514 + }, 1515 + end: { 1516 + type: 'ref', 1517 + ref: 'lex:pub.leaflet.pages.canvas#position', 1518 + }, 1519 + }, 1520 + }, 1521 + position: { 1522 + type: 'object', 1523 + required: ['block', 'offset'], 1524 + properties: { 1525 + block: { 1526 + type: 'array', 1527 + items: { 1528 + type: 'integer', 1529 + }, 1530 + }, 1531 + offset: { 1532 + type: 'integer', 1533 }, 1534 }, 1535 }, ··· 1974 PubLeafletComment: 'pub.leaflet.comment', 1975 PubLeafletDocument: 'pub.leaflet.document', 1976 PubLeafletGraphSubscription: 'pub.leaflet.graph.subscription', 1977 + PubLeafletPagesCanvas: 'pub.leaflet.pages.canvas', 1978 PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', 1979 PubLeafletPublication: 'pub.leaflet.publication', 1980 PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet',
+6 -1
lexicons/api/types/pub/leaflet/document.ts
··· 7 import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef' 9 import type * as PubLeafletPagesLinearDocument from './pages/linearDocument' 10 11 const is$typed = _is$typed, 12 validate = _validate ··· 20 publishedAt?: string 21 publication: string 22 author: string 23 - pages: ($Typed<PubLeafletPagesLinearDocument.Main> | { $type: string })[] 24 [k: string]: unknown 25 } 26
··· 7 import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef' 9 import type * as PubLeafletPagesLinearDocument from './pages/linearDocument' 10 + import type * as PubLeafletPagesCanvas from './pages/canvas' 11 12 const is$typed = _is$typed, 13 validate = _validate ··· 21 publishedAt?: string 22 publication: string 23 author: string 24 + pages: ( 25 + | $Typed<PubLeafletPagesLinearDocument.Main> 26 + | $Typed<PubLeafletPagesCanvas.Main> 27 + | { $type: string } 28 + )[] 29 [k: string]: unknown 30 } 31
+112
lexicons/api/types/pub/leaflet/pages/canvas.ts
···
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { 8 + type $Typed, 9 + is$typed as _is$typed, 10 + type OmitKey, 11 + } from '../../../../util' 12 + import type * as PubLeafletBlocksIframe from '../blocks/iframe' 13 + import type * as PubLeafletBlocksText from '../blocks/text' 14 + import type * as PubLeafletBlocksBlockquote from '../blocks/blockquote' 15 + import type * as PubLeafletBlocksHeader from '../blocks/header' 16 + import type * as PubLeafletBlocksImage from '../blocks/image' 17 + import type * as PubLeafletBlocksUnorderedList from '../blocks/unorderedList' 18 + import type * as PubLeafletBlocksWebsite from '../blocks/website' 19 + import type * as PubLeafletBlocksMath from '../blocks/math' 20 + import type * as PubLeafletBlocksCode from '../blocks/code' 21 + import type * as PubLeafletBlocksHorizontalRule from '../blocks/horizontalRule' 22 + import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost' 23 + import type * as PubLeafletBlocksPage from '../blocks/page' 24 + 25 + const is$typed = _is$typed, 26 + validate = _validate 27 + const id = 'pub.leaflet.pages.canvas' 28 + 29 + export interface Main { 30 + $type?: 'pub.leaflet.pages.canvas' 31 + id?: string 32 + blocks: Block[] 33 + } 34 + 35 + const hashMain = 'main' 36 + 37 + export function isMain<V>(v: V) { 38 + return is$typed(v, id, hashMain) 39 + } 40 + 41 + export function validateMain<V>(v: V) { 42 + return validate<Main & V>(v, id, hashMain) 43 + } 44 + 45 + export interface Block { 46 + $type?: 'pub.leaflet.pages.canvas#block' 47 + block: 48 + | $Typed<PubLeafletBlocksIframe.Main> 49 + | $Typed<PubLeafletBlocksText.Main> 50 + | $Typed<PubLeafletBlocksBlockquote.Main> 51 + | $Typed<PubLeafletBlocksHeader.Main> 52 + | $Typed<PubLeafletBlocksImage.Main> 53 + | $Typed<PubLeafletBlocksUnorderedList.Main> 54 + | $Typed<PubLeafletBlocksWebsite.Main> 55 + | $Typed<PubLeafletBlocksMath.Main> 56 + | $Typed<PubLeafletBlocksCode.Main> 57 + | $Typed<PubLeafletBlocksHorizontalRule.Main> 58 + | $Typed<PubLeafletBlocksBskyPost.Main> 59 + | $Typed<PubLeafletBlocksPage.Main> 60 + | { $type: string } 61 + x: number 62 + y: number 63 + width: number 64 + height?: number 65 + rotation?: number 66 + } 67 + 68 + const hashBlock = 'block' 69 + 70 + export function isBlock<V>(v: V) { 71 + return is$typed(v, id, hashBlock) 72 + } 73 + 74 + export function validateBlock<V>(v: V) { 75 + return validate<Block & V>(v, id, hashBlock) 76 + } 77 + 78 + export const TEXTALIGNLEFT = `${id}#textAlignLeft` 79 + export const TEXTALIGNCENTER = `${id}#textAlignCenter` 80 + export const TEXTALIGNRIGHT = `${id}#textAlignRight` 81 + 82 + export interface Quote { 83 + $type?: 'pub.leaflet.pages.canvas#quote' 84 + start: Position 85 + end: Position 86 + } 87 + 88 + const hashQuote = 'quote' 89 + 90 + export function isQuote<V>(v: V) { 91 + return is$typed(v, id, hashQuote) 92 + } 93 + 94 + export function validateQuote<V>(v: V) { 95 + return validate<Quote & V>(v, id, hashQuote) 96 + } 97 + 98 + export interface Position { 99 + $type?: 'pub.leaflet.pages.canvas#position' 100 + block: number[] 101 + offset: number 102 + } 103 + 104 + const hashPosition = 'position' 105 + 106 + export function isPosition<V>(v: V) { 107 + return is$typed(v, id, hashPosition) 108 + } 109 + 110 + export function validatePosition<V>(v: V) { 111 + return validate<Position & V>(v, id, hashPosition) 112 + }
+1
lexicons/build.ts
··· 21 PubLeafletComment, 22 PubLeafletRichTextFacet, 23 PageLexicons.PubLeafletPagesLinearDocument, 24 ...ThemeLexicons, 25 ...BlockLexicons, 26 ...Object.values(PublicationLexicons),
··· 21 PubLeafletComment, 22 PubLeafletRichTextFacet, 23 PageLexicons.PubLeafletPagesLinearDocument, 24 + PageLexicons.PubLeafletPagesCanvasDocument, 25 ...ThemeLexicons, 26 ...BlockLexicons, 27 ...Object.values(PublicationLexicons),
+2 -1
lexicons/pub/leaflet/document.json
··· 48 "items": { 49 "type": "union", 50 "refs": [ 51 - "pub.leaflet.pages.linearDocument" 52 ] 53 } 54 }
··· 48 "items": { 49 "type": "union", 50 "refs": [ 51 + "pub.leaflet.pages.linearDocument", 52 + "pub.leaflet.pages.canvas" 53 ] 54 } 55 }
+111
lexicons/pub/leaflet/pages/canvas.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.pages.canvas", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [ 8 + "blocks" 9 + ], 10 + "properties": { 11 + "id": { 12 + "type": "string" 13 + }, 14 + "blocks": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "#block" 19 + } 20 + } 21 + } 22 + }, 23 + "block": { 24 + "type": "object", 25 + "required": [ 26 + "block", 27 + "x", 28 + "y", 29 + "width" 30 + ], 31 + "properties": { 32 + "block": { 33 + "type": "union", 34 + "refs": [ 35 + "pub.leaflet.blocks.iframe", 36 + "pub.leaflet.blocks.text", 37 + "pub.leaflet.blocks.blockquote", 38 + "pub.leaflet.blocks.header", 39 + "pub.leaflet.blocks.image", 40 + "pub.leaflet.blocks.unorderedList", 41 + "pub.leaflet.blocks.website", 42 + "pub.leaflet.blocks.math", 43 + "pub.leaflet.blocks.code", 44 + "pub.leaflet.blocks.horizontalRule", 45 + "pub.leaflet.blocks.bskyPost", 46 + "pub.leaflet.blocks.page" 47 + ] 48 + }, 49 + "x": { 50 + "type": "integer" 51 + }, 52 + "y": { 53 + "type": "integer" 54 + }, 55 + "width": { 56 + "type": "integer" 57 + }, 58 + "height": { 59 + "type": "integer" 60 + }, 61 + "rotation": { 62 + "type": "integer" 63 + } 64 + } 65 + }, 66 + "textAlignLeft": { 67 + "type": "token" 68 + }, 69 + "textAlignCenter": { 70 + "type": "token" 71 + }, 72 + "textAlignRight": { 73 + "type": "token" 74 + }, 75 + "quote": { 76 + "type": "object", 77 + "required": [ 78 + "start", 79 + "end" 80 + ], 81 + "properties": { 82 + "start": { 83 + "type": "ref", 84 + "ref": "#position" 85 + }, 86 + "end": { 87 + "type": "ref", 88 + "ref": "#position" 89 + } 90 + } 91 + }, 92 + "position": { 93 + "type": "object", 94 + "required": [ 95 + "block", 96 + "offset" 97 + ], 98 + "properties": { 99 + "block": { 100 + "type": "array", 101 + "items": { 102 + "type": "integer" 103 + } 104 + }, 105 + "offset": { 106 + "type": "integer" 107 + } 108 + } 109 + } 110 + } 111 + }
+5 -1
lexicons/src/document.ts
··· 1 import { LexiconDoc } from "@atproto/lexicon"; 2 import { PubLeafletPagesLinearDocument } from "./pages/LinearDocument"; 3 4 export const PubLeafletDocument: LexiconDoc = { 5 lexicon: 1, ··· 25 type: "array", 26 items: { 27 type: "union", 28 - refs: [PubLeafletPagesLinearDocument.id], 29 }, 30 }, 31 },
··· 1 import { LexiconDoc } from "@atproto/lexicon"; 2 import { PubLeafletPagesLinearDocument } from "./pages/LinearDocument"; 3 + import { PubLeafletPagesCanvasDocument } from "./pages"; 4 5 export const PubLeafletDocument: LexiconDoc = { 6 lexicon: 1, ··· 26 type: "array", 27 items: { 28 type: "union", 29 + refs: [ 30 + PubLeafletPagesLinearDocument.id, 31 + PubLeafletPagesCanvasDocument.id, 32 + ], 33 }, 34 }, 35 },
+48
lexicons/src/pages/Canvas.ts
···
··· 1 + import { LexiconDoc } from "@atproto/lexicon"; 2 + import { BlockUnion } from "../blocks"; 3 + 4 + export const PubLeafletPagesCanvasDocument: LexiconDoc = { 5 + lexicon: 1, 6 + id: "pub.leaflet.pages.canvas", 7 + defs: { 8 + main: { 9 + type: "object", 10 + required: ["blocks"], 11 + properties: { 12 + id: { type: "string" }, 13 + blocks: { type: "array", items: { type: "ref", ref: "#block" } }, 14 + }, 15 + }, 16 + block: { 17 + type: "object", 18 + required: ["block", "x", "y", "width"], 19 + properties: { 20 + block: BlockUnion, 21 + x: { type: "integer" }, 22 + y: { type: "integer" }, 23 + width: { type: "integer" }, 24 + height: { type: "integer" }, 25 + rotation: { type: "integer" }, 26 + }, 27 + }, 28 + textAlignLeft: { type: "token" }, 29 + textAlignCenter: { type: "token" }, 30 + textAlignRight: { type: "token" }, 31 + quote: { 32 + type: "object", 33 + required: ["start", "end"], 34 + properties: { 35 + start: { type: "ref", ref: "#position" }, 36 + end: { type: "ref", ref: "#position" }, 37 + }, 38 + }, 39 + position: { 40 + type: "object", 41 + required: ["block", "offset"], 42 + properties: { 43 + block: { type: "array", items: { type: "integer" } }, 44 + offset: { type: "integer" }, 45 + }, 46 + }, 47 + }, 48 + };
+1
lexicons/src/pages/index.ts
··· 1 export { PubLeafletPagesLinearDocument } from "./LinearDocument";
··· 1 export { PubLeafletPagesLinearDocument } from "./LinearDocument"; 2 + export { PubLeafletPagesCanvasDocument } from "./Canvas";