mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Improve quick note editor interactions & fix filter chips responsiveness (#989)
Some checks failed
Publish Memory Graph / publish (push) Has been cancelled
Some checks failed
Publish Memory Graph / publish (push) Has been cancelled
This commit is contained in:
parent
670d8bf76e
commit
a8d36df38e
5 changed files with 356 additions and 118 deletions
|
|
@ -394,11 +394,14 @@ export function MemoriesGrid({
|
|||
return items
|
||||
}, [documents, isMobile, hasQuickNote, isSelectionMode, selectedDocumentIds])
|
||||
|
||||
// Stable key for Masonry based on document IDs, not item values
|
||||
// Reset Masonry when the actual rendered item set changes. Masonic caches
|
||||
// positions by index, so mobile removing the quick note must remount it.
|
||||
const masonryKey = useMemo(() => {
|
||||
const docIds = documents.map((d) => d.id).join(",")
|
||||
return `masonry-${documents.length}-${docIds}-${isChatOpen}-${hasQuickNote}`
|
||||
}, [documents, isChatOpen, hasQuickNote])
|
||||
const itemIds = masonryItems.map((item) => item.id).join(",")
|
||||
return `masonry-${isMobile ? "mobile" : "desktop"}-${masonryItems.length}-${itemIds}-${isChatOpen}`
|
||||
}, [masonryItems, isChatOpen, isMobile])
|
||||
|
||||
const getMasonryItemKey = useCallback((item: MasonryItem) => item.id, [])
|
||||
|
||||
const isLoadingMore = isFetchingNextPage
|
||||
|
||||
|
|
@ -542,9 +545,9 @@ export function MemoriesGrid({
|
|||
{!isEmpty && !isSelectionMode && (
|
||||
<div
|
||||
id="filter-pills"
|
||||
className="flex items-center justify-between gap-4 mb-3 pr-2"
|
||||
className="mb-3 flex flex-col gap-2 pr-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<div className="order-2 flex w-full min-w-0 flex-wrap items-center gap-1.5 sm:order-1">
|
||||
<Button
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
|
|
@ -577,7 +580,7 @@ export function MemoriesGrid({
|
|||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="order-1 flex shrink-0 items-center gap-2 self-end sm:order-2 sm:self-start">
|
||||
{/* View mode toggle — segmented control */}
|
||||
<div
|
||||
role="tablist"
|
||||
|
|
@ -784,6 +787,7 @@ export function MemoriesGrid({
|
|||
key={masonryKey}
|
||||
items={masonryItems}
|
||||
render={renderMasonryItem}
|
||||
itemKey={getMasonryItemKey}
|
||||
columnGutter={0}
|
||||
rowGutter={0}
|
||||
columnWidth={260}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
"use client"
|
||||
|
||||
import { useRef, useCallback } from "react"
|
||||
import { useRef, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { Maximize2, Plus, Loader2 } from "lucide-react"
|
||||
import { Maximize2, Plus, Loader2, X } from "lucide-react"
|
||||
import { useProject } from "@/stores"
|
||||
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
|
||||
import { TextEditor } from "./text-editor"
|
||||
|
||||
interface QuickNoteCardProps {
|
||||
onSave: (content: string) => void
|
||||
|
|
@ -13,33 +16,74 @@ interface QuickNoteCardProps {
|
|||
isSaving?: boolean
|
||||
}
|
||||
|
||||
type NoteRect = {
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function getExpandedRect(source: NoteRect): NoteRect {
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
const margin = viewportWidth < 768 ? 16 : 32
|
||||
const availableWidth = viewportWidth - source.left - margin
|
||||
const availableHeight = viewportHeight - source.top - margin
|
||||
const width = Math.min(1008, Math.max(source.width, availableWidth))
|
||||
const height = Math.min(720, Math.max(source.height, availableHeight))
|
||||
|
||||
return {
|
||||
left: source.left,
|
||||
top: source.top,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
export function QuickNoteCard({
|
||||
onSave,
|
||||
onMaximize,
|
||||
isSaving = false,
|
||||
}: QuickNoteCardProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const wasSavingRef = useRef(isSaving)
|
||||
const { selectedProject } = useProject()
|
||||
const { draft, setDraft } = useQuickNoteDraft(selectedProject)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [sourceRect, setSourceRect] = useState<NoteRect | null>(null)
|
||||
const [targetRect, setTargetRect] = useState<NoteRect | null>(null)
|
||||
const [expandedInitialContent, setExpandedInitialContent] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDraft(e.target.value)
|
||||
(content: string) => {
|
||||
setDraft(content)
|
||||
},
|
||||
[setDraft],
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
if (draft.trim() && !isSaving) {
|
||||
onSave(draft)
|
||||
}
|
||||
}
|
||||
},
|
||||
[draft, isSaving, onSave],
|
||||
)
|
||||
const handleExpand = useCallback(() => {
|
||||
const rect = cardRef.current?.getBoundingClientRect()
|
||||
if (!rect) return
|
||||
|
||||
const nextSourceRect = {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
}
|
||||
|
||||
setSourceRect(nextSourceRect)
|
||||
setTargetRect(getExpandedRect(nextSourceRect))
|
||||
setExpandedInitialContent(draft || undefined)
|
||||
setIsExpanded(true)
|
||||
}, [draft])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsExpanded(false)
|
||||
}, [])
|
||||
|
||||
const handleSaveClick = useCallback(() => {
|
||||
if (draft.trim() && !isSaving) {
|
||||
|
|
@ -48,112 +92,278 @@ export function QuickNoteCard({
|
|||
}, [draft, isSaving, onSave])
|
||||
|
||||
const handleMaximizeClick = useCallback(() => {
|
||||
setIsExpanded(false)
|
||||
onMaximize(draft)
|
||||
}, [draft, onMaximize])
|
||||
|
||||
const canSave = draft.trim().length > 0 && !isSaving
|
||||
const previewText = useMemo(() => {
|
||||
const trimmed = draft.trim()
|
||||
if (!trimmed) return null
|
||||
return trimmed.replace(/\s+/g, " ")
|
||||
}, [draft])
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded || !sourceRect) return
|
||||
|
||||
const handleResize = () => setTargetRect(getExpandedRect(sourceRect))
|
||||
window.addEventListener("resize", handleResize)
|
||||
return () => window.removeEventListener("resize", handleResize)
|
||||
}, [isExpanded, sourceRect])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded) return
|
||||
|
||||
const previousOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = "hidden"
|
||||
|
||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleGlobalKeyDown)
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow
|
||||
document.removeEventListener("keydown", handleGlobalKeyDown)
|
||||
}
|
||||
}, [isExpanded, handleClose])
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSavingRef.current && !isSaving && draft.trim().length === 0) {
|
||||
setIsExpanded(false)
|
||||
}
|
||||
wasSavingRef.current = isSaving
|
||||
}, [draft, isSaving])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-[#1B1F24] rounded-[22px] p-1"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
id="quick-note-inner"
|
||||
className="bg-[#0B1017] rounded-[18px] p-3 relative"
|
||||
ref={cardRef}
|
||||
className="bg-[#1B1F24] rounded-[22px] p-1"
|
||||
style={{
|
||||
boxShadow: "inset 1.421px 1.421px 4.263px 0 rgba(11, 15, 21, 0.4)",
|
||||
boxShadow:
|
||||
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleMaximizeClick}
|
||||
className="absolute top-3 right-3 text-[#737373] hover:text-white transition-colors cursor-pointer"
|
||||
aria-label="Expand to full screen"
|
||||
>
|
||||
<Maximize2 className="size-[14px]" />
|
||||
</button>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={draft}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Start writing..."
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"w-full h-[120px] bg-transparent resize-none outline-none text-[12px] leading-normal text-white placeholder:text-[#737373] pr-5 disabled:opacity-50",
|
||||
)}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="quick-note-action-bar"
|
||||
className="bg-[#1B1F24] rounded-[8px] px-2 py-1.5 flex items-center justify-center gap-8 w-full"
|
||||
id="quick-note-inner"
|
||||
className="bg-[#0B1017] rounded-[18px] p-3 relative"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 4px 20px 0 rgba(0, 0, 0, 0.25), inset 1px 1px 1px 0 rgba(255, 255, 255, 0.1)",
|
||||
boxShadow: "inset 1.421px 1.421px 4.263px 0 rgba(11, 15, 21, 0.4)",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveClick}
|
||||
disabled={!canSave}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
onClick={handleMaximizeClick}
|
||||
className="absolute top-3 right-3 text-[#737373] hover:text-white transition-colors cursor-pointer"
|
||||
aria-label="Open full screen note"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{isSaving ? (
|
||||
<Loader2 className="size-2 animate-spin text-[#fafafa]" />
|
||||
) : (
|
||||
<Plus className="size-2 text-[#fafafa]" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] font-medium text-[#fafafa]",
|
||||
)}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save note"}
|
||||
</span>
|
||||
</span>
|
||||
<Maximize2 className="size-[14px]" />
|
||||
</button>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[rgba(33,33,33,0.5)] border border-[rgba(115,115,115,0.2)] rounded px-1 py-0.5 flex items-center gap-1 h-4",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className="size-[10px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Command Key</title>
|
||||
<path
|
||||
d="M6.67 0.42C6.34 0.42 6.02 0.55 5.78 0.78C5.55 1.02 5.42 1.34 5.42 1.67V6.67C5.42 7 5.55 7.32 5.78 7.55C6.02 7.78 6.34 7.92 6.67 7.92C7 7.92 7.32 7.78 7.55 7.55C7.78 7.32 7.92 7 7.92 6.67C7.92 6.34 7.78 6.02 7.55 5.78C7.32 5.55 7 5.42 6.67 5.42H1.67C1.34 5.42 1.02 5.55 0.78 5.78C0.55 6.02 0.42 6.34 0.42 6.67C0.42 7 0.55 7.32 0.78 7.55C1.02 7.78 1.34 7.92 1.67 7.92C2 7.92 2.32 7.78 2.55 7.55C2.78 7.32 2.92 7 2.92 6.67V1.67C2.92 1.34 2.78 1.02 2.55 0.78C2.32 0.55 2 0.42 1.67 0.42C1.34 0.42 1.02 0.55 0.78 0.78C0.55 1.02 0.42 1.34 0.42 1.67C0.42 2 0.55 2.32 0.78 2.55C1.02 2.78 1.34 2.92 1.67 2.92H6.67C7 2.92 7.32 2.78 7.55 2.55C7.78 2.32 7.92 2 7.92 1.67C7.92 1.34 7.78 1.02 7.55 0.78C7.32 0.55 7 0.42 6.67 0.42Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExpand}
|
||||
disabled={isSaving}
|
||||
className="w-full h-[120px] cursor-text text-left disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="Expand quick note"
|
||||
>
|
||||
<span className="flex h-full flex-col pr-5">
|
||||
<span
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] font-medium text-[#737373]",
|
||||
"line-clamp-4 text-[12px] leading-normal text-[#D7DEE8]",
|
||||
!previewText && "text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Enter
|
||||
{previewText ?? "Start writing..."}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="quick-note-action-bar"
|
||||
className="bg-[#1B1F24] rounded-[8px] px-2 py-1.5 flex items-center justify-center gap-8 w-full"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 4px 20px 0 rgba(0, 0, 0, 0.25), inset 1px 1px 1px 0 rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveClick}
|
||||
disabled={!canSave}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{isSaving ? (
|
||||
<Loader2 className="size-2 animate-spin text-[#fafafa]" />
|
||||
) : (
|
||||
<Plus className="size-2 text-[#fafafa]" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] font-medium text-[#fafafa]",
|
||||
)}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save note"}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[rgba(33,33,33,0.5)] border border-[rgba(115,115,115,0.2)] rounded px-1 py-0.5 flex items-center gap-1 h-4",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className="size-[10px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Command Key</title>
|
||||
<path
|
||||
d="M6.67 0.42C6.34 0.42 6.02 0.55 5.78 0.78C5.55 1.02 5.42 1.34 5.42 1.67V6.67C5.42 7 5.55 7.32 5.78 7.55C6.02 7.78 6.34 7.92 6.67 7.92C7 7.92 7.32 7.78 7.55 7.55C7.78 7.32 7.92 7 7.92 6.67C7.92 6.34 7.78 6.02 7.55 5.78C7.32 5.55 7 5.42 6.67 5.42H1.67C1.34 5.42 1.02 5.55 0.78 5.78C0.55 6.02 0.42 6.34 0.42 6.67C0.42 7 0.55 7.32 0.78 7.55C1.02 7.78 1.34 7.92 1.67 7.92C2 7.92 2.32 7.78 2.55 7.55C2.78 7.32 2.92 7 2.92 6.67V1.67C2.92 1.34 2.78 1.02 2.55 0.78C2.32 0.55 2 0.42 1.67 0.42C1.34 0.42 1.02 0.55 0.78 0.78C0.55 1.02 0.42 1.34 0.42 1.67C0.42 2 0.55 2.32 0.78 2.55C1.02 2.78 1.34 2.92 1.67 2.92H6.67C7 2.92 7.32 2.78 7.55 2.55C7.78 2.32 7.92 2 7.92 1.67C7.92 1.34 7.78 1.02 7.55 0.78C7.32 0.55 7 0.42 6.67 0.42Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[10px] font-medium text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Enter
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMounted &&
|
||||
createPortal(
|
||||
<AnimatePresence>
|
||||
{isExpanded && sourceRect && targetRect && (
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<motion.button
|
||||
type="button"
|
||||
aria-label="Close quick note"
|
||||
className="absolute inset-0 cursor-default bg-[#05080D]/60 backdrop-blur-md"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="New quick note"
|
||||
className="absolute overflow-hidden rounded-[22px] bg-[#1B1F24] p-1"
|
||||
initial={sourceRect}
|
||||
animate={targetRect}
|
||||
exit={sourceRect}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 420,
|
||||
damping: 42,
|
||||
mass: 0.9,
|
||||
}}
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 28px 80px rgba(0, 0, 0, 0.55), 0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex size-full flex-col rounded-[18px] bg-[#0B1017]"
|
||||
style={{
|
||||
boxShadow:
|
||||
"inset 1.421px 1.421px 4.263px 0 rgba(11, 15, 21, 0.4)",
|
||||
}}
|
||||
>
|
||||
<header className="flex shrink-0 justify-end border-b border-[#202A36]/70 px-5 py-4 md:px-7">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleMaximizeClick}
|
||||
className="flex size-8 items-center justify-center rounded-full text-[#737373] transition-colors hover:bg-white/5 hover:text-white cursor-pointer"
|
||||
aria-label="Open full screen note"
|
||||
>
|
||||
<Maximize2 className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex size-8 items-center justify-center rounded-full text-[#737373] transition-colors hover:bg-white/5 hover:text-white cursor-pointer"
|
||||
aria-label="Close quick note"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto px-5 py-5 md:px-7 md:py-6">
|
||||
<TextEditor
|
||||
content={expandedInitialContent}
|
||||
onContentChange={handleChange}
|
||||
onSubmit={handleSaveClick}
|
||||
debounceMs={0}
|
||||
autoFocus
|
||||
placeholder="Start writing..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer className="flex shrink-0 justify-center border-t border-[#202A36]/70 px-4 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveClick}
|
||||
disabled={!canSave}
|
||||
className={cn(
|
||||
"bg-[#1B1F24] rounded-[8px] px-4 py-2.5 flex items-center justify-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 4px 20px 0 rgba(0, 0, 0, 0.25), inset 1px 1px 1px 0 rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="size-2 animate-spin text-[#fafafa]" />
|
||||
) : (
|
||||
<Plus className="size-2 text-[#fafafa]" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"text-[14px] font-medium text-[#fafafa]",
|
||||
)}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save note"}
|
||||
</span>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ import TaskList from "@tiptap/extension-task-list"
|
|||
import TaskItem from "@tiptap/extension-task-item"
|
||||
import { cx } from "class-variance-authority"
|
||||
|
||||
const placeholder = Placeholder.configure({
|
||||
placeholder: 'Write, paste anything or type "/" for commands...',
|
||||
})
|
||||
const DEFAULT_PLACEHOLDER = 'Write, paste anything or type "/" for commands...'
|
||||
|
||||
function createPlaceholder(placeholderText = DEFAULT_PLACEHOLDER) {
|
||||
return Placeholder.configure({
|
||||
placeholder: placeholderText,
|
||||
})
|
||||
}
|
||||
|
||||
const taskList = TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
|
|
@ -82,11 +86,15 @@ const starterKit = StarterKit.configure({
|
|||
gapcursor: false,
|
||||
})
|
||||
|
||||
export const defaultExtensions = [
|
||||
starterKit,
|
||||
placeholder,
|
||||
link,
|
||||
image,
|
||||
taskList,
|
||||
taskItem,
|
||||
]
|
||||
export function createDefaultExtensions(placeholderText?: string) {
|
||||
return [
|
||||
starterKit,
|
||||
createPlaceholder(placeholderText),
|
||||
link,
|
||||
image,
|
||||
taskList,
|
||||
taskItem,
|
||||
]
|
||||
}
|
||||
|
||||
export const defaultExtensions = createDefaultExtensions()
|
||||
|
|
|
|||
|
|
@ -4,30 +4,36 @@ import { useEditor, EditorContent } from "@tiptap/react"
|
|||
import { BubbleMenu } from "@tiptap/react/menus"
|
||||
import type { Editor } from "@tiptap/core"
|
||||
import { Markdown } from "@tiptap/markdown"
|
||||
import { useRef, useEffect, useCallback } from "react"
|
||||
import { defaultExtensions } from "./extensions"
|
||||
import { useRef, useEffect, useCallback, useMemo } from "react"
|
||||
import { createDefaultExtensions } from "./extensions"
|
||||
import { slashCommand } from "./suggestions"
|
||||
import { Bold, Italic, Code } from "lucide-react"
|
||||
import { useDebouncedCallback } from "use-debounce"
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
const extensions = [...defaultExtensions, slashCommand, Markdown]
|
||||
|
||||
export function TextEditor({
|
||||
content: initialContent,
|
||||
onContentChange,
|
||||
onSubmit,
|
||||
debounceMs = 500,
|
||||
autoFocus = false,
|
||||
placeholder,
|
||||
}: {
|
||||
content: string | undefined
|
||||
onContentChange: (content: string) => void
|
||||
onSubmit: () => void
|
||||
debounceMs?: number
|
||||
autoFocus?: boolean
|
||||
placeholder?: string
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const editorRef = useRef<Editor | null>(null)
|
||||
const onSubmitRef = useRef(onSubmit)
|
||||
const hasUserEditedRef = useRef(false)
|
||||
const extensions = useMemo(
|
||||
() => [...createDefaultExtensions(placeholder), slashCommand, Markdown],
|
||||
[placeholder],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
onSubmitRef.current = onSubmit
|
||||
|
|
@ -92,6 +98,16 @@ export function TextEditor({
|
|||
}
|
||||
}, [editor, initialContent])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !autoFocus) return
|
||||
|
||||
const id = window.setTimeout(() => {
|
||||
editor.commands.focus("end")
|
||||
}, 0)
|
||||
|
||||
return () => window.clearTimeout(id)
|
||||
}, [editor, autoFocus])
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest(".ProseMirror")) {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ function CommandMenu({
|
|||
if (!mounted) return null
|
||||
|
||||
return createPortal(
|
||||
<div ref={refs.setFloating} style={floatingStyles} className="z-50">
|
||||
<div ref={refs.setFloating} style={floatingStyles} className="z-[120]">
|
||||
<CommandList
|
||||
items={items}
|
||||
command={command}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue