chore(MemoEditor): enhance focus mode handling and improve editor layout
This commit is contained in:
parent
595daaa163
commit
4109fe3245
8 changed files with 69 additions and 31 deletions
|
|
@ -152,15 +152,17 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col justify-start items-start relative w-full h-auto bg-inherit",
|
"flex flex-col justify-start items-start relative w-full bg-inherit",
|
||||||
isFocusMode ? "flex-1" : EDITOR_HEIGHT.normal,
|
// Focus mode: flex-1 to grow and fill space; Normal: h-auto with max-height
|
||||||
|
isFocusMode ? "flex-1" : `h-auto ${EDITOR_HEIGHT.normal}`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
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",
|
"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}
|
rows={1}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,25 @@ export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({
|
||||||
dispatch(actions.setComposing(false));
|
dispatch(actions.setComposing(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContentChange = (content: string) => {
|
||||||
|
dispatch(actions.updateContent(content));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = () => {
|
||||||
|
// Paste handling is managed by the Editor component internally
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...dragHandlers}>
|
<div className="w-full flex flex-col flex-1" {...dragHandlers}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="memo-editor-content"
|
className="memo-editor-content"
|
||||||
initialContent={state.content}
|
initialContent={state.content}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
onContentChange={actions.updateContent}
|
isFocusMode={state.ui.isFocusMode}
|
||||||
onPaste={() => {}}
|
isInIME={state.ui.isComposing}
|
||||||
|
onContentChange={handleContentChange}
|
||||||
|
onPaste={handlePaste}
|
||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -11,24 +11,36 @@ interface EditorToolbarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel }) => {
|
export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel }) => {
|
||||||
const { state, actions } = useEditorContext();
|
const { state, actions, dispatch } = useEditorContext();
|
||||||
const { valid } = validationService.canSave(state);
|
const { valid } = validationService.canSave(state);
|
||||||
|
|
||||||
const isSaving = state.ui.isLoading.saving;
|
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 (
|
return (
|
||||||
<div className="w-full flex flex-row justify-between items-center mb-2">
|
<div className="w-full flex flex-row justify-between items-center mb-2">
|
||||||
<div className="flex flex-row justify-start items-center">
|
<div className="flex flex-row justify-start items-center">
|
||||||
<InsertMenu
|
<InsertMenu
|
||||||
isUploading={state.ui.isLoading.uploading}
|
isUploading={state.ui.isLoading.uploading}
|
||||||
location={state.metadata.location}
|
location={state.metadata.location}
|
||||||
onLocationChange={(location) => actions.setMetadata({ location })}
|
onLocationChange={handleLocationChange}
|
||||||
onToggleFocusMode={actions.toggleFocusMode}
|
onToggleFocusMode={handleToggleFocusMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end items-center gap-2">
|
<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 && (
|
{onCancel && (
|
||||||
<Button variant="ghost" onClick={onCancel} disabled={isSaving}>
|
<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 FOCUS_MODE_EXIT_KEY = "Escape";
|
||||||
|
|
||||||
export const EDITOR_HEIGHT = {
|
export const EDITOR_HEIGHT = {
|
||||||
|
// Max height for normal mode - focus mode uses flex-1 to grow dynamically
|
||||||
normal: "max-h-[50vh]",
|
normal: "max-h-[50vh]",
|
||||||
focusMode: {
|
|
||||||
mobile: "min-h-[50vh]",
|
|
||||||
desktop: "md:min-h-[60vh]",
|
|
||||||
},
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const GEOCODING = {
|
export const GEOCODING = {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { cacheService, errorService, memoService, validationService } from "./se
|
||||||
import { EditorProvider, useEditorContext } from "./state";
|
import { EditorProvider, useEditorContext } from "./state";
|
||||||
import { MemoEditorContext } from "./types";
|
import { MemoEditorContext } from "./types";
|
||||||
|
|
||||||
export interface Props {
|
export interface MemoEditorProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
cacheKey?: string;
|
cacheKey?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
@ -23,7 +23,7 @@ export interface Props {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoEditor = observer((props: Props) => {
|
const MemoEditor = observer((props: MemoEditorProps) => {
|
||||||
const { className, cacheKey, memoName, parentMemoName, autoFocus, placeholder, onConfirm, onCancel } = props;
|
const { className, cacheKey, memoName, parentMemoName, autoFocus, placeholder, onConfirm, onCancel } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -42,7 +42,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const MemoEditorImpl: React.FC<Props> = ({
|
const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
||||||
className,
|
className,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
memoName,
|
memoName,
|
||||||
|
|
@ -83,8 +83,12 @@ const MemoEditorImpl: React.FC<Props> = ({
|
||||||
// Focus mode management with body scroll lock
|
// Focus mode management with body scroll lock
|
||||||
useFocusMode(state.ui.isFocusMode);
|
useFocusMode(state.ui.isFocusMode);
|
||||||
|
|
||||||
|
const handleToggleFocusMode = () => {
|
||||||
|
dispatch(actions.toggleFocusMode());
|
||||||
|
};
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
useKeyboard(editorRef, { onSave: handleSave, onToggleFocusMode: () => dispatch(actions.toggleFocusMode()) });
|
useKeyboard(editorRef, { onSave: handleSave, onToggleFocusMode: handleToggleFocusMode });
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const { valid, reason } = validationService.canSave(state);
|
const { valid, reason } = validationService.canSave(state);
|
||||||
|
|
@ -93,7 +97,7 @@ const MemoEditorImpl: React.FC<Props> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.setLoading("saving", true);
|
dispatch(actions.setLoading("saving", true));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await memoService.save(state, { memoName, parentMemoName });
|
const result = await memoService.save(state, { memoName, parentMemoName });
|
||||||
|
|
@ -108,7 +112,7 @@ const MemoEditorImpl: React.FC<Props> = ({
|
||||||
cacheService.clear(cacheService.key(currentUser.name, cacheKey));
|
cacheService.clear(cacheService.key(currentUser.name, cacheKey));
|
||||||
|
|
||||||
// Reset editor state
|
// Reset editor state
|
||||||
actions.reset();
|
dispatch(actions.reset());
|
||||||
|
|
||||||
// Notify parent
|
// Notify parent
|
||||||
onConfirm?.(result.memoName);
|
onConfirm?.(result.memoName);
|
||||||
|
|
@ -118,28 +122,39 @@ const MemoEditorImpl: React.FC<Props> = ({
|
||||||
const message = errorService.handle(error, t);
|
const message = errorService.handle(error, t);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
actions.setLoading("saving", false);
|
dispatch(actions.setLoading("saving", false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleFocusMode = () => dispatch(actions.toggleFocusMode());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemoEditorContext.Provider value={legacyContextValue}>
|
<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
|
<div
|
||||||
className={cn(
|
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,
|
FOCUS_MODE_STYLES.transition,
|
||||||
state.ui.isFocusMode && cn(FOCUS_MODE_STYLES.container.base, FOCUS_MODE_STYLES.container.spacing),
|
state.ui.isFocusMode && cn(FOCUS_MODE_STYLES.container.base, FOCUS_MODE_STYLES.container.spacing),
|
||||||
className,
|
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} />
|
<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>
|
</div>
|
||||||
</MemoEditorContext.Provider>
|
</MemoEditorContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,9 @@ const PagedMemoList = observer((props: Props) => {
|
||||||
renderer={props.renderer}
|
renderer={props.renderer}
|
||||||
prefixElement={
|
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 />
|
<MemoFilters />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const AttachmentList = ({ attachments, mode, onAttachmentsChange, localFiles = [
|
||||||
return (
|
return (
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
<SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>
|
<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) => (
|
{items.map((item) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
{/* Uploaded items are wrapped in SortableItem for drag-and-drop */}
|
{/* Uploaded items are wrapped in SortableItem for drag-and-drop */}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ const RelationList = observer(({ relations, currentMemoName, mode, onRelationsCh
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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) => (
|
{referencingMemos.map((memo) => (
|
||||||
<RelationCard
|
<RelationCard
|
||||||
key={memo.name}
|
key={memo.name}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue