a tool for shared writing and social publishing
0

Configure Feed

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

adjusting layout and popover behavior

celine (Apr 7, 2026, 6:31 PM EDT) a3fc2fc8 de1c22c9

+175 -103
+68 -34
app/lish/[did]/[publication]/theme-settings/PostPreview.tsx
··· 2 2 3 3 import { LinearDocumentPage } from "app/lish/[did]/[publication]/[rkey]/LinearDocumentPage"; 4 4 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 5 - import { DocumentProvider } from "contexts/DocumentContext"; 5 + import { 6 + DocumentProvider, 7 + type PublicationContext, 8 + } from "contexts/DocumentContext"; 6 9 import { useIdentityData } from "components/IdentityProvider"; 7 10 import type { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 8 11 import type { PubLeafletPagesLinearDocument } from "lexicons/api"; ··· 73 76 tags: ["preview", "theme"], 74 77 }; 75 78 76 - const fakeDocument: NonNullable<PostPageData> = { 77 - data: {}, 78 - uri: FAKE_DOC_URI, 79 - normalizedDocument: fakeNormalizedDocument, 80 - normalizedPublication: null, 81 - quotesAndMentions: [], 82 - theme: null, 83 - prevNext: undefined, 84 - publication: null, 85 - comments: [], 86 - comments_on_documents: [], 87 - mentions: [], 88 - document_mentions_in_bsky: [], 89 - leaflets_in_publications: [], 90 - leafletId: null, 91 - recommendsCount: 0, 92 - documents_in_publications: [], 93 - recommends_on_documents: [], 94 - } as unknown as NonNullable<PostPageData>; 95 - 96 - const fakeDocumentContextValue: DocumentContextValue = { 97 - uri: FAKE_DOC_URI, 98 - normalizedDocument: fakeNormalizedDocument, 99 - normalizedPublication: null, 100 - theme: undefined, 101 - prevNext: undefined, 102 - quotesAndMentions: [], 103 - publication: null, 104 - comments: [], 105 - mentions: [], 106 - leafletId: null, 107 - recommendsCount: 0, 108 - }; 79 + function makeFakeDocument( 80 + publication?: { 81 + uri: string; 82 + name: string; 83 + identity_did: string; 84 + record: unknown; 85 + } | null, 86 + ): NonNullable<PostPageData> { 87 + return { 88 + data: {}, 89 + uri: FAKE_DOC_URI, 90 + normalizedDocument: fakeNormalizedDocument, 91 + normalizedPublication: null, 92 + quotesAndMentions: [], 93 + theme: null, 94 + prevNext: undefined, 95 + publication: publication || null, 96 + comments: [], 97 + comments_on_documents: [], 98 + mentions: [], 99 + document_mentions_in_bsky: [], 100 + leaflets_in_publications: [], 101 + leafletId: null, 102 + recommendsCount: 0, 103 + documents_in_publications: publication 104 + ? [{ publications: publication }] 105 + : [], 106 + recommends_on_documents: [], 107 + } as unknown as NonNullable<PostPageData>; 108 + } 109 109 110 110 export function PostPreview(props: { 111 111 showPageBackground: boolean; ··· 123 123 did: FAKE_DID, 124 124 handle: "preview.bsky.social", 125 125 displayName: "Preview Author", 126 + }; 127 + 128 + let pubInfo: PublicationContext = publication 129 + ? { 130 + uri: publication.uri, 131 + name: publication.name, 132 + identity_did: publication.identity_did, 133 + record: publication.record as NonNullable<PublicationContext>["record"], 134 + publication_subscriptions: ( 135 + publication.publication_subscriptions || [] 136 + ).map((s) => ({ 137 + created_at: s.created_at, 138 + identity: s.identity, 139 + publication: s.publication, 140 + record: s.record, 141 + uri: s.uri, 142 + })), 143 + } 144 + : null; 145 + 146 + let fakeDocument = makeFakeDocument(pubInfo); 147 + 148 + let fakeDocumentContextValue: DocumentContextValue = { 149 + uri: FAKE_DOC_URI, 150 + normalizedDocument: fakeNormalizedDocument, 151 + normalizedPublication: null, 152 + theme: undefined, 153 + prevNext: undefined, 154 + quotesAndMentions: [], 155 + publication: pubInfo, 156 + comments: [], 157 + mentions: [], 158 + leafletId: null, 159 + recommendsCount: 0, 126 160 }; 127 161 128 162 return (
+102 -68
app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx
··· 1 1 "use client"; 2 2 3 - import { useState } from "react"; 3 + import { useRef, useState } from "react"; 4 4 import { 5 5 BaseThemeProvider, 6 6 CardBorderHiddenContext, ··· 21 21 usePublicationData, 22 22 useNormalizedPublicationRecord, 23 23 } from "../dashboard/PublicationSWRProvider"; 24 + import { Separator } from "components/Layout"; 24 25 25 26 export function ThemeSettingsContent() { 27 + let toolbarRef = useRef<HTMLDivElement>(null); 26 28 let [previewMode, setPreviewMode] = useState<"post" | "pub">("post"); 27 - let [loading, setLoading] = useState(false); 28 29 let { data } = usePublicationData(); 29 30 let { publication } = data || {}; 30 31 let record = useNormalizedPublicationRecord(); ··· 38 39 pubBGImage, 39 40 leafletBGRepeat, 40 41 showPageBackground, 41 - submitTheme, 42 - toaster, 43 42 } = state; 44 43 45 44 return ( ··· 54 53 > 55 54 <div className="w-full h-screen flex relative overflow-hidden"> 56 55 {/* Theme Setter Panel */} 57 - <div className="absolute sm:top-6 sm:left-6 top-4 right-4 z-20"> 58 - <Popover 59 - align="start" 60 - side="bottom" 61 - arrowFill="white" 62 - border="#CCCCCC" 63 - className="sm:w-sm w-[1000px] rounded-lg! !p-0 bg-white! border-[#CCCCCC]!" 64 - trigger={ 65 - <button 66 - type="button" 67 - className="w-12 h-12 rounded-full bg-accent-1 text-accent-2 data-[state=open]:bg-accent-2 data-[state=open]:text-accent-1 data-[state=open]:border-accent-1 flex items-center justify-center transition-colors cursor-pointer border-2 border-acccent-2" 68 - > 69 - <PaintSmall /> 70 - </button> 71 - } 72 - asChild 73 - > 74 - <BaseThemeProvider 75 - local 76 - {...localPubTheme} 77 - headingFontId={headingFont} 78 - bodyFontId={bodyFont} 79 - hasBackgroundImage={!!image} 80 - pageWidth={pageWidth} 81 - > 82 - <div className="flex flex-col overflow-y-auto max-h-(--radix-popover-content-available-height) py-3"> 83 - <div className="p-3 pt-0 "> 84 - <ButtonPrimary 85 - fullWidth 86 - type="button" 87 - disabled={loading} 88 - onClick={async () => { 89 - let result = await submitTheme(setLoading); 90 - if (result?.success) { 91 - toaster({ 92 - content: "Theme updated!", 93 - type: "success", 94 - }); 95 - } 96 - }} 97 - > 98 - {loading ? <DotLoader /> : "Update"} 99 - </ButtonPrimary> 100 - </div> 101 - 102 - {/* Toggle + Save Header */} 103 - <div className="flex items-center justify-between px-3"> 104 - <div className="font-bold text-[#595959]">Preview Mode</div> 105 - <ToggleGroup 106 - value={previewMode} 107 - optionClassName="text-base!" 108 - onChange={setPreviewMode} 109 - options={[ 110 - { value: "post", label: "Post" }, 111 - { value: "pub", label: "Pub" }, 112 - ]} 113 - /> 114 - </div> 115 - <div className="p-3 gap-2 flex flex-col"> 116 - <PubThemePickerPanel state={state} /> 117 - </div> 118 - </div> 119 - </BaseThemeProvider> 120 - </Popover> 56 + <div 57 + ref={toolbarRef} 58 + className="absolute sm:top-6 sm:left-6 top-4 right-4 z-20 flex gap-1 bg-accent-1 w-fit p-1 pl-2 rounded-md items-center" 59 + > 60 + <PubThemePopover state={state} toolbarRef={toolbarRef} /> 61 + <Separator classname="h-8! ml-1" /> 62 + <ToggleGroup 63 + value={previewMode} 64 + optionClassName="text-base! py-1! px-2!" 65 + onChange={setPreviewMode} 66 + options={[ 67 + { value: "post", label: "Post" }, 68 + { value: "pub", label: "Pub" }, 69 + ]} 70 + /> 121 71 </div> 122 72 123 73 {/* Full-page Preview */} ··· 146 96 </CardBorderHiddenContext.Provider> 147 97 ); 148 98 } 99 + 100 + const PubThemePopover = ({ 101 + state, 102 + toolbarRef, 103 + }: { 104 + state: ReturnType<typeof usePubThemeEditorState>; 105 + toolbarRef: React.RefObject<HTMLDivElement | null>; 106 + }) => { 107 + let { 108 + localPubTheme, 109 + headingFont, 110 + bodyFont, 111 + image, 112 + pageWidth, 113 + submitTheme, 114 + toaster, 115 + } = state; 116 + let [loading, setLoading] = useState(false); 117 + 118 + return ( 119 + <Popover 120 + defaultOpen 121 + align="start" 122 + side="bottom" 123 + arrowFill="white" 124 + border="#CCCCCC" 125 + onInteractOutside={(e) => { 126 + if ( 127 + toolbarRef.current && 128 + e.target instanceof Node && 129 + toolbarRef.current.contains(e.target) 130 + ) { 131 + e.preventDefault(); 132 + } 133 + }} 134 + className="sm:w-sm w-[1000px] rounded-lg! !p-0 bg-white! border-[#CCCCCC]!" 135 + trigger={ 136 + <button 137 + type="button" 138 + className="w-8 h-8 rounded-md bg-accent-1 text-accent-2 data-[state=open]:bg-accent-2 data-[state=open]:text-accent-1 flex items-center justify-center transition-colors cursor-pointer " 139 + > 140 + <PaintSmall /> 141 + </button> 142 + } 143 + asChild 144 + > 145 + <BaseThemeProvider 146 + local 147 + {...localPubTheme} 148 + headingFontId={headingFont} 149 + bodyFontId={bodyFont} 150 + hasBackgroundImage={!!image} 151 + pageWidth={pageWidth} 152 + > 153 + <div className="flex flex-col overflow-y-auto max-h-(--radix-popover-content-available-height) py-3"> 154 + {/* Toggle + Save Header */} 155 + 156 + <div className="p-3 pt-0 "> 157 + <ButtonPrimary 158 + fullWidth 159 + type="button" 160 + disabled={loading} 161 + onClick={async () => { 162 + let result = await submitTheme(setLoading); 163 + if (result?.success) { 164 + toaster({ 165 + content: "Theme updated!", 166 + type: "success", 167 + }); 168 + } 169 + }} 170 + > 171 + {loading ? <DotLoader /> : "Update"} 172 + </ButtonPrimary> 173 + </div> 174 + 175 + <div className="px-3 gap-2 flex flex-col"> 176 + <PubThemePickerPanel state={state} /> 177 + </div> 178 + </div> 179 + </BaseThemeProvider> 180 + </Popover> 181 + ); 182 + };
+5 -1
components/Popover/index.tsx
··· 16 16 border?: string; 17 17 className?: string; 18 18 open?: boolean; 19 + defaultOpen?: boolean; 19 20 onOpenChange?: (open: boolean) => void; 20 21 onOpenAutoFocus?: (e: Event) => void; 21 22 asChild?: boolean; 22 23 arrowFill?: string; 23 24 noArrow?: boolean; 25 + onInteractOutside?: (e: Event) => void; 24 26 }) => { 25 - let [open, setOpen] = useState(props.open || false); 27 + let [open, setOpen] = useState(props.open || props.defaultOpen || false); 26 28 useEffect(() => { 27 29 if (props.open !== undefined) setOpen(props.open); 28 30 }, [props.open]); 29 31 return ( 30 32 <RadixPopover.Root 33 + defaultOpen={props.defaultOpen} 31 34 open={props.open} 32 35 onOpenChange={(o) => { 33 36 setOpen(o); ··· 56 59 sideOffset={props.sideOffset ? props.sideOffset : 4} 57 60 collisionPadding={16} 58 61 onOpenAutoFocus={props.onOpenAutoFocus} 62 + onInteractOutside={props.onInteractOutside} 59 63 > 60 64 {props.children} 61 65 {!props.noArrow && (