From 1cac5cb9ad41e828723d28dc3a79fe55a3e3b5ea Mon Sep 17 00:00:00 2001 From: NikDizell Date: Mon, 9 Mar 2026 13:14:40 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B1=D1=80=D0=B0=D0=BB=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/blog/css/article.css | 500 +++++++++++++++++++++---- static/programmer/js/theme-switcher.js | 118 +++--- 2 files changed, 492 insertions(+), 126 deletions(-) diff --git a/static/blog/css/article.css b/static/blog/css/article.css index d4c17af..4bd321f 100644 --- a/static/blog/css/article.css +++ b/static/blog/css/article.css @@ -1,90 +1,462 @@ -/* Стили для содержимого статьи */ -.article-content { - font-size: 1rem; - line-height: 1.6; +/* + * article.css + * Styles for CKEditor 5 rendered content inside .content-card + * and for the article detail page layout. + * + * Scope: all rules are under .content-card .card-content + * so they never leak into the rest of the site. + */ + +/* ===== ARTICLE PAGE LAYOUT ===== */ + +.article-meta { + display: flex; + align-items: center; + gap: 1.5rem; + flex-wrap: wrap; + font-size: 0.9rem; + color: var(--text-light); + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-light); +} + +.article-meta span { + display: flex; + align-items: center; + gap: 0.4rem; +} + +.article-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 1.5rem 0; +} + +.article-tag { + display: inline-block; + padding: 0.25rem 0.75rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-light); + border-radius: var(--radius-sm); + font-size: 0.8rem; + color: var(--text-secondary); + text-decoration: none; + transition: var(--transition); +} + +.article-tag:hover { + background: var(--primary); + color: white; + border-color: var(--primary); +} + +/* ===== CKEDITOR 5 CONTENT ===== */ + +.content-card .card-content { + font-size: 1.05rem; + line-height: 1.8; + color: var(--text-primary); + word-break: break-word; +} + +/* --- Headings --- */ + +.content-card .card-content h2, +.content-card .card-content h3, +.content-card .card-content h4, +.content-card .card-content h5, +.content-card .card-content h6 { + font-weight: 700; + line-height: 1.3; + margin-top: 2rem; + margin-bottom: 0.75rem; color: var(--text-primary); } -.article-content h1, -.article-content h2, -.article-content h3, -.article-content h4, -.article-content h5, -.article-content h6 { - margin-top: 1.5em; - margin-bottom: 0.75em; - font-weight: 600; +.content-card .card-content h2 { + font-size: 1.75rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--border-light); } -.article-content h1 { font-size: 2rem; } -.article-content h2 { font-size: 1.75rem; } -.article-content h3 { font-size: 1.5rem; } - -.article-content p { - margin-bottom: 1rem; -} - -.article-content ul, -.article-content ol { - margin-bottom: 1rem; - padding-left: 2rem; -} - -.article-content li { - margin-bottom: 0.25rem; -} - -.article-content blockquote { +.content-card .card-content h3 { + font-size: 1.375rem; + padding-left: 0.75rem; border-left: 4px solid var(--primary); - padding: 0.5rem 1rem; - background: var(--bg-secondary); - margin: 1rem 0; - font-style: italic; } -.article-content table { +.content-card .card-content h4 { + font-size: 1.125rem; + color: var(--text-secondary); +} + +/* --- Paragraphs --- */ + +.content-card .card-content p { + margin-bottom: 1.25rem; +} + +.content-card .card-content p:last-child { + margin-bottom: 0; +} + +/* --- Links --- */ + +.content-card .card-content a { + color: var(--primary); + text-decoration: underline; + text-decoration-color: rgba(255, 107, 0, 0.4); + text-underline-offset: 3px; + transition: var(--transition); +} + +.content-card .card-content a:hover { + color: var(--primary-dark); + text-decoration-color: var(--primary-dark); +} + +/* --- Lists --- */ + +.content-card .card-content ul, +.content-card .card-content ol { + margin: 0 0 1.25rem 0; + padding-left: 1.75rem; +} + +.content-card .card-content ul { + list-style: none; + padding-left: 0; +} + +.content-card .card-content ul li { + position: relative; + padding-left: 1.5rem; + margin-bottom: 0.5rem; +} + +.content-card .card-content ul li::before { + content: '▸'; + position: absolute; + left: 0; + color: var(--primary); + font-weight: bold; +} + +.content-card .card-content ol li { + margin-bottom: 0.5rem; +} + +.content-card .card-content ul ul, +.content-card .card-content ol ol, +.content-card .card-content ul ol, +.content-card .card-content ol ul { + margin-top: 0.5rem; + margin-bottom: 0; +} + +/* --- Blockquote --- */ + +.content-card .card-content blockquote { + margin: 1.5rem 0; + padding: 1.25rem 1.5rem; + border-left: 4px solid var(--primary); + background: var(--bg-secondary); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + color: var(--text-secondary); + font-style: italic; + font-size: 1.05rem; +} + +.content-card .card-content blockquote p:last-child { + margin-bottom: 0; +} + +/* --- Inline code (not inside pre) --- */ +/* + * High specificity is fine here because this selector explicitly + * excludes pre > code, so it never touches hljs blocks. + */ + +.content-card .card-content :not(pre) > code { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.875em; + background: var(--bg-tertiary); + color: var(--primary-dark); + padding: 0.15em 0.4em; + border-radius: 4px; + border: 1px solid var(--border-light); + word-break: break-all; +} + +/* --- Code blocks (CKEditor generates
) --- */
+/*
+ * ALL pre and pre code rules use :where() — zero specificity.
+ * This guarantees hljs always wins on colors, background, and padding.
+ *
+ * Responsibility split:
+ *   article.css  → layout only: margin, border-radius, overflow, shadow
+ *   hljs CSS     → everything visual: background, color, padding, font-size
+ *
+ * font-size:0 on pre prevents inherited line-height:1.8 from inflating
+ * the spacing between lines (line-height is computed from font-size,
+ * so zeroing it on the wrapper element neutralises the inheritance).
+ * font-size is restored on pre code.
+ */
+
+:where(.content-card .card-content) pre {
+    margin: 1.5rem 0;
+    padding: 0;           /* hljs sets padding:1em on code.hljs — don't double it */
+    border-radius: var(--radius-md);
+    overflow-x: auto;
+    box-shadow: var(--shadow-md);
+    font-size: 0;         /* neutralise inherited line-height */
+    line-height: 1;
+    background: none;     /* hljs controls background (#fff or #282c34) */
+}
+
+:where(.content-card .card-content) pre code {
+    font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+    font-size: 0.9rem;    /* restore font-size for hljs to work from */
+    line-height: 1.5;
+    word-break: normal;
+    white-space: pre;
+    /* NO color, NO background, NO padding — hljs owns these */
+}
+
+/* --- Tables --- */
+
+.content-card .card-content .table-wrapper,
+.content-card .card-content figure.table {
+    overflow-x: auto;
+    margin: 1.5rem 0;
+    border-radius: var(--radius-md);
+    box-shadow: var(--shadow-sm);
+}
+
+.content-card .card-content table {
     width: 100%;
     border-collapse: collapse;
-    margin: 1rem 0;
+    font-size: 0.95rem;
 }
 
-.article-content th,
-.article-content td {
-    border: 1px solid var(--border-light);
-    padding: 0.5rem;
+.content-card .card-content th {
+    background: var(--bg-secondary);
+    font-weight: 700;
+    text-align: left;
+    padding: 0.875rem 1rem;
+    border-bottom: 2px solid var(--border-medium);
+    color: var(--text-primary);
+    white-space: nowrap;
 }
 
-.article-content th {
+.content-card .card-content td {
+    padding: 0.75rem 1rem;
+    border-bottom: 1px solid var(--border-light);
+    color: var(--text-secondary);
+    vertical-align: top;
+}
+
+.content-card .card-content tr:last-child td {
+    border-bottom: none;
+}
+
+.content-card .card-content tbody tr:hover td {
     background: var(--bg-secondary);
 }
 
-.article-content img {
+/* CKEditor 5 table with header row class */
+.content-card .card-content table.ck-table-resized th,
+.content-card .card-content table.ck-table-resized td {
+    border: 1px solid var(--border-light);
+}
+
+/* --- Images (CKEditor wraps in 
) --- */ + +.content-card .card-content figure.image { + margin: 1.5rem auto; + text-align: center; + max-width: 100%; +} + +.content-card .card-content figure.image img, +.content-card .card-content img { max-width: 100%; height: auto; - margin: 1rem 0; - border-radius: 4px; + display: block; + margin: 0 auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + cursor: pointer; /* triggers the click-to-enlarge modal */ } -.article-content pre { - background: #2d2d2d; - color: #ccc; - padding: 1rem; - border-radius: 4px; - overflow-x: auto; - margin: 1rem 0; +.content-card .card-content figure.image figcaption { + margin-top: 0.5rem; + font-size: 0.875rem; + color: var(--text-light); + font-style: italic; + text-align: center; } -.article-content code { - font-family: 'Courier New', monospace; - background: #2d2d2d; - color: #ccc; - padding: 0.2rem 0.4rem; - border-radius: 3px; - font-size: 0.9em; +/* CKEditor alignment classes */ +.content-card .card-content figure.image.image-style-align-left { + float: left; + margin: 0.5rem 1.5rem 1rem 0; + max-width: 45%; } -.article-content pre code { - padding: 0; - background: none; - color: inherit; +.content-card .card-content figure.image.image-style-align-right { + float: right; + margin: 0.5rem 0 1rem 1.5rem; + max-width: 45%; +} + +/* Clearfix after floated images */ +.content-card .card-content::after { + content: ''; + display: table; + clear: both; +} + +/* --- Horizontal rule --- */ + +.content-card .card-content hr { + border: none; + height: 2px; + background: linear-gradient(90deg, transparent, var(--border-medium), transparent); + margin: 2rem 0; +} + +/* --- Alert/info boxes (CKEditor custom classes if used) --- */ + +.content-card .card-content .info-box { + padding: 1rem 1.25rem; + border-radius: var(--radius-md); + margin: 1.5rem 0; + border-left: 4px solid var(--accent); + background: rgba(0, 168, 255, 0.08); + color: var(--text-secondary); + font-size: 0.95rem; +} + +/* ===== COMMENTS SECTION ===== */ + +.comments-section { + margin-top: 3rem; +} + +.comments-section h3 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.comment-card { + background: var(--bg-secondary); + border-radius: var(--radius-lg); + padding: 1.25rem 1.5rem; + margin-bottom: 1rem; + border: 1px solid var(--border-light); + border-left: 3px solid var(--secondary); +} + +.comment-author { + font-weight: 600; + color: var(--text-primary); + font-size: 0.95rem; +} + +.comment-date { + font-size: 0.8rem; + color: var(--text-light); + margin-left: 0.75rem; +} + +.comment-body { + margin-top: 0.75rem; + color: var(--text-secondary); + line-height: 1.6; +} + +.comment-form-card { + background: var(--bg-card); + border-radius: var(--radius-xl); + padding: 2rem; + box-shadow: var(--shadow-md); + border: 1px solid var(--border-light); + margin-top: 2rem; + position: relative; +} + +.comment-form-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--gradient-secondary); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; +} + +.comment-form-card h4 { + font-size: 1.25rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary); +} + +/* ===== RESPONSIVE ===== */ + +@media (max-width: 768px) { + .content-card .card-content h2 { + font-size: 1.5rem; + } + + .content-card .card-content h3 { + font-size: 1.25rem; + } + + .content-card .card-content figure.image.image-style-align-left, + .content-card .card-content figure.image.image-style-align-right { + float: none; + max-width: 100%; + margin: 1rem auto; + } + + /* :where() keeps specificity at zero so hljs still wins on mobile */ + :where(.content-card .card-content) pre { + box-shadow: none; + margin: 1rem 0; + } + + .content-card .card-content th, + .content-card .card-content td { + padding: 0.6rem 0.75rem; + font-size: 0.875rem; + } + + .article-meta { + gap: 1rem; + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .content-card .card-content { + font-size: 1rem; + } + + .content-card .card-content h2 { + font-size: 1.375rem; + } + + .content-card .card-content h3 { + font-size: 1.125rem; + } } \ No newline at end of file diff --git a/static/programmer/js/theme-switcher.js b/static/programmer/js/theme-switcher.js index 6226fda..5386bbe 100644 --- a/static/programmer/js/theme-switcher.js +++ b/static/programmer/js/theme-switcher.js @@ -1,78 +1,72 @@ -// Theme Switcher Script -document.addEventListener('DOMContentLoaded', function() { - const themeToggle = document.getElementById('theme-toggle'); +// theme-switcher.js +document.addEventListener('DOMContentLoaded', function () { + + const themeToggle = document.getElementById('theme-toggle'); const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); - const themeCSS1C = document.getElementById('theme-css-1c'); + const themeCSS1C = document.getElementById('theme-css-1c'); + + // FIX: Derive the base path once so we never rely on the current href value. + // href.replace() was silently failing when the filename didn't match exactly. + const hljsBasePath = themeCSS1C + ? themeCSS1C.href.replace(/1c-(light|dark)\.min\.css$/, '') + : null; + + // console.log('Initial href:', themeCSS1C?.href); + // console.log('Computed basePath:', hljsBasePath); + + // ── Theme application ──────────────────────────────────────────────────── + + function applyTheme(isDark) { + if (isDark) { + document.body.classList.add('theme-dark'); + if (themeCSS1C && hljsBasePath) { + themeCSS1C.href = hljsBasePath + '1c-dark.min.css'; + } + } else { + document.body.classList.remove('theme-dark'); + if (themeCSS1C && hljsBasePath) { + themeCSS1C.href = hljsBasePath + '1c-light.min.css'; + } + } + + // FIX: CSS defines checked = dark (moon icon). + // Previously the handlers were calling the wrong function on check/uncheck. + if (themeToggle) themeToggle.checked = isDark; + if (mobileThemeToggle) mobileThemeToggle.checked = isDark; + + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + } + + // ── Initialise from saved preference ──────────────────────────────────── - // Проверяем сохраненную тему в localStorage const savedTheme = localStorage.getItem('theme'); + applyTheme(savedTheme === 'dark'); - // Устанавливаем светлую тему по умолчанию - if (savedTheme === 'dark') { - switchToDarkTheme(); - } else { - switchToLightTheme(); // Светлая тема по умолчанию - } + // ── Event listeners ────────────────────────────────────────────────────── - // Обработчик переключения темы для десктопного переключателя + // FIX: Both toggles now call the same applyTheme() — no duplicated logic, + // and both are always kept in sync with each other automatically. if (themeToggle) { - themeToggle.addEventListener('change', function() { - if (this.checked) { - switchToLightTheme(); - } else { - switchToDarkTheme(); - } + themeToggle.addEventListener('change', function () { + applyTheme(this.checked); }); } - // Обработчик переключения темы для мобильного переключателя if (mobileThemeToggle) { - mobileThemeToggle.addEventListener('change', function() { - if (this.checked) { - switchToLightTheme(); - } else { - switchToDarkTheme(); - } - // Синхронизируем оба переключателя - if (themeToggle) { - themeToggle.checked = this.checked; - } + mobileThemeToggle.addEventListener('change', function () { + applyTheme(this.checked); }); } - function switchToLightTheme() { - document.body.classList.remove('theme-dark'); - if (themeCSS1C) { - themeCSS1C.href = themeCSS1C.href.replace('1c-dark.min.css', '1c-light.min.css'); - } - if (themeToggle) themeToggle.checked = true; - if (mobileThemeToggle) mobileThemeToggle.checked = true; - localStorage.setItem('theme', 'light'); - // 👇 Обновляем иконку ИКС - updateIKSTheme(); + // ── CSS error handling ─────────────────────────────────────────────────── + + // FIX: was referencing undefined variable `themeCSS` instead of `themeCSS1C` + if (themeCSS1C) { + themeCSS1C.onerror = function () { + console.error('Ошибка загрузки CSS файла темы подсветки кода'); + // Fall back to light theme + applyTheme(false); + }; } - function switchToDarkTheme() { - document.body.classList.add('theme-dark'); - if (themeCSS1C) { - themeCSS1C.href = themeCSS1C.href.replace('1c-light.min.css', '1c-dark.min.css'); - } - if (themeToggle) themeToggle.checked = false; - if (mobileThemeToggle) mobileThemeToggle.checked = false; - localStorage.setItem('theme', 'dark'); - // 👇 Обновляем иконку ИКС - updateIKSTheme(); - } - - // Синхронизация переключателей при загрузке - if (themeToggle && mobileThemeToggle) { - mobileThemeToggle.checked = themeToggle.checked; - } - - // Обработка ошибок загрузки CSS - themeCSS.onerror = function() { - console.error('Ошибка загрузки CSS файла темы'); - // Восстанавливаем светлую тему по умолчанию при ошибке - themeCSS1C.href = themeCSS1C.href.replace('1c-light.min.css', '1c-dark.min.css'); - }; }); \ No newline at end of file