a tool for shared writing and social publishing

adjusted popover to new one step design

+200 -93
+154 -65
app/[leaflet_id]/actions/PublishButton.tsx
··· 5 5 PubIcon, 6 6 PubListEmptyContent, 7 7 } from "components/ActionBar/Publications"; 8 + import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; 9 + import { AddSmall } from "components/Icons/AddSmall"; 8 10 import { LooseLeafSmall } from "components/Icons/ArchiveSmall"; 9 11 import { PublishSmall } from "components/Icons/PublishSmall"; 10 12 import { useIdentityData } from "components/IdentityProvider"; 13 + import { InputWithLabel } from "components/Input"; 11 14 import { Menu, MenuItem } from "components/Layout"; 12 15 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 13 16 import { Popover } from "components/Popover"; ··· 19 22 import { useState } from "react"; 20 23 import { useIsMobile } from "src/hooks/isMobile"; 21 24 import { useReplicache } from "src/replicache"; 25 + import { Json } from "supabase/database.types"; 22 26 23 27 export const PublishButton = () => { 24 28 let { data: pub } = useLeafletPublicationData(); ··· 90 94 let hasPubs = 91 95 identity && identity.atp_did && identity.publications.length > 0; 92 96 93 - if (!hasPubs) 94 - return ( 95 - <Menu 96 - asChild 97 - side={isMobile ? "top" : "right"} 98 - align={isMobile ? "center" : "start"} 99 - className="flex flex-col max-w-xs text-secondary" 100 - trigger={ 101 - <ActionButton 102 - primary 103 - icon={<PublishSmall className="shrink-0" />} 104 - label={"Publish on ATP"} 105 - /> 106 - } 107 - > 108 - <div className="text-sm text-tertiary">Publish to…</div> 109 - {identity?.publications?.map((d) => { 110 - return ( 111 - <MenuItem 112 - onSelect={async () => { 113 - // TODO 114 - // make this a draft of the selected Publication 115 - // redirect to the publication publish page 116 - }} 117 - > 118 - <PubIcon 119 - record={d.record as PubLeafletPublication.Record} 120 - uri={d.uri} 121 - /> 122 - <div className=" w-full truncate font-bold">{d.name}</div> 123 - </MenuItem> 124 - ); 125 - })} 126 - <hr className="border-border-light my-1" /> 127 - <MenuItem 128 - onSelect={() => { 129 - // TODO 130 - // send to one-off /publish page 131 - }} 132 - > 133 - <LooseLeafSmall /> 134 - <div className="font-bold pb-1">Publish as One-Off</div> 135 - </MenuItem> 136 - </Menu> 137 - ); 138 - else 139 - return ( 140 - <Popover 141 - asChild 142 - side={isMobile ? "top" : "right"} 143 - align={isMobile ? "center" : "start"} 144 - className="p-1!" 145 - trigger={ 146 - <ActionButton 147 - primary 148 - icon={<PublishSmall className="shrink-0" />} 149 - label={"Publish on ATP"} 150 - /> 151 - } 152 - > 153 - {/* this component is also used on Home to populate the sidebar when PubList is empty */} 154 - {/* however, this component needs to redirect to sign in, pub creation, AND publish so we might need to just make a new component */} 97 + return ( 98 + <Popover 99 + asChild 100 + side={isMobile ? "top" : "right"} 101 + align={isMobile ? "center" : "start"} 102 + className="max-w-xs w-[1000px]" 103 + trigger={ 104 + <ActionButton 105 + primary 106 + icon={<PublishSmall className="shrink-0" />} 107 + label={"Publish on ATP"} 108 + /> 109 + } 110 + > 111 + {!identity || !identity.atp_did ? ( 112 + // this component is also used on Home to populate the sidebar when PubList is empty 113 + // when user doesn't have an AT Proto account, and redirects back to the doc (hopefully with publish open? 114 + <div className="-mx-2 -my-1"> 115 + <PubListEmptyContent compact /> 116 + </div> 117 + ) : ( 118 + <div className="flex flex-col"> 119 + <PostDetailsForm /> 120 + <hr className="border-border-light my-3" /> 121 + <div> 122 + <PubSelector publications={identity.publications} /> 123 + </div> 124 + <hr className="border-border-light mt-3 mb-2" /> 125 + 126 + <div className="flex gap-2 items-center place-self-end"> 127 + <ButtonTertiary>Save as Draft</ButtonTertiary> 128 + <ButtonPrimary>Next</ButtonPrimary> 129 + </div> 130 + </div> 131 + )} 132 + </Popover> 133 + ); 134 + }; 135 + 136 + const PostDetailsForm = () => { 137 + let [description, setDescription] = useState(""); 138 + 139 + return ( 140 + <div className=" flex flex-col gap-1"> 141 + <div className="text-sm text-tertiary">Post Details</div> 142 + <div className="flex flex-col gap-2"> 143 + <InputWithLabel label="Title" value={"Title goes here"} disabled /> 144 + <InputWithLabel 145 + label="Description (optional)" 146 + textarea 147 + value={description} 148 + className="h-[4lh]" 149 + onChange={(e) => setDescription(e.currentTarget.value)} 150 + /> 151 + </div> 152 + </div> 153 + ); 154 + }; 155 + 156 + const PubSelector = (props: { 157 + publications: { 158 + identity_did: string; 159 + indexed_at: string; 160 + name: string; 161 + record: Json | null; 162 + uri: string; 163 + }[]; 164 + }) => { 165 + let [selectedPub, setSelectedPub] = useState<string | undefined>(undefined); 166 + return ( 167 + <div className="flex flex-col gap-1"> 168 + <div className="text-sm text-tertiary">Publish to…</div> 169 + {props.publications.length === 0 || props.publications === undefined ? ( 170 + <div className="flex flex-col gap-3"> 171 + <div className="flex gap-2 menuItem"> 172 + <LooseLeafSmall className="shrink-0" /> 173 + <div className="flex flex-col leading-snug"> 174 + <div className="text-secondary font-bold"> 175 + Publish as LooseLeaf 176 + </div> 177 + <div className="text-tertiary text-sm"> 178 + Publish this as a one off doc <br /> 179 + to AT Proto 180 + </div> 181 + </div> 182 + </div> 183 + <div className="flex gap-2 menuItem"> 184 + <PublishSmall className="shrink-0" /> 185 + <div className="flex flex-col leading-snug"> 186 + <div className="text-secondary font-bold"> 187 + Start a Publication! 188 + </div> 189 + <div className="text-tertiary text-sm"> 190 + Publish your writing to a blog or newsletter on AT Proto 191 + </div> 192 + </div> 193 + </div> 194 + </div> 195 + ) : ( 196 + <div className="flex flex-col gap-3"> 197 + <PubOption 198 + selected={selectedPub === "looseleaf"} 199 + onSelect={() => setSelectedPub("looseleaf")} 200 + > 201 + <LooseLeafSmall /> 202 + Publish as Looseleaf 203 + </PubOption> 204 + <hr className="border-border-light border-dashed " /> 205 + {props.publications.map((p) => { 206 + let pubRecord = p.record as PubLeafletPublication.Record; 207 + return ( 208 + <PubOption 209 + selected={selectedPub === p.uri} 210 + onSelect={() => setSelectedPub(p.uri)} 211 + > 212 + <> 213 + <PubIcon record={pubRecord} uri={p.uri} /> 214 + {p.name} 215 + </> 216 + </PubOption> 217 + ); 218 + })} 219 + <PubOption 220 + selected={selectedPub === "create"} 221 + onSelect={() => setSelectedPub("create")} 222 + > 223 + <> 224 + <AddSmall /> Create New Publication 225 + </> 226 + </PubOption> 227 + </div> 228 + )} 229 + </div> 230 + ); 231 + }; 155 232 156 - <PubListEmptyContent /> 157 - </Popover> 158 - ); 233 + const PubOption = (props: { 234 + selected: boolean; 235 + onSelect: () => void; 236 + children: React.ReactNode; 237 + }) => { 238 + return ( 239 + <button 240 + className={`flex gap-2 menuItem font-bold text-secondary ${props.selected && "bg-test"}`} 241 + onClick={() => { 242 + props.onSelect(); 243 + }} 244 + > 245 + {props.children} 246 + </button> 247 + ); 159 248 };
+1 -1
app/login/LoginForm.tsx
··· 213 213 </ButtonPrimary> 214 214 <button 215 215 type="button" 216 - className={`${props.compact ? "text-xs" : "text-sm"} text-accent-contrast place-self-center mt-[6px]`} 216 + className={`${props.compact ? "text-xs mt-0.5" : "text-sm mt-[6px]"} text-accent-contrast place-self-center`} 217 217 onClick={() => setSigningWithHandle(true)} 218 218 > 219 219 use an ATProto handle
+6 -4
components/ActionBar/Publications.tsx
··· 118 118 ); 119 119 }; 120 120 121 - export const PubListEmptyContent = () => { 121 + export const PubListEmptyContent = (props: { compact?: boolean }) => { 122 122 let { identity } = useIdentityData(); 123 123 124 124 return ( 125 - <div className="bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm"> 125 + <div 126 + className={`bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm`} 127 + > 126 128 <div className="mx-auto pt-2 scale-90"> 127 129 <PubListEmptyIllo /> 128 130 </div> 129 131 <div className="pt-1 font-bold">Publish on AT Proto</div> 130 - {identity && identity.atp_did ? ( 132 + {identity && !identity.atp_did ? ( 131 133 // has ATProto account and no pubs 132 134 <> 133 135 <div className="pb-2 text-secondary text-xs"> ··· 144 146 // no ATProto account and no pubs 145 147 <> 146 148 <div className="pb-2 text-secondary text-xs"> 147 - Link a Bluesky account to start a new publication on AT Proto 149 + Link a Bluesky account to start <br /> a new publication on AT Proto 148 150 </div> 149 151 150 152 <BlueskyLogin compact />
+35 -21
components/Buttons.tsx
··· 10 10 import { PopoverArrow } from "./Icons/PopoverArrow"; 11 11 12 12 type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">; 13 + 13 14 export const ButtonPrimary = forwardRef< 14 15 HTMLButtonElement, 15 16 ButtonProps & { ··· 35 36 m-0 h-max 36 37 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 37 38 ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 38 - bg-accent-1 outline-transparent border border-accent-1 39 - rounded-md text-base font-bold text-accent-2 39 + bg-accent-1 disabled:bg-border-light 40 + border border-accent-1 rounded-md disabled:border-border-light 41 + outline outline-transparent outline-offset-1 focus:outline-accent-1 hover:outline-accent-1 42 + text-base font-bold text-accent-2 disabled:text-border disabled:hover:text-border 40 43 flex gap-2 items-center justify-center shrink-0 41 - transparent-outline focus:outline-accent-1 hover:outline-accent-1 outline-offset-1 42 - disabled:bg-border-light disabled:border-border-light disabled:text-border disabled:hover:text-border 43 44 ${className} 44 45 `} 45 46 > ··· 70 71 <button 71 72 {...buttonProps} 72 73 ref={ref} 73 - className={`m-0 h-max 74 + className={` 75 + m-0 h-max 74 76 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 75 - ${props.compact ? "py-0 px-1" : "px-2 py-0.5 "} 76 - bg-bg-page outline-transparent 77 - rounded-md text-base font-bold text-accent-contrast 78 - flex gap-2 items-center justify-center shrink-0 79 - transparent-outline focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 80 - border border-accent-contrast 81 - disabled:bg-border-light disabled:text-border disabled:hover:text-border 82 - ${props.className} 83 - `} 77 + ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 78 + bg-bg-page disabled:bg-border-light 79 + border border-accent-contrast rounded-md 80 + outline outline-transparent focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 81 + text-base font-bold text-accent-contrast disabled:text-border disabled:hover:text-border 82 + flex gap-2 items-center justify-center shrink-0 83 + ${props.className} 84 + `} 84 85 > 85 86 {props.children} 86 87 </button> ··· 92 93 HTMLButtonElement, 93 94 { 94 95 fullWidth?: boolean; 96 + fullWidthOnMobile?: boolean; 95 97 children: React.ReactNode; 96 98 compact?: boolean; 97 99 } & ButtonProps 98 100 >((props, ref) => { 99 - let { fullWidth, children, compact, ...buttonProps } = props; 101 + let { 102 + className, 103 + fullWidth, 104 + fullWidthOnMobile, 105 + compact, 106 + children, 107 + ...buttonProps 108 + } = props; 100 109 return ( 101 110 <button 102 111 {...buttonProps} 103 112 ref={ref} 104 - className={`m-0 h-max ${fullWidth ? "w-full" : "w-max"} ${compact ? "px-0" : "px-1"} 105 - bg-transparent text-base font-bold text-accent-contrast 106 - flex gap-2 items-center justify-center shrink-0 107 - hover:underline disabled:text-border 108 - ${props.className} 109 - `} 113 + className={` 114 + m-0 h-max 115 + ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 116 + ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 117 + bg-transparent hover:bg-[var(--accent-light)] 118 + border border-transparent rounded-md hover:border-[var(--accent-light)] 119 + outline outline-transparent focus:outline-[var(--accent-light)] hover:outline-[var(--accent-light)] outline-offset-1 120 + text-base font-bold text-accent-contrast disabled:text-border 121 + flex gap-2 items-center justify-center shrink-0 122 + ${props.className} 123 + `} 110 124 > 111 125 {children} 112 126 </button>
+4 -2
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 not-italic 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 disabled:text-tertiary disabled:cursor-not-allowed`; 104 104 return ( 105 - <label className=" input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]!"> 105 + <label 106 + className={`input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]! ${props.disabled && "bg-border-light! cursor-not-allowed!"}`} 107 + > 106 108 {props.label} 107 109 {textarea ? ( 108 110 <textarea {...inputProps} className={style} />