Introduce focus mode in notes (#891)

This commit is contained in:
Chris 2026-03-03 00:29:03 +02:00 committed by GitHub
parent c656c2aa67
commit 74d2027d17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 296 additions and 0 deletions

View file

@ -0,0 +1,200 @@
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { XMarkIcon } from '@heroicons/react/24/outline';
import MarkdownRenderer from '../Shared/MarkdownRenderer';
import { Note } from '../../entities/Note';
import { ENABLE_NOTE_COLOR } from '../../config/featureFlags';
const shouldUseLightText = (hexColor: string | undefined): boolean => {
if (!hexColor) return false;
const hex = hexColor.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance < 0.4;
};
interface NoteFocusModeProps {
note: Note;
isEditing: boolean;
saveStatus: 'saved' | 'saving' | 'unsaved';
onNoteChange: (updates: Partial<Note>) => void;
onContentChange?: (newContent: string) => void;
onEditNote: () => void;
onExitEditing: () => void;
onClose: () => void;
}
const NoteFocusMode: React.FC<NoteFocusModeProps> = ({
note,
isEditing,
saveStatus,
onNoteChange,
onContentChange,
onEditNote,
onExitEditing,
onClose,
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const noteColor = ENABLE_NOTE_COLOR ? note.color : undefined;
const lightText = shouldUseLightText(noteColor);
useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = prev;
};
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
if (isEditing) {
onExitEditing();
} else {
onClose();
}
}
};
document.addEventListener('keydown', handleKeyDown, true);
return () => document.removeEventListener('keydown', handleKeyDown, true);
}, [isEditing, onExitEditing, onClose]);
useEffect(() => {
if (isEditing && textareaRef.current) {
textareaRef.current.focus();
}
}, [isEditing]);
const textColor = noteColor
? lightText
? '#ffffff'
: '#333333'
: undefined;
const mutedColor = noteColor
? lightText
? '#e0e0e0'
: '#666666'
: undefined;
return ReactDOM.createPortal(
<div
className="fixed inset-0 z-[60] flex flex-col bg-white dark:bg-gray-900 transition-opacity duration-200"
style={noteColor ? { backgroundColor: noteColor } : undefined}
>
{/* Minimal header */}
<div className="flex items-center justify-between px-6 py-3 flex-shrink-0">
<div className="text-xs">
{isEditing && note.title && (
<>
{saveStatus === 'saving' && (
<span
className="text-blue-500 dark:text-blue-400 italic"
style={mutedColor ? { color: mutedColor } : undefined}
>
Saving...
</span>
)}
{saveStatus === 'saved' && (
<span
className="text-gray-400 dark:text-gray-500"
style={mutedColor ? { color: mutedColor } : undefined}
>
Saved
</span>
)}
{saveStatus === 'unsaved' && (
<span
className="text-amber-600 dark:text-amber-400"
style={mutedColor ? { color: mutedColor } : undefined}
>
Unsaved
</span>
)}
</>
)}
</div>
<button
onClick={onClose}
className="p-2 rounded-md text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
style={mutedColor ? { color: mutedColor } : undefined}
aria-label="Exit focus mode"
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
{/* Centered content */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-3xl mx-auto px-6 md:px-8 pb-16">
{isEditing ? (
<>
<input
type="text"
value={note.title || ''}
onChange={(e) =>
onNoteChange({ title: e.target.value })
}
placeholder="Note title..."
className="w-full bg-transparent text-gray-900 dark:text-gray-100 border-none focus:outline-none focus:ring-0 mb-6"
style={{
...(textColor ? { color: textColor } : {}),
fontSize: '2.25rem',
lineHeight: '2.5rem',
fontWeight: 500,
paddingLeft: 0,
paddingRight: 0,
}}
/>
<textarea
ref={textareaRef}
value={note.content || ''}
onChange={(e) =>
onNoteChange({ content: e.target.value })
}
placeholder="Write your note... (Markdown supported)"
className="w-full h-full min-h-[60vh] bg-transparent text-gray-900 dark:text-gray-100 border-none focus:outline-none focus:ring-0 resize-none text-lg leading-relaxed"
style={textColor ? { color: textColor } : undefined}
/>
</>
) : (
<>
<h1
onClick={onEditNote}
className="cursor-pointer text-gray-900 dark:text-gray-100 mb-6"
style={{
...(textColor ? { color: textColor } : {}),
fontSize: '2.25rem',
lineHeight: '2.5rem',
fontWeight: 500,
}}
title="Click to edit"
>
{note.title || 'Untitled Note'}
</h1>
<div
onClick={onEditNote}
className="cursor-pointer text-gray-900 dark:text-gray-100 text-lg leading-relaxed"
style={textColor ? { color: textColor } : undefined}
title="Click to edit"
>
<MarkdownRenderer
content={note.content}
noteColor={noteColor}
onContentChange={onContentChange}
/>
</div>
</>
)}
</div>
</div>
</div>,
document.body
);
};
export default NoteFocusMode;

View file

@ -16,6 +16,7 @@ import {
ClockIcon,
EllipsisVerticalIcon,
XMarkIcon,
ArrowsPointingOutIcon,
} from '@heroicons/react/24/outline';
import { useToast } from './Shared/ToastContext';
import { Link, useParams, useNavigate } from 'react-router-dom';
@ -32,6 +33,7 @@ import { deleteNoteWithStoreUpdate } from '../utils/noteDeleteUtils';
import { useStore } from '../store/useStore';
import { createProject } from '../utils/projectsService';
import { ENABLE_NOTE_COLOR } from '../config/featureFlags';
import NoteFocusMode from './Note/NoteFocusMode';
const NOTE_COLORS = [
{ name: 'None', value: '' },
@ -82,6 +84,7 @@ const Notes: React.FC = () => {
const [saveStatus, setSaveStatus] = useState<
'saved' | 'saving' | 'unsaved'
>('saved');
const [isFocusMode, setIsFocusMode] = useState(false);
const hasAutoSelected = useRef(false);
const editingNoteColor =
@ -759,6 +762,24 @@ const Notes: React.FC = () => {
)}
</div>
</div>
<div className="flex items-center">
<button
onClick={() => setIsFocusMode(true)}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
style={{
color: editingNoteColor
? shouldUseLightText(
editingNoteColor
)
? '#e0e0e0'
: '#333333'
: undefined,
}}
aria-label="Focus mode"
title="Focus mode"
>
<ArrowsPointingOutIcon className="h-5 w-5" />
</button>
<div
className="relative"
ref={noteOptionsDropdownRef}
@ -880,6 +901,7 @@ const Notes: React.FC = () => {
</div>
)}
</div>
</div>
</div>
{showProjectDropdown && (
@ -1132,6 +1154,24 @@ const Notes: React.FC = () => {
)}
</div>
</div>
<div className="flex items-center">
<button
onClick={() => setIsFocusMode(true)}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
style={{
color: previewNoteColor
? shouldUseLightText(
previewNoteColor
)
? '#e0e0e0'
: '#333333'
: undefined,
}}
aria-label="Focus mode"
title="Focus mode"
>
<ArrowsPointingOutIcon className="h-5 w-5" />
</button>
<div
className="relative"
ref={noteOptionsDropdownRef}
@ -1245,6 +1285,7 @@ const Notes: React.FC = () => {
</div>
)}
</div>
</div>
</div>
<div
@ -1309,6 +1350,61 @@ const Notes: React.FC = () => {
</div>
</div>
{isFocusMode && (isEditing ? editingNote : previewNote) && (
<NoteFocusMode
note={(isEditing ? editingNote : previewNote)!}
isEditing={isEditing}
saveStatus={saveStatus}
onNoteChange={handleNoteChange}
onContentChange={
!isEditing && previewNote
? async (newContent) => {
const updatedNote = {
...previewNote,
content: newContent,
};
setPreviewNote(updatedNote);
if (previewNote.uid) {
try {
const savedNote =
await updateNote(
previewNote.uid,
updatedNote
);
const updatedNotes = notes.map(
(n) =>
n.uid === previewNote.uid
? savedNote
: n
);
setNotes(updatedNotes);
setPreviewNote(savedNote);
} catch (err) {
console.error(
'Error updating note:',
err
);
}
}
}
: undefined
}
onEditNote={() => {
if (previewNote) {
handleEditNote(previewNote);
}
}}
onExitEditing={() => {
if (editingNote?.title) {
handleSaveInlineNote();
} else {
handleCancelEdit();
}
}}
onClose={() => setIsFocusMode(false)}
/>
)}
{isNoteModalOpen && (
<NoteModal
isOpen={isNoteModalOpen}