diff --git a/etpgrf_site/typograph/templates/typograph/base.html b/etpgrf_site/typograph/templates/typograph/base.html index 8499d1e..76f0a98 100644 --- a/etpgrf_site/typograph/templates/typograph/base.html +++ b/etpgrf_site/typograph/templates/typograph/base.html @@ -13,184 +13,10 @@ {# Bootstrap 5 CSS #} {# Bootstrap Icons #} + {# Custom CSS #} {# HTMX #} {# Alpine.js #} {# Polyfill for Import Maps #} - - - - @@ -226,108 +52,8 @@ -{# Bootstrap JS #} - - -{# Логика куки-баннера и счетчиков #} - - +{# Bootstrap JS #} +{# Custom JS #} +{% block scripts %}{% endblock %} diff --git a/etpgrf_site/typograph/templates/typograph/index.html b/etpgrf_site/typograph/templates/typograph/index.html index 6ebbd52..b26a4e9 100644 --- a/etpgrf_site/typograph/templates/typograph/index.html +++ b/etpgrf_site/typograph/templates/typograph/index.html @@ -249,105 +249,8 @@ - - +{% endblock %} + +{% block scripts %} + {% endblock %} diff --git a/public/static/css/etpgrf.css b/public/static/css/etpgrf.css new file mode 100644 index 0000000..0300eae --- /dev/null +++ b/public/static/css/etpgrf.css @@ -0,0 +1,218 @@ +/* === ЦВЕТОВАЯ СХЕМА === */ +:root { + /* Светлая тема: Темно-серый с легкой желтизной (Warm Charcoal) */ + --bs-body-bg: #f8f8f2; + --bs-body-color: #1f1f19; + --bs-primary: #4a4a44; + --bs-primary-rgb: 74, 74, 68; + --bs-link-color: #4a4a44; + --bs-link-hover-color: #2e2e2a; + --bs-focus-ring-color: rgba(74, 74, 68, 0.25); + + /* Фон навбара в светлой теме */ + --bs-navbar-bg: #b8b8d055; /* Тот же, что и body, или чуть темнее */ + --bs-navbar-color: #1f1f19; +} + +[data-bs-theme="dark"] { + /* Темная тема: Глубокий черный фон и Стальной акцент */ + --bs-body-bg: #151111; + --bs-body-color: #eceff1; + + /* Акцент: Светлый серо-голубой */ + --bs-primary: #b0bec5; + --bs-primary-rgb: 176, 190, 197; + + --bs-link-color: #90caf9; + --bs-link-hover-color: #bbdefb; + + --bs-border-color: #37474f; + --bs-border-color-translucent: rgba(255, 255, 255, 0.15); + + /* Цвет фокуса для полей ввода */ + --bs-focus-ring-color: rgba(176, 190, 197, 0.25); + + /* Фон навбара в темной теме */ + --bs-navbar-bg: #55558555; + --bs-navbar-color: #b0bec5; +} + +/* Небольшие стили для красоты */ +body { + background-color: var(--bs-body-bg); +} + +/* Навбар: используем переменную для фона */ +.navbar { + background-color: var(--bs-navbar-bg) !important; + border-bottom: 1px solid var(--bs-border-color); + padding: 0; /* Убираем отступы у навбара */ + position: sticky; + top: 0; + height: 105px; + z-index: 1000; + backdrop-filter: blur(4px); /* Эффект размытия */ + box-shadow: 0 -25px 30px 15px var(--bs-border-color); +} + +.navbar-brand { + padding: 0; /* Убираем отступы у бренда */ +} + +/* Стили для скролла */ +.navbar-scrolled { + height: 55px; +} + +/* Логотип */ +.logo-img { + width: 70%; + margin-left: -3%; /* Немного сдвигаем влево, чтобы буквы ETPGRF логотипа выровнять */ + height: 151px; /* Ограничиваем высоту */ + object-fit: contain; /* Вписываем, сохраняя пропорции */ +} + +/* Уменьшаем логотип при скролле */ +.navbar-scrolled .logo-img { + height: 78px; /* Компактная высота */ + margin-left: -5%; +} + +/* === ПЕРЕОПРЕДЕЛЕНИЕ КОМПОНЕНТОВ BOOTSTRAP === */ + +/* Кнопки Primary */ +.btn-primary { + --bs-btn-bg: var(--bs-primary); + --bs-btn-border-color: var(--bs-primary); + --bs-btn-hover-bg: var(--bs-link-hover-color); + --bs-btn-hover-border-color: var(--bs-link-hover-color); + --bs-btn-active-bg: var(--bs-link-hover-color); + --bs-btn-active-border-color: var(--bs-link-hover-color); +} + +/* В темной теме текст на кнопке должен быть темным */ +[data-bs-theme="dark"] .btn-primary { + --bs-btn-color: #151111; + --bs-btn-hover-color: #151111; + --bs-btn-active-color: #151111; +} + +/* Чекбоксы и Радио */ +.form-check-input:checked { + background-color: var(--bs-primary); + border-color: var(--bs-primary); +} + +/* В темной теме галочка должна быть темной */ +[data-bs-theme="dark"] .form-check-input:checked { + /* SVG галочки черного цвета (закодирован в base64) */ + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23151111' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e"); +} + +/* Фокус на полях ввода */ +.form-control:focus, .form-select:focus, .form-check-input:focus { + border-color: var(--bs-primary); + box-shadow: 0 0 0 0.25rem var(--bs-focus-ring-color); +} + +.result-box { + background: var(--bs-body-bg); + color: var(--bs-body-color); + border: 1px solid var(--bs-border-color); + border-radius: 0.375rem; + padding: 1rem; + min-height: 300px; + padding-left: 1.5rem; + padding-right: 1.5rem; + white-space: pre-wrap; + font-family: inherit; +} + +.cm-editor { + border: 1px solid var(--bs-border-color); + border-radius: 0.375rem; + height: 300px; +} + +/* --- Висячая пунктуация (Hanging Punctuation) --- */ +.result-box .etp-laquo { + margin-left: -0.44em; +} + +.result-box .etp-ldquo { + margin-left: -0.44em; +} + +.result-box .etp-lpar { + margin-left: -0.3em; +} + +.result-box .etp-lsqb { + margin-left: -0.3em; +} + +.result-box .etp-lcub { + margin-left: -0.3em; +} + +.result-box .etp-raquo { + margin-right: -0.44em; +} + +.result-box .etp-rdquo { + margin-right: -0.44em; +} + +.result-box .etp-rpar { + margin-right: -0.3em; +} + +.result-box .etp-rsqb { + margin-right: -0.3em; +} + +.result-box .etp-rcub { + margin-right: -0.3em; +} + +.result-box .etp-r-dot { + margin-right: -0.2em; +} + +.result-box .etp-r-comma { + margin-right: -0.2em; +} + +.result-box .etp-r-colon { + margin-right: -0.2em; +} + +/* --- Стили для Cookie Banner --- */ +#cookie-banner { + backdrop-filter: blur(10px); + border-top: 1px solid var(--bs-border-color); + z-index: 1050; + background-color: var(--bs-navbar-bg); + color: var(--bs-navbar-color); /* Используем цвет навбара для текста */ +} + +#cookie-banner a { + color: var(--bs-primary); + text-decoration: none; + border-bottom: 1px dotted var(--bs-primary); +} + +#cookie-banner a:hover { + border-bottom-style: solid; + color: var(--bs-link-hover-color); +} + +#cookie-accept { + color: var(--bs-primary); + border: 1px dashed var(--bs-primary); + background: transparent; +} + +#cookie-accept:hover { + background: rgba(var(--bs-primary-rgb), 0.1); +} \ No newline at end of file diff --git a/public/static/js/base.js b/public/static/js/base.js new file mode 100644 index 0000000..21ec88b --- /dev/null +++ b/public/static/js/base.js @@ -0,0 +1,96 @@ +(function () { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const logoImg = document.getElementById('logo-img'); + const navbar = document.getElementById('main-navbar'); + + // --- АВТОМАТИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ТЕМЫ (Dark/Light) --- + function updateTheme(e) { + const theme = e.matches ? 'dark' : 'light'; + document.documentElement.setAttribute('data-bs-theme', theme); + } + + // --- ОБНОВЛЕНИЕ ЛОГОТИПА ПРИ СКРОЛЛЕ И СМЕНЕ ТЕМЫ --- + function updateLogo() { + const isDark = darkModeMediaQuery.matches; + // Используем getBoundingClientRect для определения позиции контента + if (document.getElementById('content-container').getBoundingClientRect().top < 78) { + navbar.classList.add('navbar-scrolled'); + logoImg.src = isDark ? logoImg.dataset.srcDarkCompact : logoImg.dataset.srcLightCompact; + } else { + navbar.classList.remove('navbar-scrolled'); + logoImg.src = isDark ? logoImg.dataset.srcDark : logoImg.dataset.srcLight; + } + } + + // Инициализация + updateTheme(darkModeMediaQuery); + updateLogo(); + document.addEventListener('DOMContentLoaded', updateLogo); + + // Слушаем скролл + window.addEventListener('scroll', updateLogo); + + // Слушаем смену темы + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateLogo); + + // --- КУКИ --- + const COOKIE_KEY = 'cookie_consent'; + const TTL_MS = 60 * 1000; // 1 минута для отладки (потом поставить 90 дней: 90 * 24 * 60 * 60 * 1000 = 7776000000) + + const banner = document.getElementById('cookie-banner'); + const acceptButton = document.getElementById('cookie-accept'); + + function loadCounters() { + console.log("Загрузка счетчиков (Яндекс, Google)..."); + // Код Яндекс.Метрики + // (function(m,e,t,r,i,k,a){...})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); + // ym(XXXXXX, "init", {...}); + + // Код Google Analytics + // window.dataLayer = window.dataLayer || []; + // function gtag(){dataLayer.push(arguments);} + // gtag('js', new Date()); + // gtag('config', 'G-XXXXXXXXXX'); + + // Код Top.Mail.Ru + // (function(w, d, c) { ... })(window, document, "topmailru"); + + // и т.д. + + // alert("Отладка. Счетчики загружены (здесь должен быть реальный код счетчиков)."); + } + + function checkConsent() { + const stored = localStorage.getItem(COOKIE_KEY); + if (!stored) return false; + + try { + const data = JSON.parse(stored); + const now = Date.now(); + // Проверяем, не истек ли срок + if (now - data.timestamp > TTL_MS) { + localStorage.removeItem(COOKIE_KEY); + return false; + } + return true; + } catch (e) { + return false; + } + } + + if (checkConsent()) { + loadCounters(); + } else { + banner.style.display = 'block'; + } + + acceptButton.addEventListener('click', function () { + const data = { + value: true, + timestamp: Date.now() + }; + localStorage.setItem(COOKIE_KEY, JSON.stringify(data)); + banner.style.display = 'none'; + loadCounters(); + }); +})(); \ No newline at end of file diff --git a/public/static/js/index.js b/public/static/js/index.js new file mode 100644 index 0000000..2b8b5d9 --- /dev/null +++ b/public/static/js/index.js @@ -0,0 +1,99 @@ +// Импортируем из локального бандла (относительный путь) +import { + EditorView, + EditorState, + lineNumbers, + highlightActiveLineGutter, + highlightWhitespace, + highlightTrailingWhitespace, + drawSelection, + keymap, + highlightSpecialChars, + html, + oneDark, + syntaxHighlighting, + defaultHighlightStyle, + bracketMatching, + defaultKeymap, + Compartment +} from "../codemirror/editor.js"; + +const resultWrapper = document.getElementById('cm-result-wrapper'); + +const themeCompartment = new Compartment(); + +function getTheme() { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? oneDark : []; +} + +// Словарь названий для спецсимволов +const charNames = { + 0x00A0: "NoBreakable Space (неразрывный пробел —  )", + 0x00AD: "Soft Hyphen (мягкий перенос — ­)", + 0x2002: "En Space (полужирный пробел —  )", + 0x2003: "Em Space (жирный пробел —  )", + 0x2007: "Figure Space (цифровой пробел —  )", + 0x2008: "Punctuation Space (пунктуационный пробел —  )", + 0x2009: "Thin Space (тонкий пробел —  )", + 0x200A: "Hair Space (толщина волоса —  )", + 0x200B: "Negative Space (негативный пробел — ​)", + 0x200C: "Zero Width Non-Joiner (пробел нулевой ширины, без объединения — ‍)", + 0x200D: "Zero Width Joiner (пробел нулевой ширины, с объединением — ‌)", + 0x200E: "Left-to-Right Mark (изменить направление текста на слева-направо — ‎)", + 0x200F: "Right-to-Left Mark (изменить направление текста на справа-налево — ‏)", + 0x205F: "Medium Mathematical Space (средний пробел —  )", + 0x2060: "NoBreak (без разрыва — ⁠)", + 0x2062: "Invisible Times (невидимое умножение для семантической разметки математических выражений — ⁢)", + 0x2063: "Invisible Comma (невидимая запятая для семантической разметки математических выражений — ⁣)", +}; + +const resultState = EditorState.create({ + doc: "Здесь появится результат...", + extensions: [ + lineNumbers(), + highlightActiveLineGutter(), + // Подсветка NBSP и других специальных пробелов + highlightSpecialChars({ + specialChars: /[\u2002\u00AD\u2003\u2007\u2009\u00a0\u200A\u200B\u200C\u200D\u200E\u200F\u205F\u2060\u2062\u2063]/, + addSpecialChars: true, + render: (code) => { + let span = document.createElement("span"); + span.textContent = "•"; + span.style.background = "#ff000044"; // Полупрозрачный красный фон + span.style.color = "#ffff00"; // Желтый цвет точки + // Используем словарь для title + span.title = "U+" + code.toString(16).toUpperCase().padStart(4, '0') + " / " + (charNames[code] || "Special Char"); + return span; + } + }), + highlightWhitespace(), + highlightTrailingWhitespace(), + drawSelection(), + syntaxHighlighting(defaultHighlightStyle, {fallback: true}), + bracketMatching(), + keymap.of(defaultKeymap), + html(), + themeCompartment.of(getTheme()), + EditorState.readOnly.of(true) + ] +}); + +const resultView = new EditorView({ + state: resultState, + parent: resultWrapper +}); + +document.body.addEventListener('htmx:afterSwap', function (evt) { + if (evt.detail.target.id === 'result-area') { + const newContent = evt.detail.xhr.response; + resultView.dispatch({ + changes: {from: 0, to: resultView.state.doc.length, insert: newContent} + }); + } +}); + +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + resultView.dispatch({ + effects: themeCompartment.reconfigure(getTheme()) + }); +});