chore(MemoEditor): enhance focus mode handling and improve editor layout

pull/5340/head^2
Johnny 1 week ago
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…
Cancel
Save