/
Starolat
/
DeepDive
Обзор
Документация
Войти
/
Starolat
/
DeepDive
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
js/theme.js
628 строк
19 KB
Starolat Sergei
feat: меню функций в шапке вместо ряда иконок
30 июл 2026, 15:56
30 июл 2026, 15:56
e6bb0eb
Код
Авторство
О чём код?
// @ts-check /** * Модуль управления темами (Consta Design System) * Только 2 темы: consta-light и consta-dark * Переключение по клику на кнопку в шапке * @version 4.1.0 */ import { Close } from "./components/ConstaIcons.js"; import { TargetLucide, PanelLeftRightDashed, PanelRightOpen, PanelRightClose, SunLucide, MoonLucide, SettingsLucide, DownloadLucide, FolderOpenLucide, SaveLucide, FileTextLucide, BarChart, Truck, GitCompare, MenuLucide, } from "./components/ConstantinIcons.js"; import { LogoGraphHeader } from "./components/LogoIcons.js"; import { ToastManager } from "./components/ToastManager.js"; import config from "./config.js"; import { UserSettingsService } from "./services/UserSettingsService.js"; const THEME_KEY = "deepdive-theme"; const THEME_USS_PATH = "theme"; /** * Использовать ли UserSettingsService для хранения темы. * @private * @returns {boolean} */ function _useUserSettings() { return ( !!config.FEATURE_USER_SETTINGS_SERVICE && UserSettingsService.getInstance().isReady() ); } /** * Прочитать сохранённую тему из актуального хранилища. * @private * @returns {string|null} */ function _getStoredTheme() { if (_useUserSettings()) { return UserSettingsService.getInstance().get(THEME_USS_PATH, null); } try { return localStorage.getItem(THEME_KEY); } catch (e) { return null; } } /** * Сохранить выбранную тему в актуальное хранилище. * @private * @param {string} themeId */ function _saveTheme(themeId) { if (_useUserSettings()) { UserSettingsService.getInstance() .set(THEME_USS_PATH, themeId) .catch((e) => { console.warn("[Theme] Failed to save theme to UserSettingsService:", e); }); return; } try { localStorage.setItem(THEME_KEY, themeId); } catch (e) { console.warn("[Theme] Failed to save theme to localStorage:", e); } } /** * Конфигурация тем */ const THEMES = { "consta-light": { name: "Consta Light", file: "css/themes/consta-light.css", icon: "light_mode", }, "consta-dark": { name: "Consta Dark", file: "css/themes/consta-dark.css", icon: "dark_mode", }, }; /** * Инициализация темы при загрузке страницы */ function initTheme() { const savedTheme = _getStoredTheme(); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; // По умолчанию используем Consta Light (или Dark если системная тема темная) const defaultTheme = prefersDark ? "consta-dark" : "consta-light"; const theme = savedTheme || defaultTheme; // Инжектируем SVG иконки injectIcons(); // Применяем тему applyTheme(theme); // Инициализируем кнопки initThemeButton(); initSettingsButton(); initLoadButton(); initMaterialDeliveryButton(); initOpenFileButton(); initExportButton(); initCompareButton(); initHeaderMenu(); } /** * Инжектирует SVG иконки в DOM элементы */ function injectIcons() { // Logo icon const logoEl = document.getElementById("app-logo-icon"); if (logoEl) logoEl.innerHTML = LogoGraphHeader; // Header menu (hamburger) icon const menuIconEl = document.getElementById("header-menu-icon"); if (menuIconEl) menuIconEl.innerHTML = MenuLucide; // Sun icon const sunEl = document.getElementById("theme-icon-sun"); if (sunEl) sunEl.innerHTML = SunLucide; // Moon icon const moonEl = document.getElementById("theme-icon-moon"); if (moonEl) moonEl.innerHTML = MoonLucide; // Settings icon const settingsEl = document.getElementById("header-settings-icon"); if (settingsEl) settingsEl.innerHTML = SettingsLucide; // Load icon (PrimaveraDataLoader) const loadEl = document.getElementById("header-load-icon"); if (loadEl) loadEl.innerHTML = DownloadLucide; // Material delivery import icon const materialDeliveryEl = document.getElementById("header-material-delivery-icon"); if (materialDeliveryEl) materialDeliveryEl.innerHTML = Truck; // Open file icon const openEl = document.getElementById("header-open-icon"); if (openEl) openEl.innerHTML = FolderOpenLucide; // Compare versions icon const compareEl = document.getElementById("header-compare-icon"); if (compareEl) compareEl.innerHTML = GitCompare; // Export database icon const exportEl = document.getElementById("header-export-icon"); if (exportEl) exportEl.innerHTML = SaveLucide; // Modal close icon const closeEl = document.getElementById("modal-close-icon"); if (closeEl) closeEl.innerHTML = Close; // Zen mode icon const zenEl = document.getElementById("header-zen-icon"); const zenBtnInit = document.getElementById("header-zen-btn"); if (zenEl) zenEl.innerHTML = PanelRightClose; // Project stats icon const reportEl = document.getElementById("header-report-icon"); if (reportEl) reportEl.innerHTML = BarChart; } /** * Применяет указанную тему * @param {string} themeId - ID темы */ function applyTheme(themeId) { const theme = THEMES[themeId]; if (!theme) { console.warn(`Theme "${themeId}" not found`); return; } // Обновляем CSS файл const linkElement = document.getElementById("theme-stylesheet"); if (linkElement) { if (linkElement.getAttribute("href") !== theme.file) { const handleLoad = () => { linkElement.removeEventListener("load", handleLoad); window.dispatchEvent( new CustomEvent("consta-theme-change", { detail: { theme: themeId }, }), ); }; linkElement.addEventListener("load", handleLoad); linkElement.href = theme.file; } else { // Уже загружена — диспатчим сразу window.dispatchEvent( new CustomEvent("consta-theme-change", { detail: { theme: themeId } }), ); } } // Устанавливаем data-theme атрибут для Shadow DOM компонентов document.documentElement.setAttribute("data-theme", themeId); // Сохраняем выбор _saveTheme(themeId); // Обновляем иконку в шапке updateThemeIcon(themeId); console.log(`[Theme] Applied: ${theme.name}`); } /** * Переключает тему между светлой и темной */ function toggleTheme() { const currentTheme = _getStoredTheme() || "consta-light"; const newTheme = currentTheme === "consta-light" ? "consta-dark" : "consta-light"; applyTheme(newTheme); } /** * Обновляет иконку кнопки темы с анимацией (Consta-style) */ function updateThemeIcon(themeId) { const sunIcon = document.getElementById("theme-icon-sun"); const moonIcon = document.getElementById("theme-icon-moon"); const btn = document.getElementById("header-theme-btn"); if (!sunIcon || !moonIcon) return; // При светлой теме: солнце скрыто, луна видима // При темной теме: солнце видимо, луна скрыта const isLight = themeId === "consta-light"; if (isLight) { // Светлая тема: показываем луну (переключение на темную) sunIcon.classList.remove("visible"); sunIcon.classList.add("hidden"); moonIcon.classList.remove("hidden"); moonIcon.classList.add("visible"); } else { // Темная тема: показываем солнце (переключение на светлую) moonIcon.classList.remove("visible"); moonIcon.classList.add("hidden"); sunIcon.classList.remove("hidden"); sunIcon.classList.add("visible"); } // Обновляем title кнопки if (btn) { btn.title = isLight ? "Переключить на темную тему" : "Переключить на светлую тему"; } } /** * Инициализирует кнопку переключения темы в шапке */ function initThemeButton() { const themeBtn = document.getElementById("header-theme-btn"); if (!themeBtn) return; // Устанавливаем начальное состояние иконок без анимации const currentTheme = _getStoredTheme() || "consta-light"; const isLight = currentTheme === "consta-light"; const sunIcon = document.getElementById("theme-icon-sun"); const moonIcon = document.getElementById("theme-icon-moon"); if (sunIcon && moonIcon) { // Отключаем transition для начальной установки sunIcon.style.transition = "none"; moonIcon.style.transition = "none"; if (isLight) { sunIcon.classList.add("hidden"); moonIcon.classList.add("visible"); } else { sunIcon.classList.add("visible"); moonIcon.classList.add("hidden"); } // Включаем transition обратно после небольшой задержки setTimeout(() => { sunIcon.style.transition = ""; moonIcon.style.transition = ""; }, 50); } themeBtn.addEventListener("click", () => { toggleTheme(); }); } /** * Инициализирует кнопку настроек в шапке * (основная логика в app.js) */ function initSettingsButton() { const settingsBtn = document.getElementById("header-settings-btn"); if (!settingsBtn) return; // Добавляем только анимацию, основная логика открытия в app.js settingsBtn.addEventListener("click", () => { // Анимация вращения иконки settingsBtn.classList.add("spinning"); setTimeout(() => { settingsBtn.classList.remove("spinning"); }, 500); }); } /** * Инициализирует выпадающее меню функций в шапке */ function initHeaderMenu() { const container = document.getElementById("header-menu-container"); const menuBtn = document.getElementById("header-menu-btn"); const menu = document.getElementById("header-menu"); if (!container || !menuBtn || !menu) return; const closeMenu = () => { menu.classList.remove("open"); menuBtn.classList.remove("active"); menuBtn.setAttribute("aria-expanded", "false"); }; const openMenu = () => { menu.classList.add("open"); menuBtn.classList.add("active"); menuBtn.setAttribute("aria-expanded", "true"); }; menuBtn.addEventListener("click", (e) => { e.stopPropagation(); if (menu.classList.contains("open")) { closeMenu(); } else { openMenu(); } }); // Закрытие по клику на пункт меню menu.addEventListener("click", (e) => { const item = e.target.closest("button"); if (item) closeMenu(); }); // Закрытие по клику вне меню document.addEventListener("click", (e) => { if (!container.contains(e.target)) closeMenu(); }); // Закрытие по Escape document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeMenu(); }); } /** * Инициализирует кнопку загрузки данных в шапке */ function initLoadButton() { const loadBtn = document.getElementById("header-load-btn"); if (!loadBtn) return; loadBtn.addEventListener("click", () => { const loader = document.getElementById("primavera-data-loader"); if (loader && loader.open) { loader.open(); } }); } /** * Инициализирует кнопку загрузки реестра поставок МТР в шапке */ function initMaterialDeliveryButton() { const btn = document.getElementById("header-material-delivery-btn"); if (!btn) return; btn.addEventListener("click", () => { const modal = document.getElementById("material-delivery-import-modal"); if (modal && modal.open) { modal.open(); } }); } /** * Инициализирует кнопку сравнения версий проекта в шапке (COMP-034) */ function initCompareButton() { const btn = document.getElementById("header-compare-btn"); if (!btn) return; btn.addEventListener("click", () => { const modal = document.getElementById("project-compare-modal"); if (modal && modal.open) { modal.open(); } }); } /** * Инициализирует кнопку открытия файла БД в шапке */ function initOpenFileButton() { const openBtn = document.getElementById("header-open-btn"); const fileInput = document.getElementById("db-file-input"); if (!openBtn || !fileInput) return; openBtn.addEventListener("click", () => { fileInput.click(); }); fileInput.addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; try { // Показываем состояние загрузки openBtn.classList.add("loading"); // Уведомляем приложение о начале загрузки, чтобы скрыть дашборд // и показать спиннер по центру экрана (как при первоначальной загрузке) document.dispatchEvent( new CustomEvent("app:loading-start", { detail: {} }), ); // Читаем файл const arrayBuffer = await file.arrayBuffer(); // Импортируем в JS Data Engine через DataService if (window.DataService && window.DataService.importDatabaseFromFile) { if (!window.DataService.isInitialized) { throw new Error( "Приложение ещё загружается. Подождите несколько секунд и попробуйте снова.", ); } const result = await window.DataService.importDatabaseFromFile( arrayBuffer, file.name, ); if (result.success) { console.log( `[OpenFile] Database imported successfully: ${result.databaseId}`, ); ToastManager.getInstance().success( `Файл «${file.name}» загружен`, 4000, ); // Диспатчим событие о загрузке БД window.dispatchEvent( new CustomEvent("database-file-loaded", { detail: { databaseId: result.databaseId, name: file.name, tableCount: result.tableCount, }, }), ); } else { throw new Error(result.error || "Import failed"); } } else { // Фолбек: используем глобальный объект databaseManager если DataService не доступен if (window.databaseManager && window.databaseManager.importFromFile) { const result = await window.databaseManager.importFromFile( arrayBuffer, file.name, ); if (result.isValid) { console.log( `[OpenFile] Database imported successfully: ${result.databaseId}`, ); ToastManager.getInstance().success( `Файл «${file.name}» загружен`, 4000, ); window.dispatchEvent( new CustomEvent("database-file-loaded", { detail: { databaseId: result.databaseId, name: file.name, tableCount: result.tableCount, }, }), ); } else { throw new Error(result.error || "Import failed"); } } else { throw new Error("Database manager not available"); } } } catch (error) { console.error("[OpenFile] Failed to load database file:", error); ToastManager.getInstance().error( `Ошибка загрузки файла: ${error.message}`, 5000, ); } finally { openBtn.classList.remove("loading"); // Сбрасываем input для возможности повторной загрузки того же файла fileInput.value = ""; // Восстанавливаем корректное состояние интерфейса после загрузки if (window.app && window.app._updateAppView) { window.app._updateAppView(); } } }); } /** * Инициализирует кнопку экспорта базы данных в шапке */ function initExportButton() { const exportBtn = document.getElementById("header-export-btn"); if (!exportBtn) return; exportBtn.addEventListener("click", () => { if (window.app && window.app.exportCurrentDatabase) { window.app.exportCurrentDatabase(); } else { ToastManager.getInstance().error("Экспорт недоступен", 3000); } }); } /** * Получает текущую тему * @returns {string} ID текущей темы */ function getCurrentTheme() { return _getStoredTheme() || "consta-light"; } /** * Устанавливает конкретную тему * @param {string} themeId - ID темы ('consta-light' или 'consta-dark') */ function setTheme(themeId) { if (THEMES[themeId]) { applyTheme(themeId); } else { console.warn(`Theme "${themeId}" not found`); } } /** * Горячая клавиша Ctrl+Shift+L для переключения темы */ document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.shiftKey && e.key === "L") { e.preventDefault(); toggleTheme(); } }); // Экспортируем функции /** * Обновляет иконку Дзен режима * @param {boolean} isActive */ function setZenModeIcon(isActive) { const zenEl = document.getElementById("header-zen-icon"); const zenBtn = document.getElementById("header-zen-btn"); if (zenEl) { zenEl.innerHTML = isActive ? PanelRightOpen : PanelRightClose; } if (zenBtn) { zenBtn.title = isActive ? "Показать панели" : "Скрыть панели"; } } if (typeof window !== "undefined") { window.addEventListener("user-settings-loaded", () => { const stored = _getStoredTheme(); if (stored && THEMES[stored]) { const current = document.documentElement.getAttribute("data-theme") || "consta-light"; if (stored !== current) { applyTheme(stored); } } }); } window.ThemeManager = { init: initTheme, toggle: toggleTheme, set: setTheme, get: getCurrentTheme, THEMES: THEMES, setZenModeIcon: setZenModeIcon, }; // Авто-инициализация при загрузке DOM if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initTheme); } else { initTheme(); }