mirror of https://github.com/usememos/memos.git
chore(MemoEditor): enhance focus mode handling and improve editor layout
This commit is contained in:
parent
595daaa163
commit
4109fe3245
|
|
@ -152,15 +152,17 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col justify-start items-start relative w-full h-auto bg-inherit",
|
||||
isFocusMode ? "flex-1" : EDITOR_HEIGHT.normal,
|
||||
"flex flex-col justify-start items-start relative w-full bg-inherit",
|
||||
// Focus mode: flex-1 to grow and fill space; Normal: h-auto with max-height
|
||||
isFocusMode ? "flex-1" : `h-auto ${EDITOR_HEIGHT.normal}`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full my-1 text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent outline-none placeholder:opacity-70 whitespace-pre-wrap break-words",
|
||||
isFocusMode ? `h-auto ${EDITOR_HEIGHT.focusMode.mobile} ${EDITOR_HEIGHT.focusMode.desktop}` : "h-full",
|
||||
// Focus mode: flex-1 h-0 to grow within flex container; Normal: h-full to fill wrapper
|
||||
isFocusMode ? "flex-1 h-0" : "h-full",
|
||||
)}
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
|
|
|
|||
|
|
@ -29,15 +29,25 @@ export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({
|
|||
dispatch(actions.setComposing(false));
|
||||
};
|
||||
|
||||
const handleContentChange = (content: string) => {
|
||||
dispatch(actions.updateContent(content));
|
||||
};
|
||||
|
||||
const handlePaste = () => {
|
||||
// Paste handling is managed by the Editor component internally
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...dragHandlers}>
|
||||
<div className="w-full flex flex-col flex-1" {...dragHandlers}>
|
||||
<Editor
|
||||
ref={ref}
|
||||
className="memo-editor-content"
|
||||
initialContent={state.content}
|
||||
placeholder={placeholder || ""}
|
||||
onContentChange={actions.updateContent}
|
||||
onPaste={() => {}}
|
||||
isFocusMode={state.ui.isFocusMode}
|
||||
isInIME={state.ui.isComposing}
|
||||
onContentChange={handleContentChange}
|
||||
onPaste={handlePaste}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,24 +11,36 @@ interface EditorToolbarProps {
|
|||
}
|
||||
|
||||
export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel }) => {
|
||||
const { state, actions } = useEditorContext();
|
||||
const { state, actions, dispatch } = useEditorContext();
|
||||
const { valid } = validationService.canSave(state);
|
||||
|
||||
const isSaving = state.ui.isLoading.saving;
|
||||
|
||||
const handleLocationChange = (location: typeof state.metadata.location) => {
|
||||
dispatch(actions.setMetadata({ location }));
|
||||
};
|
||||
|
||||
const handleToggleFocusMode = () => {
|
||||
dispatch(actions.toggleFocusMode());
|
||||
};
|
||||
|
||||
const handleVisibilityChange = (visibility: typeof state.metadata.visibility) => {
|
||||
dispatch(actions.setMetadata({ visibility }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-between items-center mb-2">
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
<InsertMenu
|
||||
isUploading={state.ui.isLoading.uploading}
|
||||
location={state.metadata.location}
|
||||
onLocationChange={(location) => actions.setMetadata({ location })}
|
||||
onToggleFocusMode={actions.toggleFocusMode}
|
||||
onLocationChange={handleLocationChange}
|
||||
onToggleFocusMode={handleToggleFocusMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end items-center gap-2">
|
||||
<VisibilitySelector value={state.metadata.visibility} onChange={(v) => actions.setMetadata({ visibility: v })} />
|
||||
<VisibilitySelector value={state.metadata.visibility} onChange={handleVisibilityChange} />
|
||||
|
||||
{onCancel && (
|
||||
<Button variant="ghost" onClick={onCancel} disabled={isSaving}>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,8 @@ export const FOCUS_MODE_TOGGLE_KEY = "f";
|
|||
export const FOCUS_MODE_EXIT_KEY = "Escape";
|
||||
|
||||
export const EDITOR_HEIGHT = {
|
||||
// Max height for normal mode - focus mode uses flex-1 to grow dynamically
|
||||
normal: "max-h-[50vh]",
|
||||
focusMode: {
|
||||
mobile: "min-h-[50vh]",
|
||||
desktop: "md:min-h-[60vh]",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const GEOCODING = {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { cacheService, errorService, memoService, validationService } from "./se
|
|||
import { EditorProvider, useEditorContext } from "./state";
|
||||
import { MemoEditorContext } from "./types";
|
||||
|
||||
export interface Props {
|
||||
export interface MemoEditorProps {
|
||||
className?: string;
|
||||
cacheKey?: string;
|
||||
placeholder?: string;
|
||||
|
|
@ -23,7 +23,7 @@ export interface Props {
|
|||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const MemoEditor = observer((props: Props) => {
|
||||
const MemoEditor = observer((props: MemoEditorProps) => {
|
||||
const { className, cacheKey, memoName, parentMemoName, autoFocus, placeholder, onConfirm, onCancel } = props;
|
||||
|
||||
return (
|
||||
|
|
@ -42,7 +42,7 @@ const MemoEditor = observer((props: Props) => {
|
|||
);
|
||||
});
|
||||
|
||||
const MemoEditorImpl: React.FC<Props> = ({
|
||||
const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
||||
className,
|
||||
cacheKey,
|
||||
memoName,
|
||||
|
|
@ -83,8 +83,12 @@ const MemoEditorImpl: React.FC<Props> = ({
|
|||
// Focus mode management with body scroll lock
|
||||
useFocusMode(state.ui.isFocusMode);
|
||||
|
||||
const handleToggleFocusMode = () => {
|
||||
dispatch(actions.toggleFocusMode());
|
||||
};
|
||||
|
||||
// Keyboard shortcuts
|
||||
useKeyboard(editorRef, { onSave: handleSave, onToggleFocusMode: () => dispatch(actions.toggleFocusMode()) });
|
||||
useKeyboard(editorRef, { onSave: handleSave, onToggleFocusMode: handleToggleFocusMode });
|
||||
|
||||
async function handleSave() {
|
||||
const { valid, reason } = validationService.canSave(state);
|
||||
|
|
@ -93,7 +97,7 @@ const MemoEditorImpl: React.FC<Props> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
actions.setLoading("saving", true);
|
||||
dispatch(actions.setLoading("saving", true));
|
||||
|
||||
try {
|
||||
const result = await memoService.save(state, { memoName, parentMemoName });
|
||||
|
|
@ -108,7 +112,7 @@ const MemoEditorImpl: React.FC<Props> = ({
|
|||
cacheService.clear(cacheService.key(currentUser.name, cacheKey));
|
||||
|
||||
// Reset editor state
|
||||
actions.reset();
|
||||
dispatch(actions.reset());
|
||||
|
||||
// Notify parent
|
||||
onConfirm?.(result.memoName);
|
||||
|
|
@ -118,28 +122,39 @@ const MemoEditorImpl: React.FC<Props> = ({
|
|||
const message = errorService.handle(error, t);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
actions.setLoading("saving", false);
|
||||
dispatch(actions.setLoading("saving", false));
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFocusMode = () => dispatch(actions.toggleFocusMode());
|
||||
|
||||
return (
|
||||
<MemoEditorContext.Provider value={legacyContextValue}>
|
||||
<FocusModeOverlay isActive={state.ui.isFocusMode} onToggle={toggleFocusMode} />
|
||||
<FocusModeOverlay isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} />
|
||||
|
||||
{/*
|
||||
Layout structure:
|
||||
- Uses justify-between to push content to top and bottom
|
||||
- In focus mode: becomes fixed with specific spacing, editor grows to fill space
|
||||
- In normal mode: stays relative with max-height constraint
|
||||
*/}
|
||||
<div
|
||||
className={cn(
|
||||
"group relative w-full flex flex-col justify-start items-start bg-card px-4 pt-3 pb-2 rounded-lg border border-border",
|
||||
"group relative w-full flex flex-col justify-between items-start bg-card px-4 pt-3 pb-1 rounded-lg border border-border",
|
||||
FOCUS_MODE_STYLES.transition,
|
||||
state.ui.isFocusMode && cn(FOCUS_MODE_STYLES.container.base, FOCUS_MODE_STYLES.container.spacing),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FocusModeExitButton isActive={state.ui.isFocusMode} onToggle={toggleFocusMode} title={t("editor.exit-focus-mode")} />
|
||||
{/* Exit button is absolutely positioned in top-right corner when active */}
|
||||
<FocusModeExitButton isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} title={t("editor.exit-focus-mode")} />
|
||||
|
||||
{/* Editor content grows to fill available space in focus mode */}
|
||||
<EditorContent ref={editorRef} placeholder={placeholder} autoFocus={autoFocus} />
|
||||
<EditorMetadata />
|
||||
<EditorToolbar onSave={handleSave} onCancel={onCancel} />
|
||||
|
||||
{/* Metadata and toolbar grouped together at bottom */}
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<EditorMetadata />
|
||||
<EditorToolbar onSave={handleSave} onCancel={onCancel} />
|
||||
</div>
|
||||
</div>
|
||||
</MemoEditorContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ const PagedMemoList = observer((props: Props) => {
|
|||
renderer={props.renderer}
|
||||
prefixElement={
|
||||
<>
|
||||
{showMemoEditor ? <MemoEditor className="mb-2" cacheKey="home-memo-editor" /> : undefined}
|
||||
{showMemoEditor ? (
|
||||
<MemoEditor className="mb-2" cacheKey="home-memo-editor" placeholder={t("editor.any-thoughts")} />
|
||||
) : undefined}
|
||||
<MemoFilters />
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const AttachmentList = ({ attachments, mode, onAttachmentsChange, localFiles = [
|
|||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2 max-h-[50vh] overflow-y-auto">
|
||||
<div className="w-full flex flex-row justify-start flex-wrap gap-2 max-h-[50vh] overflow-y-auto">
|
||||
{items.map((item) => (
|
||||
<div key={item.id}>
|
||||
{/* Uploaded items are wrapped in SortableItem for drag-and-drop */}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const RelationList = observer(({ relations, currentMemoName, mode, onRelationsCh
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row gap-2 mt-2 flex-wrap">
|
||||
<div className="w-full flex flex-row gap-2 flex-wrap">
|
||||
{referencingMemos.map((memo) => (
|
||||
<RelationCard
|
||||
key={memo.name}
|
||||
|
|
|
|||
Loading…
Reference in New Issue