import type { Editor } from '@tiptap/react' import { useEditorState } from '@tiptap/react' import { useEffect, useRef, useState } from 'react' import { uploadImageInto } from '../Editor/EditorCore' interface Props { editor: Editor | null // Runs the whole-document voice-consistency pass; `voicing` shows its progress. onVoiceCheck: () => void voicing: boolean } // A formatting button. `active` gets the rose pill treatment so the writer can // see what's applied at the cursor. function TBtn({ onClick, active, disabled, label, title, children, }: { onClick: () => void active?: boolean disabled?: boolean label: string title?: string children: React.ReactNode }) { return ( ) } const Divider = () => ( ) // A popover anchored under its trigger. The trigger + panel share a relative // wrapper; `open`/`onClose` are owned by the toolbar so only one is open at once. // A pointer-down outside the wrapper closes it. function Popover({ open, onClose, trigger, children, width = 200, }: { open: boolean onClose: () => void trigger: React.ReactNode children: React.ReactNode width?: number }) { const ref = useRef(null) useEffect(() => { if (!open) return const onDown = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) onClose() } document.addEventListener('mousedown', onDown) return () => document.removeEventListener('mousedown', onDown) }, [open, onClose]) return (
{trigger} {open && (
{children}
)}
) } // Text-color swatches. `null` clears the color back to the theme default. const TEXT_COLORS: { label: string; value: string | null }[] = [ { label: 'Default', value: null }, { label: 'Rose', value: '#D98AAF' }, { label: 'Coral', value: '#E0785C' }, { label: 'Honey', value: '#C68B2E' }, { label: 'Green', value: '#4F9E7F' }, { label: 'Sky', value: '#4E8FCC' }, { label: 'Lavender', value: '#7D63C4' }, { label: 'Plum', value: '#3D2E39' }, ] // Highlighter colors — soft pastels so dark text stays readable on top. const HIGHLIGHTS: { label: string; value: string | null }[] = [ { label: 'None', value: null }, { label: 'Yellow', value: '#FFF1A8' }, { label: 'Pink', value: '#FAD4E4' }, { label: 'Mint', value: '#CDEFE2' }, { label: 'Peach', value: '#FBE0CF' }, { label: 'Lavender', value: '#E6DCFA' }, { label: 'Sky', value: '#D6E8FB' }, ] // Font-size presets. `null` clears back to the document default. const SIZES: { label: string; value: string | null; em: string }[] = [ { label: 'Small', value: '0.85em', em: '0.85em' }, { label: 'Normal', value: null, em: '1em' }, { label: 'Large', value: '1.3em', em: '1.3em' }, { label: 'Title', value: '1.7em', em: '1.7em' }, ] // A round color chip used in the color/highlight palettes. function Swatch({ color, active, onClick, title, }: { color: string | null active: boolean onClick: () => void title: string }) { return ( ) })} editor.chain().focus().toggleHeading({ level: 1 }).run()}> H1 editor.chain().focus().toggleHeading({ level: 2 }).run()}> H2 editor.chain().focus().toggleHeading({ level: 3 }).run()}> H3 editor.chain().focus().toggleBulletList().run()}> • editor.chain().focus().toggleOrderedList().run()}> 1. editor.chain().focus().setTextAlign('left').run()}> ⇤ editor.chain().focus().setTextAlign('center').run()}> ↔ editor.chain().focus().setTextAlign('right').run()}> ⇥ {/* Link */} 🔗 } >
setLinkUrl(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') applyLink() if (e.key === 'Escape') close() }} placeholder="https://…" className="w-full px-2 py-1.5 text-sm focus:outline-none" style={{ borderRadius: 'var(--radius-input)', border: '1px solid var(--color-border)', color: 'var(--color-plum)', }} />
{state.link ? ( ) : ( )}
{/* Image */} fileInputRef.current?.click()}> 🖼️ {/* Table */} setMenu(menu === 'table' ? null : 'table')}> ▦ } > {state.inTable ? (
editor.chain().focus().addRowAfter().run()} /> editor.chain().focus().addColumnAfter().run()} /> editor.chain().focus().toggleHeaderRow().run()} /> editor.chain().focus().deleteRow().run()} /> editor.chain().focus().deleteColumn().run()} /> { editor.chain().focus().deleteTable().run() close() }} />
) : ( { editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run() close() }} /> )}
{/* Outline / document map */} setMenu(menu === 'outline' ? null : 'outline')}> ☰ } >

大纲 · Outline

{headings.length === 0 ? (

用 H1/H2/H3 添加标题,这里就会出现导航。
Add headings to navigate them here.

) : (
{headings.map((h, i) => ( ))}
)}
) } // A row in the in-table editing menu. function TableAction({ label, onClick, danger }: { label: string; onClick: () => void; danger?: boolean }) { return ( ) } // A hover-to-size grid for inserting a table. Hovering a cell highlights the // rectangle from the top-left; clicking inserts that many rows × columns. function GridPicker({ onPick }: { onPick: (rows: number, cols: number) => void }) { const MAX = 6 const [hover, setHover] = useState<{ r: number; c: number }>({ r: 0, c: 0 }) return (
setHover({ r: 0, c: 0 })} > {Array.from({ length: MAX * MAX }).map((_, i) => { const r = Math.floor(i / MAX) + 1 const c = (i % MAX) + 1 const on = r <= hover.r && c <= hover.c return (
{hover.r > 0 ? `${hover.r} × ${hover.c}` : 'Pick a size'}
) }