mod: css и js вынесены во внешние файлы (будут кашироваться, что ускорит загрузку)
This commit is contained in:
218
public/static/css/etpgrf.css
Normal file
218
public/static/css/etpgrf.css
Normal file
@@ -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);
|
||||
}
|
||||
96
public/static/js/base.js
Normal file
96
public/static/js/base.js
Normal file
@@ -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();
|
||||
});
|
||||
})();
|
||||
99
public/static/js/index.js
Normal file
99
public/static/js/index.js
Normal file
@@ -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())
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user