Добавлены функции для управления темой и цветом акцента, улучшена обработка выделения текста в редакторах заметок, добавлены новые стили для темной и светлой тем, а также улучшена логика отображения плавающей панели инструментов. Исправлены ошибки в обработке контекстного меню и добавлены новые функции для копирования, вырезания и вставки текста.
This commit is contained in:
parent
3224cffafa
commit
dc8f1f53fc
5
.cursor/rules/rules.mdc
Normal file
5
.cursor/rules/rules.mdc
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
Никогда самостоятельно не пытайся запустить сервер. Если это требуется, то попроси это сделать меня!
|
||||
5
.cursor/worktrees.json
Normal file
5
.cursor/worktrees.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"setup-worktree": [
|
||||
"npm install"
|
||||
]
|
||||
}
|
||||
@ -3,7 +3,52 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#667eea" />
|
||||
<title>NoteJS Backend</title>
|
||||
|
||||
<!-- Предотвращение мерцания темы -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
// Получаем сохраненную тему
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
// Получаем системные предпочтения
|
||||
const systemPrefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
|
||||
// Определяем тему: сохраненная или системная
|
||||
const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
|
||||
|
||||
// Устанавливаем тему до загрузки CSS
|
||||
if (theme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
|
||||
// Получаем и устанавливаем accentColor
|
||||
const savedAccentColor = localStorage.getItem("accentColor");
|
||||
const accentColor = savedAccentColor || "#667eea";
|
||||
|
||||
// Устанавливаем CSS переменную для accent цвета
|
||||
document.documentElement.style.setProperty("--accent-color", accentColor);
|
||||
|
||||
// Устанавливаем цвет для meta theme-color
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute(
|
||||
"content",
|
||||
theme === "dark" ? "#1a1a1a" : accentColor
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// В случае ошибки устанавливаем светлую тему по умолчанию
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
document.documentElement.style.setProperty("--accent-color", "#667eea");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
68
index.html
68
index.html
@ -12,19 +12,85 @@
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
// Получаем сохраненную тему
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
// Получаем системные предпочтения
|
||||
const systemPrefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
|
||||
// Определяем тему: сохраненная или системная
|
||||
const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
|
||||
|
||||
// Функция для конвертации hex в RGB
|
||||
function hexToRgb(hex) {
|
||||
const cleanHex = hex.replace("#", "");
|
||||
const r = parseInt(cleanHex.substring(0, 2), 16);
|
||||
const g = parseInt(cleanHex.substring(2, 4), 16);
|
||||
const b = parseInt(cleanHex.substring(4, 6), 16);
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
|
||||
// Получаем и устанавливаем accentColor
|
||||
const savedAccentColor = localStorage.getItem("accentColor");
|
||||
const accentColor = savedAccentColor || "#007bff";
|
||||
|
||||
// Устанавливаем тему и переменные до загрузки CSS
|
||||
if (theme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Устанавливаем CSS переменные для accent цвета
|
||||
document.documentElement.style.setProperty("--accent-color", accentColor);
|
||||
document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor));
|
||||
|
||||
// Устанавливаем цвет для meta theme-color
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute(
|
||||
"content",
|
||||
theme === "dark" ? "#1a1a1a" : accentColor
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// В случае ошибки устанавливаем светлую тему по умолчанию
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
document.documentElement.style.setProperty("--accent-color", "#007bff");
|
||||
document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Критические стили темы для предотвращения flash эффекта -->
|
||||
<style>
|
||||
:root {
|
||||
--accent-color: #007bff;
|
||||
|
||||
/* Светлая тема (по умолчанию) */
|
||||
--bg-primary: #f5f5f5;
|
||||
--text-primary: #333333;
|
||||
--border-primary: #e0e0e0;
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Темная тема */
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #1a1a1a;
|
||||
--text-primary: #ffffff;
|
||||
--border-primary: #404040;
|
||||
--shadow-light: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Применяем стили сразу к body для предотвращения flash */
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"server": "cd backend && node server.js",
|
||||
"dev:all": "concurrently \"npm run dev\" \"npm run server\"",
|
||||
"dev:all": "concurrently \"npm run dev -- --host\" \"npm run server\"",
|
||||
"start": "npm run dev:all"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -35,6 +35,8 @@ axiosClient.interceptors.response.use(
|
||||
if (error.response?.status === 401) {
|
||||
// Список URL, где 401 означает неправильный пароль, а не истечение сессии
|
||||
const passwordProtectedUrls = [
|
||||
"/login", // Страница входа
|
||||
"/register", // Страница регистрации
|
||||
"/notes/archived/all", // Удаление всех архивных заметок
|
||||
"/user/delete-account", // Удаление аккаунта
|
||||
];
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface FloatingToolbarProps {
|
||||
@ -13,6 +13,7 @@ interface FloatingToolbarProps {
|
||||
italic?: boolean;
|
||||
strikethrough?: boolean;
|
||||
};
|
||||
hasSelection?: boolean; // Есть ли выделение текста
|
||||
}
|
||||
|
||||
export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
@ -23,40 +24,109 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
onHide,
|
||||
onInsertColor,
|
||||
activeFormats = {},
|
||||
hasSelection = false,
|
||||
}) => {
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && toolbarRef.current) {
|
||||
// Корректируем позицию, чтобы toolbar не выходил за границы экрана
|
||||
const toolbar = toolbarRef.current;
|
||||
const rect = toolbar.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
// Небольшая задержка для корректного расчета размеров после рендера
|
||||
setTimeout(() => {
|
||||
if (!toolbarRef.current) return;
|
||||
|
||||
const toolbar = toolbarRef.current;
|
||||
const rect = toolbar.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const padding = 10;
|
||||
|
||||
let top = position.top - toolbar.offsetHeight - 10;
|
||||
let left = position.left;
|
||||
// Получаем внутренний контейнер с кнопками для определения реальной ширины
|
||||
const toolbarContent = toolbar.querySelector('.floating-toolbar') as HTMLElement;
|
||||
const toolbarContentWidth = toolbarContent ? toolbarContent.scrollWidth : rect.width;
|
||||
const availableWidth = windowWidth - padding * 2;
|
||||
|
||||
// Если toolbar выходит за правую границу экрана
|
||||
if (left + rect.width > windowWidth) {
|
||||
left = windowWidth - rect.width - 10;
|
||||
}
|
||||
let top = position.top - rect.height - padding;
|
||||
let left = position.left;
|
||||
|
||||
// Если toolbar выходит за левую границу экрана
|
||||
if (left < 10) {
|
||||
left = 10;
|
||||
}
|
||||
// Если контент шире доступного пространства, устанавливаем maxWidth для wrapper
|
||||
if (toolbarContentWidth > availableWidth) {
|
||||
toolbar.style.maxWidth = `${availableWidth}px`;
|
||||
}
|
||||
|
||||
// Если toolbar выходит за верхнюю границу экрана
|
||||
if (top < 10) {
|
||||
top = position.top + 30; // Показываем снизу от выделения
|
||||
}
|
||||
// Если toolbar выходит за правую границу экрана
|
||||
if (left + rect.width > windowWidth - padding) {
|
||||
// Если контент шире экрана, используем прокрутку и позиционируем по левому краю
|
||||
if (toolbarContentWidth > availableWidth) {
|
||||
left = padding;
|
||||
} else {
|
||||
// Иначе выравниваем по правому краю
|
||||
left = Math.max(padding, windowWidth - rect.width - padding);
|
||||
}
|
||||
}
|
||||
|
||||
toolbar.style.top = `${top}px`;
|
||||
toolbar.style.left = `${left}px`;
|
||||
// Если toolbar выходит за левую границу экрана
|
||||
if (left < padding) {
|
||||
left = padding;
|
||||
}
|
||||
|
||||
// Если toolbar выходит за верхнюю границу экрана
|
||||
if (top < padding) {
|
||||
top = position.top + 30; // Показываем снизу от выделения
|
||||
}
|
||||
|
||||
// Проверяем нижнюю границу
|
||||
if (top + rect.height > windowHeight - padding) {
|
||||
top = windowHeight - rect.height - padding;
|
||||
}
|
||||
|
||||
toolbar.style.top = `${top}px`;
|
||||
toolbar.style.left = `${left}px`;
|
||||
}, 0);
|
||||
}
|
||||
}, [visible, position]);
|
||||
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
// Не начинаем перетаскивание если кликнули на кнопку
|
||||
if ((e.target as HTMLElement).closest('.floating-toolbar-btn')) return;
|
||||
|
||||
if (!toolbarRef.current) return;
|
||||
setIsDragging(true);
|
||||
setStartX(e.pageX - toolbarRef.current.offsetLeft);
|
||||
setScrollLeft(toolbarRef.current.scrollLeft);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging || !toolbarRef.current) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - toolbarRef.current.offsetLeft;
|
||||
const walk = (x - startX) * 2; // Увеличиваем скорость прокрутки
|
||||
toolbarRef.current.scrollLeft = scrollLeft - walk;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// Обработчики для document чтобы отслеживать mouseMove и mouseUp даже вне элемента
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
} else {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
const handleFormat = (before: string, after: string) => {
|
||||
onFormat(before, after);
|
||||
// Не скрываем toolbar - оставляем его видимым для дальнейших действий
|
||||
@ -73,82 +143,249 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
}, 0);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
const handleCopy = async () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
|
||||
if (start === end) return; // Нет выделения
|
||||
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(selectedText);
|
||||
// Можно добавить уведомление об успешном копировании
|
||||
} catch (err) {
|
||||
// Fallback для старых браузеров
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = selectedText;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCut = async () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
|
||||
if (start === end) return; // Нет выделения
|
||||
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(selectedText);
|
||||
} catch (err) {
|
||||
// Fallback для старых браузеров
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = selectedText;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Удаляем выделенный текст
|
||||
const newValue = textarea.value.substring(0, start) + textarea.value.substring(end);
|
||||
|
||||
// Обновляем значение через React-совместимое событие
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
"value"
|
||||
)?.set;
|
||||
|
||||
if (nativeInputValueSetter) {
|
||||
nativeInputValueSetter.call(textarea, newValue);
|
||||
const inputEvent = new Event("input", { bubbles: true });
|
||||
textarea.dispatchEvent(inputEvent);
|
||||
} else {
|
||||
textarea.value = newValue;
|
||||
const inputEvent = new Event("input", { bubbles: true });
|
||||
textarea.dispatchEvent(inputEvent);
|
||||
}
|
||||
|
||||
textarea.setSelectionRange(start, start);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
const handlePaste = async () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
// Вставляем текст в позицию курсора или заменяем выделенный текст
|
||||
const newValue =
|
||||
textarea.value.substring(0, start) +
|
||||
text +
|
||||
textarea.value.substring(end);
|
||||
|
||||
// Обновляем значение через React-совместимое событие
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
"value"
|
||||
)?.set;
|
||||
|
||||
if (nativeInputValueSetter) {
|
||||
nativeInputValueSetter.call(textarea, newValue);
|
||||
const inputEvent = new Event("input", { bubbles: true });
|
||||
textarea.dispatchEvent(inputEvent);
|
||||
} else {
|
||||
textarea.value = newValue;
|
||||
const inputEvent = new Event("input", { bubbles: true });
|
||||
textarea.dispatchEvent(inputEvent);
|
||||
}
|
||||
|
||||
// Устанавливаем курсор после вставленного текста
|
||||
const newCursorPos = start + text.length;
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
textarea.focus();
|
||||
} catch (err) {
|
||||
// Fallback: используем execCommand для вставки
|
||||
textarea.focus();
|
||||
document.execCommand("paste");
|
||||
}
|
||||
};
|
||||
|
||||
// Не показываем toolbar, если он не видим или нет выделения текста
|
||||
if (!visible || !hasSelection) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
className="floating-toolbar"
|
||||
className="floating-toolbar-wrapper"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${position.top}px`,
|
||||
left: `${position.left}px`,
|
||||
zIndex: 1000,
|
||||
cursor: isDragging ? 'grabbing' : (toolbarRef.current && toolbarRef.current.scrollWidth > toolbarRef.current.clientWidth ? 'grab' : 'default'),
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
// Предотвращаем потерю выделения при клике на toolbar
|
||||
e.preventDefault();
|
||||
handleMouseDown(e);
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
// Предотвращаем появление контекстного меню браузера на toolbar
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className={`floating-toolbar-btn ${activeFormats.bold ? "active" : ""}`}
|
||||
onClick={() => handleFormat("**", "**")}
|
||||
title="Жирный"
|
||||
>
|
||||
<Icon icon="mdi:format-bold" />
|
||||
</button>
|
||||
<button
|
||||
className={`floating-toolbar-btn ${
|
||||
activeFormats.italic ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleFormat("*", "*")}
|
||||
title="Курсив"
|
||||
>
|
||||
<Icon icon="mdi:format-italic" />
|
||||
</button>
|
||||
<button
|
||||
className={`floating-toolbar-btn ${
|
||||
activeFormats.strikethrough ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleFormat("~~", "~~")}
|
||||
title="Зачеркнутый"
|
||||
>
|
||||
<Icon icon="mdi:format-strikethrough" />
|
||||
</button>
|
||||
<div className="floating-toolbar">
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={onHide}
|
||||
title="Закрыть"
|
||||
>
|
||||
<Icon icon="mdi:close" />
|
||||
</button>
|
||||
|
||||
<div className="floating-toolbar-separator" />
|
||||
{hasSelection && (
|
||||
<>
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={handleCopy}
|
||||
title="Копировать"
|
||||
>
|
||||
<Icon icon="mdi:content-copy" />
|
||||
</button>
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={handleCut}
|
||||
title="Вырезать"
|
||||
>
|
||||
<Icon icon="mdi:content-cut" />
|
||||
</button>
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={handlePaste}
|
||||
title="Вставить"
|
||||
>
|
||||
<Icon icon="mdi:content-paste" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => onInsertColor?.()}
|
||||
title="Цвет текста"
|
||||
>
|
||||
<Icon icon="mdi:palette" />
|
||||
</button>
|
||||
{hasSelection && (
|
||||
<>
|
||||
<div className="floating-toolbar-separator" />
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => handleFormat("||", "||")}
|
||||
title="Скрытый текст"
|
||||
>
|
||||
<Icon icon="mdi:eye-off" />
|
||||
</button>
|
||||
<button
|
||||
className={`floating-toolbar-btn ${activeFormats.bold ? "active" : ""}`}
|
||||
onClick={() => handleFormat("**", "**")}
|
||||
title="Жирный"
|
||||
>
|
||||
<Icon icon="mdi:format-bold" />
|
||||
</button>
|
||||
<button
|
||||
className={`floating-toolbar-btn ${
|
||||
activeFormats.italic ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleFormat("*", "*")}
|
||||
title="Курсив"
|
||||
>
|
||||
<Icon icon="mdi:format-italic" />
|
||||
</button>
|
||||
<button
|
||||
className={`floating-toolbar-btn ${
|
||||
activeFormats.strikethrough ? "active" : ""
|
||||
}`}
|
||||
onClick={() => handleFormat("~~", "~~")}
|
||||
title="Зачеркнутый"
|
||||
>
|
||||
<Icon icon="mdi:format-strikethrough" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => handleFormat("`", "`")}
|
||||
title="Код"
|
||||
>
|
||||
<Icon icon="mdi:code-tags" />
|
||||
</button>
|
||||
<div className="floating-toolbar-separator" />
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => handleFormat("> ", "")}
|
||||
title="Цитата"
|
||||
>
|
||||
<Icon icon="mdi:format-quote-close" />
|
||||
</button>
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => onInsertColor?.()}
|
||||
title="Цвет текста"
|
||||
>
|
||||
<Icon icon="mdi:palette" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => handleFormat("||", "||")}
|
||||
title="Скрытый текст"
|
||||
>
|
||||
<Icon icon="mdi:eye-off" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => handleFormat("`", "`")}
|
||||
title="Код"
|
||||
>
|
||||
<Icon icon="mdi:code-tags" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={() => handleFormat("> ", "")}
|
||||
title="Цитата"
|
||||
>
|
||||
<Icon icon="mdi:format-quote-close" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -21,6 +21,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@ -41,10 +45,54 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
};
|
||||
}, [showHeaderDropdown]);
|
||||
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
// Не начинаем перетаскивание если кликнули на кнопку
|
||||
if ((e.target as HTMLElement).closest('.btnMarkdown')) return;
|
||||
|
||||
if (!containerRef.current) return;
|
||||
setIsDragging(true);
|
||||
setStartX(e.pageX - containerRef.current.offsetLeft);
|
||||
setScrollLeft(containerRef.current.scrollLeft);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging || !containerRef.current) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - containerRef.current.offsetLeft;
|
||||
const walk = (x - startX) * 2; // Увеличиваем скорость прокрутки
|
||||
containerRef.current.scrollLeft = scrollLeft - walk;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// Обработчики для document чтобы отслеживать mouseMove и mouseUp даже вне элемента
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
} else {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
const buttons = [];
|
||||
|
||||
return (
|
||||
<div className="markdown-buttons">
|
||||
<div
|
||||
className="markdown-buttons"
|
||||
ref={containerRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isDragging ? 'grabbing' : (containerRef.current && containerRef.current.scrollWidth > containerRef.current.clientWidth ? 'grab' : 'default') }}
|
||||
>
|
||||
{buttons.map((btn) => (
|
||||
<button
|
||||
key={btn.id}
|
||||
|
||||
@ -21,6 +21,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 });
|
||||
const [hasSelection, setHasSelection] = useState(false);
|
||||
const [activeFormats, setActiveFormats] = useState({
|
||||
bold: false,
|
||||
italic: false,
|
||||
@ -530,15 +531,11 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const hasSelection = start !== end;
|
||||
|
||||
// Если нет выделения, скрываем toolbar
|
||||
if (start === end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Используем середину выделения для позиционирования
|
||||
const midPosition = Math.floor((start + end) / 2);
|
||||
const text = textarea.value.substring(0, midPosition);
|
||||
// Используем середину выделения или позицию курсора для позиционирования
|
||||
const position = hasSelection ? Math.floor((start + end) / 2) : start;
|
||||
const text = textarea.value.substring(0, position);
|
||||
const lines = text.split("\n");
|
||||
const lineNumber = lines.length - 1;
|
||||
const currentLineText = lines[lines.length - 1];
|
||||
@ -563,12 +560,12 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const textWidth = measureEl.offsetWidth;
|
||||
document.body.removeChild(measureEl);
|
||||
|
||||
// Вычисляем позицию (середина выделения)
|
||||
// Вычисляем позицию (середина выделения или позиция курсора)
|
||||
const top =
|
||||
rect.top + paddingTop + lineNumber * lineHeight + lineHeight / 2;
|
||||
const left = rect.left + paddingLeft + textWidth;
|
||||
|
||||
return { top, left };
|
||||
return { top, left, hasSelection };
|
||||
}, []);
|
||||
|
||||
// Обработчик выделения текста
|
||||
@ -578,18 +575,27 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли текст в редакторе
|
||||
const hasText = content.trim().length > 0;
|
||||
|
||||
const position = getCursorPosition();
|
||||
if (position) {
|
||||
setToolbarPosition(position);
|
||||
if (position && hasText) {
|
||||
setToolbarPosition({ top: position.top, left: position.left });
|
||||
setHasSelection(position.hasSelection);
|
||||
setShowFloatingToolbar(true);
|
||||
// Определяем активные форматы
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
// Определяем активные форматы только если есть выделение
|
||||
if (position.hasSelection) {
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
} else {
|
||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||
}
|
||||
} else {
|
||||
setShowFloatingToolbar(false);
|
||||
setHasSelection(false);
|
||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||
}
|
||||
}, [isPreviewMode, getCursorPosition, getActiveFormats]);
|
||||
}, [isPreviewMode, content, getCursorPosition, getActiveFormats]);
|
||||
|
||||
// Отслеживание выделения текста
|
||||
useEffect(() => {
|
||||
@ -611,16 +617,36 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
setTimeout(handleSelection, 0);
|
||||
};
|
||||
|
||||
// Предотвращаем появление контекстного меню браузера при выделении текста
|
||||
// Но разрешаем его, если редактор пустой (чтобы можно было вставить текст)
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Проверяем, что событие происходит в textarea
|
||||
if (target === textarea || textarea.contains(target)) {
|
||||
const hasText = textarea.value.trim().length > 0;
|
||||
const hasSelection = textarea.selectionStart !== textarea.selectionEnd;
|
||||
// Блокируем браузерное меню только если есть текст И есть выделение
|
||||
if (hasText && hasSelection) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
textarea.addEventListener("mouseup", handleMouseUp);
|
||||
textarea.addEventListener("mousemove", handleMouseMove);
|
||||
textarea.addEventListener("keyup", handleKeyUp);
|
||||
textarea.addEventListener("contextmenu", handleContextMenu);
|
||||
document.addEventListener("selectionchange", handleSelection);
|
||||
document.addEventListener("contextmenu", handleContextMenu, true);
|
||||
|
||||
return () => {
|
||||
textarea.removeEventListener("mouseup", handleMouseUp);
|
||||
textarea.removeEventListener("mousemove", handleMouseMove);
|
||||
textarea.removeEventListener("keyup", handleKeyUp);
|
||||
textarea.removeEventListener("contextmenu", handleContextMenu);
|
||||
document.removeEventListener("selectionchange", handleSelection);
|
||||
document.removeEventListener("contextmenu", handleContextMenu, true);
|
||||
};
|
||||
}, [isPreviewMode, handleSelection]);
|
||||
|
||||
@ -660,10 +686,13 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const handleScroll = () => {
|
||||
const position = getCursorPosition();
|
||||
if (position) {
|
||||
setToolbarPosition(position);
|
||||
// Обновляем активные форматы при прокрутке
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
setToolbarPosition({ top: position.top, left: position.left });
|
||||
setHasSelection(position.hasSelection);
|
||||
// Обновляем активные форматы при прокрутке только если есть выделение
|
||||
if (position.hasSelection) {
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -796,6 +825,19 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onContextMenu={(e) => {
|
||||
// Предотвращаем появление контекстного меню браузера при выделении текста
|
||||
// Но разрешаем его, если редактор пустой (чтобы можно было вставить текст)
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
const hasText = textarea.value.trim().length > 0;
|
||||
const hasSelection = textarea.selectionStart !== textarea.selectionEnd;
|
||||
// Блокируем браузерное меню только если есть текст И есть выделение
|
||||
if (hasText && hasSelection) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FloatingToolbar
|
||||
textareaRef={textareaRef}
|
||||
@ -805,6 +847,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
onHide={() => setShowFloatingToolbar(false)}
|
||||
onInsertColor={insertColorMarkdown}
|
||||
activeFormats={activeFormats}
|
||||
hasSelection={hasSelection}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -46,6 +46,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 });
|
||||
const [hasSelection, setHasSelection] = useState(false);
|
||||
const [activeFormats, setActiveFormats] = useState({
|
||||
bold: false,
|
||||
italic: false,
|
||||
@ -484,15 +485,11 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const hasSelection = start !== end;
|
||||
|
||||
// Если нет выделения, скрываем toolbar
|
||||
if (start === end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Используем середину выделения для позиционирования
|
||||
const midPosition = Math.floor((start + end) / 2);
|
||||
const text = textarea.value.substring(0, midPosition);
|
||||
// Используем середину выделения или позицию курсора для позиционирования
|
||||
const position = hasSelection ? Math.floor((start + end) / 2) : start;
|
||||
const text = textarea.value.substring(0, position);
|
||||
const lines = text.split("\n");
|
||||
const lineNumber = lines.length - 1;
|
||||
const currentLineText = lines[lines.length - 1];
|
||||
@ -517,12 +514,12 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const textWidth = measureEl.offsetWidth;
|
||||
document.body.removeChild(measureEl);
|
||||
|
||||
// Вычисляем позицию (середина выделения)
|
||||
// Вычисляем позицию (середина выделения или позиция курсора)
|
||||
const top =
|
||||
rect.top + paddingTop + lineNumber * lineHeight + lineHeight / 2;
|
||||
const left = rect.left + paddingLeft + textWidth;
|
||||
|
||||
return { top, left };
|
||||
return { top, left, hasSelection };
|
||||
}, []);
|
||||
|
||||
// Обработчик выделения текста
|
||||
@ -532,18 +529,27 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли текст в редакторе
|
||||
const hasText = editContent.trim().length > 0;
|
||||
|
||||
const position = getCursorPosition();
|
||||
if (position) {
|
||||
setToolbarPosition(position);
|
||||
if (position && hasText) {
|
||||
setToolbarPosition({ top: position.top, left: position.left });
|
||||
setHasSelection(position.hasSelection);
|
||||
setShowFloatingToolbar(true);
|
||||
// Определяем активные форматы
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
// Определяем активные форматы только если есть выделение
|
||||
if (position.hasSelection) {
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
} else {
|
||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||
}
|
||||
} else {
|
||||
setShowFloatingToolbar(false);
|
||||
setHasSelection(false);
|
||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||
}
|
||||
}, [localPreviewMode, getCursorPosition, getActiveFormats]);
|
||||
}, [localPreviewMode, editContent, getCursorPosition, getActiveFormats]);
|
||||
|
||||
const handleImageButtonClick = () => {
|
||||
imageInputRef.current?.click();
|
||||
@ -768,16 +774,36 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
setTimeout(handleSelection, 0);
|
||||
};
|
||||
|
||||
// Предотвращаем появление контекстного меню браузера при выделении текста
|
||||
// Но разрешаем его, если редактор пустой (чтобы можно было вставить текст)
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Проверяем, что событие происходит в textarea
|
||||
if (target === textarea || textarea.contains(target)) {
|
||||
const hasText = textarea.value.trim().length > 0;
|
||||
const hasSelection = textarea.selectionStart !== textarea.selectionEnd;
|
||||
// Блокируем браузерное меню только если есть текст И есть выделение
|
||||
if (hasText && hasSelection) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
textarea.addEventListener("mouseup", handleMouseUp);
|
||||
textarea.addEventListener("mousemove", handleMouseMove);
|
||||
textarea.addEventListener("keyup", handleKeyUp);
|
||||
textarea.addEventListener("contextmenu", handleContextMenu);
|
||||
document.addEventListener("selectionchange", handleSelection);
|
||||
document.addEventListener("contextmenu", handleContextMenu, true);
|
||||
|
||||
return () => {
|
||||
textarea.removeEventListener("mouseup", handleMouseUp);
|
||||
textarea.removeEventListener("mousemove", handleMouseMove);
|
||||
textarea.removeEventListener("keyup", handleKeyUp);
|
||||
textarea.removeEventListener("contextmenu", handleContextMenu);
|
||||
document.removeEventListener("selectionchange", handleSelection);
|
||||
document.removeEventListener("contextmenu", handleContextMenu, true);
|
||||
};
|
||||
}, [isEditing, isPreviewMode, handleSelection]);
|
||||
|
||||
@ -819,10 +845,13 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const handleScroll = () => {
|
||||
const position = getCursorPosition();
|
||||
if (position) {
|
||||
setToolbarPosition(position);
|
||||
// Обновляем активные форматы при прокрутке
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
setToolbarPosition({ top: position.top, left: position.left });
|
||||
setHasSelection(position.hasSelection);
|
||||
// Обновляем активные форматы при прокрутке только если есть выделение
|
||||
if (position.hasSelection) {
|
||||
const formats = getActiveFormats();
|
||||
setActiveFormats(formats);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1015,6 +1044,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
onKeyDown={handleEditKeyDown}
|
||||
style={{ minHeight: "100px" }}
|
||||
onContextMenu={(e) => {
|
||||
// Предотвращаем появление контекстного меню браузера при выделении текста
|
||||
// Но разрешаем его, если редактор пустой (чтобы можно было вставить текст)
|
||||
const textarea = editTextareaRef.current;
|
||||
if (textarea) {
|
||||
const hasText = textarea.value.trim().length > 0;
|
||||
const hasSelection = textarea.selectionStart !== textarea.selectionEnd;
|
||||
// Блокируем браузерное меню только если есть текст И есть выделение
|
||||
if (hasText && hasSelection) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FloatingToolbar
|
||||
textareaRef={editTextareaRef}
|
||||
@ -1024,6 +1066,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
onHide={() => setShowFloatingToolbar(false)}
|
||||
onInsertColor={insertColorMarkdown}
|
||||
activeFormats={activeFormats}
|
||||
hasSelection={hasSelection}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||||
import { toggleTheme, setTheme } from "../store/slices/uiSlice";
|
||||
import { setAccentColor } from "../utils/colorUtils";
|
||||
|
||||
export const useTheme = () => {
|
||||
const theme = useAppSelector((state) => state.ui.theme);
|
||||
@ -9,13 +10,13 @@ export const useTheme = () => {
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
document.documentElement.style.setProperty("--accent-color", accentColor);
|
||||
setAccentColor(accentColor);
|
||||
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute(
|
||||
"content",
|
||||
theme === "dark" ? "#1a1a1a" : "#007bff"
|
||||
theme === "dark" ? "#1a1a1a" : accentColor
|
||||
);
|
||||
}
|
||||
}, [theme, accentColor]);
|
||||
|
||||
@ -11,7 +11,6 @@ const LoginPage: React.FC = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const { showNotification } = useNotification();
|
||||
@ -27,20 +26,19 @@ const LoginPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
// Проверяем наличие ошибки в URL
|
||||
if (searchParams.get("error") === "invalid_password") {
|
||||
setErrorMessage("Неверный пароль!");
|
||||
showNotification("Неверный пароль!", "error");
|
||||
}
|
||||
}, [searchParams]);
|
||||
}, [searchParams, showNotification]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!username.trim() || !password) {
|
||||
setErrorMessage("Логин и пароль обязательны");
|
||||
showNotification("Логин и пароль обязательны", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
console.log("Attempting login...");
|
||||
@ -59,7 +57,7 @@ const LoginPage: React.FC = () => {
|
||||
showNotification("Успешный вход!", "success");
|
||||
navigate("/notes");
|
||||
} else {
|
||||
setErrorMessage(data.error || "Ошибка входа");
|
||||
showNotification(data.error || "Ошибка входа", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Login error details:", error);
|
||||
@ -81,7 +79,6 @@ const LoginPage: React.FC = () => {
|
||||
errorMsg = error.message || "Ошибка соединения с сервером";
|
||||
}
|
||||
|
||||
setErrorMessage(errorMsg);
|
||||
showNotification(errorMsg, "error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@ -130,11 +127,6 @@ const LoginPage: React.FC = () => {
|
||||
placeholder="Введите пароль"
|
||||
/>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className="error-message" style={{ display: "block" }}>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" className="btnSave" disabled={isLoading}>
|
||||
{isLoading ? "Вход..." : "Войти"}
|
||||
</button>
|
||||
|
||||
@ -12,7 +12,6 @@ const RegisterPage: React.FC = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const { showNotification } = useNotification();
|
||||
@ -29,27 +28,26 @@ const RegisterPage: React.FC = () => {
|
||||
|
||||
// Клиентская валидация
|
||||
if (!username.trim() || !password || !confirmPassword) {
|
||||
setErrorMessage("Все поля обязательны");
|
||||
showNotification("Все поля обязательны", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.length < 3) {
|
||||
setErrorMessage("Логин должен быть не менее 3 символов");
|
||||
showNotification("Логин должен быть не менее 3 символов", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setErrorMessage("Пароль должен быть не менее 6 символов");
|
||||
showNotification("Пароль должен быть не менее 6 символов", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setErrorMessage("Пароли не совпадают");
|
||||
showNotification("Пароли не совпадают", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
console.log("Attempting registration...");
|
||||
@ -68,7 +66,7 @@ const RegisterPage: React.FC = () => {
|
||||
showNotification("Регистрация успешна!", "success");
|
||||
navigate("/notes");
|
||||
} else {
|
||||
setErrorMessage(data.error || "Ошибка регистрации");
|
||||
showNotification(data.error || "Ошибка регистрации", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Register error details:", error);
|
||||
@ -90,7 +88,6 @@ const RegisterPage: React.FC = () => {
|
||||
errorMsg = error.message || "Ошибка соединения с сервером";
|
||||
}
|
||||
|
||||
setErrorMessage(errorMsg);
|
||||
showNotification(errorMsg, "error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@ -151,11 +148,6 @@ const RegisterPage: React.FC = () => {
|
||||
placeholder="Подтвердите пароль"
|
||||
/>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className="error-message" style={{ display: "block" }}>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" className="btnSave" disabled={isLoading}>
|
||||
{isLoading ? "Регистрация..." : "Зарегистрироваться"}
|
||||
</button>
|
||||
|
||||
@ -6,7 +6,8 @@ import { userApi } from "../api/userApi";
|
||||
import { notesApi, logsApi, Log } from "../api/notesApi";
|
||||
import { Note } from "../types/note";
|
||||
import { setUser, setAiSettings } from "../store/slices/profileSlice";
|
||||
import { setAccentColor } from "../store/slices/uiSlice";
|
||||
import { setAccentColor as setAccentColorAction } from "../store/slices/uiSlice";
|
||||
import { setAccentColor } from "../utils/colorUtils";
|
||||
import { useNotification } from "../hooks/useNotification";
|
||||
import { Modal } from "../components/common/Modal";
|
||||
import { ThemeToggle } from "../components/common/ThemeToggle";
|
||||
@ -79,8 +80,8 @@ const SettingsPage: React.FC = () => {
|
||||
dispatch(setUser(userData));
|
||||
const accent = userData.accent_color || "#007bff";
|
||||
setSelectedAccentColor(accent);
|
||||
dispatch(setAccentColor(accent));
|
||||
document.documentElement.style.setProperty("--accent-color", accent);
|
||||
dispatch(setAccentColorAction(accent));
|
||||
setAccentColor(accent);
|
||||
|
||||
// Загружаем AI настройки
|
||||
try {
|
||||
@ -112,11 +113,8 @@ const SettingsPage: React.FC = () => {
|
||||
await userApi.updateProfile({
|
||||
accent_color: selectedAccentColor,
|
||||
});
|
||||
dispatch(setAccentColor(selectedAccentColor));
|
||||
document.documentElement.style.setProperty(
|
||||
"--accent-color",
|
||||
selectedAccentColor
|
||||
);
|
||||
dispatch(setAccentColorAction(selectedAccentColor));
|
||||
setAccentColor(selectedAccentColor);
|
||||
await loadUserInfo();
|
||||
showNotification("Цветовой акцент успешно обновлен", "success");
|
||||
} catch (error: any) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
:root {
|
||||
--accent-color: #007bff;
|
||||
--accent-color-rgb: 0, 123, 255;
|
||||
|
||||
/* Светлая тема (по умолчанию) */
|
||||
--bg-primary: #f5f5f5;
|
||||
@ -15,7 +16,7 @@
|
||||
|
||||
--border-primary: #e0e0e0;
|
||||
--border-secondary: #dddddd;
|
||||
--border-focus: #007bff;
|
||||
--border-focus: var(--accent-color);
|
||||
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: rgba(0, 0, 0, 0.15);
|
||||
@ -43,7 +44,7 @@
|
||||
|
||||
--border-primary: #404040;
|
||||
--border-secondary: #555555;
|
||||
--border-focus: #4a9eff;
|
||||
--border-focus: var(--accent-color);
|
||||
|
||||
--shadow-light: rgba(0, 0, 0, 0.3);
|
||||
--shadow-medium: rgba(0, 0, 0, 0.4);
|
||||
@ -62,64 +63,6 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent-color: #007bff;
|
||||
|
||||
/* Светлая тема (по умолчанию) */
|
||||
--bg-primary: #f5f5f5;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #f8f9fa;
|
||||
--bg-quaternary: #e9ecef;
|
||||
--bg-hover: #e7f3ff;
|
||||
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-muted: #999999;
|
||||
--text-light: #757575;
|
||||
|
||||
--border-primary: #e0e0e0;
|
||||
--border-secondary: #dddddd;
|
||||
--border-focus: #007bff;
|
||||
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Цвета иконок */
|
||||
--icon-search: #2196f3;
|
||||
--icon-tags: #4caf50;
|
||||
--icon-notes: #ff9800;
|
||||
--icon-user: #9c27b0;
|
||||
--icon-danger: #dc3545;
|
||||
}
|
||||
|
||||
/* Темная тема */
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-tertiary: #3a3a3a;
|
||||
--bg-quaternary: #4a4a4a;
|
||||
--bg-hover: #2a4a6b;
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #cccccc;
|
||||
--text-muted: #999999;
|
||||
--text-light: #aaaaaa;
|
||||
|
||||
--border-primary: #404040;
|
||||
--border-secondary: #555555;
|
||||
--border-focus: #4a9eff;
|
||||
|
||||
--shadow-light: rgba(0, 0, 0, 0.3);
|
||||
--shadow-medium: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Цвета иконок в темной теме */
|
||||
--icon-search: #64b5f6;
|
||||
--icon-tags: #81c784;
|
||||
--icon-notes: #ffb74d;
|
||||
--icon-user: #ba68c8;
|
||||
--icon-danger: #f44336;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
padding: 0;
|
||||
@ -415,7 +358,7 @@ header {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
background-color: var(--bg-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.25);
|
||||
}
|
||||
|
||||
.clear-search-btn {
|
||||
@ -491,7 +434,7 @@ header {
|
||||
|
||||
.user-avatar-mini:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
|
||||
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.user-avatar-mini img {
|
||||
@ -617,6 +560,17 @@ header {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
/* Убираем margin-top у заметок, так как gap уже обеспечивает отступы */
|
||||
.notes-container .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Добавляем отступ сверху для контейнера заметок, чтобы расстояние между блоком "Мои заметки" и первой заметкой было таким же, как между заметками */
|
||||
.notes-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@ -648,7 +602,7 @@ header {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
background-color: var(--bg-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.25);
|
||||
}
|
||||
|
||||
/* Toggle Switch Styles */
|
||||
@ -787,6 +741,11 @@ textarea {
|
||||
margin-bottom: 5px;
|
||||
overflow-y: hidden;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
/* Разрешаем выделение текста */
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
@ -1135,7 +1094,7 @@ textarea:focus {
|
||||
|
||||
/* Hover эффект для чекбоксов */
|
||||
.textNote .task-list-item:hover {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
background-color: rgba(var(--accent-color-rgb), 0.05);
|
||||
border-radius: 4px;
|
||||
padding-left: 4px;
|
||||
margin-left: -24px;
|
||||
@ -1173,7 +1132,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.textNote li:has(input[type="checkbox"]):hover {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
background-color: rgba(var(--accent-color-rgb), 0.05);
|
||||
border-radius: 4px;
|
||||
padding-left: 4px;
|
||||
margin-left: -24px;
|
||||
@ -1185,6 +1144,8 @@ textarea:focus {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 60px; /* Отступ снизу для footer */
|
||||
margin-top: 16px; /* Отступ сверху для одинакового расстояния между блоком "Мои заметки" и первой заметкой */
|
||||
gap: 16px; /* Отступ между заметками */
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@ -1199,19 +1160,32 @@ textarea:focus {
|
||||
.markdown-buttons {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.markdown-buttons::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Кнопки markdown в редакторе редактирования: те же отступы и поведение */
|
||||
.markdown-buttons.markdown-buttons--edit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.markdown-buttons.markdown-buttons--edit::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-buttons.markdown-buttons--edit .btnMarkdown {
|
||||
@ -1328,6 +1302,21 @@ textarea:focus {
|
||||
}
|
||||
|
||||
/* Плавающая панель форматирования */
|
||||
.floating-toolbar-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
/* Скрываем скроллбар */
|
||||
scrollbar-width: none;
|
||||
/* Плавная прокрутка */
|
||||
scroll-behavior: smooth;
|
||||
/* Максимальная ширина с учетом отступов */
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
|
||||
.floating-toolbar-wrapper::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.floating-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
@ -1338,6 +1327,10 @@ textarea:focus {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
align-items: center;
|
||||
/* Предотвращаем сжатие кнопок */
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@ -1375,6 +1368,12 @@ textarea:focus {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.floating-toolbar-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.floating-toolbar-btn .iconify {
|
||||
font-size: 18px;
|
||||
}
|
||||
@ -1560,7 +1559,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background-color: #007bff;
|
||||
background-color: var(--accent-color, #007bff);
|
||||
}
|
||||
|
||||
/* Состояние видимости уведомления */
|
||||
@ -1632,7 +1631,7 @@ textarea:focus {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* Мини-календарь */
|
||||
/* Мини-календарь - базовые стили для всех устройств */
|
||||
.mini-calendar {
|
||||
width: 100%;
|
||||
}
|
||||
@ -1648,6 +1647,8 @@ textarea:focus {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
@ -1661,7 +1662,8 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.calendar-nav:hover {
|
||||
color: #0056b3;
|
||||
color: var(--accent-color, #007bff);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.calendar-weekdays {
|
||||
@ -1715,9 +1717,76 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.calendar-day.selected {
|
||||
background-color: #0056b3;
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Стили календаря для PC версии (темный стиль как на изображении) */
|
||||
@media (min-width: 769px) {
|
||||
.mini-calendar,
|
||||
.container-leftside .mini-calendar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.calendar-month-year {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
font-size: 20px;
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.calendar-nav:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.calendar-weekdays {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.calendar-weekday {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.calendar-day.other-month {
|
||||
color: #999999;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.calendar-day.selected {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
opacity: 1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
/* Индикатор для дней с заметками (зеленый кружок) */
|
||||
@ -1797,38 +1866,61 @@ textarea:focus {
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Темная тема для календаря */
|
||||
[data-theme="dark"] .calendar-day {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
/* Темная тема для календаря (PC версия) */
|
||||
@media (min-width: 769px) {
|
||||
[data-theme="dark"] .mini-calendar,
|
||||
[data-theme="dark"] .container-leftside .mini-calendar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day:hover {
|
||||
background-color: var(--bg-quaternary);
|
||||
}
|
||||
[data-theme="dark"] .calendar-day {
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day.other-month {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
[data-theme="dark"] .calendar-day:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day.today {
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
}
|
||||
[data-theme="dark"] .calendar-day.other-month {
|
||||
background-color: transparent;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day.selected {
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
opacity: 0.9;
|
||||
}
|
||||
[data-theme="dark"] .calendar-day.today {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-nav:hover {
|
||||
color: var(--border-focus);
|
||||
}
|
||||
[data-theme="dark"] .calendar-day.selected {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-weekday {
|
||||
color: var(--text-secondary);
|
||||
[data-theme="dark"] .calendar-nav {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-nav:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-weekday {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-month-year {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day.has-notes::after {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day.has-edited-notes::before {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
}
|
||||
|
||||
/* Индикаторы заметок в темной теме */
|
||||
@ -1918,7 +2010,7 @@ textarea:focus {
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
|
||||
box-shadow: 0 2px 4px rgba(var(--accent-color-rgb), 0.3);
|
||||
}
|
||||
|
||||
/* Стили для подсветки результатов поиска */
|
||||
@ -2047,7 +2139,8 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.mobile-sidebar .calendar-nav:hover {
|
||||
color: #0056b3;
|
||||
color: var(--accent-color, #007bff);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Календарь дней в слайдере */
|
||||
@ -2085,9 +2178,10 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.mobile-sidebar .calendar-day.selected {
|
||||
background-color: #0056b3;
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.mobile-sidebar .calendar-day.other-month {
|
||||
@ -2171,7 +2265,7 @@ textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color, #007bff);
|
||||
background-color: var(--bg-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.25);
|
||||
}
|
||||
|
||||
/* Теги в слайдере */
|
||||
@ -2312,10 +2406,25 @@ textarea:focus {
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Уменьшаем отступ сверху для первого блока "Мои заметки" на мобильных */
|
||||
.center > .container:first-child {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Убираем margin-top у заметок на мобильных, так как gap уже обеспечивает отступы */
|
||||
.notes-container .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Добавляем отступ сверху для контейнера заметок на мобильных */
|
||||
.notes-container {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Адаптируем заголовок заметок */
|
||||
.notes-header {
|
||||
@ -2374,10 +2483,17 @@ textarea:focus {
|
||||
/* Адаптируем кнопки markdown */
|
||||
.markdown-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.markdown-buttons::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-buttons .btnMarkdown {
|
||||
@ -2407,7 +2523,8 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.btnSave,
|
||||
.btnAI {
|
||||
.btnAI,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
@ -2650,11 +2767,11 @@ textarea:focus {
|
||||
/* Специальные стили для темной темы в предпросмотре */
|
||||
[data-theme="dark"] .note-preview-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-color, #4a9eff);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-preview-header {
|
||||
color: var(--accent-color, #4a9eff);
|
||||
color: var(--accent-color);
|
||||
border-bottom-color: var(--border-primary);
|
||||
}
|
||||
|
||||
@ -2683,7 +2800,7 @@ textarea:focus {
|
||||
|
||||
[data-theme="dark"] .note-preview-content blockquote {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-left-color: var(--accent-color, #4a9eff);
|
||||
border-left-color: var(--accent-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@ -2749,7 +2866,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.note-preview-content .task-list-item:hover {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
background-color: rgba(var(--accent-color-rgb), 0.05);
|
||||
border-radius: 4px;
|
||||
padding-left: 4px;
|
||||
margin-left: -24px;
|
||||
@ -2799,7 +2916,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.note-preview-content li:has(input[type="checkbox"]):hover {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
background-color: rgba(var(--accent-color-rgb), 0.05);
|
||||
border-radius: 4px;
|
||||
padding-left: 4px;
|
||||
margin-left: -24px;
|
||||
@ -3399,7 +3516,7 @@ textarea:focus {
|
||||
|
||||
.settings-tab:hover {
|
||||
color: var(--accent-color, #007bff);
|
||||
background: rgba(0, 123, 255, 0.05);
|
||||
background: rgba(var(--accent-color-rgb), 0.05);
|
||||
}
|
||||
|
||||
.settings-tab.active {
|
||||
@ -3496,9 +3613,9 @@ textarea:focus {
|
||||
|
||||
/* Стили для новых кнопок */
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
border: 1px solid #007bff;
|
||||
border: 1px solid var(--accent-color, #007bff);
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
@ -3510,8 +3627,10 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #004085;
|
||||
background-color: var(--accent-color, #007bff);
|
||||
border-color: var(--accent-color, #007bff);
|
||||
opacity: 0.9;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
@ -4336,15 +4455,26 @@ textarea:focus {
|
||||
max-width: 100%;
|
||||
margin-top: 60px; /* Отступ для кнопки меню */
|
||||
}
|
||||
|
||||
|
||||
/* Уменьшаем отступ сверху для первого блока "Мои заметки" на мобильных */
|
||||
.center > .container:first-child {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Убираем margin-top у заметок на мобильных, так как gap уже обеспечивает отступы */
|
||||
.notes-container .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Заметки */
|
||||
.notes-container {
|
||||
padding-bottom: 80px;
|
||||
margin-top: 16px; /* Отступ сверху для одинакового расстояния между блоком "Мои заметки" и первой заметкой на мобильных */
|
||||
gap: 16px; /* Отступ между заметками на мобильных */
|
||||
}
|
||||
|
||||
/* Header адаптация */
|
||||
.notes-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@ -4357,7 +4487,6 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.user-info {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@ -4378,7 +4507,7 @@ textarea:focus {
|
||||
/* Textarea */
|
||||
.textInput {
|
||||
font-size: 14px;
|
||||
min-height: 150px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Изображения */
|
||||
@ -4433,7 +4562,4 @@ textarea:focus {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.notes-header-left span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
27
src/utils/colorUtils.ts
Normal file
27
src/utils/colorUtils.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Конвертирует hex цвет в RGB значения
|
||||
* @param hex - hex цвет (например, "#007bff" или "007bff")
|
||||
* @returns строка с RGB значениями (например, "0, 123, 255")
|
||||
*/
|
||||
export const hexToRgb = (hex: string): string => {
|
||||
// Убираем # если есть
|
||||
const cleanHex = hex.replace("#", "");
|
||||
|
||||
// Парсим hex
|
||||
const r = parseInt(cleanHex.substring(0, 2), 16);
|
||||
const g = parseInt(cleanHex.substring(2, 4), 16);
|
||||
const b = parseInt(cleanHex.substring(4, 6), 16);
|
||||
|
||||
return `${r}, ${g}, ${b}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Устанавливает цвет акцента и его RGB значение в CSS переменные
|
||||
* @param color - hex цвет акцента
|
||||
*/
|
||||
export const setAccentColor = (color: string) => {
|
||||
document.documentElement.style.setProperty("--accent-color", color);
|
||||
const rgb = hexToRgb(color);
|
||||
document.documentElement.style.setProperty("--accent-color-rgb", rgb);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user