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

+87 -12
actions/publishToPublication.ts
··· 12 12 PubLeafletBlocksUnorderedList, 13 13 PubLeafletDocument, 14 14 PubLeafletPagesLinearDocument, 15 + PubLeafletPagesCanvas, 15 16 PubLeafletRichtextFacet, 16 17 PubLeafletBlocksWebsite, 17 18 PubLeafletBlocksCode, ··· 95 96 $type: "pub.leaflet.pages.linearDocument", 96 97 blocks: firstPageBlocks, 97 98 }, 98 - ...pages.map((p) => ({ 99 - $type: "pub.leaflet.pages.linearDocument", 100 - id: p.id, 101 - blocks: p.blocks, 102 - })), 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 + }), 103 114 ], 104 115 }; 105 116 let rkey = draft?.doc ? new AtUri(draft.doc).rkey : TID.nextStr(); ··· 139 150 root_entity: string, 140 151 ) { 141 152 let scan = scanIndexLocal(facts); 142 - let pages: { id: string; blocks: PubLeafletPagesLinearDocument.Block[] }[] = 143 - []; 153 + let pages: { 154 + id: string; 155 + blocks: 156 + | PubLeafletPagesLinearDocument.Block[] 157 + | PubLeafletPagesCanvas.Block[]; 158 + type: "doc" | "canvas"; 159 + }[] = []; 144 160 145 161 let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 146 162 if (!firstEntity) throw new Error("No root page"); ··· 228 244 if (b.type === "card") { 229 245 let [page] = scan.eav(b.value, "block/card"); 230 246 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 - }); 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 + 236 265 let block: $Typed<PubLeafletBlocksPage.Main> = { 237 266 $type: "pub.leaflet.blocks.page", 238 267 id: page.data.value, ··· 358 387 return block; 359 388 } 360 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); 361 436 } 362 437 } 363 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 5 import type { Json } from "supabase/database.types"; 6 6 import { create } from "zustand"; 7 7 import type { Comment } from "./Comments"; 8 - import { QuotePosition } from "../quotePosition"; 8 + import { decodeQuotePosition, QuotePosition } from "../quotePosition"; 9 9 import { useContext } from "react"; 10 10 import { PostPageContext } from "../PostPageContext"; 11 11 import { scrollIntoView } from "src/utils/scrollIntoView"; 12 + import { PostPageData } from "../getPostPageData"; 13 + import { PubLeafletComment } from "lexicons/api"; 12 14 13 15 export type InteractionState = { 14 16 drawerOpen: undefined | boolean; ··· 149 151 </div> 150 152 ); 151 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 9 PubLeafletBlocksWebsite, 10 10 PubLeafletDocument, 11 11 PubLeafletPagesLinearDocument, 12 + PubLeafletPagesCanvas, 12 13 PubLeafletBlocksHorizontalRule, 13 14 PubLeafletBlocksBlockquote, 14 15 PubLeafletBlocksBskyPost, ··· 46 47 className?: string; 47 48 prerenderedCodeBlocks?: Map<string, string>; 48 49 bskyPostData: AppBskyFeedDefs.PostView[]; 49 - pages: PubLeafletPagesLinearDocument.Main[]; 50 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 50 51 }) { 51 52 return ( 52 53 <div ··· 73 74 ); 74 75 } 75 76 76 - let Block = ({ 77 + export let Block = ({ 77 78 block, 78 79 did, 79 80 isList, ··· 91 92 block: PubLeafletPagesLinearDocument.Block; 92 93 did: string; 93 94 isList?: boolean; 94 - pages: PubLeafletPagesLinearDocument.Main[]; 95 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 95 96 previousBlock?: PubLeafletPagesLinearDocument.Block; 96 97 prerenderedCodeBlocks?: Map<string, string>; 97 98 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 136 137 let id = b.block.id; 137 138 let page = pages.find((p) => p.id === id); 138 139 if (!page) return; 140 + 141 + const isCanvas = PubLeafletPagesCanvas.isMain(page); 142 + 139 143 return ( 140 144 <PublishedPageLinkBlock 141 145 blocks={page.blocks} ··· 143 147 parentPageId={pageId} 144 148 did={did} 145 149 bskyPostData={bskyPostData} 150 + isCanvas={isCanvas} 151 + pages={pages} 146 152 className={className} 147 153 /> 148 154 ); ··· 354 360 355 361 function ListItem(props: { 356 362 index: number[]; 357 - pages: PubLeafletPagesLinearDocument.Main[]; 363 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 358 364 item: PubLeafletBlocksUnorderedList.ListItem; 359 365 did: string; 360 366 className?: string;
+64 -109
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 1 1 "use client"; 2 2 import { 3 - PubLeafletComment, 4 3 PubLeafletDocument, 5 4 PubLeafletPagesLinearDocument, 5 + PubLeafletPagesCanvas, 6 6 PubLeafletPublication, 7 7 } from "lexicons/api"; 8 8 import { PostPageData } from "./getPostPageData"; 9 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 10 import { AppBskyFeedDefs } from "@atproto/api"; 18 11 import { create } from "zustand/react"; 19 12 import { ··· 23 16 import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; 24 17 import { PageOptionButton } from "components/Pages/PageOptions"; 25 18 import { CloseTiny } from "components/Icons/CloseTiny"; 26 - import { PageWrapper } from "components/Pages/Page"; 27 19 import { Fragment, useEffect } from "react"; 28 20 import { flushSync } from "react-dom"; 29 21 import { scrollIntoView } from "src/utils/scrollIntoView"; 30 22 import { useParams } from "next/navigation"; 31 23 import { decodeQuotePosition } from "./quotePosition"; 24 + import { LinearDocumentPage } from "./LinearDocumentPage"; 25 + import { CanvasPage } from "./CanvasPage"; 32 26 33 27 const usePostPageUIState = create(() => ({ 34 28 pages: [] as string[], ··· 124 118 bskyPostData: AppBskyFeedDefs.PostView[]; 125 119 preferences: { showComments?: boolean }; 126 120 }) { 127 - let { identity } = useIdentityData(); 128 121 let drawer = useDrawerOpen(document_uri); 129 122 useInitializeOpenPages(); 130 123 let pages = useOpenPages(); ··· 132 125 return null; 133 126 134 127 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 128 + let record = document.data as PubLeafletDocument.Record; 129 + 135 130 let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; 136 - let record = document.data as PubLeafletDocument.Record; 137 131 return ( 138 132 <> 139 133 {!fullPageScroll && <BookendSpacer />} 140 - <PageWrapper 141 - pageType="doc" 134 + <LinearDocumentPage 135 + document={document} 136 + blocks={blocks} 137 + did={did} 138 + profile={profile} 142 139 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> 140 + preferences={preferences} 141 + pubRecord={pubRecord} 142 + prerenderedCodeBlocks={prerenderedCodeBlocks} 143 + bskyPostData={bskyPostData} 144 + document_uri={document_uri} 145 + /> 204 146 205 147 {drawer && !drawer.pageId && ( 206 148 <InteractionDrawer ··· 217 159 218 160 {pages.map((p) => { 219 161 let page = record.pages.find( 220 - (page) => (page as PubLeafletPagesLinearDocument.Main).id === p, 221 - ) as PubLeafletPagesLinearDocument.Main | undefined; 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; 222 172 if (!page) return null; 173 + 174 + const isCanvas = PubLeafletPagesCanvas.isMain(page); 175 + 223 176 return ( 224 177 <Fragment key={p}> 225 178 <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} 179 + {isCanvas ? ( 180 + <CanvasPage 181 + fullPageScroll={false} 182 + document={document} 183 + blocks={(page as PubLeafletPagesCanvas.Main).blocks} 245 184 did={did} 185 + preferences={preferences} 186 + profile={profile} 187 + pubRecord={pubRecord} 246 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 + } 247 199 /> 248 - <Interactions 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} 249 211 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 212 + pageOptions={ 213 + <PageOptions 214 + onClick={() => closePage(page?.id!)} 215 + hasPageBackground={hasPageBackground} 216 + /> 262 217 } 263 218 /> 264 - </PageWrapper> 219 + )} 265 220 {drawer && drawer.pageId === page.id && ( 266 221 <InteractionDrawer 267 222 pageId={page.id}
+104 -5
app/lish/[did]/[publication]/[rkey]/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 } from "./PostContent"; 7 + import { PostContent, Block } from "./PostContent"; 8 8 import { 9 9 PubLeafletBlocksHeader, 10 10 PubLeafletBlocksText, 11 11 PubLeafletComment, 12 12 PubLeafletPagesLinearDocument, 13 + PubLeafletPagesCanvas, 13 14 PubLeafletPublication, 14 15 } from "lexicons/api"; 15 16 import { AppBskyFeedDefs } from "@atproto/api"; ··· 23 24 } from "./Interactions/Interactions"; 24 25 import { CommentTiny } from "components/Icons/CommentTiny"; 25 26 import { QuoteTiny } from "components/Icons/QuoteTiny"; 27 + import { CanvasBackgroundPattern } from "components/Canvas"; 26 28 27 29 export function PublishedPageLinkBlock(props: { 28 - blocks: PubLeafletPagesLinearDocument.Block[]; 30 + blocks: PubLeafletPagesLinearDocument.Block[] | PubLeafletPagesCanvas.Block[]; 29 31 parentPageId: string | undefined; 30 32 pageId: string; 31 33 did: string; ··· 33 35 className?: string; 34 36 prerenderedCodeBlocks?: Map<string, string>; 35 37 bskyPostData: AppBskyFeedDefs.PostView[]; 38 + isCanvas?: boolean; 39 + pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 36 40 }) { 37 41 //switch to use actually state 38 42 let openPages = useOpenPages(); ··· 56 60 openPage(props.parentPageId, props.pageId); 57 61 }} 58 62 > 59 - <DocLinkBlock {...props} /> 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 + )} 60 77 </div> 61 78 ); 62 79 } ··· 203 220 openInteractionDrawer("quotes", document_uri, props.pageId); 204 221 else setInteractionState(document_uri, { drawerOpen: false }); 205 222 }} 206 - aria-label="Page quotes" 207 223 > 224 + <span className="sr-only">Page quotes</span> 208 225 <QuoteTiny aria-hidden /> {quotes}{" "} 209 226 </button> 210 227 )} ··· 221 238 openInteractionDrawer("comments", document_uri, props.pageId); 222 239 else setInteractionState(document_uri, { drawerOpen: false }); 223 240 }} 224 - aria-label="Page comments" 225 241 > 242 + <span className="sr-only">Page comments</span> 226 243 <CommentTiny aria-hidden /> {comments}{" "} 227 244 </button> 228 245 )} 229 246 </div> 230 247 ); 231 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 369 name: "New Canvas", 370 370 icon: <BlockCanvasPageSmall />, 371 371 type: "page", 372 - hiddenInPublication: true, 373 372 onSelect: async (rep, props, um) => { 374 373 props.entityID && clearCommandSearchText(props.entityID); 375 374 let entity = await createBlockWithType(rep, props, "card");
+52 -30
components/Canvas.tsx
··· 14 14 import { TooltipButton } from "./Buttons"; 15 15 import { useBlockKeyboardHandlers } from "./Blocks/useBlockKeyboardHandlers"; 16 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"; 17 28 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 18 29 19 - export function Canvas(props: { entityID: string; preview?: boolean }) { 30 + export function Canvas(props: { 31 + entityID: string; 32 + preview?: boolean; 33 + first?: boolean; 34 + }) { 20 35 let entity_set = useEntitySetContext(); 21 36 let ref = useRef<HTMLDivElement>(null); 22 37 useEffect(() => { ··· 45 60 return () => abort.abort(); 46 61 }); 47 62 48 - let narrowWidth = useEntity(props.entityID, "canvas/narrow-width")?.data 49 - .value; 50 - 51 63 return ( 52 64 <div 53 65 ref={ref} ··· 59 71 `} 60 72 > 61 73 <AddCanvasBlockButton entityID={props.entityID} entity_set={entity_set} /> 74 + 75 + <CanvasMetadata isSubpage={!props.first} /> 76 + 62 77 <CanvasContent {...props} /> 63 - <CanvasWidthHandle entityID={props.entityID} /> 64 78 </div> 65 79 ); 66 80 } ··· 150 164 ); 151 165 } 152 166 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; 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 + 158 174 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 - /> 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> 177 199 ); 178 - } 200 + }; 179 201 180 202 const AddCanvasBlockButton = (props: { 181 203 entityID: string; ··· 187 209 188 210 if (!permissions.write) return null; 189 211 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"> 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"> 191 213 <TooltipButton 192 214 side="left" 193 215 open={blocks.length === 0 ? true : undefined}
+1 -1
components/Input.tsx
··· 100 100 JSX.IntrinsicElements["textarea"], 101 101 ) => { 102 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`; 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 104 return ( 105 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 106 {props.label}
+8 -13
components/Pages/Page.tsx
··· 33 33 return focusedPageID === props.entityID; 34 34 }); 35 35 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 36 - let canvasNarrow = useEntity(props.entityID, "canvas/narrow-width")?.data 37 - .value; 38 36 let cardBorderHidden = useCardBorderHidden(props.entityID); 37 + 39 38 let drawerOpen = useDrawerOpen(props.entityID); 40 39 return ( 41 40 <CardThemeProvider entityID={props.entityID}> ··· 53 52 isFocused={isFocused} 54 53 fullPageScroll={props.fullPageScroll} 55 54 pageType={pageType} 56 - canvasNarrow={canvasNarrow} 57 55 pageOptions={ 58 56 <PageOptions 59 57 entityID={props.entityID} ··· 64 62 > 65 63 {props.first && ( 66 64 <> 67 - <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 65 + <PublicationMetadata /> 68 66 </> 69 67 )} 70 - <PageContent entityID={props.entityID} /> 68 + <PageContent entityID={props.entityID} first={props.first} /> 71 69 </PageWrapper> 72 70 <DesktopPageFooter pageID={props.entityID} /> 73 71 </CardThemeProvider> ··· 83 81 isFocused?: boolean; 84 82 onClickAction?: (e: React.MouseEvent) => void; 85 83 pageType: "canvas" | "doc"; 86 - canvasNarrow?: boolean | undefined; 87 84 drawerOpen: boolean | undefined; 88 85 }) => { 89 86 return ( ··· 103 100 className={` 104 101 pageScrollWrapper 105 102 grow 106 - 107 103 shrink-0 snap-center 108 104 overflow-y-scroll 109 105 ${ ··· 119 115 ${ 120 116 props.pageType === "canvas" && 121 117 !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))]") 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))]" 125 119 } 126 120 127 121 `} ··· 139 133 </div> 140 134 ); 141 135 }; 142 - // ${narrowWidth ? " sm:max-w-(--page-width-units)" : } 143 - const PageContent = (props: { entityID: string }) => { 136 + 137 + const PageContent = (props: { entityID: string; first?: boolean }) => { 144 138 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 145 139 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 146 - return <Canvas entityID={props.entityID} />; 140 + return <Canvas entityID={props.entityID} first={props.first} />; 147 141 }; 148 142 149 143 const DocContent = (props: { entityID: string }) => { ··· 209 203 /> 210 204 ) : null} 211 205 <Blocks entityID={props.entityID} /> 206 + <div className="h-4 sm:h-6 w-full" /> 212 207 {/* we handle page bg in this sepate div so that 213 208 we can apply an opacity the background image 214 209 without affecting the opacity of the rest of the page */}
+1 -5
components/Pages/PublicationMetadata.tsx
··· 13 13 import { useSubscribe } from "src/replicache/useSubscribe"; 14 14 import { useEntitySetContext } from "components/EntitySetProvider"; 15 15 import { timeAgo } from "src/utils/timeAgo"; 16 - export const PublicationMetadata = ({ 17 - cardBorderHidden, 18 - }: { 19 - cardBorderHidden: boolean; 20 - }) => { 16 + export const PublicationMetadata = () => { 21 17 let { rep } = useReplicache(); 22 18 let { data: pub } = useLeafletPublicationData(); 23 19 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title"));
+5
lexicons/api/index.ts
··· 38 38 import * as PubLeafletComment from './types/pub/leaflet/comment' 39 39 import * as PubLeafletDocument from './types/pub/leaflet/document' 40 40 import * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' 41 + import * as PubLeafletPagesCanvas from './types/pub/leaflet/pages/canvas' 41 42 import * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 42 43 import * as PubLeafletPublication from './types/pub/leaflet/publication' 43 44 import * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' ··· 73 74 export * as PubLeafletComment from './types/pub/leaflet/comment' 74 75 export * as PubLeafletDocument from './types/pub/leaflet/document' 75 76 export * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' 77 + export * as PubLeafletPagesCanvas from './types/pub/leaflet/pages/canvas' 76 78 export * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 77 79 export * as PubLeafletPublication from './types/pub/leaflet/publication' 78 80 export * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' ··· 80 82 export * as PubLeafletThemeColor from './types/pub/leaflet/theme/color' 81 83 82 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', 83 88 LinearDocumentTextAlignLeft: 'pub.leaflet.pages.linearDocument#textAlignLeft', 84 89 LinearDocumentTextAlignCenter: 85 90 'pub.leaflet.pages.linearDocument#textAlignCenter',
+103 -1
lexicons/api/lexicons.ts
··· 1405 1405 type: 'array', 1406 1406 items: { 1407 1407 type: 'union', 1408 - refs: ['lex:pub.leaflet.pages.linearDocument'], 1408 + refs: [ 1409 + 'lex:pub.leaflet.pages.linearDocument', 1410 + 'lex:pub.leaflet.pages.canvas', 1411 + ], 1409 1412 }, 1410 1413 }, 1411 1414 }, ··· 1429 1432 type: 'string', 1430 1433 format: 'at-uri', 1431 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', 1432 1533 }, 1433 1534 }, 1434 1535 }, ··· 1873 1974 PubLeafletComment: 'pub.leaflet.comment', 1874 1975 PubLeafletDocument: 'pub.leaflet.document', 1875 1976 PubLeafletGraphSubscription: 'pub.leaflet.graph.subscription', 1977 + PubLeafletPagesCanvas: 'pub.leaflet.pages.canvas', 1876 1978 PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', 1877 1979 PubLeafletPublication: 'pub.leaflet.publication', 1878 1980 PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet',
+6 -1
lexicons/api/types/pub/leaflet/document.ts
··· 7 7 import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 8 import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef' 9 9 import type * as PubLeafletPagesLinearDocument from './pages/linearDocument' 10 + import type * as PubLeafletPagesCanvas from './pages/canvas' 10 11 11 12 const is$typed = _is$typed, 12 13 validate = _validate ··· 20 21 publishedAt?: string 21 22 publication: string 22 23 author: string 23 - pages: ($Typed<PubLeafletPagesLinearDocument.Main> | { $type: string })[] 24 + pages: ( 25 + | $Typed<PubLeafletPagesLinearDocument.Main> 26 + | $Typed<PubLeafletPagesCanvas.Main> 27 + | { $type: string } 28 + )[] 24 29 [k: string]: unknown 25 30 } 26 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 21 PubLeafletComment, 22 22 PubLeafletRichTextFacet, 23 23 PageLexicons.PubLeafletPagesLinearDocument, 24 + PageLexicons.PubLeafletPagesCanvasDocument, 24 25 ...ThemeLexicons, 25 26 ...BlockLexicons, 26 27 ...Object.values(PublicationLexicons),
+2 -1
lexicons/pub/leaflet/document.json
··· 48 48 "items": { 49 49 "type": "union", 50 50 "refs": [ 51 - "pub.leaflet.pages.linearDocument" 51 + "pub.leaflet.pages.linearDocument", 52 + "pub.leaflet.pages.canvas" 52 53 ] 53 54 } 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 1 import { LexiconDoc } from "@atproto/lexicon"; 2 2 import { PubLeafletPagesLinearDocument } from "./pages/LinearDocument"; 3 + import { PubLeafletPagesCanvasDocument } from "./pages"; 3 4 4 5 export const PubLeafletDocument: LexiconDoc = { 5 6 lexicon: 1, ··· 25 26 type: "array", 26 27 items: { 27 28 type: "union", 28 - refs: [PubLeafletPagesLinearDocument.id], 29 + refs: [ 30 + PubLeafletPagesLinearDocument.id, 31 + PubLeafletPagesCanvasDocument.id, 32 + ], 29 33 }, 30 34 }, 31 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 1 export { PubLeafletPagesLinearDocument } from "./LinearDocument"; 2 + export { PubLeafletPagesCanvasDocument } from "./Canvas";