Files
david_bai 27375c1a4d refactor(theme): use design tokens and fix dark mode visuals
- Replace hardcoded Tailwind colors (bg-white, bg-gray-50/100, text-gray-, border-gray-, divide-gray-*, text-blue-600/800, bg-blue-50) with design tokens (bg-card, bg-muted, text-foreground, text-muted-foreground, border-
    border, text-primary, hover:bg-accent, bg-primary/10).
  - ClipboardApp: update RichTextEditor toolbar/editor, FileUploadHandler, ShareCard, FileListDisplay, SendTabPanel, RetrieveTabPanel, FileTransferButton.
  - Blog UI: unify styles in list page, tag page, post page, ArticleListItem, and TableOfContents.
  - MDX/prose: normalize pre/code/table/blockquote/lists and figure captions; switch rehype table divider to theme token.
  - Misc: adjust HomeClient and HowItWorks copy colors to tokens.
  - No functional changes; light mode parity; improved contrast and consistency in dark mode.
2025-11-25 21:52:45 +08:00

176 lines
5.5 KiB
TypeScript

import React, { useState, useRef, useCallback, useEffect } from "react";
import { EditorProps, CustomClipboardEvent, DOMNodeWithStyle } from "./types";
import { fontFamilies, fontSizes, colors } from "./constants";
import { useEditorCommands } from "./hooks/useEditorCommands";
import { useSelection } from "./hooks/useSelection";
import { useStyleManagement } from "./hooks/useStyleManagement";
import { BasicFormatTools } from "./EditorToolbar/BasicFormatTools";
import { FontTools } from "./EditorToolbar/FontTools";
import { AlignmentTools } from "./EditorToolbar/AlignmentTools";
import { InsertTools } from "./EditorToolbar/InsertTools";
import { Divider } from "./Divider";
const RichTextEditor: React.FC<EditorProps> = ({ onChange, value = "" }) => {
const editorRef = useRef<HTMLDivElement>(null);
const [html, setHtml] = useState(value);
const [isMounted, setIsMounted] = useState(false);
const isInternalChange = useRef(false);
useEffect(() => {
setIsMounted(true);
}, []);
// Update editor content after mounting, listen for external value changes
useEffect(() => {
if (isMounted && editorRef.current && !isInternalChange.current) {
// Only update when the content is truly different
if (editorRef.current.innerHTML !== value) {
editorRef.current.innerHTML = value;
setHtml(value);
}
}
isInternalChange.current = false;
}, [value, isMounted]);
// Handle content change
const handleChange = useCallback(() => {
if (editorRef.current) {
const content = (editorRef.current as HTMLDivElement).innerHTML;
if (content !== html) {
// If the content has not changed, do not trigger an update
isInternalChange.current = true;
setHtml(content);
onChange(content);
}
}
}, [html, onChange]);
const {
formatText,
alignText,
setFontStyle,
insertLink,
insertImage,
insertCodeBlock,
} = useEditorCommands(editorRef, handleChange);
const getSelection = useSelection();
const { findStyleParent } = useStyleManagement(editorRef);
// Check the style of the currently selected text
const isStyleActive = useCallback(
(style: string): boolean => {
if (typeof window === "undefined") return false;
const selectionInfo = getSelection();
if (!selectionInfo || !selectionInfo.selection.toString()) return false;
const node = selectionInfo.selection.anchorNode;
if (!node) return false;
const styleParent = findStyleParent(node as DOMNodeWithStyle, style);
return !!styleParent;
},
[findStyleParent, getSelection]
);
const handlePaste = useCallback(
(e: CustomClipboardEvent) => {
// Handle image pasting
if (
Array.from(e.clipboardData.items).some(
(item) => item.type.indexOf("image") !== -1
)
) {
const items = Array.from(e.clipboardData.items);
const imageItem = items.find(
(item) => item.type.indexOf("image") !== -1
);
if (imageItem) {
e.preventDefault();
const blob = imageItem.getAsFile();
if (!blob) return;
const reader = new FileReader();
reader.onload = (event: ProgressEvent<FileReader>) => {
if (!event.target || !event.target.result) return;
const img = document.createElement("img");
img.src = event.target.result as string;
img.style.maxWidth = "100%";
img.style.height = "auto";
img.style.margin = "10px 0";
const selectionInfo = getSelection();
if (!selectionInfo) return;
const { range } = selectionInfo;
range.deleteContents();
range.insertNode(img);
handleChange();
};
reader.readAsDataURL(blob);
}
return;
}
// Handle plain text
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
if (typeof document !== "undefined") {
document.execCommand("insertText", false, text);
}
},
[getSelection, handleChange]
);
if (!isMounted) {
return <div>Loading...</div>;
}
return (
<div className="w-full space-x-2 mb-4">
<div className="border rounded-lg shadow-sm overflow-hidden">
{/* Toolbar - Add light gray background and bottom border */}
<div className="flex flex-wrap gap-1 p-2 bg-muted border-b border-border">
{/* Basic format tool group */}
<BasicFormatTools
isStyleActive={isStyleActive}
formatText={formatText}
/>
<Divider />
{/* Font-related selector group */}
<FontTools
fontFamilies={fontFamilies}
fontSizes={fontSizes}
colors={colors}
setFontStyle={setFontStyle}
/>
<Divider />
{/* Alignment tool group */}
<AlignmentTools alignText={alignText} />
<Divider />
{/* Insert tool group */}
<InsertTools
insertLink={insertLink}
insertImage={insertImage}
insertCodeBlock={insertCodeBlock}
/>
</div>
{/* Editor area - use theme tokens for background */}
<div
ref={editorRef}
className="p-4 min-h-[200px] md:min-h-[400px] focus:outline-none bg-card shadow-inner"
contentEditable
onPaste={handlePaste}
onInput={handleChange}
suppressContentEditableWarning
/>
</div>
</div>
);
};
export default RichTextEditor;