a tool for shared writing and social publishing

unified page, page wrapper, and post layouts, made subpage look nice in post

+603 -526
+1 -2
app/[leaflet_id]/Leaflet.tsx
··· 12 12 import { AddLeafletToHomepage } from "components/utils/AddLeafletToHomepage"; 13 13 import { UpdateLeafletTitle } from "components/utils/UpdateLeafletTitle"; 14 14 import { useUIState } from "src/useUIState"; 15 - import { LeafletSidebar } from "./Sidebar"; 16 15 import { LeafletLayout } from "components/LeafletLayout"; 17 16 18 17 export function Leaflet(props: { ··· 37 36 <SelectionManager /> 38 37 {/* we need the padding bottom here because if we don't have it the mobile footer will cut off... 39 38 the dropshadow on the page... the padding is compensated by a negative top margin in mobile footer */} 40 - <LeafletLayout className="!pb-[70px] sm:!pb-6"> 39 + <LeafletLayout className="!pb-[64px] sm:!pb-6"> 41 40 <Pages rootPage={props.leaflet_id} /> 42 41 </LeafletLayout> 43 42 <LeafletFooter entityID={props.leaflet_id} />
+36 -30
app/[leaflet_id]/Sidebar.tsx
··· 12 12 import { useUIState } from "src/useUIState"; 13 13 import { BackToPubButton, PublishButton } from "./Actions"; 14 14 import { useIdentityData } from "components/IdentityProvider"; 15 + import { useReplicache } from "src/replicache"; 15 16 16 - export function LeafletSidebar(props: { leaflet_id: string }) { 17 + export function LeafletSidebar() { 17 18 let entity_set = useEntitySetContext(); 19 + let { rootEntity } = useReplicache(); 18 20 let { data: pub } = useLeafletPublicationData(); 19 21 let { identity } = useIdentityData(); 20 22 21 23 return ( 22 - <Media 23 - mobile={false} 24 - className="sidebarContainer relative flex flex-col justify-end h-full w-16" 25 - > 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 - </> 24 + <Media mobile={false} className="w-0 h-full relative"> 25 + <div 26 + className="absolute top-0 left-0 h-full flex justify-end " 27 + style={{ width: `calc(50vw - ((var(--page-width-units)/2))` }} 28 + > 29 + <div className="sidebarContainer flex flex-col justify-end h-full w-16 relative"> 30 + {entity_set.permissions.write && ( 31 + <Sidebar> 32 + {pub?.publications && 33 + identity?.atp_did && 34 + pub.publications.identity_did === identity.atp_did ? ( 35 + <> 36 + <PublishButton /> 37 + <ShareOptions /> 38 + <ThemePopover entityID={rootEntity} /> 39 + <HelpPopover /> 40 + <hr className="text-border" /> 41 + <BackToPubButton publication={pub.publications} /> 42 + </> 43 + ) : ( 44 + <> 45 + <ShareOptions /> 46 + <ThemePopover entityID={rootEntity} /> 47 + <HelpPopover /> 48 + <hr className="text-border" /> 49 + <HomeButton /> 50 + </> 51 + )} 52 + </Sidebar> 47 53 )} 48 - </Sidebar> 49 - )} 50 - <div className="h-full flex items-end"> 51 - <Watermark /> 54 + <div className="h-full flex items-end"> 55 + <Watermark /> 56 + </div> 57 + </div> 52 58 </div> 53 59 </Media> 54 60 );
+2 -2
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 25 25 import { PubCodeBlock } from "./PubCodeBlock"; 26 26 import { AppBskyFeedDefs } from "@atproto/api"; 27 27 import { PubBlueskyPostBlock } from "./PublishBskyPostBlock"; 28 - import { openPage, usePostPageUIState } from "./PostPage"; 28 + import { openPage } from "./PostPages"; 29 29 30 30 export function PostContent({ 31 31 blocks, ··· 47 47 return ( 48 48 <div 49 49 id="post-content" 50 - className={`postContent flex flex-col pb-1 sm:pb-2 pt-1 sm:pt-2 ${className}`} 50 + className={`postContent flex flex-col sm:px-4 px-3 sm:pt-3 pt-2 pb-1 sm:pb-2 ${className}`} 51 51 > 52 52 {blocks.map((b, index) => { 53 53 return (
+64 -72
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 25 25 if (!document?.data || !document.documents_in_publications[0].publications) 26 26 return; 27 27 return ( 28 - <> 29 - {/* <CollapsedPostHeader 30 - pubIcon={ 31 - pubRecord?.icon && pub 32 - ? blobRefToSrc(pubRecord.icon.ref, new AtUri(pub.uri).host) 33 - : undefined 34 - } 35 - title={record.title} 36 - quotes={document.document_mentions_in_bsky} 37 - /> */} 38 - <div className="max-w-prose w-full mx-auto" id="post-header"> 39 - <div className="pubHeader flex flex-col pb-5"> 40 - <div className="flex justify-between w-full"> 41 - <SpeedyLink 42 - className="font-bold hover:no-underline text-accent-contrast" 43 - href={ 44 - document && 45 - getPublicationURL( 46 - document.documents_in_publications[0].publications, 47 - ) 48 - } 49 - > 50 - {props.name} 51 - </SpeedyLink> 52 - {identity && 53 - identity.atp_did === 54 - document.documents_in_publications[0]?.publications 55 - .identity_did && ( 56 - <a 57 - className=" rounded-full flex place-items-center" 58 - href={`https://leaflet.pub/${document.leaflets_in_publications[0].leaflet}`} 59 - > 60 - <EditTiny className="shrink-0" /> 61 - </a> 62 - )} 63 - </div> 64 - <h2 className="">{record.title}</h2> 65 - {record.description ? ( 66 - <p className="italic text-secondary">{record.description}</p> 67 - ) : null} 28 + <div 29 + className="max-w-prose w-full mx-auto px-3 sm:px-4 sm:pt-3 pt-2" 30 + id="post-header" 31 + > 32 + <div className="pubHeader flex flex-col pb-5"> 33 + <div className="flex justify-between w-full"> 34 + <SpeedyLink 35 + className="font-bold hover:no-underline text-accent-contrast" 36 + href={ 37 + document && 38 + getPublicationURL( 39 + document.documents_in_publications[0].publications, 40 + ) 41 + } 42 + > 43 + {props.name} 44 + </SpeedyLink> 45 + {identity && 46 + identity.atp_did === 47 + document.documents_in_publications[0]?.publications 48 + .identity_did && ( 49 + <a 50 + className=" rounded-full flex place-items-center" 51 + href={`https://leaflet.pub/${document.leaflets_in_publications[0].leaflet}`} 52 + > 53 + <EditTiny className="shrink-0" /> 54 + </a> 55 + )} 56 + </div> 57 + <h2 className="">{record.title}</h2> 58 + {record.description ? ( 59 + <p className="italic text-secondary">{record.description}</p> 60 + ) : null} 68 61 69 - <div className="text-sm text-tertiary pt-3 flex gap-1 flex-wrap"> 70 - {profile ? ( 71 - <> 72 - <a 73 - className="text-tertiary" 74 - href={`https://bsky.app/profile/${profile.handle}`} 75 - > 76 - by {profile.displayName || profile.handle} 77 - </a> 78 - </> 79 - ) : null} 80 - {record.publishedAt ? ( 81 - <> 82 - | 83 - <p> 84 - {new Date(record.publishedAt).toLocaleDateString(undefined, { 85 - year: "numeric", 86 - month: "long", 87 - day: "2-digit", 88 - })} 89 - </p> 90 - </> 91 - ) : null} 92 - |{" "} 93 - <Interactions 94 - showComments={props.preferences.showComments} 95 - compact 96 - quotesCount={document.document_mentions_in_bsky.length} 97 - commentsCount={document.comments_on_documents.length} 98 - /> 99 - </div> 62 + <div className="text-sm text-tertiary pt-3 flex gap-1 flex-wrap"> 63 + {profile ? ( 64 + <> 65 + <a 66 + className="text-tertiary" 67 + href={`https://bsky.app/profile/${profile.handle}`} 68 + > 69 + by {profile.displayName || profile.handle} 70 + </a> 71 + </> 72 + ) : null} 73 + {record.publishedAt ? ( 74 + <> 75 + | 76 + <p> 77 + {new Date(record.publishedAt).toLocaleDateString(undefined, { 78 + year: "numeric", 79 + month: "long", 80 + day: "2-digit", 81 + })} 82 + </p> 83 + </> 84 + ) : null} 85 + |{" "} 86 + <Interactions 87 + showComments={props.preferences.showComments} 88 + compact 89 + quotesCount={document.document_mentions_in_bsky.length} 90 + commentsCount={document.comments_on_documents.length} 91 + /> 100 92 </div> 101 93 </div> 102 - </> 94 + </div> 103 95 ); 104 96 }
+51 -16
app/lish/[did]/[publication]/[rkey]/PostPage.tsx app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 16 16 import { AppBskyFeedDefs } from "@atproto/api"; 17 17 import { create } from "zustand/react"; 18 18 import { InteractionDrawer } from "./Interactions/InteractionDrawer"; 19 - import { BookendSpacers, SandwichSpacer } from "components/LeafletLayout"; 19 + import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; 20 + import { CSS } from "@react-spring/web"; 21 + import { PageOptionButton } from "components/Pages/PageOptions"; 22 + import { CloseTiny } from "components/Icons/CloseTiny"; 23 + import { PageWrapper } from "components/Pages/Page"; 20 24 export const usePostPageUIState = create(() => ({ 21 25 pages: [] as string[], 22 26 })); ··· 68 72 return null; 69 73 70 74 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 75 + let fullPageScroll = !hasPageBackground && !drawerOpen && pages.length === 0; 71 76 return ( 72 77 <> 73 - {(drawerOpen || hasPageBackground) && <BookendSpacers />} 78 + {!fullPageScroll && <BookendSpacer />} 74 79 <PageWrapper 75 - hasPageBackground={hasPageBackground} 76 - drawerOpen={drawerOpen} 80 + fullPageScroll={fullPageScroll} 81 + cardBorderHidden={!hasPageBackground} 82 + id={"post-page"} 77 83 > 78 84 <PostHeader 79 85 data={document} ··· 141 147 <> 142 148 <SandwichSpacer /> 143 149 <PageWrapper 144 - hasPageBackground={hasPageBackground} 145 - drawerOpen={drawerOpen} 150 + cardBorderHidden={!hasPageBackground} 151 + id={"post-page"} 152 + fullPageScroll={false} 153 + pageOptions={ 154 + <PageOptions 155 + onClick={() => closePage(page?.id!)} 156 + hasPageBackground={hasPageBackground} 157 + /> 158 + } 146 159 > 147 - <button onClick={() => closePage(page?.id!)}>close</button> 148 - 149 160 <PostContent 150 161 pageId={page.id} 151 162 bskyPostData={bskyPostData} ··· 157 168 </> 158 169 ); 159 170 })} 160 - <BookendSpacers /> 171 + {!fullPageScroll && <BookendSpacer />} 161 172 </> 162 173 ); 163 174 } 164 175 165 - const PageWrapper = (props: { 176 + const PageOptions = (props: { 177 + onClick: () => void; 178 + hasPageBackground: boolean; 179 + }) => { 180 + return ( 181 + <div 182 + className={`pageOptions w-fit z-10 183 + absolute sm:-right-[20px] right-3 sm:top-3 top-0 184 + flex sm:flex-col flex-row-reverse gap-1 items-start`} 185 + > 186 + <PageOptionButton 187 + cardBorderHidden={!props.hasPageBackground} 188 + onClick={props.onClick} 189 + > 190 + <CloseTiny /> 191 + </PageOptionButton> 192 + </div> 193 + ); 194 + }; 195 + 196 + const PostPageWrapper = (props: { 166 197 children: React.ReactNode; 167 198 hasPageBackground: boolean; 168 - drawerOpen: boolean | undefined; 199 + fullPageScroll: boolean; 169 200 }) => { 170 201 return ( 171 202 <div 172 203 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"} 204 + className={` 205 + postPageWrapper 206 + relative overflow-y-auto 207 + w-full sm:mx-0 208 + shrink-0 snap-center 209 + ${!props.fullPageScroll && "max-w-[var(--page-width-units)]"} 175 210 ${ 176 211 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 " 212 + ? "h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] rounded-lg border border-border pt-2 pb-3" 213 + : "sm:h-[calc(100%+48px)] h-[calc(100%+28px)] sm:-my-6 sm:py-6 -my-3 py-3 " 179 214 }`} 180 215 > 181 216 <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"}`} 217 + className={`postPageContent sm:max-w-[var(--page-width-units)] mx-auto h-fit w-full ${!props.hasPageBackground ? "px-4 sm:pt-3 pt-2" : "px-3 sm:px-4"}`} 183 218 > 184 219 {props.children} 185 220 </div>
+1 -1
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 { PostPages } from "./PostPage"; 20 + import { PostPages } from "./PostPages"; 21 21 import { extractCodeBlocks } from "./extractCodeBlocks"; 22 22 import { LeafletLayout } from "components/LeafletLayout"; 23 23
-1
app/lish/[did]/[publication]/[rkey]/useHighlight.tsx
··· 20 20 let highlights = activeHighlight ? [activeHighlight] : []; 21 21 let decodedQuote = quote ? decodeQuotePosition(quote as string) : null; 22 22 if (decodedQuote) highlights.push(decodedQuote); 23 - console.log(highlights); 24 23 return highlights 25 24 .map((quotePosition) => { 26 25 if (!quotePosition) return null;
+2 -3
components/LeafletLayout.tsx
··· 11 11 flex items-stretch grow`} 12 12 id="page-carousel" 13 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 */} 14 + {/* if you adjust this padding, remember to adjust the negative margins on page in components/Pages/Page.tsx in pageScrollWrapper when card borders are hidden */} 16 15 <div 17 16 id="pages" 18 17 className={`pagesWrapper ··· 28 27 ); 29 28 }; 30 29 31 - export const BookendSpacers = (props: { 30 + export const BookendSpacer = (props: { 32 31 onClick?: (e: React.MouseEvent) => void; 33 32 children?: React.ReactNode; 34 33 }) => {
+200
components/Pages/Page.tsx
··· 1 + "use client"; 2 + 3 + import React from "react"; 4 + import { useUIState } from "src/useUIState"; 5 + 6 + import { elementId } from "src/utils/elementId"; 7 + 8 + import { useEntity, useReferenceToEntity, useReplicache } from "src/replicache"; 9 + 10 + import { DesktopPageFooter } from "../DesktopFooter"; 11 + import { Canvas } from "../Canvas"; 12 + import { Blocks } from "components/Blocks"; 13 + import { PublicationMetadata } from "./PublicationMetadata"; 14 + import { useCardBorderHidden } from "./useCardBorderHidden"; 15 + import { focusPage } from "."; 16 + import { PageOptions } from "./PageOptions"; 17 + import { CardThemeProvider } from "components/ThemeManager/ThemeProvider"; 18 + 19 + export function Page(props: { 20 + entityID: string; 21 + first?: boolean; 22 + fullPageScroll: boolean; 23 + }) { 24 + let { rep } = useReplicache(); 25 + 26 + let isFocused = useUIState((s) => { 27 + let focusedElement = s.focusedEntity; 28 + let focusedPageID = 29 + focusedElement?.entityType === "page" 30 + ? focusedElement.entityID 31 + : focusedElement?.parent; 32 + return focusedPageID === props.entityID; 33 + }); 34 + let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 35 + let cardBorderHidden = useCardBorderHidden(props.entityID); 36 + return ( 37 + <CardThemeProvider entityID={props.entityID}> 38 + <PageWrapper 39 + onClickAction={(e) => { 40 + if (e.defaultPrevented) return; 41 + if (rep) { 42 + if (isFocused) return; 43 + focusPage(props.entityID, rep); 44 + } 45 + }} 46 + id={elementId.page(props.entityID).container} 47 + cardBorderHidden={!!cardBorderHidden} 48 + isFocused={isFocused} 49 + fullPageScroll={props.fullPageScroll} 50 + pageType={pageType} 51 + pageOptions={ 52 + <PageOptions 53 + entityID={props.entityID} 54 + first={props.first} 55 + isFocused={isFocused} 56 + /> 57 + } 58 + > 59 + {props.first && ( 60 + <> 61 + <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 62 + </> 63 + )} 64 + <PageContent entityID={props.entityID} /> 65 + </PageWrapper> 66 + <DesktopPageFooter pageID={props.entityID} /> 67 + </CardThemeProvider> 68 + ); 69 + } 70 + 71 + export const PageWrapper = (props: { 72 + children: React.ReactNode; 73 + pageOptions?: React.ReactNode; 74 + id: string; 75 + cardBorderHidden: boolean; 76 + fullPageScroll: boolean; 77 + isFocused?: boolean; 78 + onClickAction?: (e: React.MouseEvent) => void; 79 + pageType?: "canvas" | "doc"; 80 + }) => { 81 + return ( 82 + // this div wraps the contents AND the page options. 83 + // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions 84 + <div 85 + className={`pageWrapper relative shrink-0 ${props.fullPageScroll ? "w-full" : "w-max"}`} 86 + > 87 + {/* 88 + this div is the scrolling container that wraps only the contents div. 89 + 90 + it needs to be a separate div so that the user can scroll from anywhere on the page if there isn't a card border 91 + */} 92 + <div 93 + onClick={props.onClickAction} 94 + id={props.id} 95 + className={` 96 + pageScrollWrapper 97 + grow 98 + w-[10000px] sm:mx-0 99 + shrink-0 snap-center 100 + overflow-y-scroll 101 + ${ 102 + !props.cardBorderHidden && 103 + `h-full rounded-lg border 104 + bg-[rgba(var(--bg-page),var(--bg-page-alpha))] 105 + ${props.isFocused ? "shadow-md border-border" : "border-border-light"}` 106 + } 107 + ${props.cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 108 + } 109 + ${props.fullPageScroll ? "max-w-full" : "max-w-[var(--page-width-units)]"} 110 + `} 111 + > 112 + {/* this div controls the width of the content*/} 113 + <div 114 + className={`postPageContent mx-auto h-fit w-full 115 + ${props.pageType === "canvas" ? "!lg:max-w-[1152px]" : "sm:max-w-[var(--page-width-units)]"} 116 + `} 117 + > 118 + {props.children} 119 + </div> 120 + </div> 121 + {props.pageOptions} 122 + </div> 123 + ); 124 + }; 125 + 126 + const PageContent = (props: { entityID: string }) => { 127 + let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 128 + if (pageType === "doc") return <DocContent entityID={props.entityID} />; 129 + return <Canvas entityID={props.entityID} />; 130 + }; 131 + 132 + const DocContent = (props: { entityID: string }) => { 133 + let { rootEntity } = useReplicache(); 134 + 135 + let cardBorderHidden = useCardBorderHidden(props.entityID); 136 + let rootBackgroundImage = useEntity( 137 + rootEntity, 138 + "theme/card-background-image", 139 + ); 140 + let rootBackgroundRepeat = useEntity( 141 + rootEntity, 142 + "theme/card-background-image-repeat", 143 + ); 144 + let rootBackgroundOpacity = useEntity( 145 + rootEntity, 146 + "theme/card-background-image-opacity", 147 + ); 148 + 149 + let cardBackgroundImage = useEntity( 150 + props.entityID, 151 + "theme/card-background-image", 152 + ); 153 + 154 + let cardBackgroundImageRepeat = useEntity( 155 + props.entityID, 156 + "theme/card-background-image-repeat", 157 + ); 158 + 159 + let cardBackgroundImageOpacity = useEntity( 160 + props.entityID, 161 + "theme/card-background-image-opacity", 162 + ); 163 + 164 + let backgroundImage = cardBackgroundImage || rootBackgroundImage; 165 + let backgroundImageRepeat = cardBackgroundImage 166 + ? cardBackgroundImageRepeat?.data?.value 167 + : rootBackgroundRepeat?.data.value; 168 + let backgroundImageOpacity = cardBackgroundImage 169 + ? cardBackgroundImageOpacity?.data.value 170 + : rootBackgroundOpacity?.data.value || 1; 171 + 172 + return ( 173 + <> 174 + {!cardBorderHidden ? ( 175 + <div 176 + className={`pageBackground 177 + absolute top-0 left-0 right-0 bottom-0 178 + pointer-events-none 179 + rounded-lg 180 + `} 181 + style={{ 182 + backgroundImage: backgroundImage 183 + ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})` 184 + : undefined, 185 + backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 186 + backgroundPosition: "center", 187 + backgroundSize: !backgroundImageRepeat 188 + ? "cover" 189 + : backgroundImageRepeat, 190 + opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1, 191 + }} 192 + /> 193 + ) : null} 194 + <Blocks entityID={props.entityID} /> 195 + {/* we handle page bg in this sepate div so that 196 + we can apply an opacity the background image 197 + without affecting the opacity of the rest of the page */} 198 + </> 199 + ); 200 + };
+217
components/Pages/PageOptions.tsx
··· 1 + "use client"; 2 + 3 + import React, { JSX, useState } from "react"; 4 + import { useUIState } from "src/useUIState"; 5 + import { useEntitySetContext } from "../EntitySetProvider"; 6 + 7 + import { useReplicache } from "src/replicache"; 8 + 9 + import { Media } from "../Media"; 10 + import { MenuItem, Menu } from "../Layout"; 11 + import { PageThemeSetter } from "../ThemeManager/PageThemeSetter"; 12 + import { PageShareMenu } from "./PageShareMenu"; 13 + import { useUndoState } from "src/undoManager"; 14 + import { CloseTiny } from "components/Icons/CloseTiny"; 15 + import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 16 + import { PaintSmall } from "components/Icons/PaintSmall"; 17 + import { ShareSmall } from "components/Icons/ShareSmall"; 18 + import { useCardBorderHidden } from "./useCardBorderHidden"; 19 + import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 20 + 21 + export const PageOptionButton = ({ 22 + children, 23 + secondary, 24 + cardBorderHidden, 25 + className, 26 + disabled, 27 + ...props 28 + }: { 29 + children: React.ReactNode; 30 + secondary?: boolean; 31 + cardBorderHidden: boolean | undefined; 32 + className?: string; 33 + disabled?: boolean; 34 + } & Omit<JSX.IntrinsicElements["button"], "content">) => { 35 + return ( 36 + <button 37 + className={` 38 + pageOptionsTrigger 39 + shrink-0 40 + pt-[2px] h-5 w-5 p-0.5 mx-auto 41 + border border-border 42 + ${secondary ? "bg-border text-bg-page" : "bg-bg-page text-border"} 43 + ${disabled && "opacity-50"} 44 + ${cardBorderHidden ? "rounded-md" : `rounded-b-md sm:rounded-l-none sm:rounded-r-md`} 45 + flex items-center justify-center 46 + ${className} 47 + 48 + `} 49 + {...props} 50 + > 51 + {children} 52 + </button> 53 + ); 54 + }; 55 + 56 + export const PageOptions = (props: { 57 + entityID: string; 58 + first: boolean | undefined; 59 + isFocused: boolean; 60 + }) => { 61 + let cardBorderHidden = useCardBorderHidden(props.entityID); 62 + 63 + return ( 64 + <div 65 + className={`pageOptions w-fit z-10 66 + ${props.isFocused ? "block" : "sm:hidden block"} 67 + absolute sm:-right-[20px] right-3 sm:top-3 top-0 68 + flex sm:flex-col flex-row-reverse gap-1 items-start`} 69 + > 70 + {!props.first && ( 71 + <PageOptionButton 72 + cardBorderHidden={cardBorderHidden} 73 + secondary 74 + onClick={() => { 75 + useUIState.getState().closePage(props.entityID); 76 + }} 77 + > 78 + <CloseTiny /> 79 + </PageOptionButton> 80 + )} 81 + <OptionsMenu 82 + entityID={props.entityID} 83 + first={!!props.first} 84 + cardBorderHidden={cardBorderHidden} 85 + /> 86 + <UndoButtons cardBorderHidden={cardBorderHidden} /> 87 + </div> 88 + ); 89 + }; 90 + 91 + export const UndoButtons = (props: { 92 + cardBorderHidden: boolean | undefined; 93 + }) => { 94 + let undoState = useUndoState(); 95 + let { undoManager } = useReplicache(); 96 + return ( 97 + <Media mobile> 98 + {undoState.canUndo && ( 99 + <div className="gap-1 flex sm:flex-col"> 100 + <PageOptionButton 101 + secondary 102 + cardBorderHidden={props.cardBorderHidden} 103 + onClick={() => undoManager.undo()} 104 + > 105 + <UndoTiny /> 106 + </PageOptionButton> 107 + 108 + <PageOptionButton 109 + secondary 110 + cardBorderHidden={props.cardBorderHidden} 111 + onClick={() => undoManager.undo()} 112 + disabled={!undoState.canRedo} 113 + > 114 + <RedoTiny /> 115 + </PageOptionButton> 116 + </div> 117 + )} 118 + </Media> 119 + ); 120 + }; 121 + 122 + export const OptionsMenu = (props: { 123 + entityID: string; 124 + first: boolean; 125 + cardBorderHidden: boolean | undefined; 126 + }) => { 127 + let [state, setState] = useState<"normal" | "theme" | "share">("normal"); 128 + let { permissions } = useEntitySetContext(); 129 + if (!permissions.write) return null; 130 + 131 + let { data: pub, mutate } = useLeafletPublicationData(); 132 + if (pub && props.first) return; 133 + return ( 134 + <Menu 135 + align="end" 136 + asChild 137 + onOpenChange={(open) => { 138 + if (!open) setState("normal"); 139 + }} 140 + trigger={ 141 + <PageOptionButton 142 + cardBorderHidden={props.cardBorderHidden} 143 + className="!w-8 !h-5 sm:!w-5 sm:!h-8" 144 + > 145 + <MoreOptionsTiny className="sm:rotate-90" /> 146 + </PageOptionButton> 147 + } 148 + > 149 + {state === "normal" ? ( 150 + <> 151 + {!props.first && ( 152 + <MenuItem 153 + onSelect={(e) => { 154 + e.preventDefault(); 155 + setState("share"); 156 + }} 157 + > 158 + <ShareSmall /> Share Page 159 + </MenuItem> 160 + )} 161 + {!pub && ( 162 + <MenuItem 163 + onSelect={(e) => { 164 + e.preventDefault(); 165 + setState("theme"); 166 + }} 167 + > 168 + <PaintSmall /> Theme Page 169 + </MenuItem> 170 + )} 171 + </> 172 + ) : state === "theme" ? ( 173 + <PageThemeSetter entityID={props.entityID} /> 174 + ) : state === "share" ? ( 175 + <PageShareMenu entityID={props.entityID} /> 176 + ) : null} 177 + </Menu> 178 + ); 179 + }; 180 + 181 + const UndoTiny = () => { 182 + return ( 183 + <svg 184 + width="16" 185 + height="16" 186 + viewBox="0 0 16 16" 187 + fill="none" 188 + xmlns="http://www.w3.org/2000/svg" 189 + > 190 + <path 191 + fillRule="evenodd" 192 + clipRule="evenodd" 193 + d="M5.98775 3.14543C6.37828 2.75491 6.37828 2.12174 5.98775 1.73122C5.59723 1.34069 4.96407 1.34069 4.57354 1.73122L1.20732 5.09744C0.816798 5.48796 0.816798 6.12113 1.20732 6.51165L4.57354 9.87787C4.96407 10.2684 5.59723 10.2684 5.98775 9.87787C6.37828 9.48735 6.37828 8.85418 5.98775 8.46366L4.32865 6.80456H9.6299C12.1732 6.80456 13.0856 8.27148 13.0856 9.21676C13.0856 9.84525 12.8932 10.5028 12.5318 10.9786C12.1942 11.4232 11.6948 11.7367 10.9386 11.7367H9.43173C8.87944 11.7367 8.43173 12.1844 8.43173 12.7367C8.43173 13.2889 8.87944 13.7367 9.43173 13.7367H10.9386C12.3587 13.7367 13.4328 13.0991 14.1246 12.1883C14.7926 11.3086 15.0856 10.2062 15.0856 9.21676C15.0856 6.92612 13.0205 4.80456 9.6299 4.80456L4.32863 4.80456L5.98775 3.14543Z" 194 + fill="currentColor" 195 + /> 196 + </svg> 197 + ); 198 + }; 199 + 200 + const RedoTiny = () => { 201 + return ( 202 + <svg 203 + width="16" 204 + height="16" 205 + viewBox="0 0 16 16" 206 + fill="none" 207 + xmlns="http://www.w3.org/2000/svg" 208 + > 209 + <path 210 + fillRule="evenodd" 211 + clipRule="evenodd" 212 + d="M10.0122 3.14543C9.62172 2.75491 9.62172 2.12174 10.0122 1.73122C10.4028 1.34069 11.0359 1.34069 11.4265 1.73122L14.7927 5.09744C15.1832 5.48796 15.1832 6.12113 14.7927 6.51165L11.4265 9.87787C11.0359 10.2684 10.4028 10.2684 10.0122 9.87787C9.62172 9.48735 9.62172 8.85418 10.0122 8.46366L11.6713 6.80456H6.3701C3.82678 6.80456 2.91443 8.27148 2.91443 9.21676C2.91443 9.84525 3.10681 10.5028 3.46817 10.9786C3.8058 11.4232 4.30523 11.7367 5.06143 11.7367H6.56827C7.12056 11.7367 7.56827 12.1844 7.56827 12.7367C7.56827 13.2889 7.12056 13.7367 6.56827 13.7367H5.06143C3.6413 13.7367 2.56723 13.0991 1.87544 12.1883C1.20738 11.3086 0.914429 10.2062 0.914429 9.21676C0.914429 6.92612 2.97946 4.80456 6.3701 4.80456L11.6714 4.80456L10.0122 3.14543Z" 213 + fill="currentColor" 214 + /> 215 + </svg> 216 + ); 217 + };
+2 -4
components/Pages/PublicationMetadata.tsx
··· 43 43 description = pub?.description || ""; 44 44 } 45 45 return ( 46 - <div 47 - className={`flex flex-col px-3 sm:px-4 pb-5 ${cardBorderHidden ? "sm:pt-6 pt-0" : "sm:pt-3 pt-2"}`} 48 - > 46 + <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}> 49 47 <div className="flex gap-2"> 50 48 <Link 51 49 href={`${getBasePublicationURL(pub.publications)}/dashboard`} 52 - className="text-accent-contrast font-bold hover:no-underline" 50 + className="leafletMetadata text-accent-contrast font-bold hover:no-underline" 53 51 > 54 52 {pub.publications?.name} 55 53 </Link>
+27 -395
components/Pages/index.tsx
··· 1 1 "use client"; 2 2 3 - import React, { JSX, useState } from "react"; 3 + import React from "react"; 4 4 import { useUIState } from "src/useUIState"; 5 - import { useEntitySetContext } from "../EntitySetProvider"; 6 5 import { useSearchParams } from "next/navigation"; 7 6 8 7 import { focusBlock } from "src/utils/focusBlock"; 9 8 import { elementId } from "src/utils/elementId"; 10 9 11 10 import { Replicache } from "replicache"; 12 - import { 13 - Fact, 14 - ReplicacheMutators, 15 - useEntity, 16 - useReferenceToEntity, 17 - useReplicache, 18 - } from "src/replicache"; 11 + import { Fact, ReplicacheMutators, useEntity } from "src/replicache"; 19 12 20 - import { Media } from "../Media"; 21 - import { DesktopPageFooter } from "../DesktopFooter"; 22 - import { ThemePopover } from "../ThemeManager/ThemeSetter"; 23 - import { Canvas } from "../Canvas"; 24 - import { Blocks } from "components/Blocks"; 25 - import { MenuItem, Menu } from "../Layout"; 26 13 import { scanIndex } from "src/replicache/utils"; 27 - import { PageThemeSetter } from "../ThemeManager/PageThemeSetter"; 28 14 import { CardThemeProvider } from "../ThemeManager/ThemeProvider"; 29 - import { PageShareMenu } from "./PageShareMenu"; 30 15 import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded"; 31 - import { useUndoState } from "src/undoManager"; 32 - import { CloseTiny } from "components/Icons/CloseTiny"; 33 - import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 34 - import { PaintSmall } from "components/Icons/PaintSmall"; 35 - import { ShareSmall } from "components/Icons/ShareSmall"; 36 - import { PublicationMetadata } from "./PublicationMetadata"; 37 16 import { useCardBorderHidden } from "./useCardBorderHidden"; 38 - import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 39 - import { BookendSpacers, SandwichSpacer } from "components/LeafletLayout"; 17 + import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; 40 18 import { LeafletSidebar } from "app/[leaflet_id]/Sidebar"; 19 + import { Page } from "./Page"; 41 20 42 21 export function Pages(props: { rootPage: string }) { 43 22 let rootPage = useEntity(props.rootPage, "root/page")[0]; ··· 45 24 let params = useSearchParams(); 46 25 let queryRoot = params.get("page"); 47 26 let firstPage = queryRoot || rootPage?.data.value || props.rootPage; 48 - 49 - return ( 50 - <> 51 - <BookendSpacers 52 - onClick={(e) => { 53 - e.currentTarget === e.target && blurPage(); 54 - }} 55 - > 56 - <LeafletSidebar leaflet_id={props.rootPage} /> 57 - </BookendSpacers> 58 - <div className="flex items-stretch"> 59 - <CardThemeProvider entityID={firstPage}> 60 - <Page entityID={firstPage} first /> 61 - </CardThemeProvider> 62 - </div> 63 - {pages.map((page) => ( 64 - <div className="flex items-stretch" key={page}> 65 - <CardThemeProvider entityID={page}> 66 - <Page entityID={page} /> 67 - </CardThemeProvider> 68 - </div> 69 - ))} 70 - <BookendSpacers 71 - onClick={(e) => { 72 - e.currentTarget === e.target && blurPage(); 73 - }} 74 - /> 75 - </> 76 - ); 77 - } 27 + let cardBorderHidden = useCardBorderHidden(rootPage.id); 78 28 79 - export const LeafletOptions = (props: { entityID: string }) => { 80 - return ( 81 - <> 82 - <ThemePopover entityID={props.entityID} /> 83 - </> 84 - ); 85 - }; 29 + let fullPageScroll = !!cardBorderHidden && pages.length === 0; 86 30 87 - function Page(props: { entityID: string; first?: boolean }) { 88 - let { rep } = useReplicache(); 89 - let isDraft = useReferenceToEntity("mailbox/draft", props.entityID); 90 - 91 - let isFocused = useUIState((s) => { 92 - let focusedElement = s.focusedEntity; 93 - let focusedPageID = 94 - focusedElement?.entityType === "page" 95 - ? focusedElement.entityID 96 - : focusedElement?.parent; 97 - return focusedPageID === props.entityID; 98 - }); 99 - let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 100 - let cardBorderHidden = useCardBorderHidden(props.entityID); 101 31 return ( 102 32 <> 103 - {!props.first && ( 104 - <SandwichSpacer 33 + <LeafletSidebar /> 34 + {!fullPageScroll && ( 35 + <BookendSpacer 105 36 onClick={(e) => { 106 37 e.currentTarget === e.target && blurPage(); 107 38 }} 108 39 /> 109 40 )} 110 - <div className="pageWrapper w-fit flex relative snap-center"> 111 - <div 112 - onClick={(e) => { 113 - if (e.defaultPrevented) return; 114 - if (rep) { 115 - if (isFocused) return; 116 - focusPage(props.entityID, rep); 117 - } 118 - }} 119 - id={elementId.page(props.entityID).container} 120 - style={{ 121 - width: pageType === "doc" ? "var(--page-width-units)" : undefined, 122 - backgroundColor: cardBorderHidden 123 - ? "" 124 - : "rgba(var(--bg-page), var(--bg-page-alpha))", 125 - }} 126 - className={` 127 - ${pageType === "canvas" ? "!lg:max-w-[1152px]" : "max-w-[var(--page-width-units)]"} 128 - page 129 - grow flex flex-col 130 - overscroll-y-none 131 - overflow-y-auto 132 - ${cardBorderHidden ? "border-0 !shadow-none sm:-mt-6 sm:-mb-12 -mt-2 -mb-1 pt-3 " : "border rounded-lg"} 133 - ${isFocused ? "shadow-md border-border" : "border-border-light"} 134 - `} 135 - > 136 - <PageOptions 137 - entityID={props.entityID} 138 - first={props.first} 139 - isFocused={isFocused} 41 + 42 + <Page entityID={firstPage} first fullPageScroll={fullPageScroll} /> 43 + {pages.map((page) => ( 44 + <React.Fragment key={page}> 45 + <SandwichSpacer 46 + onClick={(e) => { 47 + e.currentTarget === e.target && blurPage(); 48 + }} 140 49 /> 141 - {props.first && ( 142 - <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 143 - )} 144 - <PageContent entityID={props.entityID} /> 145 - </div> 146 - <DesktopPageFooter pageID={props.entityID} /> 147 - </div> 148 - </> 149 - ); 150 - } 151 - 152 - const PageContent = (props: { entityID: string }) => { 153 - let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 154 - if (pageType === "doc") return <DocContent entityID={props.entityID} />; 155 - return <Canvas entityID={props.entityID} />; 156 - }; 157 - 158 - const DocContent = (props: { entityID: string }) => { 159 - let { rootEntity } = useReplicache(); 160 - let isFocused = useUIState((s) => { 161 - let focusedElement = s.focusedEntity; 162 - let focusedPageID = 163 - focusedElement?.entityType === "page" 164 - ? focusedElement.entityID 165 - : focusedElement?.parent; 166 - return focusedPageID === props.entityID; 167 - }); 168 - 169 - let cardBorderHidden = useCardBorderHidden(props.entityID); 170 - let rootBackgroundImage = useEntity( 171 - rootEntity, 172 - "theme/card-background-image", 173 - ); 174 - let rootBackgroundRepeat = useEntity( 175 - rootEntity, 176 - "theme/card-background-image-repeat", 177 - ); 178 - let rootBackgroundOpacity = useEntity( 179 - rootEntity, 180 - "theme/card-background-image-opacity", 181 - ); 182 - 183 - let cardBackgroundImage = useEntity( 184 - props.entityID, 185 - "theme/card-background-image", 186 - ); 187 - 188 - let cardBackgroundImageRepeat = useEntity( 189 - props.entityID, 190 - "theme/card-background-image-repeat", 191 - ); 192 - 193 - let cardBackgroundImageOpacity = useEntity( 194 - props.entityID, 195 - "theme/card-background-image-opacity", 196 - ); 197 - 198 - let backgroundImage = cardBackgroundImage || rootBackgroundImage; 199 - let backgroundImageRepeat = cardBackgroundImage 200 - ? cardBackgroundImageRepeat?.data?.value 201 - : rootBackgroundRepeat?.data.value; 202 - let backgroundImageOpacity = cardBackgroundImage 203 - ? cardBackgroundImageOpacity?.data.value 204 - : rootBackgroundOpacity?.data.value || 1; 205 - 206 - return ( 207 - <> 208 - {!cardBorderHidden ? ( 209 - <div 210 - className={`pageBackground 211 - absolute top-0 left-0 right-0 bottom-0 212 - pointer-events-none 213 - rounded-lg border 214 - ${isFocused ? " border-border" : "border-border-light"} 215 - `} 216 - style={{ 217 - backgroundImage: backgroundImage 218 - ? `url(${backgroundImage.data.src}), url(${backgroundImage.data.fallback})` 219 - : undefined, 220 - backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 221 - backgroundPosition: "center", 222 - backgroundSize: !backgroundImageRepeat 223 - ? "cover" 224 - : backgroundImageRepeat, 225 - opacity: backgroundImage?.data.src ? backgroundImageOpacity : 1, 50 + <Page entityID={page} fullPageScroll={false} /> 51 + </React.Fragment> 52 + ))} 53 + {!fullPageScroll && ( 54 + <BookendSpacer 55 + onClick={(e) => { 56 + e.currentTarget === e.target && blurPage(); 226 57 }} 227 58 /> 228 - ) : null} 229 - <Blocks entityID={props.entityID} /> 230 - {/* we handle page bg in this sepate div so that 231 - we can apply an opacity the background image 232 - without affecting the opacity of the rest of the page */} 233 - </> 234 - ); 235 - }; 236 - 237 - const PageOptionButton = ({ 238 - children, 239 - secondary, 240 - cardBorderHidden, 241 - className, 242 - disabled, 243 - ...props 244 - }: { 245 - children: React.ReactNode; 246 - secondary?: boolean; 247 - cardBorderHidden: boolean | undefined; 248 - className?: string; 249 - disabled?: boolean; 250 - } & Omit<JSX.IntrinsicElements["button"], "content">) => { 251 - return ( 252 - <button 253 - className={` 254 - pageOptionsTrigger 255 - shrink-0 256 - pt-[2px] h-5 w-5 p-0.5 mx-auto 257 - border border-border 258 - ${secondary ? "bg-border text-bg-page" : "bg-bg-page text-border"} 259 - ${disabled && "opacity-50"} 260 - ${cardBorderHidden ? "rounded-md" : `rounded-b-md sm:rounded-l-none sm:rounded-r-md`} 261 - flex items-center justify-center 262 - ${className} 263 - 264 - `} 265 - {...props} 266 - > 267 - {children} 268 - </button> 269 - ); 270 - }; 271 - 272 - const PageOptions = (props: { 273 - entityID: string; 274 - first: boolean | undefined; 275 - isFocused: boolean; 276 - }) => { 277 - let cardBorderHidden = useCardBorderHidden(props.entityID); 278 - 279 - return ( 280 - <div 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`} 285 - > 286 - {!props.first && ( 287 - <PageOptionButton 288 - cardBorderHidden={cardBorderHidden} 289 - secondary 290 - onClick={() => { 291 - useUIState.getState().closePage(props.entityID); 292 - }} 293 - > 294 - <CloseTiny /> 295 - </PageOptionButton> 296 59 )} 297 - <OptionsMenu 298 - entityID={props.entityID} 299 - first={!!props.first} 300 - cardBorderHidden={cardBorderHidden} 301 - /> 302 - <UndoButtons cardBorderHidden={cardBorderHidden} /> 303 - </div> 60 + </> 304 61 ); 305 - }; 306 - 307 - const UndoButtons = (props: { cardBorderHidden: boolean | undefined }) => { 308 - let undoState = useUndoState(); 309 - let { undoManager } = useReplicache(); 310 - return ( 311 - <Media mobile> 312 - {undoState.canUndo && ( 313 - <div className="gap-1 flex sm:flex-col"> 314 - <PageOptionButton 315 - secondary 316 - cardBorderHidden={props.cardBorderHidden} 317 - onClick={() => undoManager.undo()} 318 - > 319 - <UndoTiny /> 320 - </PageOptionButton> 321 - 322 - <PageOptionButton 323 - secondary 324 - cardBorderHidden={props.cardBorderHidden} 325 - onClick={() => undoManager.undo()} 326 - disabled={!undoState.canRedo} 327 - > 328 - <RedoTiny /> 329 - </PageOptionButton> 330 - </div> 331 - )} 332 - </Media> 333 - ); 334 - }; 335 - 336 - const OptionsMenu = (props: { 337 - entityID: string; 338 - first: boolean; 339 - cardBorderHidden: boolean | undefined; 340 - }) => { 341 - let [state, setState] = useState<"normal" | "theme" | "share">("normal"); 342 - let { permissions } = useEntitySetContext(); 343 - if (!permissions.write) return null; 344 - 345 - let { data: pub, mutate } = useLeafletPublicationData(); 346 - if (pub && props.first) return; 347 - return ( 348 - <Menu 349 - align="end" 350 - asChild 351 - onOpenChange={(open) => { 352 - if (!open) setState("normal"); 353 - }} 354 - trigger={ 355 - <PageOptionButton 356 - cardBorderHidden={props.cardBorderHidden} 357 - className="!w-8 !h-5 sm:!w-5 sm:!h-8" 358 - > 359 - <MoreOptionsTiny className="sm:rotate-90" /> 360 - </PageOptionButton> 361 - } 362 - > 363 - {state === "normal" ? ( 364 - <> 365 - {!props.first && ( 366 - <MenuItem 367 - onSelect={(e) => { 368 - e.preventDefault(); 369 - setState("share"); 370 - }} 371 - > 372 - <ShareSmall /> Share Page 373 - </MenuItem> 374 - )} 375 - {!pub && ( 376 - <MenuItem 377 - onSelect={(e) => { 378 - e.preventDefault(); 379 - setState("theme"); 380 - }} 381 - > 382 - <PaintSmall /> Theme Page 383 - </MenuItem> 384 - )} 385 - </> 386 - ) : state === "theme" ? ( 387 - <PageThemeSetter entityID={props.entityID} /> 388 - ) : state === "share" ? ( 389 - <PageShareMenu entityID={props.entityID} /> 390 - ) : null} 391 - </Menu> 392 - ); 393 - }; 62 + } 394 63 395 64 export async function focusPage( 396 65 pageID: string, ··· 458 127 }, 50); 459 128 } 460 129 461 - const blurPage = () => { 130 + export const blurPage = () => { 462 131 useUIState.setState(() => ({ 463 132 focusedEntity: null, 464 133 selectedBlocks: [], 465 134 })); 466 135 }; 467 - const UndoTiny = () => { 468 - return ( 469 - <svg 470 - width="16" 471 - height="16" 472 - viewBox="0 0 16 16" 473 - fill="none" 474 - xmlns="http://www.w3.org/2000/svg" 475 - > 476 - <path 477 - fillRule="evenodd" 478 - clipRule="evenodd" 479 - d="M5.98775 3.14543C6.37828 2.75491 6.37828 2.12174 5.98775 1.73122C5.59723 1.34069 4.96407 1.34069 4.57354 1.73122L1.20732 5.09744C0.816798 5.48796 0.816798 6.12113 1.20732 6.51165L4.57354 9.87787C4.96407 10.2684 5.59723 10.2684 5.98775 9.87787C6.37828 9.48735 6.37828 8.85418 5.98775 8.46366L4.32865 6.80456H9.6299C12.1732 6.80456 13.0856 8.27148 13.0856 9.21676C13.0856 9.84525 12.8932 10.5028 12.5318 10.9786C12.1942 11.4232 11.6948 11.7367 10.9386 11.7367H9.43173C8.87944 11.7367 8.43173 12.1844 8.43173 12.7367C8.43173 13.2889 8.87944 13.7367 9.43173 13.7367H10.9386C12.3587 13.7367 13.4328 13.0991 14.1246 12.1883C14.7926 11.3086 15.0856 10.2062 15.0856 9.21676C15.0856 6.92612 13.0205 4.80456 9.6299 4.80456L4.32863 4.80456L5.98775 3.14543Z" 480 - fill="currentColor" 481 - /> 482 - </svg> 483 - ); 484 - }; 485 - 486 - const RedoTiny = () => { 487 - return ( 488 - <svg 489 - width="16" 490 - height="16" 491 - viewBox="0 0 16 16" 492 - fill="none" 493 - xmlns="http://www.w3.org/2000/svg" 494 - > 495 - <path 496 - fillRule="evenodd" 497 - clipRule="evenodd" 498 - d="M10.0122 3.14543C9.62172 2.75491 9.62172 2.12174 10.0122 1.73122C10.4028 1.34069 11.0359 1.34069 11.4265 1.73122L14.7927 5.09744C15.1832 5.48796 15.1832 6.12113 14.7927 6.51165L11.4265 9.87787C11.0359 10.2684 10.4028 10.2684 10.0122 9.87787C9.62172 9.48735 9.62172 8.85418 10.0122 8.46366L11.6713 6.80456H6.3701C3.82678 6.80456 2.91443 8.27148 2.91443 9.21676C2.91443 9.84525 3.10681 10.5028 3.46817 10.9786C3.8058 11.4232 4.30523 11.7367 5.06143 11.7367H6.56827C7.12056 11.7367 7.56827 12.1844 7.56827 12.7367C7.56827 13.2889 7.12056 13.7367 6.56827 13.7367H5.06143C3.6413 13.7367 2.56723 13.0991 1.87544 12.1883C1.20738 11.3086 0.914429 10.2062 0.914429 9.21676C0.914429 6.92612 2.97946 4.80456 6.3701 4.80456L11.6714 4.80456L10.0122 3.14543Z" 499 - fill="currentColor" 500 - /> 501 - </svg> 502 - ); 503 - };