a tool for shared writing and social publishing

borderless style

+168 -114
+3 -2
app/[leaflet_id]/Leaflet.tsx
··· 34 34 <UpdateLeafletTitle entityID={props.leaflet_id} /> 35 35 <AddLeafletToHomepage /> 36 36 <SelectionManager /> 37 - {/* we need the padding bottom here because if we don't have it the mobile footer will cut off 37 + {/* we need the padding bottom here because if we don't have it the mobile footer will cut off... 38 38 the dropshadow on the page... the padding is compensated by a negative top margin in mobile footer */} 39 39 <div 40 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 41 id="page-carousel" 42 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)*/} 43 44 <div 44 45 id="pages" 45 - className="pages flex pt-2 pb-1 sm:pb-8 sm:py-6" 46 + className="pages flex pt-2 pb-1 sm:pb-8 sm:pt-6" 46 47 onClick={(e) => { 47 48 e.currentTarget === e.target && blurPage(); 48 49 }}
+3 -4
app/[leaflet_id]/Sidebar.tsx
··· 24 24 > 25 25 <Media 26 26 mobile={false} 27 - className="sidebarContainer relative flex flex-col justify-between h-full w-16 bg-bg-page/50 border-bg-page" 27 + className="sidebarContainer relative flex flex-col justify-end h-full w-16 bg-bg-page/50 border-bg-page" 28 28 > 29 29 <Sidebar> 30 30 {entity_set.permissions.write ? ( ··· 41 41 </div> 42 42 )} 43 43 </Sidebar> 44 - <div className="justify-end justify-self-end"> 45 - <Watermark /> 46 - </div> 44 + <div className="h-full pointer-events-none" /> 45 + <Watermark /> 47 46 </Media> 48 47 </div> 49 48 );
+104 -91
components/Pages/index.tsx
··· 1 1 "use client"; 2 2 3 - import { useState } from "react"; 3 + import React, { useState } from "react"; 4 4 import { useUIState } from "src/useUIState"; 5 5 import { useEntitySetContext } from "../EntitySetProvider"; 6 6 import { useSearchParams } from "next/navigation"; ··· 79 79 }; 80 80 81 81 function Page(props: { entityID: string; first?: boolean }) { 82 - let { rep } = useReplicache(); 82 + let { rep, rootEntity } = useReplicache(); 83 83 let isDraft = useReferenceToEntity("mailbox/draft", props.entityID); 84 84 85 85 let isFocused = useUIState((s) => { ··· 91 91 return focusedPageID === props.entityID; 92 92 }); 93 93 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 94 + let rootCardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); 94 95 96 + let cardBorderHidden = 97 + useEntity(props.entityID, "theme/card-border-hidden")?.data.value || 98 + rootCardBorderHidden?.data.value; 95 99 return ( 96 100 <> 97 101 {!props.first && ( ··· 114 118 id={elementId.page(props.entityID).container} 115 119 style={{ 116 120 width: pageType === "doc" ? "var(--page-width-units)" : undefined, 117 - backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 121 + backgroundColor: cardBorderHidden 122 + ? "" 123 + : "rgba(var(--bg-page), var(--bg-page-alpha))", 118 124 }} 119 125 className={` 120 126 ${pageType === "canvas" ? "!lg:max-w-[1152px]" : "max-w-[var(--page-width-units)]"} ··· 122 128 grow flex flex-col 123 129 overscroll-y-none 124 130 overflow-y-auto 125 - rounded-lg border 131 + ${cardBorderHidden ? "border-0 !shadow-none sm:-mt-6 sm:-mb-12 -mt-2 -mb-1 pt-3 " : "border rounded-lg"} 126 132 ${isFocused ? "shadow-md border-border" : "border-border-light"} 127 133 `} 128 134 > ··· 170 176 : focusedElement?.parent; 171 177 return focusedPageID === props.entityID; 172 178 }); 179 + 180 + let rootCardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); 173 181 let rootBackgroundImage = useEntity( 174 182 rootEntity, 175 183 "theme/card-background-image", ··· 183 191 "theme/card-background-image-opacity", 184 192 ); 185 193 194 + let cardBorderHidden = 195 + useEntity(props.entityID, "theme/card-border-hidden")?.data.value || 196 + rootCardBorderHidden?.data.value; 186 197 let cardBackgroundImage = 187 198 useEntity(props.entityID, "theme/card-background-image") || 188 199 rootBackgroundImage; ··· 194 205 .value || 195 206 rootBackgroundOpacity?.data.value || 196 207 1; 208 + 197 209 return ( 198 210 <> 199 - <div 200 - className={`pageBackground 211 + {!cardBorderHidden && ( 212 + <div 213 + className={`pageBackground 201 214 absolute top-0 left-0 right-0 bottom-0 202 215 pointer-events-none 203 216 rounded-lg border 204 217 ${isFocused ? " border-border" : "border-border-light"} 205 218 `} 206 - style={{ 207 - backgroundImage: cardBackgroundImage 208 - ? `url(${cardBackgroundImage.data.src}), url(${cardBackgroundImage.data.fallback})` 209 - : undefined, 210 - backgroundRepeat: cardBackgroundImageRepeat ? "repeat" : "no-repeat", 211 - backgroundPosition: "center", 212 - backgroundSize: !cardBackgroundImageRepeat 213 - ? "cover" 214 - : cardBackgroundImageRepeat?.data.value, 215 - opacity: cardBackgroundImage?.data.src 216 - ? cardBackgroundImageOpacity 217 - : 1, 218 - }} 219 - /> 219 + style={{ 220 + backgroundImage: cardBackgroundImage 221 + ? `url(${cardBackgroundImage.data.src}), url(${cardBackgroundImage.data.fallback})` 222 + : undefined, 223 + backgroundRepeat: cardBackgroundImageRepeat 224 + ? "repeat" 225 + : "no-repeat", 226 + backgroundPosition: "center", 227 + backgroundSize: !cardBackgroundImageRepeat 228 + ? "cover" 229 + : cardBackgroundImageRepeat?.data.value, 230 + opacity: cardBackgroundImage?.data.src 231 + ? cardBackgroundImageOpacity 232 + : 1, 233 + }} 234 + /> 235 + )} 220 236 <Blocks entityID={props.entityID} /> 221 237 {/* we handle page bg in this sepate div so that 222 238 we can apply an opacity the background image ··· 225 241 ); 226 242 }; 227 243 228 - let greyButtonStyle = 229 - "pt-[2px] h-5 w-5 p-0.5 mx-auto bg-border text-bg-page sm:rounded-r-md sm:rounded-l-none rounded-b-md hover:bg-accent-1 hover:text-accent-2"; 230 - let whiteButtonStyle = ` 231 - pageOptionsTrigger 232 - shrink-0 233 - bg-bg-page text-border 234 - outline-none border sm:border-l-0 border-t-1 border-border sm:rounded-r-md sm:rounded-l-none rounded-b-md 235 - hover:shadow-[0_1px_0_theme(colors.border)_inset,_0_-1px_0_theme(colors.border)_inset,_-1px_0_0_theme(colors.border)_inset] 236 - flex items-center justify-center`; 244 + const PageOptionButton = (props: { 245 + onClick?: () => void; 246 + children: React.ReactNode; 247 + secondary?: boolean; 248 + cardBorderHidden: boolean | undefined; 249 + className?: string; 250 + disabled?: boolean; 251 + }) => { 252 + return ( 253 + <button 254 + className={` 255 + pageOptionsTrigger 256 + shrink-0 257 + pt-[2px] h-5 w-5 p-0.5 mx-auto 258 + border border-border 259 + ${props.secondary ? "bg-border text-bg-page" : "bg-bg-page text-border"} 260 + ${props.disabled && "opacity-50"} 261 + ${props.cardBorderHidden ? "rounded-md" : `rounded-b-md sm:rounded-l-none sm:rounded-r-md`} 262 + flex items-center justify-center 263 + ${props.className} 264 + 265 + `} 266 + onClick={() => { 267 + props.onClick && props.onClick(); 268 + }} 269 + > 270 + {props.children} 271 + </button> 272 + ); 273 + }; 274 + 237 275 const PageOptions = (props: { 238 276 entityID: string; 239 277 first: boolean | undefined; 240 278 }) => { 279 + let { rootEntity } = useReplicache(); 280 + let rootCardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden"); 281 + 282 + let cardBorderHidden = 283 + useEntity(props.entityID, "theme/card-border-hidden")?.data.value || 284 + rootCardBorderHidden?.data.value; 285 + 241 286 return ( 242 - <div className=" z-10 w-fit absolute sm:top-3 sm:-right-[19px] top-0 right-3 flex sm:flex-col flex-row-reverse gap-1 items-start"> 287 + <div 288 + 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`} 289 + > 243 290 {!props.first && ( 244 - <button 245 - className={greyButtonStyle} 291 + <PageOptionButton 292 + cardBorderHidden={cardBorderHidden} 293 + secondary 246 294 onClick={() => { 247 295 useUIState.getState().closePage(props.entityID); 248 296 }} 249 297 > 250 298 <CloseTiny /> 251 - </button> 299 + </PageOptionButton> 252 300 )} 253 301 <OptionsMenu 254 302 entityID={props.entityID} 255 303 first={!!props.first} 256 - buttonStyle={whiteButtonStyle} 304 + cardBorderHidden={cardBorderHidden} 257 305 /> 258 - <UndoButtons /> 306 + <UndoButtons cardBorderHidden={cardBorderHidden} /> 259 307 </div> 260 308 ); 261 309 }; 262 310 263 - const UndoButtons = () => { 311 + const UndoButtons = (props: { cardBorderHidden: boolean | undefined }) => { 264 312 let undoState = useUndoState(); 265 313 let { undoManager } = useReplicache(); 266 314 return ( 267 315 <Media mobile> 268 - <div className="gap-1 flex sm:flex-col"> 269 - {undoState.canUndo && ( 270 - <button 271 - className={`${whiteButtonStyle} h-5 w-5 p-0.5`} 316 + {undoState.canUndo && ( 317 + <div className="gap-1 flex sm:flex-col"> 318 + <PageOptionButton 319 + secondary 320 + cardBorderHidden={props.cardBorderHidden} 272 321 onClick={() => undoManager.undo()} 273 322 > 274 323 <UndoTiny /> 275 - </button> 276 - )} 277 - {undoState.canRedo ? ( 278 - <button 279 - className={`${whiteButtonStyle} h-5 w-5 p-0.5`} 324 + </PageOptionButton> 325 + 326 + <PageOptionButton 327 + secondary 328 + cardBorderHidden={props.cardBorderHidden} 280 329 onClick={() => undoManager.undo()} 330 + disabled={!undoState.canRedo} 281 331 > 282 332 <RedoTiny /> 283 - </button> 284 - ) : ( 285 - <div className="h-5 w-5 p-0.5" /> 286 - )} 287 - </div> 333 + </PageOptionButton> 334 + </div> 335 + )} 288 336 </Media> 289 337 ); 290 338 }; ··· 292 340 const OptionsMenu = (props: { 293 341 entityID: string; 294 342 first: boolean; 295 - buttonStyle: string; 343 + cardBorderHidden: boolean | undefined; 296 344 }) => { 297 345 let [state, setState] = useState<"normal" | "theme" | "share">("normal"); 298 346 let { permissions } = useEntitySetContext(); ··· 300 348 return ( 301 349 <Menu 302 350 align="end" 351 + asChild 303 352 onOpenChange={(open) => { 304 353 if (!open) setState("normal"); 305 354 }} 306 355 trigger={ 307 - <div 308 - className={`pageOptionsTrigger 309 - ${props.buttonStyle} 310 - sm:h-8 sm:w-5 h-5 w-8 311 - `} 356 + <PageOptionButton 357 + cardBorderHidden={props.cardBorderHidden} 358 + className="!w-8 !h-5 sm:!w-5 sm:!h-8" 312 359 > 313 360 <MoreOptionsTiny className="sm:rotate-90" /> 314 - </div> 361 + </PageOptionButton> 315 362 } 316 363 > 317 364 {state === "normal" ? ( ··· 343 390 </Menu> 344 391 ); 345 392 }; 346 - 347 - const PageMenuItem = (props: { 348 - children: React.ReactNode; 349 - onClick: () => void; 350 - }) => { 351 - return ( 352 - <button 353 - className="pageOptionsMenuItem z-10 text-left text-secondary py-1 px-2 flex gap-2 hover:bg-accent-1 hover:text-accent-2" 354 - onClick={() => { 355 - props.onClick(); 356 - }} 357 - > 358 - {props.children} 359 - </button> 360 - ); 361 - }; 362 - 363 - const DeletePageToast = { 364 - content: ( 365 - <div className="flex gap-2"> 366 - You deleted a page.{" "} 367 - <button 368 - className="underline font-bold sm:font-normal sm:hover:font-bold italic" 369 - onClick={() => { 370 - // TODO: WIRE UP UNDO DELETE 371 - }} 372 - > 373 - Undo? 374 - </button> 375 - </div> 376 - ), 377 - type: "info", 378 - duration: 5000, 379 - } as const; 380 393 381 394 export async function focusPage( 382 395 pageID: string,
+25 -3
components/ThemeManager/PageThemePickers.tsx
··· 12 12 ColorSwatch, 13 13 } from "react-aria-components"; 14 14 import { Checkbox } from "components/Checkbox"; 15 - import { useMemo } from "react"; 15 + import { useMemo, useState } from "react"; 16 16 import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 17 17 import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; 18 18 import { Separator } from "components/Layout"; ··· 40 40 let pageValue = useColorAttribute(props.entityID, "theme/card-background"); 41 41 let primaryValue = useColorAttribute(props.entityID, "theme/primary"); 42 42 let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 43 + let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden"); 43 44 44 45 return ( 45 46 <div ··· 103 104 closePicker={() => props.setOpenPicker("null")} 104 105 /> 105 106 <hr /> 107 + <PageBorderHider entityID={props.entityID} /> 108 + </div> 109 + ); 110 + }; 106 111 107 - <Checkbox checked={true} onChange={(e) => {}}> 112 + export const PageBorderHider = (props: { entityID: string }) => { 113 + let { rep } = useReplicache(); 114 + let pageBorderHidden = 115 + useEntity(props.entityID, "theme/card-border-hidden")?.data.value || false; 116 + 117 + return ( 118 + <> 119 + <Checkbox 120 + checked={pageBorderHidden} 121 + onChange={(e) => { 122 + rep?.mutate.assertFact({ 123 + entity: props.entityID, 124 + attribute: "theme/card-border-hidden", 125 + data: { type: "boolean", value: !pageBorderHidden }, 126 + }); 127 + console.log(pageBorderHidden); 128 + }} 129 + > 108 130 Hide Page Borders 109 131 </Checkbox> 110 - </div> 132 + </> 111 133 ); 112 134 }; 113 135
+29 -14
components/ThemeManager/ThemeSetter.tsx
··· 225 225 props.entityID, 226 226 "theme/card-background-image-opacity", 227 227 ); 228 + let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden") 229 + ?.data.value; 228 230 229 231 return ( 230 232 <div 231 233 onClick={(e) => { 232 234 e.currentTarget === e.target && props.setOpenPicker("page"); 233 235 }} 234 - className={`${props.home ? "rounded-md " : "rounded-t-lg "} relative cursor-pointer p-2 border border-border border-b-transparent shadow-md text-primary`} 235 - style={{ 236 - backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 237 - }} 236 + className={ 237 + pageBorderHidden 238 + ? "py-2 px-0 border border-transparent" 239 + : `${props.home ? "rounded-md " : "rounded-t-lg "} relative cursor-pointer p-2 border border-border border-b-transparent shadow-md text-primary 240 + ` 241 + } 242 + style={ 243 + pageBorderHidden 244 + ? undefined 245 + : { 246 + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 247 + } 248 + } 238 249 > 239 250 <div 240 251 className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" 241 - style={{ 242 - backgroundImage: pageBGImage 243 - ? `url(${pageBGImage.data.src})` 244 - : undefined, 252 + style={ 253 + pageBorderHidden 254 + ? undefined 255 + : { 256 + backgroundImage: pageBGImage 257 + ? `url(${pageBGImage.data.src})` 258 + : undefined, 245 259 246 - backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 247 - opacity: pageBGOpacity?.data.value || 1, 248 - backgroundSize: !pageBGRepeat 249 - ? "cover" 250 - : `calc(${pageBGRepeat.data.value}px / 2 )`, 251 - }} 260 + backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 261 + opacity: pageBGOpacity?.data.value || 1, 262 + backgroundSize: !pageBGRepeat 263 + ? "cover" 264 + : `calc(${pageBGRepeat.data.value}px / 2 )`, 265 + } 266 + } 252 267 /> 253 268 <div> 254 269 <p
+4
src/replicache/attributes.ts
··· 199 199 type: "number", 200 200 cardinality: "one", 201 201 }, 202 + "theme/card-border-hidden": { 203 + type: "boolean", 204 + cardinality: "one", 205 + }, 202 206 "theme/primary": { 203 207 type: "color", 204 208 cardinality: "one",