chore: streamline MemoEditor components and remove unused code

This commit is contained in:
Johnny 2025-11-30 12:30:00 +08:00
parent 26cb357685
commit 7aa8262ef2
21 changed files with 147 additions and 488 deletions

View file

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite";
import OverflowTip from "@/components/kit/OverflowTip";
import { Command } from "../types/command";
import { EditorRefActions } from ".";
import type { EditorRefActions } from ".";
import type { Command } from "./commands";
import { SuggestionsPopup } from "./SuggestionsPopup";
import { useSuggestions } from "./useSuggestions";

View file

@ -1,4 +1,11 @@
import { Command } from "@/components/MemoEditor/types/command";
/**
* Command type for slash commands in the editor
*/
export interface Command {
name: string;
run: () => string;
cursorOffset?: number;
}
export const editorCommands: Command[] = [
{

View file

@ -1,7 +1,6 @@
import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react";
import { cn } from "@/lib/utils";
import { EDITOR_HEIGHT } from "../constants";
import { Command } from "../types/command";
import CommandSuggestions from "./CommandSuggestions";
import { editorCommands } from "./commands";
import TagSuggestions from "./TagSuggestions";
@ -27,8 +26,6 @@ interface Props {
className: string;
initialContent: string;
placeholder: string;
tools?: ReactNode;
commands?: Command[];
onContentChange: (content: string) => void;
onPaste: (event: React.ClipboardEvent) => void;
/** Whether Focus Mode is active - adjusts height constraints for immersive writing */

View file

@ -3,8 +3,6 @@ import type { EditorRefActions } from "./index";
/**
* Handles keyboard shortcuts for markdown formatting
* Requires Cmd/Ctrl key to be pressed
*
* @alias handleEditorKeydownWithMarkdownShortcuts - for backward compatibility
*/
export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: EditorRefActions): void {
switch (event.key.toLowerCase()) {
@ -23,9 +21,6 @@ export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: Edit
}
}
// Backward compatibility alias
export const handleEditorKeydownWithMarkdownShortcuts = handleMarkdownShortcuts;
/**
* Inserts a hyperlink for the selected text
* If selected text is a URL, creates a link with empty text

View file

@ -197,7 +197,6 @@ const InsertMenu = observer((props: Props) => {
filteredMemos={linkMemo.filteredMemos}
isFetching={linkMemo.isFetching}
onSelectMemo={linkMemo.addMemoRelation}
getHighlightedContent={linkMemo.getHighlightedContent}
/>
<LocationDialog

View file

@ -3,6 +3,34 @@ import { Input } from "@/components/ui/input";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n";
/**
* Highlights search text within content string
*/
function highlightSearchText(content: string, searchText: string): React.ReactNode {
if (!searchText) return content;
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
if (index === -1) return content;
let before = content.slice(0, index);
if (before.length > 20) {
before = "..." + before.slice(before.length - 20);
}
const highlighted = content.slice(index, index + searchText.length);
let after = content.slice(index + searchText.length);
if (after.length > 20) {
after = after.slice(0, 20) + "...";
}
return (
<>
{before}
<mark className="font-medium">{highlighted}</mark>
{after}
</>
);
}
interface LinkMemoDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -11,7 +39,6 @@ interface LinkMemoDialogProps {
filteredMemos: Memo[];
isFetching: boolean;
onSelectMemo: (memo: Memo) => void;
getHighlightedContent: (content: string) => React.ReactNode;
}
export const LinkMemoDialog = ({
@ -22,7 +49,6 @@ export const LinkMemoDialog = ({
filteredMemos,
isFetching,
onSelectMemo,
getHighlightedContent,
}: LinkMemoDialogProps) => {
const t = useTranslate();
@ -54,7 +80,7 @@ export const LinkMemoDialog = ({
<div className="w-full flex flex-col justify-start items-start">
<p className="text-xs text-muted-foreground select-none">{memo.displayTime?.toLocaleString()}</p>
<p className="mt-0.5 text-sm leading-5 line-clamp-2">
{searchText ? getHighlightedContent(memo.content) : memo.snippet}
{searchText ? highlightSearchText(memo.content, searchText) : memo.snippet}
</p>
</div>
</div>

View file

@ -1,19 +1,14 @@
// Custom hooks for MemoEditor
// Custom hooks for MemoEditor (internal use only)
export { useAbortController } from "./useAbortController";
export { useBlobUrls } from "./useBlobUrls";
export { useDebounce } from "./useDebounce";
export { useDragAndDrop } from "./useDragAndDrop";
export { useFileUpload } from "./useFileUpload";
export { useFocusMode } from "./useFocusMode";
export { useLinkMemo } from "./useLinkMemo";
export { useLocalFileManager } from "./useLocalFileManager";
export { useLocation } from "./useLocation";
export type { UseMemoEditorHandlersOptions, UseMemoEditorHandlersReturn } from "./useMemoEditorHandlers";
export { useMemoEditorHandlers } from "./useMemoEditorHandlers";
export type { UseMemoEditorInitOptions, UseMemoEditorInitReturn } from "./useMemoEditorInit";
export { useMemoEditorInit } from "./useMemoEditorInit";
export type { UseMemoEditorKeyboardOptions } from "./useMemoEditorKeyboard";
export { useMemoEditorKeyboard } from "./useMemoEditorKeyboard";
export type { UseMemoEditorStateReturn } from "./useMemoEditorState";
export { useMemoEditorState } from "./useMemoEditorState";
export { useMemoSave } from "./useMemoSave";

View file

@ -1,78 +1,23 @@
import { useEffect, useRef } from "react";
/**
* Custom hook for managing AbortController lifecycle
* Useful for canceling async operations like fetch requests
*
* @returns Object with methods to create and abort requests
*
* @example
* ```tsx
* const { getSignal, abort, abortAndCreate } = useAbortController();
*
* // Create signal for fetch
* const signal = getSignal();
* fetch(url, { signal });
*
* // Cancel on user action
* abort();
*
* // Or cancel previous and create new
* const newSignal = abortAndCreate();
* fetch(newUrl, { signal: newSignal });
* ```
* Hook for managing AbortController lifecycle
*/
export function useAbortController() {
const controllerRef = useRef<AbortController | null>(null);
// Clean up on unmount
useEffect(() => {
return () => {
controllerRef.current?.abort();
};
}, []);
useEffect(() => () => controllerRef.current?.abort(), []);
/**
* Aborts the current request if one exists
*/
const abort = (): void => {
const abort = () => {
controllerRef.current?.abort();
controllerRef.current = null;
};
/**
* Creates a new AbortController and returns its signal
* Does not abort previous controller
*/
const create = (): AbortSignal => {
const controller = new AbortController();
controllerRef.current = controller;
return controller.signal;
};
/**
* Aborts current request and creates a new AbortController
* Useful for debounced requests
*/
const abortAndCreate = (): AbortSignal => {
abort();
return create();
};
/**
* Gets the signal from the current controller, or creates new one
*/
const getSignal = (): AbortSignal => {
if (!controllerRef.current) {
return create();
}
controllerRef.current = new AbortController();
return controllerRef.current.signal;
};
return {
abort,
create,
abortAndCreate,
getSignal,
};
return { abort, abortAndCreate };
}

View file

@ -1,65 +1,31 @@
import { useEffect, useRef } from "react";
/**
* Custom hook for managing blob URLs lifecycle
* Automatically tracks and cleans up all blob URLs on unmount to prevent memory leaks
*
* @returns Object with methods to create, revoke, and manage blob URLs
*
* @example
* ```tsx
* const { createBlobUrl, revokeBlobUrl, revokeAll } = useBlobUrls();
*
* // Create blob URL (automatically tracked)
* const url = createBlobUrl(file);
*
* // Manually revoke when needed
* revokeBlobUrl(url);
*
* // All URLs are automatically revoked on unmount
* ```
* Hook for managing blob URLs lifecycle with automatic cleanup
*/
export function useBlobUrls() {
const blobUrlsRef = useRef<Set<string>>(new Set());
const urlsRef = useRef<Set<string>>(new Set());
// Clean up all blob URLs on unmount
useEffect(() => {
return () => {
blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
blobUrlsRef.current.clear();
};
}, []);
/**
* Creates a blob URL from a file or blob and tracks it for automatic cleanup
*/
const createBlobUrl = (blob: Blob | File): string => {
const url = URL.createObjectURL(blob);
blobUrlsRef.current.add(url);
return url;
};
/**
* Revokes a specific blob URL and removes it from tracking
*/
const revokeBlobUrl = (url: string): void => {
if (blobUrlsRef.current.has(url)) {
URL.revokeObjectURL(url);
blobUrlsRef.current.delete(url);
}
};
/**
* Revokes all tracked blob URLs
*/
const revokeAll = (): void => {
blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
blobUrlsRef.current.clear();
};
useEffect(
() => () => {
for (const url of urlsRef.current) {
URL.revokeObjectURL(url);
}
},
[],
);
return {
createBlobUrl,
revokeBlobUrl,
revokeAll,
createBlobUrl: (blob: Blob | File): string => {
const url = URL.createObjectURL(blob);
urlsRef.current.add(url);
return url;
},
revokeBlobUrl: (url: string) => {
if (urlsRef.current.has(url)) {
URL.revokeObjectURL(url);
urlsRef.current.delete(url);
}
},
};
}

View file

@ -1,49 +0,0 @@
import { useCallback, useEffect, useRef } from "react";
/**
* Custom hook for debouncing function calls
*
* @param callback - Function to debounce
* @param delay - Delay in milliseconds before invoking the callback
* @returns Debounced version of the callback function
*
* @example
* ```tsx
* const debouncedSearch = useDebounce((query: string) => {
* performSearch(query);
* }, 300);
*
* // Call multiple times, only last call executes after 300ms
* debouncedSearch("hello");
* ```
*/
export function useDebounce<T extends (...args: any[]) => void>(callback: T, delay: number): (...args: Parameters<T>) => void {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const callbackRef = useRef(callback);
// Keep callback ref up to date
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return useCallback(
(...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
},
[delay],
);
}

View file

@ -1,59 +1,32 @@
import { useState } from "react";
interface UseDragAndDropOptions {
onDrop: (files: FileList) => void;
}
/**
* Custom hook for handling drag-and-drop file uploads
* Manages drag state and event handlers
*
* @param options - Configuration options
* @returns Drag state and event handlers
*
* @example
* ```tsx
* const { isDragging, dragHandlers } = useDragAndDrop({
* onDrop: (files) => handleFiles(files),
* });
*
* <div {...dragHandlers} className={isDragging ? 'border-dashed' : ''}>
* Drop files here
* </div>
* ```
* Hook for handling drag-and-drop file uploads
*/
export function useDragAndDrop({ onDrop }: UseDragAndDropOptions) {
export function useDragAndDrop(onDrop: (files: FileList) => void) {
const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (event: React.DragEvent): void => {
if (event.dataTransfer && event.dataTransfer.types.includes("Files")) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
if (!isDragging) {
setIsDragging(true);
}
}
};
const handleDragLeave = (event: React.DragEvent): void => {
event.preventDefault();
setIsDragging(false);
};
const handleDrop = (event: React.DragEvent): void => {
if (event.dataTransfer && event.dataTransfer.files.length > 0) {
event.preventDefault();
setIsDragging(false);
onDrop(event.dataTransfer.files);
}
};
return {
isDragging,
dragHandlers: {
onDragOver: handleDragOver,
onDragLeave: handleDragLeave,
onDrop: handleDrop,
onDragOver: (e: React.DragEvent) => {
if (e.dataTransfer?.types.includes("Files")) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
setIsDragging(true);
}
},
onDragLeave: (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
},
onDrop: (e: React.DragEvent) => {
if (e.dataTransfer?.files.length) {
e.preventDefault();
setIsDragging(false);
onDrop(e.dataTransfer.files);
}
},
},
};
}

View file

@ -1,40 +1,13 @@
import { useCallback, useEffect } from "react";
interface UseFocusModeOptions {
isFocusMode: boolean;
onToggle: () => void;
}
interface UseFocusModeReturn {
toggleFocusMode: () => void;
}
import { useEffect } from "react";
/**
* Custom hook for managing focus mode functionality
* Handles:
* - Body scroll lock when focus mode is active
* - Toggle functionality
* - Cleanup on unmount
* Hook to lock body scroll when focus mode is active
*/
export function useFocusMode({ isFocusMode, onToggle }: UseFocusModeOptions): UseFocusModeReturn {
// Lock body scroll when focus mode is active to prevent background scrolling
export function useFocusMode(isFocusMode: boolean): void {
useEffect(() => {
if (isFocusMode) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
// Cleanup on unmount
document.body.style.overflow = isFocusMode ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [isFocusMode]);
const toggleFocusMode = useCallback(() => {
onToggle();
}, [onToggle]);
return {
toggleFocusMode,
};
}

View file

@ -59,39 +59,11 @@ export const useLinkMemo = ({ isOpen, currentMemoName, existingRelations, onAddR
onAddRelation(relation);
};
const getHighlightedContent = (content: string): React.ReactNode => {
if (!searchText) return content;
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
if (index === -1) {
return content;
}
let before = content.slice(0, index);
if (before.length > 20) {
before = "..." + before.slice(before.length - 20);
}
const highlighted = content.slice(index, index + searchText.length);
let after = content.slice(index + searchText.length);
if (after.length > 20) {
after = after.slice(0, 20) + "...";
}
return (
<>
{before}
<mark className="font-medium">{highlighted}</mark>
{after}
</>
);
};
return {
searchText,
setSearchText,
isFetching,
filteredMemos,
addMemoRelation,
getHighlightedContent,
};
};

View file

@ -2,7 +2,7 @@ import { useCallback } from "react";
import { TAB_SPACE_WIDTH } from "@/helpers/consts";
import { FOCUS_MODE_EXIT_KEY, FOCUS_MODE_TOGGLE_KEY } from "../constants";
import type { EditorRefActions } from "../Editor";
import { handleEditorKeydownWithMarkdownShortcuts } from "../Editor/markdownShortcuts";
import { handleMarkdownShortcuts } from "../Editor/markdownShortcuts";
export interface UseMemoEditorKeyboardOptions {
editorRef: React.RefObject<EditorRefActions>;
@ -48,7 +48,7 @@ export const useMemoEditorKeyboard = (options: UseMemoEditorKeyboardOptions) =>
onSave();
return;
}
handleEditorKeydownWithMarkdownShortcuts(event, editorRef.current);
handleMarkdownShortcuts(event, editorRef.current);
}
// Tab handling

View file

@ -1,11 +1,9 @@
import { useCallback, useState } from "react";
import { useState } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service";
import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_service";
import { Visibility } from "@/types/proto/api/v1/memo_service";
import type { MemoEditorState } from "../types/memo-editor";
export interface UseMemoEditorStateReturn {
state: MemoEditorState;
interface MemoEditorState {
memoVisibility: Visibility;
attachmentList: Attachment[];
relationList: MemoRelation[];
@ -15,25 +13,12 @@ export interface UseMemoEditorStateReturn {
isRequesting: boolean;
isComposing: boolean;
isDraggingFile: boolean;
setMemoVisibility: (visibility: Visibility) => void;
setAttachmentList: (attachments: Attachment[]) => void;
setRelationList: (relations: MemoRelation[]) => void;
setLocation: (location: Location | undefined) => void;
setIsFocusMode: (isFocusMode: boolean) => void;
toggleFocusMode: () => void;
setUploadingAttachment: (isUploading: boolean) => void;
setRequesting: (isRequesting: boolean) => void;
setComposing: (isComposing: boolean) => void;
setDraggingFile: (isDragging: boolean) => void;
resetState: () => void;
}
/**
* Hook for managing MemoEditor state
* Centralizes all state management and provides clean setters
*/
export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PRIVATE): UseMemoEditorStateReturn => {
export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PRIVATE) => {
const [state, setState] = useState<MemoEditorState>({
memoVisibility: initialVisibility,
isFocusMode: false,
@ -46,79 +31,29 @@ export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PR
isDraggingFile: false,
});
const setMemoVisibility = useCallback((visibility: Visibility) => {
setState((prev) => ({ ...prev, memoVisibility: visibility }));
}, []);
const setAttachmentList = useCallback((attachments: Attachment[]) => {
setState((prev) => ({ ...prev, attachmentList: attachments }));
}, []);
const setRelationList = useCallback((relations: MemoRelation[]) => {
setState((prev) => ({ ...prev, relationList: relations }));
}, []);
const setLocation = useCallback((location: Location | undefined) => {
setState((prev) => ({ ...prev, location }));
}, []);
const setIsFocusMode = useCallback((isFocusMode: boolean) => {
setState((prev) => ({ ...prev, isFocusMode }));
}, []);
const toggleFocusMode = useCallback(() => {
setState((prev) => ({ ...prev, isFocusMode: !prev.isFocusMode }));
}, []);
const setUploadingAttachment = useCallback((isUploading: boolean) => {
setState((prev) => ({ ...prev, isUploadingAttachment: isUploading }));
}, []);
const setRequesting = useCallback((isRequesting: boolean) => {
setState((prev) => ({ ...prev, isRequesting }));
}, []);
const setComposing = useCallback((isComposing: boolean) => {
setState((prev) => ({ ...prev, isComposing }));
}, []);
const setDraggingFile = useCallback((isDragging: boolean) => {
setState((prev) => ({ ...prev, isDraggingFile: isDragging }));
}, []);
const resetState = useCallback(() => {
setState((prev) => ({
...prev,
isRequesting: false,
attachmentList: [],
relationList: [],
location: undefined,
isDraggingFile: false,
}));
}, []);
const update = <K extends keyof MemoEditorState>(key: K, value: MemoEditorState[K]) => {
setState((prev) => ({ ...prev, [key]: value }));
};
return {
state,
memoVisibility: state.memoVisibility,
attachmentList: state.attachmentList,
relationList: state.relationList,
location: state.location,
isFocusMode: state.isFocusMode,
isUploadingAttachment: state.isUploadingAttachment,
isRequesting: state.isRequesting,
isComposing: state.isComposing,
isDraggingFile: state.isDraggingFile,
setMemoVisibility,
setAttachmentList,
setRelationList,
setLocation,
setIsFocusMode,
toggleFocusMode,
setUploadingAttachment,
setRequesting,
setComposing,
setDraggingFile,
resetState,
...state,
setMemoVisibility: (v: Visibility) => update("memoVisibility", v),
setAttachmentList: (v: Attachment[]) => update("attachmentList", v),
setRelationList: (v: MemoRelation[]) => update("relationList", v),
setLocation: (v: Location | undefined) => update("location", v),
toggleFocusMode: () => setState((prev) => ({ ...prev, isFocusMode: !prev.isFocusMode })),
setUploadingAttachment: (v: boolean) => update("isUploadingAttachment", v),
setRequesting: (v: boolean) => update("isRequesting", v),
setComposing: (v: boolean) => update("isComposing", v),
setDraggingFile: (v: boolean) => update("isDraggingFile", v),
resetState: () =>
setState((prev) => ({
...prev,
isRequesting: false,
attachmentList: [],
relationList: [],
location: undefined,
isDraggingFile: false,
})),
};
};

View file

@ -18,7 +18,6 @@ import { ErrorBoundary, FocusModeExitButton, FocusModeOverlay } from "./componen
import { FOCUS_MODE_STYLES, LOCALSTORAGE_DEBOUNCE_DELAY } from "./constants";
import Editor, { type EditorRefActions } from "./Editor";
import {
useDebounce,
useDragAndDrop,
useFocusMode,
useLocalFileManager,
@ -31,12 +30,19 @@ import {
import InsertMenu from "./Toolbar/InsertMenu";
import VisibilitySelector from "./Toolbar/VisibilitySelector";
import { MemoEditorContext } from "./types";
import type { MemoEditorProps } from "./types/memo-editor";
// Re-export for backward compatibility
export type { MemoEditorProps as Props };
export interface Props {
className?: string;
cacheKey?: string;
placeholder?: string;
memoName?: string;
parentMemoName?: string;
autoFocus?: boolean;
onConfirm?: (memoName: string) => void;
onCancel?: () => void;
}
const MemoEditor = observer((props: MemoEditorProps) => {
const MemoEditor = observer((props: Props) => {
const { className, cacheKey, memoName, parentMemoName, autoFocus, onConfirm, onCancel } = props;
const t = useTranslate();
const { i18n } = useTranslation();
@ -160,29 +166,31 @@ const MemoEditor = observer((props: MemoEditorProps) => {
});
// Focus mode management with body scroll lock
useFocusMode({
isFocusMode,
onToggle: toggleFocusMode,
});
useFocusMode(isFocusMode);
// Drag-and-drop for file uploads
const { isDragging, dragHandlers } = useDragAndDrop({
onDrop: addFiles,
});
const { isDragging, dragHandlers } = useDragAndDrop(addFiles);
// Sync drag state with component state
useEffect(() => {
setDraggingFile(isDragging);
}, [isDragging, setDraggingFile]);
// Debounced cache setter to avoid writing to localStorage on every keystroke
const saveContentToCache = useDebounce((content: string) => {
if (content !== "") {
setContentCache(content);
} else {
localStorage.removeItem(contentCacheKey);
}
}, LOCALSTORAGE_DEBOUNCE_DELAY);
// Debounced cache setter
const cacheTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const saveContentToCache = useCallback(
(content: string) => {
clearTimeout(cacheTimeoutRef.current);
cacheTimeoutRef.current = setTimeout(() => {
if (content !== "") {
setContentCache(content);
} else {
localStorage.removeItem(contentCacheKey);
}
}, LOCALSTORAGE_DEBOUNCE_DELAY);
},
[contentCacheKey, setContentCache],
);
// Compute reference relations
const referenceRelations = useMemo(() => {

View file

@ -1,5 +0,0 @@
export type Command = {
name: string;
run: () => string;
cursorOffset?: number;
};

View file

@ -1,5 +1,3 @@
// MemoEditor type exports
export type { Command } from "./command";
export { MemoEditorContext, type MemoEditorContextValue } from "./context";
export type { LinkMemoState, LocationState } from "./insert-menu";
export type { EditorConfig, MemoEditorProps, MemoEditorState } from "./memo-editor";
export type { LocationState } from "./insert-menu";

View file

@ -1,5 +1,4 @@
import { LatLng } from "leaflet";
import { Memo } from "@/types/proto/api/v1/memo_service";
export interface LocationState {
placeholder: string;
@ -7,9 +6,3 @@ export interface LocationState {
latInput: string;
lngInput: string;
}
export interface LinkMemoState {
searchText: string;
isFetching: boolean;
fetchedMemos: Memo[];
}

View file

@ -1,63 +0,0 @@
import type { Attachment } from "@/types/proto/api/v1/attachment_service";
import type { Location, MemoRelation, Visibility } from "@/types/proto/api/v1/memo_service";
/**
* Props for the MemoEditor component
*/
export interface MemoEditorProps {
/** Optional CSS class name */
className?: string;
/** Cache key for localStorage persistence */
cacheKey?: string;
/** Placeholder text for empty editor */
placeholder?: string;
/** Name of the memo being edited (for edit mode) */
memoName?: string;
/** Name of parent memo (for comment/reply mode) */
parentMemoName?: string;
/** Whether to auto-focus the editor on mount */
autoFocus?: boolean;
/** Callback when memo is saved successfully */
onConfirm?: (memoName: string) => void;
/** Callback when editing is canceled */
onCancel?: () => void;
}
/**
* Internal state for MemoEditor component
*/
export interface MemoEditorState {
/** Visibility level of the memo */
memoVisibility: Visibility;
/** List of attachments */
attachmentList: Attachment[];
/** List of related memos */
relationList: MemoRelation[];
/** Geographic location */
location: Location | undefined;
/** Whether attachments are currently being uploaded */
isUploadingAttachment: boolean;
/** Whether save/update request is in progress */
isRequesting: boolean;
/** Whether IME composition is active (for Asian languages) */
isComposing: boolean;
/** Whether files are being dragged over the editor */
isDraggingFile: boolean;
/** Whether Focus Mode is enabled */
isFocusMode: boolean;
}
/**
* Configuration for the Editor sub-component
*/
export interface EditorConfig {
className: string;
initialContent: string;
placeholder: string;
onContentChange: (content: string) => void;
onPaste: (event: React.ClipboardEvent) => void;
isFocusMode: boolean;
isInIME: boolean;
onCompositionStart: () => void;
onCompositionEnd: () => void;
}

View file

@ -1,9 +1,3 @@
// UNKNOWN_ID is the symbol for unknown id.
export const UNKNOWN_ID = -1;
// DAILY_TIMESTAMP is the timestamp for a day.
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
// TAB_SPACE_WIDTH is the default tab space width.
export const TAB_SPACE_WIDTH = 2;