diff --git a/.cursor/rules/rules.mdc b/.cursor/rules/rules.mdc new file mode 100644 index 0000000..a31ed33 --- /dev/null +++ b/.cursor/rules/rules.mdc @@ -0,0 +1,5 @@ +--- +alwaysApply: true +--- + +Никогда самостоятельно не пытайся запустить сервер. Если это требуется, то попроси это сделать меня! \ No newline at end of file diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 0000000..77e9744 --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,5 @@ +{ + "setup-worktree": [ + "npm install" + ] +} diff --git a/backend/public/index.html b/backend/public/index.html index e6e691b..628d24e 100644 --- a/backend/public/index.html +++ b/backend/public/index.html @@ -3,7 +3,52 @@ + NoteJS Backend + + + + = ({ @@ -23,40 +24,109 @@ export const FloatingToolbar: React.FC = ({ onHide, onInsertColor, activeFormats = {}, + hasSelection = false, }) => { const toolbarRef = useRef(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 = ({ }, 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 (
toolbarRef.current.clientWidth ? 'grab' : 'default'), }} onMouseDown={(e) => { // Предотвращаем потерю выделения при клике на toolbar e.preventDefault(); + handleMouseDown(e); + }} + onContextMenu={(e) => { + // Предотвращаем появление контекстного меню браузера на toolbar + e.preventDefault(); }} > - - - +
+ -
+ {hasSelection && ( + <> + + + + + )} - + {hasSelection && ( + <> +
- + + + - +
- + + + + + + + + + )} +
); }; diff --git a/src/components/notes/MarkdownToolbar.tsx b/src/components/notes/MarkdownToolbar.tsx index 83ff7c6..b25316d 100644 --- a/src/components/notes/MarkdownToolbar.tsx +++ b/src/components/notes/MarkdownToolbar.tsx @@ -21,6 +21,10 @@ export const MarkdownToolbar: React.FC = ({ const [showHeaderDropdown, setShowHeaderDropdown] = useState(false); const dispatch = useAppDispatch(); const dropdownRef = useRef(null); + const containerRef = useRef(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 = ({ }; }, [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 ( -
+
containerRef.current.clientWidth ? 'grab' : 'default') }} + > {buttons.map((btn) => (
- {errorMessage && ( -
- {errorMessage} -
- )} diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx index d96417f..e5b48ea 100644 --- a/src/pages/RegisterPage.tsx +++ b/src/pages/RegisterPage.tsx @@ -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="Подтвердите пароль" />
- {errorMessage && ( -
- {errorMessage} -
- )} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 7e2536f..7d7d32f 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -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) { diff --git a/src/styles/style.css b/src/styles/style.css index 23a454a..0735660 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -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; - } } diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts new file mode 100644 index 0000000..748c705 --- /dev/null +++ b/src/utils/colorUtils.ts @@ -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); +}; +