Created
January 3, 2024 17:59
-
-
Save pablomikel/f6bf79d0af53ec76ce7a1718fe4c0e6e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useState } from "react"; | |
import { useRouter } from "next/router"; | |
import { RedirectToSignIn, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; | |
import * as Dialog from "@radix-ui/react-dialog"; | |
import isEqual from "deep-eql"; | |
import { AnimatePresence, motion, useAnimate } from "framer-motion"; | |
import { useHotkeys } from "react-hotkeys-hook"; | |
import { | |
TbBold, | |
TbCheck, | |
TbChevronUp, | |
TbClipboard, | |
TbClipboardCheck, | |
TbClipboardX, | |
TbCloudUpload, | |
TbFilePlus, | |
TbFiles, | |
TbItalic, | |
TbLoader, | |
TbPhotoPlus, | |
TbTrash, | |
TbTrashX, | |
TbX, | |
} from "react-icons/tb"; | |
import styled from "styled-components"; | |
import { useSessionStorage } from "usehooks-ts"; | |
import { allExtensions } from "@poplar/tiptap"; | |
import type { Editor } from "@poplar/tiptap/core"; | |
import type { JSONContent } from "@poplar/tiptap/react"; | |
import { BubbleMenu, EditorContent, useEditor } from "@poplar/tiptap/react"; | |
import Button, { ButtonStyled } from "~/components/Button"; | |
import { FolderSelector } from "~/components/FolderSelector"; | |
import Skeleton from "~/components/Skeleton"; | |
import { StyledInput } from "~/components/Tabs"; | |
import Toolbar from "~/components/Toolbar"; | |
import Version from "~/components/Version"; | |
import { theme } from "~/styles/theme"; | |
import { api } from "~/utils/api"; | |
import extractIdFromParam from "~/utils/extractIdFromParam"; | |
import jsonContentIsValid from "~/utils/jsonContentIsValid"; | |
import { useUploadThing } from "~/utils/uploadthing"; | |
import useDebounce from "~/utils/useDebounce"; | |
import { Main } from "../.."; | |
export const EditorContainer = styled.div` | |
border: 1px solid ${theme.colors.stroke.actionable}; | |
border-bottom: none; | |
border-radius: 6px 6px 0px 0px; | |
width: calc(100% - 32px); | |
max-width: 800px; | |
position: relative; | |
min-height: fill-available; | |
display: grid; | |
justify-self: center; | |
margin: 0px 16px 0 16px; | |
overflow: hidden; | |
box-sizing: border-box; | |
`; | |
export const SkeletonContainer = styled.div` | |
position: absolute; | |
inset: 0; | |
align-content: start; | |
z-index: 10; | |
pointer-events: none; | |
display: grid; | |
justify-content: stretch; | |
align-content: stretch; | |
${Skeleton} { | |
min-height: 100%; | |
min-width: 100%; | |
} | |
`; | |
export const Container = styled.div` | |
grid-template-rows: 1fr; | |
min-height: fill-available; | |
display: grid; | |
grid-auto-flow: row; | |
justify-content: stretch; | |
position: relative; | |
`; | |
const SaveStatus = styled.div` | |
display: grid; | |
grid-auto-flow: column; | |
padding: 0 8px; | |
gap: 8px; | |
font-size: 16px; | |
color: ${theme.colors.text.base}; | |
justify-self: stretch; | |
align-items: center; | |
justify-items: end; | |
span { | |
font-size: 14px; | |
justify-self: start; | |
} | |
`; | |
export const VersionContainer = styled.div` | |
backdrop-filter: saturate(180%) blur(8px); | |
:before { | |
content: ""; | |
position: absolute; | |
inset: 0; | |
background-color: ${theme.colors.bg.subdued}; | |
z-index: -1; | |
opacity: 0.5; | |
} | |
`; | |
const ClipBoardButton = ({ editor }: { editor: Editor }) => { | |
const [copiedState, setCopiedState] = useState< | |
"idle" | "success" | "failure" | |
>("idle"); | |
useEffect(() => { | |
if (copiedState === "idle") { | |
return; | |
} | |
const timeout = setTimeout(() => { | |
setCopiedState("idle"); | |
}, 1000); | |
return () => { | |
clearTimeout(timeout); | |
}; | |
}, [copiedState]); | |
return ( | |
<Button | |
onClick={() => { | |
const { from, to, empty } = editor.state.selection; | |
if (empty) { | |
return null; | |
} | |
const text = editor.state.doc.textBetween(from, to, " "); | |
navigator.clipboard | |
.writeText(text) | |
.then(() => { | |
setCopiedState("success"); | |
}) | |
.catch(() => { | |
setCopiedState("failure"); | |
}); | |
}} | |
> | |
{copiedState === "idle" ? ( | |
<TbClipboard /> | |
) : copiedState === "success" ? ( | |
<TbClipboardCheck /> | |
) : ( | |
<TbClipboardX /> | |
)} | |
</Button> | |
); | |
}; | |
const EditDocumentPage = () => { | |
const router = useRouter(); | |
const { id } = router.query; | |
const [versionCountRef, animate] = useAnimate<HTMLSpanElement>(); | |
const auth = useAuth(); | |
const { isSignedIn } = auth; | |
const context = api.useContext(); | |
const [saveStatus, setSaveStatus] = useState< | |
"saved" | "saving" | "error" | "" | |
>(""); | |
const documentId = extractIdFromParam(id); | |
const [slug, setSlug] = useState<string>(""); | |
const [slugActive, setSlugActive] = useState<boolean>(false); | |
const document = api.document.getDocumentById.useQuery( | |
{ id: documentId ?? "" }, | |
{ | |
enabled: router.isReady && !!documentId && isSignedIn, | |
onSuccess: (data) => { | |
if (!data) { | |
return; | |
} | |
setSlug(data.slug ?? ""); | |
}, | |
}, | |
); | |
const updateDocument = api.document.updateDocument.useMutation({ | |
onSuccess: (res) => { | |
setSaveStatus("saved"); | |
context.document.getDocumentById.setData({ id: res.id }, () => { | |
return res; | |
}); | |
setTimeout(() => { | |
setSaveStatus(""); | |
}, 1000); | |
}, | |
}); | |
const newVersion = api.document.createVersion.useMutation({ | |
onSuccess: (data) => { | |
void animate( | |
versionCountRef.current, | |
{ | |
scale: [1.3, 1], | |
}, | |
{ duration: 0.3 }, | |
); | |
if (!documentId) { | |
return; | |
} | |
const oldDocumentById = context.document.getDocumentById.getData({ | |
id: documentId, | |
}); | |
if (!oldDocumentById) { | |
return; | |
} | |
context.document.getDocumentById.setData({ id: documentId }, () => { | |
return { | |
...oldDocumentById, | |
versions: data.versions, | |
}; | |
}); | |
}, | |
}); | |
const deleteDocument = api.document.deleteDocumentById.useMutation({ | |
onSuccess: async (data) => { | |
if (!documentId) { | |
return; | |
} | |
/* | |
const oldDocumentsByCurrentUser = | |
context.document.getDocumentsByCurrentUser.getData(); | |
context.document.getDocumentsByCurrentUser.setData(undefined, () => { | |
return assign(oldDocumentsByCurrentUser, data); | |
}); | |
*/ | |
await router.push("/files").then(() => { | |
context.document.getDocumentById.setData( | |
{ id: documentId }, | |
(oldData) => { | |
if (!oldData) { | |
return; | |
} | |
return { | |
...oldData, | |
...data, | |
}; | |
}, | |
); | |
}); | |
}, | |
}); | |
const restoreDocument = api.document.restoreDocumentById.useMutation({ | |
onSuccess: (data) => { | |
if (!documentId) { | |
return; | |
} | |
/* | |
const oldData = context.document.getDocumentsByCurrentUser.getData(); | |
context.document.getDocumentsByCurrentUser.setData(undefined, () => { | |
return oldData?.filter((d) => d.id !== documentId); | |
}); | |
*/ | |
context.document.getDocumentById.setData({ id: documentId }, () => { | |
return data; | |
}); | |
}, | |
}); | |
const [showVersions, setShowVersions] = useState(false); | |
const updateSlug = api.document.updateDocumentSlug.useMutation({ | |
onMutate: () => { | |
setSaveStatus("saving"); | |
}, | |
onSuccess: (res) => { | |
if (!documentId) { | |
return; | |
} | |
context.document.getDocumentById.setData({ id: documentId }, (data) => { | |
if (!data) { | |
return; | |
} | |
return { | |
...data, | |
slug: res, | |
}; | |
}); | |
setSaveStatus("saved"); | |
setTimeout(() => { | |
setSaveStatus(""); | |
}, 1000); | |
}, | |
}); | |
const debouncedUpdateSlug = useDebounce(async () => { | |
if (!documentId) { | |
return null; | |
} | |
await updateSlug.mutateAsync({ | |
documentId: documentId, | |
slug: slug, | |
}); | |
}, 400); | |
const newAutoSave = useDebounce(async () => { | |
const content = editor?.getJSON(); | |
if (!content) { | |
return; | |
} | |
if (!documentId) { | |
return; | |
} | |
if (!document.data) { | |
return; | |
} | |
if (document.data?.deleted) { | |
return; | |
} | |
const serverContent = document.data?.versions?.[0]?.content; | |
const isValid = jsonContentIsValid(content, allExtensions); | |
const isChanged = !isEqual(content, serverContent); | |
if (documentId && isValid && isChanged) { | |
// if (serverContent) { | |
// console.log("DIFF", detailedDiff(serverContent, content)); | |
// } | |
await updateDocument | |
.mutateAsync({ id: documentId, content: content }) | |
.then(async () => { | |
await context.document.getDocumentById.invalidate({ id: documentId }); | |
}); | |
} else { | |
setSaveStatus("saved"); | |
setTimeout(() => { | |
setSaveStatus(""); | |
}, 1000); | |
} | |
}, 400); | |
const [editorContent, setEditorContent] = useState<JSONContent | null>(null); | |
const [init, setInit] = useState(false); | |
const documentContent = document.data?.versions?.[0]?.content; | |
const editor = useEditor( | |
{ | |
extensions: allExtensions, | |
editable: document.data?.deleted ? false : document.data ? true : false, | |
content: editorContent, | |
onUpdate: () => { | |
setSaveStatus("saving"); | |
newAutoSave(); | |
}, | |
}, | |
[editorContent, document.data?.deleted], | |
); | |
useEffect(() => { | |
if (documentContent && !editorContent && !init) { | |
setEditorContent(documentContent); | |
} | |
}, [documentContent, editorContent, editor, init]); | |
useEffect(() => { | |
if (document.data) { | |
setInit(true); | |
} | |
}, [document.data]); | |
/* const flowers = api.user.getPreferences.useQuery(undefined, { | |
staleTime: Infinity, | |
refetchOnWindowFocus: false, | |
enabled: isSignedIn, | |
}).data?.flowers; */ | |
const [menuOpen, setMenuOpen] = useSessionStorage("editorMenu", false); | |
useHotkeys("meta+j", () => setMenuOpen(!menuOpen), { | |
enableOnContentEditable: true, | |
}); | |
/* useEffect(() => { | |
router.events.on('routeChangeStart', () => { | |
const content = editor?.getJSON(); | |
if (!content) { | |
return; | |
} | |
if (!documentId) { | |
return; | |
} | |
const isValid = jsonContentIsValid(content, extensions); | |
if (!isValid) { | |
return; | |
} | |
updateDocument.mutate({ id: documentId, content: content }); | |
}); | |
}, [documentId, editor, router.events, updateDocument]); | |
useBeforeunload(() => { | |
const content = editor?.getJSON(); | |
if (!content) { | |
return; | |
} | |
if (!documentId) { | |
return; | |
} | |
const isValid = jsonContentIsValid(content, extensions); | |
if (!isValid) { | |
return; | |
} | |
updateDocument.mutate({ id: documentId, content: content }); | |
}); */ | |
const uploader = useUploadThing("imageUploader", { | |
onClientUploadComplete: (res) => { | |
res?.map((r) => { | |
editor?.chain().focus().setImage({ src: r.url }).run(); | |
}); | |
}, | |
}); | |
const { startUpload, isUploading } = uploader; | |
return ( | |
<> | |
<SignedOut> | |
<RedirectToSignIn /> | |
</SignedOut> | |
<SignedIn> | |
{!document.isLoading && !document.data ? ( | |
<Main> | |
<p>No data</p> | |
</Main> | |
) : document.isError ? ( | |
<Main> | |
<p>Something went wrong</p> | |
<p>{document.error.message}</p> | |
</Main> | |
) : ( | |
<> | |
<ButtonsContainer | |
as={motion.div} | |
initial={false} | |
animate={ | |
menuOpen | |
? { | |
background: theme.colors.bg.base.toString(), | |
boxShadow: theme.shadow.subtle.toString(), | |
borderColor: theme.colors.stroke.default.toString(), | |
backdropFilter: "saturate(180%) blur(8px)", | |
} | |
: { | |
background: "transparent", | |
boxShadow: "none", | |
borderColor: "transparent", | |
backdropFilter: "none", | |
} | |
} | |
> | |
<ButtonsContainerBg | |
initial={false} | |
animate={ | |
menuOpen | |
? { | |
opacity: 0.85, | |
} | |
: { | |
opacity: 0, | |
} | |
} | |
/> | |
<ButtonsSubContainer | |
$menuOpen={menuOpen} | |
initial={false} | |
as={motion.div} | |
animate={ | |
menuOpen | |
? { | |
height: "auto", | |
marginTop: 16, | |
marginBottom: 12, | |
opacity: 1, | |
} | |
: { height: 0, marginTop: 12, marginBottom: 0, opacity: 0 } | |
} | |
> | |
{document.data?.versions[0]?.active ? ( | |
document.data.slug ? ( | |
<p | |
style={{ | |
margin: 0, | |
padding: "4px 8px", | |
borderRadius: 4, | |
fontSize: 12, | |
boxShadow: theme.shadow.subtle.toString(), | |
background: theme.colors.avatar.bg.toString(), | |
color: theme.colors.avatar.text.toString(), | |
border: `1px solid ${theme.colors.avatar.border.toString()}`, | |
justifySelf: "start", | |
}} | |
> | |
Published | |
</p> | |
) : ( | |
<p | |
style={{ | |
margin: 0, | |
padding: "4px 8px", | |
borderRadius: 4, | |
fontSize: 12, | |
boxShadow: theme.shadow.subtle.toString(), | |
background: | |
theme.colors.button.danger.default.bg.toString(), | |
color: | |
theme.colors.button.danger.default.text.toString(), | |
border: `1px solid ${theme.colors.button.danger.default.border.toString()}`, | |
justifySelf: "start", | |
}} | |
> | |
Needs slug | |
</p> | |
) | |
) : ( | |
<p | |
style={{ | |
margin: 0, | |
padding: "4px 8px", | |
borderRadius: 4, | |
fontSize: 12, | |
boxShadow: theme.shadow.subtle.toString(), | |
background: theme.colors.bg.subdued.toString(), | |
color: theme.colors.text.subdued.toString(), | |
border: `1px solid ${theme.colors.stroke.default.toString()}`, | |
justifySelf: "start", | |
}} | |
> | |
Not published | |
</p> | |
)} | |
<AnimatePresence mode="popLayout"> | |
{document.data?.deleted ? null : saveStatus === "saving" ? ( | |
<SaveStatus | |
key={"saving"} | |
as={motion.div} | |
animate={{ opacity: [0.5, 0.5, 1, 0.5, 0.5] }} | |
transition={{ | |
duration: 2.5, | |
repeat: Infinity, | |
repeatType: "loop", | |
}} | |
> | |
<span>Saving...</span> <TbCloudUpload /> | |
</SaveStatus> | |
) : ( | |
<SaveStatus | |
key={"saved"} | |
as={motion.div} | |
initial={{ scale: 1 }} | |
animate={{ | |
opacity: saveStatus === "saved" ? 1 : 0.5, | |
scale: saveStatus === "saved" ? 1.01 : 1, | |
}} | |
transition={{ | |
duration: 0.2, | |
}} | |
> | |
<span>Saved</span> <TbCheck /> | |
</SaveStatus> | |
)} | |
</AnimatePresence> | |
<Button | |
style={{ gridTemplateColumns: "auto 1fr auto" }} | |
onClick={() => { | |
if (documentId) { | |
context.document.getDocumentById.setData( | |
{ id: documentId }, | |
(data) => { | |
const content = editor?.getJSON(); | |
if (!data || !content) { | |
return; | |
} | |
const newVersions = data.versions.map( | |
(version, i) => { | |
if (i === 0) { | |
return { | |
...version, | |
content: content, | |
}; | |
} | |
return version; | |
}, | |
); | |
return { | |
...data, | |
versions: newVersions, | |
}; | |
}, | |
); | |
setShowVersions(true); | |
} | |
}} | |
disabled={ | |
!( | |
document.data?.versions && | |
document.data.versions.length > 0 | |
) | |
} | |
> | |
<TbFiles /> | |
<span> | |
<span | |
style={{ | |
position: "relative", | |
}} | |
> | |
<span> | |
{document?.data?.versions.length && | |
document.data.versions.length > 1 ? ( | |
<span | |
style={{ | |
display: "inline-block", | |
fontVariantNumeric: "normal", | |
}} | |
ref={versionCountRef} | |
> | |
{(document.data.versions.length - 1).toString()} | |
</span> | |
) : ( | |
"No" | |
)} | |
</span> | |
</span>{" "} | |
snapshot | |
{document.data?.versions.length === 0 || | |
(document.data?.versions.length && | |
document.data?.versions.length === 2) | |
? "" | |
: "s"} | |
</span> | |
{document.data?.versions.find( | |
(version) => version.active, | |
) && ( | |
<div | |
style={{ | |
width: 8, | |
height: 8, | |
borderRadius: 4, | |
background: theme.colors.avatar.border.toString(), | |
justifySelf: "end", | |
}} | |
/> | |
)} | |
</Button> | |
{document.data?.deleted ? null : ( | |
<> | |
<Button | |
onClick={() => { | |
documentId && newVersion.mutate({ id: documentId }); | |
}} | |
disabled={ | |
isEqual( | |
editor?.getJSON(), | |
document.data?.versions?.[1]?.content, | |
) || | |
!documentId || | |
saveStatus === "saving" || | |
!document.data?.versions?.[0]?.content | |
} | |
loading={newVersion.isLoading} | |
> | |
<TbFilePlus /> <span>Snapshot</span> | |
</Button> | |
</> | |
)} | |
{documentId && document.data && ( | |
<FolderSelector | |
defaultValue={document.data.folderId ?? "no-folder"} | |
documentId={documentId} | |
/> | |
)} | |
<div style={{ position: "relative" }}> | |
<StyledInput | |
style={{ | |
position: "absolute", | |
inset: 0, | |
zIndex: 2, | |
pointerEvents: "none", | |
opacity: slugActive ? 0 : 1, | |
}} | |
placeholder="Slug..." | |
value={encodeURI(slug)} | |
/> | |
<StyledInput | |
style={{ position: "relative", zIndex: 1 }} | |
value={slug} | |
onBlur={() => setSlugActive(false)} | |
onFocus={() => setSlugActive(true)} | |
onChange={(e) => { | |
setSlug(e.target.value); | |
debouncedUpdateSlug(); | |
}} | |
/> | |
</div> | |
<div | |
style={{ | |
position: "relative", | |
justifySelf: "stretch", | |
display: "grid", | |
}} | |
> | |
<ButtonStyled style={{ justifyContent: "start" }}> | |
{isUploading ? ( | |
<> | |
<Spinning> | |
<TbLoader /> | |
</Spinning> | |
Uploading... | |
</> | |
) : ( | |
<> | |
<TbPhotoPlus /> | |
Add image | |
</> | |
)} | |
</ButtonStyled> | |
<input | |
style={{ | |
position: "absolute", | |
inset: 0, | |
opacity: 0, | |
width: "100%", | |
height: "100%", | |
cursor: "pointer", | |
}} | |
type="file" | |
onChange={async (e) => { | |
const file = e.target.files?.[0]; | |
if (!file) return; | |
await startUpload([file]); | |
}} | |
/> | |
</div> | |
{document.data?.deleted ? ( | |
<Button | |
onClick={() => | |
documentId && restoreDocument.mutate({ id: documentId }) | |
} | |
> | |
<TbTrashX /> <span>Restore</span> | |
</Button> | |
) : ( | |
<Button | |
variant="danger" | |
onClick={() => | |
documentId && deleteDocument.mutate({ id: documentId }) | |
} | |
> | |
<TbTrash /> <span>Delete</span> | |
</Button> | |
)} | |
</ButtonsSubContainer> | |
<Button onClick={() => setMenuOpen(!menuOpen)}> | |
<TbChevronUp | |
style={{ | |
width: 14, | |
height: 14, | |
rotate: menuOpen ? "-180deg" : "0deg", | |
transition: "rotate 0.2s ease-in-out", | |
}} | |
/> | |
</Button> | |
</ButtonsContainer> | |
<Container> | |
<EditorContainer | |
// as={motion.div} | |
// initial={{ y: 4 }} | |
// animate={{ y: 0 }} | |
> | |
<> | |
{document.isLoading && ( | |
<SkeletonContainer> | |
<Skeleton /> | |
</SkeletonContainer> | |
)} | |
</> | |
{editor && ( | |
<StyledBubbleMenu | |
editor={editor} | |
tippyOptions={{ duration: 100 }} | |
shouldShow={(props) => | |
(props.editor.isActive("image") ? false : true) && | |
!props.editor.state.selection.empty | |
} | |
> | |
<Button | |
onClick={() => editor.chain().focus().toggleBold().run()} | |
active={editor.isActive("bold")} | |
> | |
<TbBold /> | |
</Button> | |
<Button | |
onClick={() => | |
editor.chain().focus().toggleItalic().run() | |
} | |
active={editor.isActive("italic")} | |
> | |
<TbItalic /> | |
</Button> | |
<ClipBoardButton editor={editor} /> | |
</StyledBubbleMenu> | |
)} | |
{/* | |
{editor && ( | |
<StyledFloatingMenu | |
editor={editor} | |
tippyOptions={{ duration: 100 }} | |
> | |
<Button onClick={addImage}> | |
<TbPhotoPlus /> | |
</Button> | |
</StyledFloatingMenu> | |
)} | |
*/} | |
<EditorContent | |
editor={editor} | |
style={{ | |
display: "flex", | |
minHeight: "fill-available", | |
}} | |
/> | |
</EditorContainer> | |
<Dialog.Root open={showVersions} onOpenChange={setShowVersions}> | |
<Dialog.Portal> | |
<Dialog.Overlay style={{ position: "absolute", inset: 0 }} /> | |
<Dialog.Content | |
asChild | |
onOpenAutoFocus={(event) => event.preventDefault()} | |
> | |
<VersionContainer | |
style={{ | |
display: "grid", | |
gridTemplateRows: "1fr", | |
position: "fixed", | |
inset: 0, | |
alignContent: "stretch", | |
alignItems: "stretch", | |
overflow: "hidden", | |
zIndex: 100000, | |
}} | |
> | |
<Toolbar | |
showBorder={false} | |
slot={{ | |
left: ( | |
<Button onClick={() => setShowVersions(false)}> | |
<TbX /> | |
<span>Close</span> | |
</Button> | |
), | |
}} | |
/> | |
<div | |
style={{ | |
display: "grid", | |
gridAutoFlow: "column", | |
overflowX: "auto", | |
alignContent: "stretch", | |
paddingLeft: | |
"clamp(48px, calc(50vw - 332px), calc(50vw - 332px))", | |
paddingRight: | |
"clamp(48px, calc(50vw - 332px), calc(50vw - 332px))", | |
scrollSnapType: "x mandatory", | |
scrollPaddingLeft: | |
"clamp(48px, calc(50vw - 332px), calc(50vw - 332px))", | |
paddingTop: 90, | |
}} | |
> | |
<AnimatePresence> | |
{document.data?.versions.map((version, i) => { | |
return ( | |
<Version | |
key={version.id} | |
version={version} | |
i={i} | |
documentId={documentId} | |
/> | |
); | |
})} | |
</AnimatePresence> | |
</div> | |
</VersionContainer> | |
</Dialog.Content> | |
</Dialog.Portal> | |
</Dialog.Root> | |
{/* {!!flowers && ( | |
<div | |
style={{ | |
position: "fixed", | |
bottom: "0", | |
left: "0", | |
right: "0", | |
display: "flex", | |
justifyContent: "center", | |
alignContent: "end", | |
zIndex: -1, | |
}} | |
> | |
<Flowers | |
numOfFlowers={ | |
( | |
editor?.storage.characterCount as CharacterCountStorage | |
)?.words() ?? 0 | |
} | |
salt={documentId ?? ""} | |
/> | |
</div> | |
)} */} | |
</Container> | |
</> | |
)} | |
</SignedIn> | |
</> | |
); | |
}; | |
export default EditDocumentPage; | |
const StyledBubbleMenu = styled(BubbleMenu)` | |
display: grid; | |
grid-auto-flow: column; | |
gap: 8px; | |
button { | |
font-size: 16px; | |
padding: 4px 4px; | |
border-radius: 4px; | |
outline-width: 2px; | |
height: unset; | |
} | |
`; | |
/* const StyledFloatingMenu = styled(FloatingMenu)` | |
button { | |
font-size: 16px; | |
padding: 4px 4px; | |
border-radius: 4px; | |
outline-width: 2px; | |
height: unset; | |
} | |
`; */ | |
export const ButtonsContainer = styled.div` | |
display: grid; | |
grid-auto-flow: row; | |
justify-content: end; | |
justify-items: end; | |
align-items: center; | |
justify-self: center; | |
position: fixed; | |
bottom: 24px; | |
right: 24px; | |
z-index: 100; | |
padding: 12px; | |
padding-top: 0; | |
box-shadow: ${theme.shadow.subtle}; | |
border-radius: 12px; | |
border: 1px solid ${theme.colors.stroke.default}; | |
background: transparent; | |
backdrop-filter: saturate(180%) blur(8px); | |
overflow: hidden; | |
box-sizing: border-box; | |
`; | |
export const ButtonsContainerBg = styled(motion.div)` | |
position: absolute; | |
inset: 0; | |
background-color: ${theme.colors.bg.base}; | |
opacity: 0.85; | |
z-index: 0; | |
`; | |
export const ButtonsSubContainer = styled.div<{ $menuOpen: boolean }>` | |
display: grid; | |
grid-auto-flow: row; | |
justify-content: stretch; | |
justify-items: stretch; | |
align-items: end; | |
align-content: end; | |
gap: 16px; | |
box-sizing: border-box; | |
pointer-events: ${(props) => (props.$menuOpen ? "all" : "none")}; | |
position: relative; | |
z-index: 1; | |
button { | |
justify-content: start; | |
} | |
`; | |
const Spinning = styled.div` | |
display: flex; | |
@-webkit-keyframes rotating /* Safari and Chrome */ { | |
from { | |
-webkit-transform: rotate(0deg); | |
-o-transform: rotate(0deg); | |
transform: rotate(0deg); | |
} | |
to { | |
-webkit-transform: rotate(360deg); | |
-o-transform: rotate(360deg); | |
transform: rotate(360deg); | |
} | |
} | |
@keyframes rotating { | |
from { | |
-ms-transform: rotate(0deg); | |
-moz-transform: rotate(0deg); | |
-webkit-transform: rotate(0deg); | |
-o-transform: rotate(0deg); | |
transform: rotate(0deg); | |
} | |
to { | |
-ms-transform: rotate(360deg); | |
-moz-transform: rotate(360deg); | |
-webkit-transform: rotate(360deg); | |
-o-transform: rotate(360deg); | |
transform: rotate(360deg); | |
} | |
} | |
animation: rotating 2s linear infinite; | |
`; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment