Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
4109fe32
Commit
4109fe32
authored
Dec 23, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore(MemoEditor): enhance focus mode handling and improve editor layout
parent
595daaa1
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
70 additions
and
32 deletions
+70
-32
index.tsx
web/src/components/MemoEditor/Editor/index.tsx
+5
-3
EditorContent.tsx
web/src/components/MemoEditor/components/EditorContent.tsx
+13
-3
EditorToolbar.tsx
web/src/components/MemoEditor/components/EditorToolbar.tsx
+16
-4
constants.ts
web/src/components/MemoEditor/constants.ts
+1
-4
index.tsx
web/src/components/MemoEditor/index.tsx
+30
-15
PagedMemoList.tsx
web/src/components/PagedMemoList/PagedMemoList.tsx
+3
-1
AttachmentList.tsx
web/src/components/memo-metadata/AttachmentList.tsx
+1
-1
RelationList.tsx
web/src/components/memo-metadata/RelationList.tsx
+1
-1
No files found.
web/src/components/MemoEditor/Editor/index.tsx
View file @
4109fe32
...
...
@@ -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
}
...
...
web/src/components/MemoEditor/components/EditorContent.tsx
View file @
4109fe32
...
...
@@ -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
}
/>
...
...
web/src/components/MemoEditor/components/EditorToolbar.tsx
View file @
4109fe32
...
...
@@ -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
.
t
oggleFocusMode
}
onLocationChange=
{
handleLocationChange
}
onToggleFocusMode=
{
handleT
oggleFocusMode
}
/>
</
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
}
>
...
...
web/src/components/MemoEditor/constants.ts
View file @
4109fe32
...
...
@@ -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
=
{
...
...
web/src/components/MemoEditor/index.tsx
View file @
4109fe32
...
...
@@ -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
MemoEditor
Props
{
className
?:
string
;
cacheKey
?:
string
;
placeholder
?:
string
;
...
...
@@ -23,7 +23,7 @@ export interface Props {
onCancel
?:
()
=>
void
;
}
const
MemoEditor
=
observer
((
props
:
Props
)
=>
{
const
MemoEditor
=
observer
((
props
:
MemoEditor
Props
)
=>
{
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
<
MemoEditor
Props
>
=
({
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
>
);
...
...
web/src/components/PagedMemoList/PagedMemoList.tsx
View file @
4109fe32
...
...
@@ -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
/>
</>
}
...
...
web/src/components/memo-metadata/AttachmentList.tsx
View file @
4109fe32
...
...
@@ -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 m
t-2 m
ax-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 */
}
...
...
web/src/components/memo-metadata/RelationList.tsx
View file @
4109fe32
...
...
@@ -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
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment