diff --git a/app/src/assets/editor.less b/app/src/assets/editor.less new file mode 100644 index 0000000..ab0630f --- /dev/null +++ b/app/src/assets/editor.less @@ -0,0 +1,93 @@ +.editor-action { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + background: hsl(var(--input)) !important; + padding: 6px; + border-radius: 50%; + cursor: pointer; + transition: 0.1s; + outline: 0; + opacity: 0; + + &.active { + opacity: 1; + } + + &:hover { + background: hsl(var(--border-hover)) !important; + } +} + +.editor-dialog { + max-width: min(90vw, 920px) !important; +} + +.editor-container { + padding: 2px 4px 0; +} + +.editor-wrapper { + padding: 4px 0; +} + +.editor-object { + position: relative; + display: grid; + grid-gap: 12px; + height: 100%; + + &.show-editor { + grid-template-columns: 1fr; + grid-template-rows: 1fr; + grid-template-areas: 'editor'; + } + + &.show-preview { + grid-template-columns: 1fr; + grid-template-rows: 1fr; + grid-template-areas: 'markdown'; + } + + &.show-editor.show-preview { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + grid-template-areas: 'editor markdown'; + } +} + +.editor-input { + grid-area: editor; + scrollbar-width: thin; + height: max-content; + transition: .1s; + min-height: 20vh !important; + color: hsl(var(--text)); + font-size: 16px !important; + padding: 14px 12px !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; +} + +.editor-preview { + height: 100%; + grid-area: markdown; + overflow: auto; + padding: 12px; + border-radius: 4px; + background: hsl(var(--input)); + color: hsl(var(--text)); + border: 1px solid hsl(var(--border)); + scrollbar-width: thin; + transition: 0.1s; + min-height: 20vh !important; + font-size: 16px; +} + +.editor-toolbar { + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 4px; + margin: 4px 0; +} diff --git a/app/src/assets/file.less b/app/src/assets/file.less index d5db50e..f4d6cec 100644 --- a/app/src/assets/file.less +++ b/app/src/assets/file.less @@ -1,7 +1,7 @@ .file-action { position: absolute; top: 50%; - left: 6px; + left: 8px; transform: translateY(-50%); background: hsl(var(--input)) !important; padding: 6px; diff --git a/app/src/assets/home.less b/app/src/assets/home.less index e43f26e..c0f69b0 100644 --- a/app/src/assets/home.less +++ b/app/src/assets/home.less @@ -282,6 +282,7 @@ width: 100%; text-align: center; color: hsl(var(--text)); + white-space: pre-wrap; &::placeholder { color: hsl(var(--text-secondary)); diff --git a/app/src/components/RichEditor.tsx b/app/src/components/RichEditor.tsx new file mode 100644 index 0000000..6bbfe31 --- /dev/null +++ b/app/src/components/RichEditor.tsx @@ -0,0 +1,148 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog.tsx"; +import { Edit, Image, MenuSquare, PanelRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import "../assets/editor.less"; +import { Textarea } from "./ui/textarea.tsx"; +import Markdown from "./Markdown.tsx"; +import { useEffect, useRef, useState } from "react"; +import { Toggle } from "./ui/toggle.tsx"; +import {mobile} from "../utils.ts"; + +type RichEditorProps = { + value: string; + onChange: (value: string) => void; + className?: string; + id?: string; + placeholder?: string; + maxLength?: number; +}; + +function RichEditor({ + value, + onChange, + className, + id, + placeholder, + maxLength, +}: RichEditorProps) { + const { t } = useTranslation(); + const input = useRef(null); + const [openPreview, setOpenPreview] = useState(!mobile); + const [openInput, setOpenInput] = useState(true); + + useEffect(() => { + if (!input.current) return; + const target = input.current as HTMLElement; + const preview = target.parentElement?.querySelector( + ".editor-preview", + ) as HTMLElement; + + const listener = () => { + preview.style.height = `${target.clientHeight}px`; + }; + target.addEventListener("transitionstart", listener); + const task = setInterval(listener, 250); + target.addEventListener("scroll", () => { + preview.scrollTop = target.scrollTop; + }); + + preview.style.height = `${target.clientHeight}px`; + + return () => { + target.removeEventListener("transitionstart", listener); + clearInterval(task); + }; + }, [input]); + + return ( + <> + + +
+ +
+
+ + + {t("edit")} + +
+
+
+ { + setOpenPreview(false); + setOpenInput(true); + }} + > + + + + { + setOpenPreview(true); + setOpenInput(true); + }} + > + + + + { + setOpenPreview(true); + setOpenInput(false); + }} + > + + +
+
+
+ {openInput && ( +