mod: css и js вынесены во внешние файлы (будут кашироваться, что ускорит загрузку)
This commit is contained in:
@@ -13,184 +13,10 @@
|
|||||||
<link rel="manifest" href="{% static 'site.webmanifest' %}" />
|
<link rel="manifest" href="{% static 'site.webmanifest' %}" />
|
||||||
{# Bootstrap 5 CSS #}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
{# Bootstrap 5 CSS #}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
{# Bootstrap Icons #}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
{# Bootstrap Icons #}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
||||||
|
{# Custom CSS #}<link href="{% static 'css/etpgrf.css' %}" rel="stylesheet" />
|
||||||
{# HTMX #}<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
{# HTMX #}<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
{# Alpine.js #}<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
{# Alpine.js #}<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
{# Polyfill for Import Maps #}<script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js"></script>
|
{# Polyfill for Import Maps #}<script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* === ЦВЕТОВАЯ СХЕМА === */
|
|
||||||
: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);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -226,108 +52,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Bootstrap JS #}
|
{# Bootstrap JS #}<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
{# Custom JS #}<script src="{% static 'js/base.js' %}" defer></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
{# Логика куки-баннера и счетчиков #}
|
|
||||||
<script>
|
|
||||||
(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();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -249,105 +249,8 @@
|
|||||||
<div id="result-area" style="display: none;"></div>
|
<div id="result-area" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
<script type="module">
|
|
||||||
// Импортируем из локального бандла
|
{% block scripts %}
|
||||||
import {
|
<script type="module" src="{% static 'js/index.js' %}" defer></script>
|
||||||
EditorView,
|
|
||||||
EditorState,
|
|
||||||
lineNumbers,
|
|
||||||
highlightActiveLineGutter,
|
|
||||||
highlightWhitespace,
|
|
||||||
highlightTrailingWhitespace,
|
|
||||||
drawSelection,
|
|
||||||
keymap,
|
|
||||||
highlightSpecialChars,
|
|
||||||
html,
|
|
||||||
oneDark,
|
|
||||||
syntaxHighlighting,
|
|
||||||
defaultHighlightStyle,
|
|
||||||
bracketMatching,
|
|
||||||
defaultKeymap,
|
|
||||||
Compartment
|
|
||||||
} from "{% static '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())
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
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