Compare commits

...

2 Commits

12 changed files with 675 additions and 26 deletions

View File

@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "/index.html", "url": "/index.html",
"revision": "0.9eood2uf828" "revision": "0.q2r0i2lj5ro"
}], { }], {
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/] "ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
}); });

10
package-lock.json generated
View File

@ -14,6 +14,7 @@
"axios": "^1.13.1", "axios": "^1.13.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"highlight.js": "^11.11.1",
"marked": "^16.4.1", "marked": "^16.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -4616,6 +4617,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/idb": { "node_modules/idb": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",

View File

@ -19,6 +19,7 @@
"axios": "^1.13.1", "axios": "^1.13.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"highlight.js": "^11.11.1",
"marked": "^16.4.1", "marked": "^16.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -254,7 +254,7 @@ export const GenerateTagsModal: React.FC<GenerateTagsModalProps> = ({
> >
Применить ({selectedTags.length}) Применить ({selectedTags.length})
</button> </button>
<button className="btn-secondary" onClick={onClose} disabled={isLoading}> <button className="btn-secondary" onClick={onClose}>
Отмена Отмена
</button> </button>
</div> </div>

View File

@ -151,7 +151,7 @@ export const ImproveTextModal: React.FC<ImproveTextModalProps> = ({
> >
Применить Применить
</button> </button>
<button className="btn-secondary" onClick={onClose} disabled={isLoading}> <button className="btn-secondary" onClick={onClose}>
Отмена Отмена
</button> </button>
</div> </div>

View File

@ -21,6 +21,13 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
onInsertColor, onInsertColor,
}) => { }) => {
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false); const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
const [showCodeModal, setShowCodeModal] = useState(false);
const [codeLanguage, setCodeLanguage] = useState("");
const codeLanguageInputRef = useRef<HTMLInputElement>(null);
const [showLinkModal, setShowLinkModal] = useState(false);
const [linkText, setLinkText] = useState("");
const [linkUrl, setLinkUrl] = useState("");
const linkTextInputRef = useRef<HTMLInputElement>(null);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
@ -51,8 +58,8 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
if (buttonRef.current && showHeaderDropdown) { if (buttonRef.current && showHeaderDropdown) {
const rect = buttonRef.current.getBoundingClientRect(); const rect = buttonRef.current.getBoundingClientRect();
setMenuPosition({ setMenuPosition({
top: rect.bottom + window.scrollY + 2, top: rect.bottom + 2,
left: rect.left + window.scrollX, left: rect.left,
}); });
} }
}; };
@ -115,6 +122,118 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
}; };
}, [isDragging]); }, [isDragging]);
// Фокус на поле ввода при открытии модального окна
useEffect(() => {
if (showCodeModal && codeLanguageInputRef.current) {
setTimeout(() => {
codeLanguageInputRef.current?.focus();
}, 100);
}
}, [showCodeModal]);
// Фокус на поле ввода текста ссылки при открытии модального окна
useEffect(() => {
if (showLinkModal && linkTextInputRef.current) {
setTimeout(() => {
linkTextInputRef.current?.focus();
}, 100);
}
}, [showLinkModal]);
// Обработка Escape для закрытия модального окна
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (showCodeModal) {
setShowCodeModal(false);
setCodeLanguage("");
}
if (showLinkModal) {
setShowLinkModal(false);
setLinkText("");
setLinkUrl("");
}
}
};
if (showCodeModal || showLinkModal) {
document.addEventListener("keydown", handleEscape);
}
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [showCodeModal, showLinkModal]);
const handleCodeButtonClick = () => {
setShowCodeModal(true);
setCodeLanguage("");
};
const handleCodeModalConfirm = () => {
const language = codeLanguage.trim();
if (language) {
onInsert("```" + language + "\n", "\n```");
} else {
onInsert("```\n", "\n```");
}
setShowCodeModal(false);
setCodeLanguage("");
};
const handleCodeModalClose = () => {
setShowCodeModal(false);
setCodeLanguage("");
};
const handleCodeModalKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleCodeModalConfirm();
}
};
const handleLinkButtonClick = () => {
setShowLinkModal(true);
setLinkText("");
setLinkUrl("");
};
const handleLinkModalConfirm = () => {
const text = linkText.trim() || "текст ссылки";
const url = linkUrl.trim() || "#";
onInsert(`[${text}](`, `${url})`);
setShowLinkModal(false);
setLinkText("");
setLinkUrl("");
};
const handleLinkModalClose = () => {
setShowLinkModal(false);
setLinkText("");
setLinkUrl("");
};
const handleLinkTextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
// Переход к полю URL
const urlInput = document.querySelector(
".link-modal-url-input"
) as HTMLInputElement;
if (urlInput) {
urlInput.focus();
}
}
};
const handleLinkUrlKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleLinkModalConfirm();
}
};
const buttons: Array<{ const buttons: Array<{
id: string; id: string;
icon: string; icon: string;
@ -184,9 +303,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
left: `${menuPosition.left}px`, left: `${menuPosition.left}px`,
}} }}
> >
{[1, 2, 3, 4, 5].map((level) => ( {[1, 2, 3, 4, 5, 6].map((level) => (
<button <button
key={level} key={level}
className={`header-dropdown-item header-level-${level}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onInsert("#".repeat(level) + " ", ""); onInsert("#".repeat(level) + " ", "");
@ -194,7 +314,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
setMenuPosition(null); setMenuPosition(null);
}} }}
> >
H{level} Заголовок {level}
</button> </button>
))} ))}
</div> </div>
@ -267,7 +387,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
<button <button
className="btnMarkdown" className="btnMarkdown"
onClick={() => onInsert("`", "`")} onClick={handleCodeButtonClick}
title="Код" title="Код"
> >
<Icon icon="mdi:code-tags" /> <Icon icon="mdi:code-tags" />
@ -275,7 +395,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
<button <button
className="btnMarkdown" className="btnMarkdown"
onClick={() => onInsert("[текст ссылки](", ")")} onClick={handleLinkButtonClick}
title="Ссылка" title="Ссылка"
> >
<Icon icon="mdi:link" /> <Icon icon="mdi:link" />
@ -312,6 +432,113 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
> >
<Icon icon="mdi:monitor-eye" /> <Icon icon="mdi:monitor-eye" />
</button> </button>
{/* Модальное окно для ввода языка программирования */}
{showCodeModal && (
<div
className="modal"
style={{ display: "block" }}
onClick={handleCodeModalClose}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>Введите язык программирования</h3>
<span className="modal-close" onClick={handleCodeModalClose}>
&times;
</span>
</div>
<div className="modal-body">
<input
ref={codeLanguageInputRef}
type="text"
className="modal-password-input"
placeholder="Например: javascript, python, typescript..."
value={codeLanguage}
onChange={(e) => setCodeLanguage(e.target.value)}
onKeyDown={handleCodeModalKeyDown}
style={{ width: "100%" }}
/>
</div>
<div className="modal-footer">
<button
className="btn-primary"
onClick={handleCodeModalConfirm}
>
Вставить
</button>
<button className="btn-secondary" onClick={handleCodeModalClose}>
Отмена
</button>
</div>
</div>
</div>
)}
{/* Модальное окно для ввода ссылки */}
{showLinkModal && (
<div
className="modal"
style={{ display: "block" }}
onClick={handleLinkModalClose}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>Введите ссылку</h3>
<span className="modal-close" onClick={handleLinkModalClose}>
&times;
</span>
</div>
<div className="modal-body">
<div style={{ marginBottom: "15px" }}>
<label style={{ display: "block", marginBottom: "5px", fontWeight: "500" }}>
Текст ссылки:
</label>
<input
ref={linkTextInputRef}
type="text"
className="modal-password-input"
placeholder="Текст ссылки"
value={linkText}
onChange={(e) => setLinkText(e.target.value)}
onKeyDown={handleLinkTextKeyDown}
style={{ width: "100%" }}
/>
</div>
<div>
<label style={{ display: "block", marginBottom: "5px", fontWeight: "500" }}>
URL:
</label>
<input
type="text"
className="modal-password-input link-modal-url-input"
placeholder="https://example.com"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onKeyDown={handleLinkUrlKeyDown}
style={{ width: "100%" }}
/>
</div>
</div>
<div className="modal-footer">
<button
className="btn-primary"
onClick={handleLinkModalConfirm}
>
Вставить
</button>
<button className="btn-secondary" onClick={handleLinkModalClose}>
Отмена
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -14,7 +14,7 @@ import { ImproveTextModal } from "./ImproveTextModal";
import { extractTags } from "../../utils/markdown"; import { extractTags } from "../../utils/markdown";
interface NoteEditorProps { interface NoteEditorProps {
onSave: () => void; onSave: (noteId?: number | string) => void;
} }
export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => { export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
@ -80,7 +80,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
setContent(""); setContent("");
setImages([]); setImages([]);
setFiles([]); setFiles([]);
onSave(); onSave(note.id);
} catch (error) { } catch (error) {
console.error("Ошибка сохранения заметки:", error); console.error("Ошибка сохранения заметки:", error);
showNotification("Ошибка сохранения заметки", "error"); showNotification("Ошибка сохранения заметки", "error");
@ -106,7 +106,9 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
console.error("Ошибка улучшения текста:", error); console.error("Ошибка улучшения текста:", error);
setImproveError(true); setImproveError(true);
setImproveErrorMessage( setImproveErrorMessage(
error.response?.data?.error || error.message || "Ошибка улучшения текста" error.response?.data?.error ||
error.message ||
"Ошибка улучшения текста"
); );
} finally { } finally {
setIsAiLoading(false); setIsAiLoading(false);
@ -147,7 +149,10 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
console.error("Детали ошибки:", error.response?.data); console.error("Детали ошибки:", error.response?.data);
setTagsGenerationError(true); setTagsGenerationError(true);
setShowTagsModal(false); setShowTagsModal(false);
const errorMessage = error.response?.data?.error || error.message || "Ошибка генерации тегов"; const errorMessage =
error.response?.data?.error ||
error.message ||
"Ошибка генерации тегов";
showNotification(errorMessage, "error"); showNotification(errorMessage, "error");
} finally { } finally {
setIsGeneratingTags(false); setIsGeneratingTags(false);
@ -159,13 +164,19 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
const existingTags = extractTags(content); const existingTags = extractTags(content);
const tagsToAdd = tags const tagsToAdd = tags
.filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase())) .filter(
(tag) =>
!existingTags.some(
(existing) => existing.toLowerCase() === tag.toLowerCase()
)
)
.map((tag) => `#${tag}`) .map((tag) => `#${tag}`)
.join(" "); .join(" ");
if (tagsToAdd) { if (tagsToAdd) {
// Добавляем теги в конец заметки // Добавляем теги в конец заметки
const newContent = content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd; const newContent =
content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd;
setContent(newContent); setContent(newContent);
showNotification(`Добавлено тегов: ${tags.length}`, "success"); showNotification(`Добавлено тегов: ${tags.length}`, "success");
} else { } else {
@ -1108,6 +1119,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
setShowTagsModal(false); setShowTagsModal(false);
setSuggestedTags([]); setSuggestedTags([]);
setTagsGenerationError(false); setTagsGenerationError(false);
setIsGeneratingTags(false);
}} }}
onSelectTags={handleSelectTags} onSelectTags={handleSelectTags}
suggestedTags={suggestedTags} suggestedTags={suggestedTags}
@ -1123,6 +1135,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
setImprovedText(""); setImprovedText("");
setImproveError(false); setImproveError(false);
setImproveErrorMessage(""); setImproveErrorMessage("");
setIsAiLoading(false);
}} }}
onApply={handleApplyImprovedText} onApply={handleApplyImprovedText}
originalText={content} originalText={content}

View File

@ -21,6 +21,7 @@ import { ImageUpload } from "./ImageUpload";
import { FileUpload } from "./FileUpload"; import { FileUpload } from "./FileUpload";
import { aiApi } from "../../api/aiApi"; import { aiApi } from "../../api/aiApi";
import { GenerateTagsModal } from "./GenerateTagsModal"; import { GenerateTagsModal } from "./GenerateTagsModal";
import { ImproveTextModal } from "./ImproveTextModal";
import { extractTags } from "../../utils/markdown"; import { extractTags } from "../../utils/markdown";
interface NoteItemProps { interface NoteItemProps {
@ -31,6 +32,7 @@ interface NoteItemProps {
onReload: () => void; onReload: () => void;
isSelected?: boolean; isSelected?: boolean;
onSelect?: (id: number | string) => void; onSelect?: (id: number | string) => void;
onFocusNote?: (id: number | string) => void;
} }
export const NoteItem: React.FC<NoteItemProps> = ({ export const NoteItem: React.FC<NoteItemProps> = ({
@ -41,6 +43,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
onReload, onReload,
isSelected = false, isSelected = false,
onSelect, onSelect,
onFocusNote,
}) => { }) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState(note.content); const [editContent, setEditContent] = useState(note.content);
@ -65,6 +68,10 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const [suggestedTags, setSuggestedTags] = useState<string[]>([]); const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
const [isGeneratingTags, setIsGeneratingTags] = useState(false); const [isGeneratingTags, setIsGeneratingTags] = useState(false);
const [tagsGenerationError, setTagsGenerationError] = useState(false); const [tagsGenerationError, setTagsGenerationError] = useState(false);
const [showImproveModal, setShowImproveModal] = useState(false);
const [improvedText, setImprovedText] = useState("");
const [improveError, setImproveError] = useState(false);
const [improveErrorMessage, setImproveErrorMessage] = useState<string>("");
const editTextareaRef = useRef<HTMLTextAreaElement>(null); const editTextareaRef = useRef<HTMLTextAreaElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null); const imageInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -138,6 +145,13 @@ export const NoteItem: React.FC<NoteItemProps> = ({
setDeletedImageIds([]); setDeletedImageIds([]);
setDeletedFileIds([]); setDeletedFileIds([]);
onReload(); onReload();
// Устанавливаем фокус на заметку после сохранения
if (onFocusNote) {
// Небольшая задержка для завершения обновления DOM
setTimeout(() => {
onFocusNote(note.id);
}, 100);
}
} catch (error) { } catch (error) {
console.error("Ошибка обновления заметки:", error); console.error("Ошибка обновления заметки:", error);
showNotification("Ошибка обновления заметки", "error"); showNotification("Ошибка обновления заметки", "error");
@ -179,18 +193,36 @@ export const NoteItem: React.FC<NoteItemProps> = ({
} }
setIsAiLoading(true); setIsAiLoading(true);
setImproveError(false);
setImproveErrorMessage("");
setImprovedText("");
setShowImproveModal(true);
try { try {
const improvedText = await aiApi.improveText(editContent); const improved = await aiApi.improveText(editContent);
setEditContent(improvedText); setImprovedText(improved);
showNotification("Текст улучшен!", "success"); setImproveError(false);
} catch (error) { } catch (error: any) {
console.error("Ошибка улучшения текста:", error); console.error("Ошибка улучшения текста:", error);
showNotification("Ошибка улучшения текста", "error"); setImproveError(true);
setImproveErrorMessage(
error.response?.data?.error ||
error.message ||
"Ошибка улучшения текста"
);
} finally { } finally {
setIsAiLoading(false); setIsAiLoading(false);
} }
}; };
const handleApplyImprovedText = (text: string) => {
setEditContent(text);
showNotification("Текст улучшен!", "success");
setShowImproveModal(false);
setImprovedText("");
setImproveError(false);
setImproveErrorMessage("");
};
const handleGenerateTags = async () => { const handleGenerateTags = async () => {
if (!editContent.trim()) { if (!editContent.trim()) {
showNotification("Введите текст для генерации тегов", "warning"); showNotification("Введите текст для генерации тегов", "warning");
@ -1620,6 +1652,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
setShowTagsModal(false); setShowTagsModal(false);
setSuggestedTags([]); setSuggestedTags([]);
setTagsGenerationError(false); setTagsGenerationError(false);
setIsGeneratingTags(false);
}} }}
onSelectTags={handleSelectTags} onSelectTags={handleSelectTags}
suggestedTags={suggestedTags} suggestedTags={suggestedTags}
@ -1627,6 +1660,23 @@ export const NoteItem: React.FC<NoteItemProps> = ({
isLoading={isGeneratingTags} isLoading={isGeneratingTags}
hasError={tagsGenerationError} hasError={tagsGenerationError}
/> />
<ImproveTextModal
isOpen={showImproveModal}
onClose={() => {
setShowImproveModal(false);
setImprovedText("");
setImproveError(false);
setImproveErrorMessage("");
setIsAiLoading(false);
}}
onApply={handleApplyImprovedText}
originalText={editContent}
improvedText={improvedText}
isLoading={isAiLoading}
hasError={improveError}
errorMessage={improveErrorMessage}
/>
</> </>
); );
}; };

View File

@ -8,6 +8,7 @@ import { extractTags } from "../../utils/markdown";
export interface NotesListRef { export interface NotesListRef {
reloadNotes: () => void; reloadNotes: () => void;
focusNote: (noteId: number | string) => void;
} }
interface NotesListProps { interface NotesListProps {
@ -81,8 +82,34 @@ export const NotesList = forwardRef<NotesListRef, NotesListProps>(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, searchQuery, selectedDate, selectedTag]); }, [userId, searchQuery, selectedDate, selectedTag]);
const focusNote = (noteId: number | string) => {
// Используем data-атрибут для поиска элемента заметки
const noteElement = document.querySelector(
`[data-note-id="${noteId}"]`
) as HTMLDivElement | null;
if (noteElement) {
// Прокручиваем к верхней части заметки
noteElement.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
// Добавляем визуальное выделение на короткое время
// Очищаем предыдущий таймер, если он был установлен
const existingTimeout = (noteElement as any).__focusTimeout;
if (existingTimeout) {
clearTimeout(existingTimeout);
}
noteElement.style.transition = "box-shadow 0.3s ease";
noteElement.style.boxShadow = "0 0 0 3px rgba(33, 150, 243, 0.5)";
const timeout = setTimeout(() => {
noteElement.style.boxShadow = "";
(noteElement as any).__focusTimeout = null;
}, 2000);
(noteElement as any).__focusTimeout = timeout;
}
};
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
reloadNotes: loadNotes, reloadNotes: loadNotes,
focusNote: focusNote,
})); }));
const handleDelete = async (id: number | string) => { const handleDelete = async (id: number | string) => {
@ -167,6 +194,7 @@ export const NotesList = forwardRef<NotesListRef, NotesListProps>(
onReload={loadNotes} onReload={loadNotes}
isSelected={selectedNoteIds.includes(note.id)} isSelected={selectedNoteIds.includes(note.id)}
onSelect={onNoteSelect} onSelect={onNoteSelect}
onFocusNote={focusNote}
/> />
))} ))}
</div> </div>

View File

@ -63,10 +63,32 @@ const NotesPage: React.FC = () => {
const activeFilters = getActiveFilters(); const activeFilters = getActiveFilters();
const handleNoteSave = () => { const handleNoteSave = (noteId?: number | string) => {
// Вызываем перезагрузку заметок после создания новой заметки // Вызываем перезагрузку заметок после создания новой заметки
if (notesListRef.current) { if (notesListRef.current) {
notesListRef.current.reloadNotes(); notesListRef.current.reloadNotes();
// Устанавливаем фокус на сохраненную заметку после небольшой задержки,
// чтобы дать время для перезагрузки и рендеринга заметок
if (noteId) {
let attempts = 0;
const maxAttempts = 15;
const attemptFocus = () => {
attempts++;
if (notesListRef.current) {
// Проверяем, есть ли элемент заметки в DOM
const noteElement = document.querySelector(
`[data-note-id="${noteId}"]`
);
if (noteElement || attempts >= maxAttempts) {
notesListRef.current.focusNote(noteId);
} else if (attempts < maxAttempts) {
// Пытаемся еще раз, если заметка еще не появилась в DOM
setTimeout(attemptFocus, 100);
}
}
};
setTimeout(attemptFocus, 200);
}
} }
}; };

View File

@ -1424,6 +1424,94 @@ textarea:focus {
word-break: break-word; word-break: break-word;
} }
/* Стили для блоков кода с подсветкой синтаксиса в заметках */
.textNote .code-block {
position: relative;
margin: 15px 0;
background: #2d2d2d;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border-primary);
}
.textNote .code-block code {
display: block;
padding: 15px;
overflow-x: auto;
font-family: "Courier New", "Consolas", "Monaco", monospace;
font-size: 14px;
line-height: 1.5;
color: #f8f8f2;
background: transparent;
border: none;
border-radius: 0;
word-wrap: normal;
overflow-wrap: normal;
word-break: normal;
white-space: pre;
}
.textNote .code-block .code-language {
position: absolute;
top: 8px;
right: 12px;
font-size: 11px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
background: rgba(0, 0, 0, 0.3);
padding: 4px 8px;
border-radius: 4px;
font-family: "Open Sans", sans-serif;
z-index: 1;
}
/* Светлая тема для блоков кода в заметках */
[data-theme="light"] .textNote .code-block,
.textNote .code-block {
background: #f5f5f5;
border-color: var(--border-primary);
}
[data-theme="light"] .textNote .code-block code,
.textNote .code-block code {
color: #333;
}
[data-theme="light"] .textNote .code-block .code-language,
.textNote .code-block .code-language {
color: #666;
background: rgba(255, 255, 255, 0.7);
}
/* Темная тема для блоков кода в заметках */
[data-theme="dark"] .textNote .code-block {
background: #1e1e1e;
border-color: var(--border-primary);
}
[data-theme="dark"] .textNote .code-block code {
color: #d4d4d4;
}
[data-theme="dark"] .textNote .code-block .code-language {
color: #999;
background: rgba(0, 0, 0, 0.5);
}
/* Стили для inline кода в заметках (не блоки) */
.textNote code:not(.code-block code) {
background-color: var(--bg-tertiary);
color: var(--text-primary);
padding: 2px 4px;
border-radius: 5px;
font-size: 14px;
}
/* Стили highlight.js уже включены через импорт CSS файлов */
/* Дополнительные стили для адаптации к теме приложения */
/* Стили для чекбоксов (to-do списков) */ /* Стили для чекбоксов (to-do списков) */
.textNote input[type="checkbox"] { .textNote input[type="checkbox"] {
cursor: pointer; cursor: pointer;
@ -1655,8 +1743,8 @@ textarea:focus {
border-radius: 5px; border-radius: 5px;
box-shadow: 0 2px 8px var(--shadow-medium); box-shadow: 0 2px 8px var(--shadow-medium);
z-index: 1001; z-index: 1001;
min-width: 60px; min-width: 140px;
max-width: 120px; max-width: 250px;
} }
.header-dropdown-menu button { .header-dropdown-menu button {
@ -1667,10 +1755,9 @@ textarea:focus {
background: none; background: none;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
transition: background 0.2s ease; transition: background 0.2s ease;
white-space: nowrap;
} }
.header-dropdown-menu button:hover { .header-dropdown-menu button:hover {
@ -1685,6 +1772,50 @@ textarea:focus {
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
} }
/* Стили для визуальных примеров заголовков */
.header-dropdown-menu .header-level-1 {
font-size: 32px;
font-weight: 700;
line-height: 1.2;
padding: 12px 16px;
}
.header-dropdown-menu .header-level-2 {
font-size: 28px;
font-weight: 700;
line-height: 1.2;
padding: 11px 16px;
}
.header-dropdown-menu .header-level-3 {
font-size: 24px;
font-weight: 600;
line-height: 1.3;
padding: 10px 16px;
}
.header-dropdown-menu .header-level-4 {
font-size: 20px;
font-weight: 600;
line-height: 1.3;
padding: 9px 16px;
}
.header-dropdown-menu .header-level-5 {
font-size: 18px;
font-weight: 600;
line-height: 1.4;
padding: 8px 16px;
}
.header-dropdown-menu .header-level-6 {
font-size: 16px;
font-weight: 600;
line-height: 1.4;
padding: 8px 16px;
color: var(--text-secondary);
}
/* Плавающая панель форматирования */ /* Плавающая панель форматирования */
.floating-toolbar-wrapper { .floating-toolbar-wrapper {
overflow-x: auto; overflow-x: auto;
@ -3240,6 +3371,90 @@ textarea:focus {
border-radius: 0; border-radius: 0;
} }
/* Стили для блоков кода с подсветкой синтаксиса */
.note-preview-content .code-block {
position: relative;
margin: 15px 0;
background: #2d2d2d;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border-primary);
}
.note-preview-content .code-block code {
display: block;
padding: 15px;
overflow-x: auto;
font-family: "Courier New", "Consolas", "Monaco", monospace;
font-size: 14px;
line-height: 1.5;
color: #f8f8f2;
background: transparent;
border: none;
border-radius: 0;
}
.note-preview-content .code-block .code-language {
position: absolute;
top: 8px;
right: 12px;
font-size: 11px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
background: rgba(0, 0, 0, 0.3);
padding: 4px 8px;
border-radius: 4px;
font-family: "Open Sans", sans-serif;
z-index: 1;
}
/* Светлая тема для блоков кода */
[data-theme="light"] .note-preview-content .code-block,
.note-preview-content .code-block {
background: #f5f5f5;
border-color: var(--border-primary);
}
[data-theme="light"] .note-preview-content .code-block code,
.note-preview-content .code-block code {
color: #333;
}
[data-theme="light"] .note-preview-content .code-block .code-language,
.note-preview-content .code-block .code-language {
color: #666;
background: rgba(255, 255, 255, 0.7);
}
/* Темная тема для блоков кода */
[data-theme="dark"] .note-preview-content .code-block {
background: #1e1e1e;
border-color: var(--border-primary);
}
[data-theme="dark"] .note-preview-content .code-block code {
color: #d4d4d4;
}
[data-theme="dark"] .note-preview-content .code-block .code-language {
color: #999;
background: rgba(0, 0, 0, 0.5);
}
/* Стили для inline кода (не блоки) */
.note-preview-content code:not(.code-block code) {
background: var(--bg-quaternary);
color: var(--text-primary);
padding: 2px 6px;
border-radius: 4px;
font-family: "Courier New", monospace;
}
/* Стили highlight.js уже включены через импорт CSS файлов */
/* Дополнительные стили для адаптации к теме приложения */
.note-preview-content blockquote { .note-preview-content blockquote {
border-left: 4px solid var(--accent-color, #007bff); border-left: 4px solid var(--accent-color, #007bff);
padding-left: 15px; padding-left: 15px;

View File

@ -1,4 +1,8 @@
import { marked } from "marked"; import { marked } from "marked";
import hljs from "highlight.js";
// Импортируем стили
import "highlight.js/styles/github-dark.css";
import "highlight.js/styles/github.css";
// Расширение для спойлеров // Расширение для спойлеров
const spoilerExtension = { const spoilerExtension = {
@ -75,7 +79,72 @@ function renderTokens(tokens: any[], renderer: any): string {
.join(""); .join("");
} }
// Кастомный renderer для внешних ссылок и чекбоксов // Функция для экранирования HTML
const escapeHtml = (text: string): string => {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
};
// Маппинг алиасов языков
const languageAliases: Record<string, string> = {
js: "javascript",
ts: "typescript",
py: "python",
sh: "bash",
shell: "bash",
zsh: "bash",
yml: "yaml",
md: "markdown",
html: "markup",
xml: "markup",
svg: "markup",
mathml: "markup",
ssml: "markup",
atom: "markup",
rss: "markup",
};
// Функция для нормализации названия языка
const normalizeLanguage = (lang: string): string => {
const normalized = lang.toLowerCase().trim();
// Проверяем алиасы
return languageAliases[normalized] || normalized;
};
// Функция для подсветки синтаксиса кода
const highlightCode = (code: string, lang?: string): string => {
if (!lang) {
// Если язык не указан, просто экранируем HTML
return escapeHtml(code);
}
// Нормализуем название языка
const normalizedLang = normalizeLanguage(lang);
try {
// Используем highlight.js для подсветки синтаксиса
const highlighted = hljs.highlight(code, { language: normalizedLang });
return highlighted.value;
} catch (e: any) {
// Если произошла ошибка, пробуем автоопределение
try {
const autoHighlighted = hljs.highlightAuto(code);
return autoHighlighted.value;
} catch (autoError: any) {
// Если автоопределение тоже не сработало, логируем и экранируем
console.warn(
"Highlight.js error:",
e?.message || e,
"Language:",
normalizedLang
);
return escapeHtml(code);
}
}
};
// Кастомный renderer для внешних ссылок, чекбоксов и блоков кода
const renderer: any = { const renderer: any = {
link(token: any) { link(token: any) {
const href = token.href; const href = token.href;
@ -123,6 +192,20 @@ const renderer: any = {
} }
return `<li>${content}</li>\n`; return `<li>${content}</li>\n`;
}, },
// Кастомный renderer для блоков кода с подсветкой синтаксиса
code(token: any) {
const code = token.text || "";
// В marked токен блока кода имеет поле lang
const lang = (token.lang || token.language || "").trim();
const highlightedCode = highlightCode(code, lang);
// Если язык указан, добавляем класс для стилизации (highlight.js использует класс hljs)
const langClass = lang ? `language-${lang}` : "";
const langLabel = lang ? `<span class="code-language">${lang}</span>` : "";
return `<pre class="code-block">${langLabel}<code class="hljs ${langClass}">${highlightedCode}</code></pre>`;
},
}; };
// Настройка marked // Настройка marked