a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

update many styles

Jared Pereira (May 19, 2026, 11:28 PM EDT) b2f1b0fe 10f06375

+321 -240
+4 -1
actions/publishPublicationPages.ts
··· 16 16 17 17 type PublishPagesResult = 18 18 | { success: true; published: { id: number; uri: string }[] } 19 - | { success: false; error: OAuthSessionError | { type: "other"; message: string } }; 19 + | { 20 + success: false; 21 + error: OAuthSessionError | { type: "other"; message: string }; 22 + }; 20 23 21 24 export async function publishPublicationPages({ 22 25 publication_uri,
+1 -1
app/api/oauth/[route]/oauth-metadata.ts
··· 8 8 : "https://leaflet.pub"; 9 9 10 10 const scope = 11 - "atproto transition:email account:email?action=read include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*"; 11 + "atproto transition:email account:email?action=read include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/* repo:pub.leaflet.publicationPage"; 12 12 const localconfig: OAuthClientMetadataInput = { 13 13 client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, 14 14 client_name: `Leaflet`,
+7 -14
app/api/oauth/[route]/route.ts
··· 64 64 65 65 const url = await client.authorize(handle || "https://bsky.social", { 66 66 scope: 67 - "atproto account:email?action=read include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*", 67 + "atproto repo:pub.leaflet.publicationPage account:email?action=read include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*", 68 68 signal: ac.signal, 69 69 state: JSON.stringify(state), 70 70 ...(signup ? { prompt: "create" } : {}), ··· 86 86 .eq("atp_did", session.did) 87 87 .single(); 88 88 89 - const currentAuthToken = (await cookies()).get(AUTH_TOKEN_COOKIE)?.value; 89 + const currentAuthToken = (await cookies()).get( 90 + AUTH_TOKEN_COOKIE, 91 + )?.value; 90 92 let currentIdentity: { 91 93 id: string; 92 94 email: string | null; ··· 183 185 targetId: identity.id, 184 186 }); 185 187 if (!merged.ok) { 186 - console.error( 187 - "[oauth/callback] autoMerge failed:", 188 - merged.error, 189 - ); 188 + console.error("[oauth/callback] autoMerge failed:", merged.error); 190 189 await stagePendingMerge(identity.id, redirectPath); 191 190 } 192 191 } else { ··· 247 246 // success (via `redirect()`), so callers only reach the line after the call 248 247 // when the token insert failed — at which point they should fall through to 249 248 // the normal sign-in flow rather than blocking the user. 250 - const stagePendingMerge = async ( 251 - identityId: string, 252 - redirectPath: string, 253 - ) => { 249 + const stagePendingMerge = async (identityId: string, redirectPath: string) => { 254 250 const { data: targetToken, error } = await supabaseServerClient 255 251 .from("email_auth_tokens") 256 252 .insert({ ··· 261 257 .select("id") 262 258 .single(); 263 259 if (error) 264 - console.error( 265 - "[oauth/callback] pending merge token insert failed:", 266 - error, 267 - ); 260 + console.error("[oauth/callback] pending merge token insert failed:", error); 268 261 if (!targetToken) return; 269 262 await setPendingMergeToken(targetToken.id); 270 263 redirect(`/merge-accounts?redirect=${encodeURIComponent(redirectPath)}`);
+44 -37
app/lish/[did]/[publication]/PublicationContent.tsx
··· 3 3 import { PublicationAuthor } from "./PublicationAuthor"; 4 4 import { PublicationHeader } from "./PublicationHeader"; 5 5 import { PublicationNav } from "./PublicationNav"; 6 + import { PublicationStickyHeader } from "./PublicationStickyHeader"; 6 7 import { 7 8 normalizePublicationRecord, 8 9 normalizeDocumentRecord, 9 10 } from "src/utils/normalizeRecords"; 10 11 import { FontLoader } from "components/FontLoader"; 11 12 import { SpeedyLink } from "components/SpeedyLink"; 13 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 14 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 12 15 import { SubscribeInput } from "components/Subscribe/SubscribeButton"; 13 16 import { 14 17 PublicationPostsList, ··· 83 86 <PublicationHomeLayout 84 87 uri={publication.uri} 85 88 showPageBackground={!!showPageBackground} 86 - > 87 - <PublicationHeader 88 - iconUrl={ 89 - record?.icon 90 - ? `/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]}` 91 - : undefined 92 - } 93 - publicationName={publication.name} 94 - description={record?.description} 95 - author={ 96 - profile ? ( 97 - <PublicationAuthor 98 - did={profile.did} 99 - displayName={profile.displayName} 100 - handle={profile.handle} 89 + stickyHeader={ 90 + <PublicationStickyHeader 91 + nav={ 92 + <PublicationNav 93 + publicationUrl={getPublicationURL(publication)} 94 + pages={(publication.publication_pages ?? []).filter( 95 + (p) => p.record_uri, 96 + )} 97 + activePath="/" 101 98 /> 102 - ) : undefined 103 - } 104 - subscribeButton={ 105 - <div className="max-w-sm mx-auto"> 106 - <SubscribeInput 107 - publicationUri={publication.uri} 108 - publicationUrl={record?.url} 109 - publicationName={record?.name ?? publication.name} 110 - publicationDescription={record?.description} 111 - newsletterMode={newsletterMode} 112 - /> 113 - </div> 114 - } 115 - /> 116 - <PublicationNav 117 - did={did} 118 - publicationName={publication.name} 119 - pages={(publication.publication_pages ?? []).filter( 120 - (p) => p.record_uri, 121 - )} 122 - activePath="/" 123 - /> 99 + } 100 + > 101 + <PublicationHeader 102 + iconUrl={ 103 + record?.icon ? blobRefToSrc(record.icon.ref, did) : undefined 104 + } 105 + publicationName={publication.name} 106 + description={record?.description} 107 + author={ 108 + profile ? ( 109 + <PublicationAuthor 110 + did={profile.did} 111 + displayName={profile.displayName} 112 + handle={profile.handle} 113 + /> 114 + ) : undefined 115 + } 116 + subscribeButton={ 117 + <div className="max-w-sm mx-auto"> 118 + <SubscribeInput 119 + publicationUri={publication.uri} 120 + publicationUrl={record?.url} 121 + publicationName={record?.name ?? publication.name} 122 + publicationDescription={record?.description} 123 + newsletterMode={newsletterMode} 124 + /> 125 + </div> 126 + } 127 + /> 128 + </PublicationStickyHeader> 129 + } 130 + > 124 131 <PublicationPostsList 125 132 publication={publication} 126 133 publicationRecord={record}
+60 -16
app/lish/[did]/[publication]/PublicationHeader.tsx
··· 6 6 description?: string; 7 7 author?: React.ReactNode; 8 8 subscribeButton?: React.ReactNode; 9 + variant?: "stacked" | "inline"; 9 10 }) { 11 + let variant = props.variant ?? "stacked"; 12 + let icon = props.iconUrl ? ( 13 + <div 14 + className={`shrink-0 rounded-full${variant === "stacked" ? " mx-auto" : ""}`} 15 + style={{ 16 + width: "calc(40px - 16px * var(--header-shrink, 0))", 17 + height: "calc(40px - 16px * var(--header-shrink, 0))", 18 + backgroundImage: `url(${props.iconUrl})`, 19 + backgroundRepeat: "no-repeat", 20 + backgroundPosition: "center", 21 + backgroundSize: "cover", 22 + }} 23 + /> 24 + ) : null; 25 + let title = ( 26 + <h2 27 + className={`text-accent-contrast${variant === "stacked" ? " pt-1" : ""}`} 28 + style={{ fontSize: "calc(22px - 6px * var(--header-shrink, 0))" }} 29 + > 30 + {props.publicationName} 31 + </h2> 32 + ); 33 + 10 34 return ( 11 - <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 12 - {props.iconUrl && ( 35 + <div 36 + className="pubHeader flex flex-col w-full text-center justify-center" 37 + style={{ 38 + paddingBottom: "calc(32px - 32px * var(--header-shrink, 0))", 39 + ...(variant === "stacked" 40 + ? { paddingTop: "calc(4px - 4px * var(--header-shrink, 0))" } 41 + : null), 42 + }} 43 + > 44 + {variant === "inline" ? ( 13 45 <div 14 - className="shrink-0 w-10 h-10 rounded-full mx-auto" 15 - style={{ 16 - backgroundImage: `url(${props.iconUrl})`, 17 - backgroundRepeat: "no-repeat", 18 - backgroundPosition: "center", 19 - backgroundSize: "cover", 20 - }} 21 - /> 46 + className="flex items-center justify-center" 47 + style={{ gap: "calc(12px - 6px * var(--header-shrink, 0))" }} 48 + > 49 + {icon} 50 + {title} 51 + </div> 52 + ) : ( 53 + <> 54 + {icon} 55 + {title} 56 + </> 22 57 )} 23 - <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 24 - {props.publicationName} 25 - </h2> 26 - <p className="sm:text-lg text-secondary">{props.description} </p> 27 - {props.author} 28 - <div className="sm:pt-4 pt-4">{props.subscribeButton}</div> 58 + <div 59 + className="overflow-hidden" 60 + style={{ 61 + maxHeight: "calc((1 - var(--header-shrink, 0)) * 400px)", 62 + opacity: "calc(1 - var(--header-shrink, 0))", 63 + }} 64 + > 65 + {props.description && ( 66 + <p className="sm:text-lg text-secondary">{props.description}</p> 67 + )} 68 + {props.author} 69 + {props.subscribeButton && ( 70 + <div className="sm:pt-4 pt-4">{props.subscribeButton}</div> 71 + )} 72 + </div> 29 73 </div> 30 74 ); 31 75 }
-29
app/lish/[did]/[publication]/PublicationHeaderEditor.tsx
··· 1 - export function PublicationHeaderEditor(props: { 2 - iconUrl?: string; 3 - publicationName: string; 4 - description?: string; 5 - }) { 6 - return ( 7 - <div className="pubHeader flex flex-col pb-8 w-full text-center"> 8 - <div className="flex items-center justify-center gap-3"> 9 - {props.iconUrl && ( 10 - <div 11 - className="shrink-0 w-10 h-10 rounded-full" 12 - style={{ 13 - backgroundImage: `url(${props.iconUrl})`, 14 - backgroundRepeat: "no-repeat", 15 - backgroundPosition: "center", 16 - backgroundSize: "cover", 17 - }} 18 - /> 19 - )} 20 - <h2 className="text-accent-contrast sm:text-xl text-[22px]"> 21 - {props.publicationName} 22 - </h2> 23 - </div> 24 - {props.description && ( 25 - <p className="sm:text-lg text-secondary">{props.description}</p> 26 - )} 27 - </div> 28 - ); 29 - }
+24 -8
app/lish/[did]/[publication]/PublicationHomeLayout.tsx
··· 5 5 export function PublicationHomeLayout(props: { 6 6 uri: string; 7 7 showPageBackground: boolean; 8 + stickyHeader?: React.ReactNode; 8 9 children: React.ReactNode; 9 10 }) { 10 11 let { ref } = usePreserveScroll<HTMLDivElement>(props.uri); 12 + let inner = ( 13 + <> 14 + {props.stickyHeader} 15 + <div className="pubContent sm:max-w-(--page-width-units) sm:min-w-(--page-width-units) w-full mx-auto px-3 sm:px-4 pb-5"> 16 + {props.children} 17 + </div> 18 + </> 19 + ); 20 + if (props.showPageBackground) { 21 + return ( 22 + <div className="pubWrapper flex flex-col sm:py-6 h-full max-w-(--page-width-units) mx-auto px-0 py-2"> 23 + <div 24 + ref={ref} 25 + className="pubContentScroll overflow-auto h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] border border-border rounded-lg flex flex-col" 26 + > 27 + {inner} 28 + </div> 29 + </div> 30 + ); 31 + } 11 32 return ( 12 33 <div 13 - ref={props.showPageBackground ? null : ref} 14 - className={`pubWrapper flex flex-col sm:py-6 h-full ${props.showPageBackground ? "max-w-(--page-width-units) mx-auto px-0 py-2" : "w-full overflow-y-scroll"}`} 34 + ref={ref} 35 + className="pubWrapper flex flex-col sm:pb-6 h-full w-full overflow-y-scroll" 15 36 > 16 - <div 17 - ref={!props.showPageBackground ? null : ref} 18 - className={`pubContent sm:max-w-(--page-width-units) sm:min-w-(--page-width-units) w-full mx-auto px-3 sm:px-4 py-5 ${props.showPageBackground ? "overflow-auto h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] border border-border rounded-lg" : "h-fit"}`} 19 - > 20 - {props.children} 21 - </div> 37 + {inner} 22 38 </div> 23 39 ); 24 40 }
+7 -22
app/lish/[did]/[publication]/PublicationNav.tsx
··· 7 7 }; 8 8 9 9 export function PublicationNav(props: { 10 - did: string; 11 - publicationName: string; 10 + publicationUrl: string; 12 11 pages: PublicationNavPage[]; 13 12 activePath: string | null; 14 13 }) { 15 14 if (props.pages.length === 0) return null; 16 15 17 - let baseHref = `/lish/${props.did}/${props.publicationName}`; 18 - 19 16 let sortedPages = [...props.pages].sort((a, b) => { 20 17 let aHome = a.path === "/" ? 0 : 1; 21 18 let bHome = b.path === "/" ? 0 : 1; ··· 23 20 return (a.path ?? "").localeCompare(b.path ?? ""); 24 21 }); 25 22 26 - let homeActive = props.activePath === null || props.activePath === "/"; 27 - 28 23 return ( 29 - <nav className="publicationNav border-b border-border-light w-full mx-auto"> 30 - <div className="flex items-center gap-1 px-3 sm:px-4 py-2 overflow-x-auto sm:max-w-(--page-width-units) mx-auto"> 31 - <SpeedyLink 32 - href={baseHref} 33 - className={`shrink-0 px-2 py-1 rounded-md text-sm hover:no-underline! ${ 34 - homeActive 35 - ? "bg-accent-1 text-accent-2" 36 - : "text-secondary hover:bg-border-light" 37 - }`} 38 - > 39 - Posts 40 - </SpeedyLink> 24 + <nav className="publicationNav border-t border-b border-border-light w-full sm:max-w-[calc(var(--page-width-units)*1.25)] mx-auto"> 25 + <div className="flex items-center gap-1 px-2 sm:px-3 py-1 overflow-x-auto sm:max-w-(--page-width-units) mx-auto"> 41 26 {sortedPages.map((page) => { 42 27 if (!page.path) return null; 43 28 let segment = page.path === "/" ? "" : page.path; 44 - let href = `${baseHref}${segment}`; 29 + let href = `${props.publicationUrl}${segment}`; 45 30 let active = props.activePath === page.path; 46 31 return ( 47 32 <SpeedyLink 48 33 key={page.id} 49 34 href={href} 50 - className={`shrink-0 px-2 py-1 rounded-md text-sm hover:no-underline! ${ 35 + className={`shrink-0 px-2 py-0.5 rounded-md text-sm hover:no-underline! ${ 51 36 active 52 - ? "bg-accent-1 text-accent-2" 53 - : "text-secondary hover:bg-border-light" 37 + ? "bg-accent-1 text-accent-2 font-semibold" 38 + : "text-secondary" 54 39 }`} 55 40 > 56 41 {page.title || page.path}
+31 -14
app/lish/[did]/[publication]/PublicationPostItem.tsx
··· 15 15 coverImageAlt?: string; 16 16 }; 17 17 18 + type MediumProps = CommonProps & { 19 + coverImageSrc?: string; 20 + coverImageAlt?: string; 21 + }; 22 + 18 23 function MetaRow({ 19 24 author, 20 25 date, ··· 83 88 ); 84 89 } 85 90 86 - export function PublicationPostItemMedium(props: CommonProps) { 91 + export function PublicationPostItemMedium(props: MediumProps) { 92 + const hasCoverImage = !!props.coverImageSrc; 87 93 return ( 88 94 <> 89 - <div className="flex w-full grow flex-col"> 90 - <PostLink href={props.href}> 91 - {props.title && <h3 className="text-primary">{props.title}</h3>} 92 - <p className="italic text-secondary line-clamp-3"> 93 - {props.description} 94 - </p> 95 - </PostLink> 96 - <MetaRow 97 - author={props.author} 98 - date={props.date} 99 - interactions={props.interactions} 100 - textClassName="text-sm" 101 - /> 95 + <div className="flex w-full gap-3 items-stretch"> 96 + <div className="flex w-full grow flex-col min-w-0"> 97 + <PostLink href={props.href}> 98 + {props.title && <h3 className="text-primary">{props.title}</h3>} 99 + <p className="italic text-secondary line-clamp-3"> 100 + {props.description} 101 + </p> 102 + </PostLink> 103 + <MetaRow 104 + author={props.author} 105 + date={props.date} 106 + interactions={props.interactions} 107 + textClassName="text-sm" 108 + /> 109 + </div> 110 + {hasCoverImage && ( 111 + <div className="self-stretch shrink-0 max-w-24"> 112 + <img 113 + src={props.coverImageSrc} 114 + alt={props.coverImageAlt || props.title || ""} 115 + className="h-full w-auto object-cover rounded" 116 + /> 117 + </div> 118 + )} 102 119 </div> 103 120 <hr className="last:hidden border-border-light" /> 104 121 </>
+7 -4
app/lish/[did]/[publication]/PublicationPostsList.tsx
··· 122 122 ? "small" 123 123 : "medium"; 124 124 125 + const postDid = new AtUri(post.uri).host; 126 + const coverImageSrc = doc_record.coverImage 127 + ? blobRefToSrc(doc_record.coverImage.ref, postDid) 128 + : undefined; 129 + 125 130 if (Variant === "large") { 126 - const postDid = new AtUri(post.uri).host; 127 - const coverImageSrc = doc_record.coverImage 128 - ? blobRefToSrc(doc_record.coverImage.ref, postDid) 129 - : undefined; 130 131 return ( 131 132 <PublicationPostItemLarge 132 133 key={post.uri} ··· 165 166 } 166 167 date={date} 167 168 interactions={interactions} 169 + coverImageSrc={coverImageSrc} 170 + coverImageAlt={doc_record.title} 168 171 /> 169 172 ); 170 173 })}
+94
app/lish/[did]/[publication]/PublicationStickyHeader.tsx
··· 1 + "use client"; 2 + import { useEffect, useRef } from "react"; 3 + import { usePathname } from "next/navigation"; 4 + 5 + export function PublicationStickyHeader(props: { 6 + sticky?: boolean; 7 + scrollContainerSelector?: string; 8 + nav?: React.ReactNode; 9 + children: React.ReactNode; 10 + }) { 11 + let ref = useRef<HTMLDivElement>(null); 12 + let pathname = usePathname(); 13 + let sticky = props.sticky ?? true; 14 + let selector = props.scrollContainerSelector; 15 + 16 + useEffect(() => { 17 + let el = ref.current; 18 + let parent = el?.parentElement; 19 + if (!el || !parent) return; 20 + 21 + let rafId: number | null = null; 22 + let writeProgress = (target: HTMLElement) => { 23 + let p = Math.min(Math.max(target.scrollTop / 100, 0), 1); 24 + el!.style.setProperty("--header-shrink", String(p)); 25 + }; 26 + let schedule = (target: HTMLElement) => { 27 + if (rafId !== null) return; 28 + rafId = requestAnimationFrame(() => { 29 + rafId = null; 30 + writeProgress(target); 31 + }); 32 + }; 33 + 34 + if (selector) { 35 + let onScroll = (e: Event) => { 36 + let t = e.target; 37 + if (!(t instanceof HTMLElement)) return; 38 + if (!t.matches(selector)) return; 39 + schedule(t); 40 + }; 41 + parent.addEventListener("scroll", onScroll, { 42 + passive: true, 43 + capture: true, 44 + }); 45 + let initial = parent.querySelector(selector) as HTMLElement | null; 46 + if (initial) writeProgress(initial); 47 + return () => { 48 + if (rafId !== null) cancelAnimationFrame(rafId); 49 + parent.removeEventListener("scroll", onScroll, true); 50 + }; 51 + } 52 + 53 + let p: HTMLElement | null = parent; 54 + while (p && p !== document.body) { 55 + let s = getComputedStyle(p); 56 + if (s.overflowY === "auto" || s.overflowY === "scroll") break; 57 + p = p.parentElement; 58 + } 59 + if (!p || p === document.body) return; 60 + let target = p; 61 + let onScroll = () => schedule(target); 62 + target.addEventListener("scroll", onScroll, { passive: true }); 63 + writeProgress(target); 64 + return () => { 65 + if (rafId !== null) cancelAnimationFrame(rafId); 66 + target.removeEventListener("scroll", onScroll); 67 + }; 68 + }, [selector]); 69 + 70 + useEffect(() => { 71 + ref.current?.style.setProperty("--header-shrink", "0"); 72 + }, [pathname]); 73 + 74 + return ( 75 + <div 76 + ref={ref} 77 + className={ 78 + sticky 79 + ? "pubStickyHeader sticky top-0 z-10 bg-bg-page shrink-0" 80 + : "pubStickyHeader shrink-0" 81 + } 82 + > 83 + <div 84 + className="sm:max-w-(--page-width-units) w-full mx-auto px-3 sm:px-4" 85 + style={{ 86 + paddingTop: "calc(20px - 20px * var(--header-shrink, 0))", 87 + }} 88 + > 89 + {props.children} 90 + </div> 91 + {props.nav} 92 + </div> 93 + ); 94 + }
+2 -2
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 31 31 import { FontLoader } from "components/FontLoader"; 32 32 import { mergePreferences } from "src/utils/mergePreferences"; 33 33 import { PublicationNav } from "../PublicationNav"; 34 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 34 35 35 36 export async function DocumentPageRenderer({ 36 37 did, ··· 170 171 <LeafletLayout> 171 172 {document.publication?.pages?.length ? ( 172 173 <PublicationNav 173 - did={did} 174 - publicationName={document.publication.name} 174 + publicationUrl={getPublicationURL(document.publication)} 175 175 pages={document.publication.pages} 176 176 activePath={null} 177 177 />
+26 -28
app/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx
··· 34 34 import { PublicationNav } from "../PublicationNav"; 35 35 import { PublicationHeader } from "../PublicationHeader"; 36 36 import { PublicationHomeLayout } from "../PublicationHomeLayout"; 37 + import { PublicationStickyHeader } from "../PublicationStickyHeader"; 38 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 39 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 37 40 38 41 import { extractCodeBlocks } from "./extractCodeBlocks"; 39 42 import { fetchPollData } from "./fetchPollData"; ··· 169 172 170 173 const theme = normalizedPublication?.theme; 171 174 const showPageBackground = !!theme?.showPageBackground; 172 - const profileResp = await agent.getProfile({ actor: did }).then( 173 - (res) => res.data, 174 - () => undefined, 175 - ); 176 175 177 176 const documentContextValue: DocumentContextValue = { 178 177 uri: page.record.publication, ··· 216 215 <PublicationHomeLayout 217 216 uri={publication.uri} 218 217 showPageBackground={showPageBackground} 218 + stickyHeader={ 219 + <PublicationStickyHeader 220 + nav={ 221 + <PublicationNav 222 + publicationUrl={getPublicationURL(publication)} 223 + pages={(publication.publication_pages ?? []).filter( 224 + (p) => p.record_uri, 225 + )} 226 + activePath={page.path} 227 + /> 228 + } 229 + > 230 + <PublicationHeader 231 + iconUrl={ 232 + normalizedPublication?.icon 233 + ? blobRefToSrc(normalizedPublication.icon.ref, did) 234 + : undefined 235 + } 236 + publicationName={publication.name} 237 + description={normalizedPublication?.description} 238 + /> 239 + </PublicationStickyHeader> 240 + } 219 241 > 220 - <PublicationHeader 221 - iconUrl={ 222 - normalizedPublication?.icon 223 - ? `/api/atproto_images?did=${did}&cid=${(normalizedPublication.icon.ref as unknown as { $link: string })["$link"]}` 224 - : undefined 225 - } 226 - publicationName={publication.name} 227 - description={normalizedPublication?.description} 228 - author={ 229 - profileResp ? ( 230 - <span className="text-sm text-secondary"> 231 - by {profileResp.displayName || profileResp.handle} 232 - </span> 233 - ) : undefined 234 - } 235 - /> 236 - <PublicationNav 237 - did={did} 238 - publicationName={publication.name} 239 - pages={(publication.publication_pages ?? []).filter( 240 - (p) => p.record_uri, 241 - )} 242 - activePath={page.path} 243 - /> 244 242 <div className="pubPageContent pt-6"> 245 243 <PostContent 246 244 blocks={allBlocks}
-54
app/lish/[did]/[publication]/edit/[[...route]]/PublicationCollapsibleHeader.tsx
··· 1 - "use client"; 2 - import { useEffect, useRef } from "react"; 3 - import { usePathname } from "next/navigation"; 4 - 5 - export function PublicationCollapsibleHeader(props: { 6 - children: React.ReactNode; 7 - }) { 8 - let ref = useRef<HTMLDivElement>(null); 9 - let pathname = usePathname(); 10 - 11 - useEffect(() => { 12 - let el = ref.current; 13 - let parent = el?.parentElement; 14 - if (!el || !parent) return; 15 - 16 - let rafId: number | null = null; 17 - 18 - let onScroll = (e: Event) => { 19 - let target = e.target; 20 - if (!(target instanceof HTMLElement)) return; 21 - if (!target.classList.contains("pageScrollWrapper")) return; 22 - if (rafId !== null) return; 23 - rafId = requestAnimationFrame(() => { 24 - rafId = null; 25 - if (!el) return; 26 - let height = el.offsetHeight; 27 - let collapsed = Math.max(0, Math.min(target.scrollTop, height)); 28 - el.style.marginTop = `-${collapsed}px`; 29 - }); 30 - }; 31 - 32 - parent.addEventListener("scroll", onScroll, { 33 - passive: true, 34 - capture: true, 35 - }); 36 - return () => { 37 - if (rafId !== null) cancelAnimationFrame(rafId); 38 - parent.removeEventListener("scroll", onScroll, true); 39 - }; 40 - }, []); 41 - 42 - useEffect(() => { 43 - if (ref.current) ref.current.style.marginTop = ""; 44 - }, [pathname]); 45 - 46 - return ( 47 - <div 48 - ref={ref} 49 - className="px-3 sm:px-4 pt-5 w-full sm:max-w-(--page-width-units) mx-auto shrink-0" 50 - > 51 - {props.children} 52 - </div> 53 - ); 54 - }
+12 -9
app/lish/[did]/[publication]/edit/[[...route]]/layout.tsx
··· 7 7 import { LoginModal } from "components/LoginButton"; 8 8 import { AtUri } from "@atproto/syntax"; 9 9 import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; 10 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 10 11 import { PublicationSWRDataProvider } from "../../dashboard/PublicationSWRProvider"; 11 12 import { PublicationPagesNav } from "./PublicationPagesNav"; 12 13 import { PublicationEditHeader } from "./PublicationEditHeader"; 13 - import { PublicationHeaderEditor } from "../../PublicationHeaderEditor"; 14 - import { PublicationCollapsibleHeader } from "./PublicationCollapsibleHeader"; 14 + import { PublicationHeader } from "../../PublicationHeader"; 15 + import { PublicationStickyHeader } from "../../PublicationStickyHeader"; 15 16 16 17 export async function generateMetadata(props: { 17 18 params: Promise<{ publication: string; did: string }>; ··· 77 78 } 78 79 79 80 let uri = new AtUri(publication.uri); 80 - const iconUrl = record?.icon 81 - ? `/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]}` 82 - : undefined; 81 + const iconUrl = record?.icon ? blobRefToSrc(record.icon.ref, did) : undefined; 83 82 84 83 return ( 85 84 <PublicationSWRDataProvider ··· 93 92 did={params.did} 94 93 publicationName={params.publication} 95 94 /> 96 - <div className="flex flex-col grow min-h-0 bg-bg-page rounded-t-lg overflow-hidden"> 97 - <PublicationCollapsibleHeader> 98 - <PublicationHeaderEditor 95 + <div className="pubWrapper flex flex-col grow min-h-0 bg-bg-page rounded-t-lg overflow-hidden"> 96 + <PublicationStickyHeader 97 + sticky={false} 98 + scrollContainerSelector=".pageScrollWrapper" 99 + > 100 + <PublicationHeader 101 + variant="inline" 99 102 iconUrl={iconUrl} 100 103 publicationName={publication.name} 101 104 description={record?.description} 102 105 /> 103 - </PublicationCollapsibleHeader> 106 + </PublicationStickyHeader> 104 107 <PublicationPagesNav 105 108 did={params.did} 106 109 publicationName={params.publication}
+2 -1
app/lish/[did]/[publication]/page.tsx
··· 60 60 61 61 try { 62 62 if (homePage && homePage.record) { 63 - let pageRecord = homePage.record as unknown as PubLeafletPublicationPage.Record; 63 + let pageRecord = 64 + homePage.record as unknown as PubLeafletPublicationPage.Record; 64 65 return ( 65 66 <PublicationPageRenderer 66 67 did={did}