a tool for shared writing and social publishing

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

+242 -292
+1 -1
app/[leaflet_id]/Footer.tsx
··· 20 20 let { data: pub } = useLeafletPublicationData(); 21 21 22 22 return ( 23 - <Media mobile className="mobileFooter w-full z-10 touch-none -mt-4 "> 23 + <Media mobile className="mobileFooter w-full z-10 touch-none -mt-[54px] "> 24 24 {focusedBlock && 25 25 focusedBlock.entityType == "block" && 26 26 entity_set.permissions.write ? (
+4 -23
app/[leaflet_id]/Leaflet.tsx
··· 13 13 import { UpdateLeafletTitle } from "components/utils/UpdateLeafletTitle"; 14 14 import { useUIState } from "src/useUIState"; 15 15 import { LeafletSidebar } from "./Sidebar"; 16 + import { LeafletLayout } from "components/LeafletLayout"; 16 17 17 18 export function Leaflet(props: { 18 19 token: PermissionToken; ··· 36 37 <SelectionManager /> 37 38 {/* we need the padding bottom here because if we don't have it the mobile footer will cut off... 38 39 the dropshadow on the page... the padding is compensated by a negative top margin in mobile footer */} 39 - <div 40 - className="leafletContentWrapper w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full pb-4 pwa-padding" 41 - id="page-carousel" 42 - > 43 - {/* if you adjust this padding, remember to adjust the negative margins on page in Pages/index when card borders are hidden (also applies for the pb in the parent div)*/} 44 - <div 45 - id="pages" 46 - className="pages flex pt-2 pb-1 sm:pb-8 sm:pt-6" 47 - onClick={(e) => { 48 - e.currentTarget === e.target && blurPage(); 49 - }} 50 - > 51 - <LeafletSidebar leaflet_id={props.leaflet_id} /> 52 - <Pages rootPage={props.leaflet_id} /> 53 - </div> 54 - </div> 40 + <LeafletLayout className="!pb-[70px] sm:!pb-6"> 41 + <Pages rootPage={props.leaflet_id} /> 42 + </LeafletLayout> 55 43 <LeafletFooter entityID={props.leaflet_id} /> 56 44 </ThemeBackgroundProvider> 57 45 </ThemeProvider> ··· 59 47 </ReplicacheProvider> 60 48 ); 61 49 } 62 - 63 - const blurPage = () => { 64 - useUIState.setState(() => ({ 65 - focusedEntity: null, 66 - selectedBlocks: [], 67 - })); 68 - };
+31 -39
app/[leaflet_id]/Sidebar.tsx
··· 19 19 let { identity } = useIdentityData(); 20 20 21 21 return ( 22 - <div 23 - className="spacer flex justify-end items-start" 24 - style={{ width: `calc(50vw - ((var(--page-width-units)/2))` }} 25 - onClick={(e) => { 26 - e.currentTarget === e.target && blurPage(); 27 - }} 22 + <Media 23 + mobile={false} 24 + className="sidebarContainer relative flex flex-col justify-end h-full w-16" 28 25 > 29 - <Media 30 - mobile={false} 31 - className="sidebarContainer relative flex flex-col justify-end h-full w-16" 32 - > 33 - {entity_set.permissions.write && ( 34 - <Sidebar> 35 - {pub?.publications && 36 - identity?.atp_did && 37 - pub.publications.identity_did === identity.atp_did ? ( 38 - <> 39 - <PublishButton /> 40 - <ShareOptions /> 41 - <ThemePopover entityID={props.leaflet_id} /> 42 - <HelpPopover /> 43 - <hr className="text-border" /> 44 - <BackToPubButton publication={pub.publications} /> 45 - </> 46 - ) : ( 47 - <> 48 - <ShareOptions /> 49 - <ThemePopover entityID={props.leaflet_id} /> 50 - <HelpPopover /> 51 - <hr className="text-border" /> 52 - <HomeButton /> 53 - </> 54 - )} 55 - </Sidebar> 56 - )} 57 - <div className="h-full flex items-end"> 58 - <Watermark /> 59 - </div> 60 - </Media> 61 - </div> 26 + {entity_set.permissions.write && ( 27 + <Sidebar> 28 + {pub?.publications && 29 + identity?.atp_did && 30 + pub.publications.identity_did === identity.atp_did ? ( 31 + <> 32 + <PublishButton /> 33 + <ShareOptions /> 34 + <ThemePopover entityID={props.leaflet_id} /> 35 + <HelpPopover /> 36 + <hr className="text-border" /> 37 + <BackToPubButton publication={pub.publications} /> 38 + </> 39 + ) : ( 40 + <> 41 + <ShareOptions /> 42 + <ThemePopover entityID={props.leaflet_id} /> 43 + <HelpPopover /> 44 + <hr className="text-border" /> 45 + <HomeButton /> 46 + </> 47 + )} 48 + </Sidebar> 49 + )} 50 + <div className="h-full flex items-end"> 51 + <Watermark /> 52 + </div> 53 + </Media> 62 54 ); 63 55 } 64 56
+15 -15
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 5 5 import { Json } from "supabase/database.types"; 6 6 import { Comment, Comments } from "./Comments"; 7 7 import { useSearchParams } from "next/navigation"; 8 + import { SandwichSpacer } from "components/LeafletLayout"; 8 9 9 10 export const InteractionDrawer = (props: { 10 11 document_uri: string; ··· 20 21 let currentDrawer = drawer || interactionDrawerSearchParam; 21 22 return ( 22 23 <> 23 - <div className="sm:pr-4 pr-[6px] snap-center"> 24 - <div className="shrink-0 w-[calc(var(--page-width-units)-12px)] sm:w-[var(--page-width-units)] h-full flex z-10"> 25 - <div 26 - id="interaction-drawer" 27 - className="opaque-container !rounded-lg h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll " 28 - > 29 - {currentDrawer === "quotes" ? ( 30 - <Quotes {...props} /> 31 - ) : ( 32 - <Comments 33 - document_uri={props.document_uri} 34 - comments={props.comments} 35 - /> 36 - )} 37 - </div> 24 + <SandwichSpacer className="!w-1 sm:!w-6" /> 25 + <div className="snap-center h-full flex z-10 shrink-0 w-[calc(var(--page-width-units)-12px)] sm:w-[var(--page-width-units)]"> 26 + <div 27 + id="interaction-drawer" 28 + className="opaque-container !rounded-lg h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll " 29 + > 30 + {currentDrawer === "quotes" ? ( 31 + <Quotes {...props} /> 32 + ) : ( 33 + <Comments 34 + document_uri={props.document_uri} 35 + comments={props.comments} 36 + /> 37 + )} 38 38 </div> 39 39 </div> 40 40 </>
-23
app/lish/[did]/[publication]/[rkey]/PageLayout.tsx
··· 1 - "use client"; 2 - 3 - import { useInteractionState } from "./Interactions/Interactions"; 4 - 5 - export function PageLayout(props: { children: React.ReactNode }) { 6 - let { drawerOpen } = useInteractionState(); 7 - return ( 8 - <div 9 - onScroll={(e) => {}} 10 - className="post w-full relative overflow-x-scroll snap-x snap-mandatory no-scrollbar grow items-stretch flex h-full pwa-padding mx-auto " 11 - id="page-carousel" 12 - > 13 - {/* if you adjust this padding, remember to adjust the negative margins on page 14 - in [rkey]/page/PostPage when card borders are hidden */} 15 - <div 16 - id="pages" 17 - className="postWrapper flex h-full gap-0 sm:gap-3 py-2 sm:py-6 w-full" 18 - > 19 - {props.children} 20 - </div> 21 - </div> 22 - ); 23 - }
+105 -84
app/lish/[did]/[publication]/[rkey]/PostPage.tsx
··· 15 15 import { useIdentityData } from "components/IdentityProvider"; 16 16 import { AppBskyFeedDefs } from "@atproto/api"; 17 17 import { create } from "zustand/react"; 18 + import { InteractionDrawer } from "./Interactions/InteractionDrawer"; 19 + import { BookendSpacers, SandwichSpacer } from "components/LeafletLayout"; 18 20 export const usePostPageUIState = create(() => ({ 19 21 pages: [] as string[], 20 22 })); ··· 38 40 }; 39 41 }); 40 42 41 - export function PostPage({ 43 + export function PostPages({ 42 44 document, 43 45 blocks, 44 46 name, ··· 68 70 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 69 71 return ( 70 72 <> 71 - {(drawerOpen || hasPageBackground) && ( 72 - <div 73 - className="spacer sm:block hidden" 74 - style={{ 75 - width: `calc(50vw - 12px - ((var(--page-width-units)/2))`, 76 - }} 73 + {(drawerOpen || hasPageBackground) && <BookendSpacers />} 74 + <PageWrapper 75 + hasPageBackground={hasPageBackground} 76 + drawerOpen={drawerOpen} 77 + > 78 + <PostHeader 79 + data={document} 80 + profile={profile} 81 + name={name} 82 + preferences={preferences} 83 + /> 84 + <PostContent 85 + bskyPostData={bskyPostData} 86 + blocks={blocks} 87 + did={did} 88 + prerenderedCodeBlocks={prerenderedCodeBlocks} 89 + /> 90 + <Interactions 91 + showComments={preferences.showComments} 92 + quotesCount={document.document_mentions_in_bsky.length} 93 + commentsCount={document.comments_on_documents.length} 94 + /> 95 + <hr className="border-border-light mb-4 mt-4" /> 96 + {identity && 97 + identity.atp_did === 98 + document.documents_in_publications[0]?.publications?.identity_did ? ( 99 + <a 100 + href={`https://leaflet.pub/${document.leaflets_in_publications[0]?.leaflet}`} 101 + 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" 102 + > 103 + <EditTiny /> Edit Post 104 + </a> 105 + ) : ( 106 + <SubscribeWithBluesky 107 + isPost 108 + base_url={getPublicationURL( 109 + document.documents_in_publications[0].publications, 110 + )} 111 + pub_uri={document.documents_in_publications[0].publications.uri} 112 + subscribers={ 113 + document.documents_in_publications[0].publications 114 + .publication_subscriptions 115 + } 116 + pubName={name} 117 + /> 118 + )} 119 + </PageWrapper> 120 + 121 + {drawerOpen && ( 122 + <InteractionDrawer 123 + document_uri={document.uri} 124 + comments={ 125 + pubRecord.preferences?.showComments === false 126 + ? [] 127 + : document.comments_on_documents 128 + } 129 + quotes={document.document_mentions_in_bsky} 130 + did={did} 77 131 /> 78 132 )} 79 - <div 80 - id="post-page" 81 - className={`postPageWrapper relative overflow-y-auto sm:mx-0 mx-[6px] w-full 82 - ${drawerOpen || hasPageBackground ? "max-w-[var(--page-width-units)] shrink-0 snap-center " : "w-full"} 83 - ${ 84 - hasPageBackground 85 - ? "h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] rounded-lg border border-border " 86 - : "sm:h-[calc(100%+48px)] h-[calc(100%+24px)] sm:-my-6 -my-3 " 87 - }`} 88 - > 89 - <div 90 - className={`postPageContent sm:max-w-prose mx-auto h-fit w-full px-3 sm:px-4 ${hasPageBackground ? " pt-2 pb-3 sm:pb-6" : "py-6 sm:py-9"}`} 91 - > 92 - <PostHeader 93 - data={document} 94 - profile={profile} 95 - name={name} 96 - preferences={preferences} 97 - /> 98 - <PostContent 99 - bskyPostData={bskyPostData} 100 - blocks={blocks} 101 - did={did} 102 - prerenderedCodeBlocks={prerenderedCodeBlocks} 103 - /> 104 - <Interactions 105 - showComments={preferences.showComments} 106 - quotesCount={document.document_mentions_in_bsky.length} 107 - commentsCount={document.comments_on_documents.length} 108 - /> 109 - <hr className="border-border-light mb-4 mt-4" /> 110 - {identity && 111 - identity.atp_did === 112 - document.documents_in_publications[0]?.publications 113 - ?.identity_did ? ( 114 - <a 115 - href={`https://leaflet.pub/${document.leaflets_in_publications[0]?.leaflet}`} 116 - 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" 117 - > 118 - <EditTiny /> Edit Post 119 - </a> 120 - ) : ( 121 - <SubscribeWithBluesky 122 - isPost 123 - base_url={getPublicationURL( 124 - document.documents_in_publications[0].publications, 125 - )} 126 - pub_uri={document.documents_in_publications[0].publications.uri} 127 - subscribers={ 128 - document.documents_in_publications[0].publications 129 - .publication_subscriptions 130 - } 131 - pubName={name} 132 - /> 133 - )} 134 - </div> 135 - </div> 133 + 136 134 {pages.map((p) => { 137 135 let record = document.data as PubLeafletDocument.Record; 138 136 let page = record.pages.find( ··· 140 138 ) as PubLeafletPagesLinearDocument.Main | undefined; 141 139 if (!page) return null; 142 140 return ( 143 - <div 144 - key={page.id} 145 - id="post-page" 146 - className={`postPageWrapper relative overflow-y-auto sm:mx-0 mx-[6px] w-full 147 - ${drawerOpen || hasPageBackground ? "max-w-[var(--page-width-units)] shrink-0 snap-center " : "w-full"} 148 - ${ 149 - hasPageBackground 150 - ? "h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] rounded-lg border border-border " 151 - : "sm:h-[calc(100%+48px)] h-[calc(100%+24px)] sm:-my-6 -my-3 " 152 - }`} 153 - > 154 - <button onClick={() => closePage(page?.id!)}>close</button> 155 - <PostContent 156 - pageId={page.id} 157 - bskyPostData={bskyPostData} 158 - blocks={page.blocks} 159 - did={did} 160 - prerenderedCodeBlocks={prerenderedCodeBlocks} 161 - /> 162 - </div> 141 + <> 142 + <SandwichSpacer /> 143 + <PageWrapper 144 + hasPageBackground={hasPageBackground} 145 + drawerOpen={drawerOpen} 146 + > 147 + <button onClick={() => closePage(page?.id!)}>close</button> 148 + 149 + <PostContent 150 + pageId={page.id} 151 + bskyPostData={bskyPostData} 152 + blocks={page.blocks} 153 + did={did} 154 + prerenderedCodeBlocks={prerenderedCodeBlocks} 155 + /> 156 + </PageWrapper> 157 + </> 163 158 ); 164 159 })} 160 + <BookendSpacers /> 165 161 </> 166 162 ); 167 163 } 164 + 165 + const PageWrapper = (props: { 166 + children: React.ReactNode; 167 + hasPageBackground: boolean; 168 + drawerOpen: boolean | undefined; 169 + }) => { 170 + return ( 171 + <div 172 + id="post-page" 173 + className={`postPageWrapper relative overflow-y-auto sm:mx-0 w-full 174 + ${props.drawerOpen || props.hasPageBackground ? "max-w-[var(--page-width-units)] shrink-0 snap-center " : "w-full"} 175 + ${ 176 + props.hasPageBackground 177 + ? "h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] rounded-lg border border-border " 178 + : "sm:h-[calc(100%+48px)] h-[calc(100%+24px)] sm:-my-6 -my-3 " 179 + }`} 180 + > 181 + <div 182 + className={`postPageContent sm:max-w-prose mx-auto h-fit w-full px-3 sm:px-4 ${props.hasPageBackground ? " pt-2 pb-3 sm:pb-6" : "py-6 sm:py-9"}`} 183 + > 184 + {props.children} 185 + </div> 186 + </div> 187 + ); 188 + };
+5 -16
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 17 17 } from "components/ThemeManager/PublicationThemeProvider"; 18 18 import { getPostPageData } from "./getPostPageData"; 19 19 import { PostPageContextProvider } from "./PostPageContext"; 20 - import { PostPage } from "./PostPage"; 21 - import { PageLayout } from "./PageLayout"; 20 + import { PostPages } from "./PostPage"; 22 21 import { extractCodeBlocks } from "./extractCodeBlocks"; 22 + import { LeafletLayout } from "components/LeafletLayout"; 23 23 24 24 export async function generateMetadata(props: { 25 25 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 122 122 let pubRecord = document.documents_in_publications[0]?.publications 123 123 .record as PubLeafletPublication.Record; 124 124 125 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 126 125 let prerenderedCodeBlocks = await extractCodeBlocks(blocks); 127 126 128 127 return ( ··· 153 152 on chrome, if you scroll backward, things stop working 154 153 seems like if you use an older browser, sel direction is not a thing yet 155 154 */} 156 - <PageLayout> 157 - <PostPage 155 + <LeafletLayout> 156 + <PostPages 158 157 preferences={pubRecord.preferences || {}} 159 158 pubRecord={pubRecord} 160 159 profile={JSON.parse(JSON.stringify(profile.data))} ··· 165 164 name={decodeURIComponent((await props.params).publication)} 166 165 prerenderedCodeBlocks={prerenderedCodeBlocks} 167 166 /> 168 - <InteractionDrawer 169 - document_uri={document.uri} 170 - comments={ 171 - pubRecord.preferences?.showComments === false 172 - ? [] 173 - : document.comments_on_documents 174 - } 175 - quotes={document.document_mentions_in_bsky} 176 - did={did} 177 - /> 178 - </PageLayout> 167 + </LeafletLayout> 179 168 180 169 <QuoteHandler /> 181 170 </PublicationBackgroundProvider>
-61
components/Blocks/MailboxBlock.tsx
··· 372 372 ); 373 373 }; 374 374 375 - export const DraftPostOptions = (props: { mailboxEntity: string }) => { 376 - let toaster = useToaster(); 377 - let draft = useEntity(props.mailboxEntity, "mailbox/draft"); 378 - let { rep, permission_token } = useReplicache(); 379 - let entity_set = useEntitySetContext(); 380 - let pagetitle = usePageTitle(permission_token.root_entity); 381 - let subscriber_count = useEntity( 382 - props.mailboxEntity, 383 - "mailbox/subscriber-count", 384 - ); 385 - if (!draft) return null; 386 - 387 - // once the send button is clicked, close the page and show a toast. 388 - return ( 389 - <div className="flex justify-between items-center text-sm"> 390 - <div className="flex gap-2"> 391 - <em>Draft</em> 392 - </div> 393 - <button 394 - className="font-bold text-accent-2 bg-accent-1 border hover:bg-accent-2 hover:text-accent-1 rounded-md px-2" 395 - onClick={async () => { 396 - if (!rep) return; 397 - let blocks = 398 - (await rep?.query((tx) => 399 - getBlocksWithType(tx, draft.data.value), 400 - )) || []; 401 - let html = (await getBlocksAsHTML(rep, blocks))?.join("\n"); 402 - await sendPostToSubscribers({ 403 - title: pagetitle, 404 - permission_token, 405 - mailboxEntity: props.mailboxEntity, 406 - messageEntity: draft.data.value, 407 - contents: { 408 - html, 409 - markdown: htmlToMarkdown(html), 410 - }, 411 - }); 412 - 413 - rep?.mutate.archiveDraft({ 414 - entity_set: entity_set.set, 415 - mailboxEntity: props.mailboxEntity, 416 - newBlockEntity: v7(), 417 - archiveEntity: v7(), 418 - }); 419 - 420 - toaster({ 421 - content: <div className="font-bold">Sent Post to Readers!</div>, 422 - type: "success", 423 - }); 424 - }} 425 - > 426 - Send 427 - {!subscriber_count || 428 - (subscriber_count.data.value !== 0 && 429 - ` to ${subscriber_count.data.value} Reader${subscriber_count.data.value === 1 ? "" : "s"}`)} 430 - ! 431 - </button> 432 - </div> 433 - ); 434 - }; 435 - 436 375 const GoToArchive = (props: { 437 376 entityID: string; 438 377 parent: string;
+58
components/LeafletLayout.tsx
··· 1 + export const LeafletLayout = (props: { 2 + children: React.ReactNode; 3 + className?: string; 4 + }) => { 5 + return ( 6 + <div 7 + className={` 8 + leafetLayout 9 + w-full h-full relative 10 + mx-auto pwa-padding 11 + flex items-stretch grow`} 12 + id="page-carousel" 13 + > 14 + {/* if you adjust this padding, remember to adjust the negative margins on page 15 + in [rkey]/page/PostPage when card borders are hidden */} 16 + <div 17 + id="pages" 18 + className={`pagesWrapper 19 + w-full h-full 20 + flex gap-0 21 + py-2 sm:py-6 22 + overflow-x-scroll snap-x snap-mandatory no-scrollbar 23 + ${props.className}`} 24 + > 25 + {props.children} 26 + </div> 27 + </div> 28 + ); 29 + }; 30 + 31 + export const BookendSpacers = (props: { 32 + onClick?: (e: React.MouseEvent) => void; 33 + children?: React.ReactNode; 34 + }) => { 35 + // these spacers go at the end of the first and last pages so that those pages can be scrolled to the center of the screen 36 + return ( 37 + <div 38 + className="spacer shrink-0 flex justify-end items-start" 39 + style={{ width: `calc(50vw - ((var(--page-width-units)/2))` }} 40 + onClick={props.onClick ? props.onClick : () => {}} 41 + > 42 + {props.children} 43 + </div> 44 + ); 45 + }; 46 + 47 + export const SandwichSpacer = (props: { 48 + onClick?: (e: React.MouseEvent) => void; 49 + className?: string; 50 + }) => { 51 + // these spacers are used between pages so that the page carousel can fit two pages side by side by snapping in between pages 52 + return ( 53 + <div 54 + onClick={props.onClick} 55 + className={`spacer shrink-0 w-6 lg:snap-center ${props.className}`} 56 + /> 57 + ); 58 + };
+23 -30
components/Pages/index.tsx
··· 21 21 import { DesktopPageFooter } from "../DesktopFooter"; 22 22 import { ThemePopover } from "../ThemeManager/ThemeSetter"; 23 23 import { Canvas } from "../Canvas"; 24 - import { DraftPostOptions } from "../Blocks/MailboxBlock"; 25 24 import { Blocks } from "components/Blocks"; 26 25 import { MenuItem, Menu } from "../Layout"; 27 26 import { scanIndex } from "src/replicache/utils"; ··· 37 36 import { PublicationMetadata } from "./PublicationMetadata"; 38 37 import { useCardBorderHidden } from "./useCardBorderHidden"; 39 38 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 39 + import { BookendSpacers, SandwichSpacer } from "components/LeafletLayout"; 40 + import { LeafletSidebar } from "app/[leaflet_id]/Sidebar"; 40 41 41 42 export function Pages(props: { rootPage: string }) { 42 43 let rootPage = useEntity(props.rootPage, "root/page")[0]; ··· 47 48 48 49 return ( 49 50 <> 51 + <BookendSpacers 52 + onClick={(e) => { 53 + e.currentTarget === e.target && blurPage(); 54 + }} 55 + > 56 + <LeafletSidebar leaflet_id={props.rootPage} /> 57 + </BookendSpacers> 50 58 <div className="flex items-stretch"> 51 59 <CardThemeProvider entityID={firstPage}> 52 60 <Page entityID={firstPage} first /> ··· 59 67 </CardThemeProvider> 60 68 </div> 61 69 ))} 62 - <div 63 - className="spacer" 64 - style={{ width: `calc(50vw - ((var(--page-width-units)/2))` }} 70 + <BookendSpacers 65 71 onClick={(e) => { 66 72 e.currentTarget === e.target && blurPage(); 67 73 }} ··· 79 85 }; 80 86 81 87 function Page(props: { entityID: string; first?: boolean }) { 82 - let { rep, rootEntity } = useReplicache(); 88 + let { rep } = useReplicache(); 83 89 let isDraft = useReferenceToEntity("mailbox/draft", props.entityID); 84 90 85 91 let isFocused = useUIState((s) => { ··· 95 101 return ( 96 102 <> 97 103 {!props.first && ( 98 - <div 99 - className="w-6 lg:snap-center" 104 + <SandwichSpacer 100 105 onClick={(e) => { 101 106 e.currentTarget === e.target && blurPage(); 102 107 }} ··· 128 133 ${isFocused ? "shadow-md border-border" : "border-border-light"} 129 134 `} 130 135 > 131 - <Media mobile={true}> 132 - <PageOptions entityID={props.entityID} first={props.first} /> 133 - </Media> 134 - <DesktopPageFooter pageID={props.entityID} /> 135 - {isDraft.length > 0 && ( 136 - <div 137 - className={`pageStatus pt-[6px] pb-1 ${!props.first ? "pr-10 pl-3 sm:px-4" : "px-3 sm:px-4"} border-b border-border text-tertiary`} 138 - style={{ 139 - backgroundColor: 140 - "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", 141 - }} 142 - > 143 - <DraftPostOptions mailboxEntity={isDraft[0].entity} /> 144 - </div> 145 - )} 146 - 136 + <PageOptions 137 + entityID={props.entityID} 138 + first={props.first} 139 + isFocused={isFocused} 140 + /> 147 141 {props.first && ( 148 142 <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 149 143 )} 150 144 <PageContent entityID={props.entityID} /> 151 145 </div> 152 - <Media mobile={false}> 153 - {isFocused && ( 154 - <PageOptions entityID={props.entityID} first={props.first} /> 155 - )} 156 - </Media> 146 + <DesktopPageFooter pageID={props.entityID} /> 157 147 </div> 158 148 </> 159 149 ); ··· 282 272 const PageOptions = (props: { 283 273 entityID: string; 284 274 first: boolean | undefined; 275 + isFocused: boolean; 285 276 }) => { 286 - let { rootEntity } = useReplicache(); 287 277 let cardBorderHidden = useCardBorderHidden(props.entityID); 288 278 289 279 return ( 290 280 <div 291 - className={`z-10 w-fit absolute ${cardBorderHidden ? "top-1" : "sm:top-3"} sm:-right-[19px] top-0 right-3 flex sm:flex-col flex-row-reverse gap-1 items-start`} 281 + className={`pageOptions w-fit z-10 282 + ${props.isFocused ? "block" : "sm:hidden block"} 283 + absolute sm:-right-[19px] right-3 ${cardBorderHidden ? "top-1" : "sm:top-3 top-0"} 284 + flex sm:flex-col flex-row-reverse gap-1 items-start`} 292 285 > 293 286 {!props.first && ( 294 287 <PageOptionButton