mod: оптимизация CSS и спрятана часть tailwind "класс-колбас"
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 31s

This commit is contained in:
2026-09-26 16:29:15 +03:00
parent 7c44f2ef35
commit ebf78c2c48
7 changed files with 300 additions and 98 deletions
+24
View File
@@ -1,4 +1,6 @@
import hashlib import hashlib
from html import unescape
import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
from django.db.models import F from django.db.models import F
@@ -159,6 +161,17 @@ class TbHypn0Item(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.s_title} ({self.s_hash_id})" return f"{self.s_title} ({self.s_hash_id})"
@property
def s_title_plain(self) -> str:
"""
Возвращает чистый заголовок картины без HTML-тегов и с декодированными мнемониками (  и др.)
для безопасного использования в HTML-атрибутах (title, aria-label, alt, meta).
"""
if not self.s_title:
return ""
text = re.sub(r"<[^>]+>", "", self.s_title)
return unescape(text).replace("\xa0", " ").strip()
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
"""Возвращает канонический URL детальной страницы картины.""" """Возвращает канонический URL детальной страницы картины."""
return reverse("hypn0_site:gallery_detail", kwargs={"hash_id": self.s_hash_id}) return reverse("hypn0_site:gallery_detail", kwargs={"hash_id": self.s_hash_id})
@@ -564,6 +577,17 @@ class TbBlogPost(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return self.s_title return self.s_title
@property
def s_title_plain(self) -> str:
"""
Возвращает чистый заголовок статьи без HTML-тегов и с декодированными мнемониками
для безопасного использования в мета-тегах и атрибутах.
"""
if not self.s_title:
return ""
text = re.sub(r"<[^>]+>", "", self.s_title)
return unescape(text).replace("\xa0", " ").strip()
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
"""Возвращает канонический URL статьи блога.""" """Возвращает канонический URL статьи блога."""
return f"/blog/{self.slug}" if self.slug else f"/blog/{self.pk}" return f"/blog/{self.slug}" if self.slug else f"/blog/{self.pk}"
+14
View File
@@ -1505,3 +1505,17 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
resp500 = error_500(req500) resp500 = error_500(req500)
self.assertEqual(resp500.status_code, 500) self.assertEqual(resp500.status_code, 500)
self.assertIn("Критический перегрев неокортекса", resp500.content.decode("utf-8")) self.assertIn("Критический перегрев неокортекса", resp500.content.decode("utf-8"))
def test_s_title_plain_property(self):
"""Проверка очистки HTML-тегов и мнемоник в s_title_plain для моделей TbHypn0Item и TbBlogPost."""
item = TbHypn0Item(
s_title="<nobr>Шедевр&nbsp;ноосферы</nobr> &laquo;Альфа&raquo;",
s_hash_id="testPlain",
)
self.assertEqual(item.s_title_plain, "Шедевр ноосферы «Альфа»")
post = TbBlogPost(
s_title="<b>Записки&nbsp;Гипножабы:</b> Раздел&amp;Смысл",
slug="test-plain",
)
self.assertEqual(post.s_title_plain, "Записки Гипножабы: Раздел&Смысл")
+23 -59
View File
@@ -1,61 +1,25 @@
{% load static %} <div id="gallery-card-{{ item.s_hash_id }}" style="{{ item.card_bg_style }}" class="card group">{% load static %}
<div id="gallery-card-{{ item.s_hash_id }}" {# <!-- Ссылка на полный просмотр картины -->#}<a href="{% url 'hypn0_site:gallery_detail' item.s_hash_id %}"
style="{{ item.card_bg_style }}" class="card-link"
class="aspect-square rounded-2xl border border-stone-300/80 dark:border-zinc-800/80 overflow-hidden relative group transition-all duration-300 hover:scale-[1.03] hover:shadow-2xl hover:border-amber-500/60 dark:hover:border-amber-500/50 flex items-center justify-center select-none shadow-sm bg-[var(--card-bg-light)] dark:bg-[var(--card-bg-dark)]"> title="«{{ item.s_title_plain }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG"
aria-label="«{{ item.s_title_plain }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG">
<!-- Ссылка на полный просмотр картины --> <div x-data="hypn0LazySvg('{% if item.file_svg %}{{ item.file_svg.url }}{% endif %}')" class="card-svg hypn0-card-svg">
<a href="{% url 'hypn0_site:gallery_detail' item.s_hash_id %}" <noscript><div class="card-noscript">
class="absolute inset-0 z-10 flex items-center justify-center p-3 sm:p-4 border-0 border-none no-underline hover:border-0 hover:border-none focus:outline-none" <span class="card-noscript-warn">Включите JS для анимации</span>
title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG" <span class="card-noscript-title">«{{ item.s_title|safe }}»</span>
aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG"> </div>
<div x-data="hypn0LazySvg('{% if item.file_svg %}{{ item.file_svg.url }}{% endif %}')" </noscript>
class="hypn0-card-svg w-full h-full flex items-center justify-center drop-shadow-sm transition-transform duration-300 group-hover:scale-105 border-0">
<noscript>
<div class="flex flex-col items-center justify-center p-3 text-center gap-1">
<span class="text-[11px] font-bold text-amber-600 dark:text-amber-400">Требуется JS для анимации</span>
<span class="text-[10px] text-stone-500 dark:text-stone-400 underline">«{{ item.s_title }}»</span>
</div> </div>
</noscript> </a>
{# <!-- Верхний бейдж: Хэш и уровень -->#}<div class="card-top">{% if item.i_level >= 1000 %}
{# БЕССМЕРТНЫЕ #}<span class="card-tag card-tag-immortal">⁂{{ item.s_hash_id }}</span>{% elif item.i_level >= 30 %}
{# ОДОБРЕНО ГИПНОЖАБОЙ #}<span class="card-tag card-tag-curated">✦{{ item.s_hash_id }}</span>{% else %}
{# СВЕЖЕЕ #}<span class="card-tag card-tag-fresh">¤{{ item.s_hash_id }}</span>{% endif %}
</div>
{# <!-- Нижняя панель статистики (Лайки и Просмотры) -->#}<div class="card-bottom">{% if item.i_level >= 1000 %}
{# БЕССМЕРТНЫЕ #}<span class="card-stat card-stat-immortal">♥&nbsp;{{ item.i_likes_count }}</span>{% elif item.i_level >= 30 %}
{# ОДОБРЕНО ГИПНОЖАБОЙ #}<span class="card-stat card-stat-curated">♥&nbsp;{{ item.i_likes_count }}</span>{% else %}
{# СВЕЖЕЕ #}<span class="card-stat card-stat-fresh">♥&nbsp;{{ item.i_likes_count }}</span>{% endif %}
<span class="card-stat card-stat-views">👁&nbsp;{{ item.i_views_count }}</span>
</div>
</div> </div>
</a>
<!-- Верхний бейдж: Хэш и уровень -->
<div class="absolute top-2 left-2 z-20 pointer-events-none flex items-center gap-1">
{% if item.i_level >= 1000 %}
<span class="text-[10px] font-mono font-bold tracking-tight px-1.5 py-0.5 rounded bg-emerald-950/80 text-emerald-300 backdrop-blur-md border border-emerald-500/30 opacity-80 group-hover:opacity-100 transition-opacity">
★ ¤{{ item.s_hash_id }}
</span>
{% elif item.i_level >= 30 %}
<span class="text-[10px] font-mono font-bold tracking-tight px-1.5 py-0.5 rounded bg-cyan-950/80 text-cyan-300 backdrop-blur-md border border-cyan-500/30 opacity-80 group-hover:opacity-100 transition-opacity">
◉ ¤{{ item.s_hash_id }}
</span>
{% else %}
<span class="text-[10px] font-mono font-bold tracking-tight px-1.5 py-0.5 rounded bg-stone-900/70 text-stone-200 backdrop-blur-md border border-white/10 opacity-75 group-hover:opacity-100 transition-opacity">
¤{{ item.s_hash_id }}
</span>
{% endif %}
</div>
<!-- Нижняя панель статистики (Лайки и Просмотры) -->
<div class="absolute bottom-2 left-2 right-2 z-20 flex items-center justify-between pointer-events-none">
<!-- Бейдж лайков -->
{% if item.i_level >= 1000 %}
<span class="inline-flex items-center gap-1 text-[11px] font-bold font-mono px-2 py-0.5 rounded-full bg-stone-900/80 text-emerald-400 backdrop-blur-md border border-emerald-500/20 shadow-sm opacity-90 group-hover:opacity-100 transition-opacity">
♥ {{ item.i_likes_count }}
</span>
{% elif item.i_level >= 30 %}
<span class="inline-flex items-center gap-1 text-[11px] font-bold font-mono px-2 py-0.5 rounded-full bg-stone-900/80 text-cyan-400 backdrop-blur-md border border-cyan-500/20 shadow-sm opacity-90 group-hover:opacity-100 transition-opacity">
♥ {{ item.i_likes_count }}
</span>
{% else %}
<span class="inline-flex items-center gap-1 text-[11px] font-bold font-mono px-2 py-0.5 rounded-full bg-stone-900/80 text-amber-400 backdrop-blur-md border border-amber-500/20 shadow-sm opacity-90 group-hover:opacity-100 transition-opacity">
♥ {{ item.i_likes_count }}
</span>
{% endif %}
<!-- Бейдж просмотров (проявляется при наведении) -->
<span class="inline-flex items-center gap-1 text-[10px] font-mono px-1.5 py-0.5 rounded-full bg-stone-900/80 text-stone-400 backdrop-blur-md opacity-0 group-hover:opacity-100 transition-opacity">
👁 {{ item.i_views_count }}
</span>
</div>
</div>
+203
View File
@@ -89,6 +89,209 @@
} }
} }
/* ==========================================================================
КОМПОНЕНТЫ КАРТОЧКИ ГАЛЕРЕИ (HYPNO GALLERY CARD)
========================================================================== */
@layer components {
/* Карточка картины */
.card {
@apply aspect-square rounded-2xl border border-stone-300/80 dark:border-zinc-800/80
overflow-hidden relative transition-all duration-300
hover:scale-[1.03] hover:shadow-2xl hover:border-amber-500/60 dark:hover:border-amber-500/50
flex items-center justify-center select-none shadow-sm
bg-[var(--card-bg-light)] dark:bg-[var(--card-bg-dark)];
}
/* Ссылка и оверлей */
.card-link {
@apply absolute inset-0 z-10 flex items-center justify-center p-3 sm:p-4
border-0 border-none no-underline hover:border-0 hover:border-none focus:outline-none;
}
/* Контейнер SVG */
.card-svg {
@apply w-full h-full flex items-center justify-center drop-shadow-sm
transition-transform duration-300 group-hover:scale-105 border-0;
}
/* Верхний бейдж хэша и уровня */
.card-tag {
@apply text-[10px] font-mono font-bold tracking-tight px-1.5 py-0.5 rounded
backdrop-blur-md transition-opacity;
}
.card-tag-immortal {
@apply bg-emerald-950/80 text-emerald-300 border border-emerald-500/30 opacity-80 group-hover:opacity-100;
}
.card-tag-curated {
@apply bg-cyan-950/80 text-cyan-300 border border-cyan-500/30 opacity-80 group-hover:opacity-100;
}
.card-tag-fresh {
@apply bg-stone-900/70 text-stone-200 border border-white/10 opacity-75 group-hover:opacity-100;
}
/* Нижний бейдж статистики */
.card-stat {
@apply inline-flex items-center gap-1 font-mono px-2 py-0.5 rounded-full
backdrop-blur-md shadow-sm transition-opacity;
}
.card-stat-immortal {
@apply text-[11px] font-bold bg-stone-900/80 text-emerald-400 border border-emerald-500/20 opacity-90 group-hover:opacity-100;
}
.card-stat-curated {
@apply text-[11px] font-bold bg-stone-900/80 text-cyan-400 border border-cyan-500/20 opacity-90 group-hover:opacity-100;
}
.card-stat-fresh {
@apply text-[11px] font-bold bg-stone-900/80 text-amber-400 border border-amber-500/20 opacity-90 group-hover:opacity-100;
}
.card-stat-views {
@apply text-[10px] bg-stone-900/80 text-stone-400 px-1.5 py-0.5 opacity-0 group-hover:opacity-100;
}
/* Панели карточки */
.card-top {
@apply absolute top-2 left-2 z-20 pointer-events-none flex items-center gap-1;
}
.card-bottom {
@apply absolute bottom-2 left-2 right-2 z-20 flex items-center justify-between pointer-events-none;
}
/* Заглушка noscript внутри карточки */
.card-noscript {
@apply flex flex-col items-center justify-center p-3 text-center gap-1;
}
.card-noscript-warn {
@apply text-[11px] font-bold text-amber-600 dark:text-amber-400;
}
.card-noscript-title {
@apply text-[10px] text-stone-500 dark:text-stone-400 underline;
}
}
/* ==========================================================================
КОМПОНЕНТЫ СТРАНИЦЫ АРХИВА ГАЛЕРЕИ (GALLERY ARCHIVE)
========================================================================== */
@layer components {
/* Навигация и заголовок архива */
.archive-back {
@apply inline-flex items-center gap-2 text-xs font-bold uppercase tracking-wider
text-stone-500 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100
transition-colors border-0 no-underline;
}
.archive-phase {
@apply text-xs font-mono text-stone-500 dark:text-stone-400;
}
.archive-header {
@apply border-b border-stone-200 dark:border-zinc-800 pb-6 space-y-2;
}
.archive-title {
@apply text-3xl font-black uppercase tracking-tight text-stone-900 dark:text-stone-100;
}
.archive-badge {
@apply text-xs font-bold font-mono px-2.5 py-1 rounded-full
bg-purple-100 dark:bg-purple-950/70 text-purple-700 dark:text-purple-400
border border-purple-300 dark:border-purple-800/50;
}
.archive-desc {
@apply text-sm text-stone-500 dark:text-stone-400;
}
/* Панель фильтров архива */
.archive-filter-bar {
@apply flex flex-col md:flex-row md:items-center justify-between gap-4
bg-stone-100/70 dark:bg-zinc-900/70 p-3 rounded-2xl
border border-stone-200 dark:border-zinc-800 backdrop-blur-sm;
}
.archive-tab {
@apply inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold
transition-all border-0 no-underline;
}
.archive-tab-all-active {
@apply bg-purple-600 text-white shadow-sm;
}
.archive-tab-all-idle {
@apply text-stone-600 dark:text-stone-300 hover:bg-stone-200/70 dark:hover:bg-zinc-800;
}
.archive-tab-fresh-active {
@apply bg-amber-500 text-white shadow-sm;
}
.archive-tab-fresh-idle {
@apply text-stone-600 dark:text-stone-300 hover:bg-amber-100/60 dark:hover:bg-amber-950/40;
}
.archive-tab-curated-active {
@apply bg-cyan-600 text-white shadow-sm;
}
.archive-tab-curated-idle {
@apply text-stone-600 dark:text-stone-300 hover:bg-cyan-100/60 dark:hover:bg-cyan-950/40;
}
.archive-tab-top-active {
@apply bg-emerald-600 text-white shadow-sm;
}
.archive-tab-top-idle {
@apply text-stone-600 dark:text-stone-300 hover:bg-emerald-100/60 dark:hover:bg-emerald-950/40;
}
/* Сортировка */
.archive-sort-wrap {
@apply flex items-center gap-2 self-end md:self-auto;
}
.archive-sort-label {
@apply text-xs font-bold text-stone-500 dark:text-stone-400 shrink-0;
}
.archive-sort-select {
@apply text-xs font-medium bg-white dark:bg-zinc-800 text-stone-800 dark:text-stone-200
border border-stone-300 dark:border-zinc-700 rounded-xl px-3 py-1.5
focus:outline-none focus:ring-2 focus:ring-purple-500/50 cursor-pointer shadow-sm;
}
/* Сетка и пустое состояние */
.gallery-grid {
@apply grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6;
}
.archive-empty {
@apply col-span-full py-16 text-center text-stone-400 space-y-3
bg-stone-100/50 dark:bg-zinc-900/50 rounded-2xl border border-stone-200 dark:border-zinc-800;
}
.archive-empty-icon {
@apply w-12 h-12 mx-auto opacity-30 animate-pulse;
}
.archive-empty-text {
@apply font-medium text-sm;
}
/* Пагинация */
.paginator {
@apply flex items-center justify-between pt-6 border-t border-stone-200 dark:border-zinc-800;
}
.paginator-btn {
@apply inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold
transition-all border-0 no-underline;
}
.paginator-btn-prev {
@apply bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700
text-stone-700 dark:text-stone-300 hover:scale-105 active:scale-95 shadow-sm
border border-stone-200 dark:border-zinc-700/60 cursor-pointer;
}
.paginator-btn-next {
@apply bg-purple-600 hover:bg-purple-700 text-white hover:scale-105 active:scale-95 shadow-md cursor-pointer;
}
.paginator-btn-disabled {
@apply text-stone-400 dark:text-stone-600 bg-stone-100/50 dark:bg-zinc-900/50
border border-stone-200/50 dark:border-zinc-800/50 opacity-50 cursor-not-allowed;
}
.paginator-pages {
@apply hidden sm:flex items-center gap-1.5 font-mono text-xs;
}
.paginator-page-active {
@apply w-8 h-8 rounded-lg bg-purple-600 text-white font-bold flex items-center justify-center shadow-md;
}
.paginator-page-link {
@apply w-8 h-8 rounded-lg bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700
text-stone-700 dark:text-stone-300 flex items-center justify-center transition-colors border-0 no-underline;
}
}
/* ========================================================================== /* ==========================================================================
ТИПОГРАФИКА И СТИЛИ ДЛЯ СТАТЕЙ БЛОГА (CodeMirror / Rich HTML Content) ТИПОГРАФИКА И СТИЛИ ДЛЯ СТАТЕЙ БЛОГА (CodeMirror / Rich HTML Content)
========================================================================== */ ========================================================================== */
+27 -30
View File
@@ -27,49 +27,48 @@
<!-- Хлебные крошки и навигация --> <!-- Хлебные крошки и навигация -->
<div class="flex flex-wrap items-center justify-between gap-4"> <div class="flex flex-wrap items-center justify-between gap-4">
<a href="{% url 'hypn0_site:index' %}" <a href="{% url 'hypn0_site:index' %}" class="archive-back">
class="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-stone-500 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg> </svg>
<span>К генератору</span> <span>К генератору</span>
</a> </a>
<span class="text-xs font-mono text-stone-500 dark:text-stone-400"> <span class="archive-phase">
Фаза {{ page_obj.number }} из {{ page_obj.paginator.num_pages }} (Всего: {{ page_obj.paginator.count }}) Фаза {{ page_obj.number }} из {{ page_obj.paginator.num_pages }} (Всего: {{ page_obj.paginator.count }})
</span> </span>
</div> </div>
<!-- Шапка архива --> <!-- Шапка архива -->
<div class="border-b border-stone-200 dark:border-zinc-800 pb-6 space-y-2"> <div class="archive-header">
<div class="flex flex-wrap items-center gap-3"> <div class="flex flex-wrap items-center gap-3">
<h1 class="text-3xl font-black uppercase tracking-tight text-stone-900 dark:text-stone-100"> <h1 class="archive-title">
Галерея транса Галерея транса
</h1> </h1>
<span class="text-xs font-bold font-mono px-2.5 py-1 rounded-full bg-purple-100 dark:bg-purple-950/70 text-purple-700 dark:text-purple-400 border border-purple-300 dark:border-purple-800/50"> <span class="archive-badge">
ARCHIVE ARCHIVE
</span> </span>
</div> </div>
<p class="text-sm text-stone-500 dark:text-stone-400"> <p class="archive-desc">
Полный архив психоделических генераций • Сортировка по гравитации, признанию и свежести Полный архив психоделических генераций • Сортировка по гравитации, признанию и свежести
</p> </p>
</div> </div>
<!-- Панель фильтров (Табы этажей + Селект сортировки) --> <!-- Панель фильтров (Табы этажей + Селект сортировки) -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-stone-100/70 dark:bg-zinc-900/70 p-3 rounded-2xl border border-stone-200 dark:border-zinc-800 backdrop-blur-sm"> <div class="archive-filter-bar">
<!-- ТАБЫ ЭТАЖЕЙ --> <!-- ТАБЫ ЭТАЖЕЙ -->
<div class="flex flex-wrap items-center gap-1.5 sm:gap-2"> <div class="flex flex-wrap items-center gap-1.5 sm:gap-2">
<!-- Все волны --> <!-- Все волны -->
<a href="?floor=all&sort={{ current_sort }}" <a href="?floor=all&sort={{ current_sort }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all {% if current_floor == 'all' %}bg-purple-600 text-white shadow-sm{% else %}text-stone-600 dark:text-stone-300 hover:bg-stone-200/70 dark:hover:bg-zinc-800{% endif %}"> class="archive-tab {% if current_floor == 'all' %}archive-tab-all-active{% else %}archive-tab-all-idle{% endif %}">
<span>Все волны</span> <span>Все волны</span>
<span class="font-mono text-[10px] opacity-75">({{ counts.all }})</span> <span class="font-mono text-[10px] opacity-75">({{ counts.all }})</span>
</a> </a>
<!-- 1-й этаж: Инкубатор --> <!-- 1-й этаж: Инкубатор -->
<a href="?floor=fresh&sort={{ current_sort }}" <a href="?floor=fresh&sort={{ current_sort }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all {% if current_floor == 'fresh' %}bg-amber-500 text-white shadow-sm{% else %}text-stone-600 dark:text-stone-300 hover:bg-amber-100/60 dark:hover:bg-amber-950/40{% endif %}"> class="archive-tab {% if current_floor == 'fresh' %}archive-tab-fresh-active{% else %}archive-tab-fresh-idle{% endif %}">
<span class="w-2 h-2 rounded-full bg-amber-400"></span> <span class="w-2 h-2 rounded-full bg-amber-400"></span>
<span>Инкубатор</span> <span>Инкубатор</span>
<span class="font-mono text-[10px] opacity-75">({{ counts.fresh }})</span> <span class="font-mono text-[10px] opacity-75">({{ counts.fresh }})</span>
@@ -77,7 +76,7 @@
<!-- 2-й этаж: Одобрено --> <!-- 2-й этаж: Одобрено -->
<a href="?floor=curated&sort={{ current_sort }}" <a href="?floor=curated&sort={{ current_sort }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all {% if current_floor == 'curated' %}bg-cyan-600 text-white shadow-sm{% else %}text-stone-600 dark:text-stone-300 hover:bg-cyan-100/60 dark:hover:bg-cyan-950/40{% endif %}"> class="archive-tab {% if current_floor == 'curated' %}archive-tab-curated-active{% else %}archive-tab-curated-idle{% endif %}">
<span class="w-2 h-2 rounded-full bg-cyan-400"></span> <span class="w-2 h-2 rounded-full bg-cyan-400"></span>
<span>Одобрено</span> <span>Одобрено</span>
<span class="font-mono text-[10px] opacity-75">({{ counts.curated }})</span> <span class="font-mono text-[10px] opacity-75">({{ counts.curated }})</span>
@@ -85,7 +84,7 @@
<!-- 3-й этаж: Золотой фонд --> <!-- 3-й этаж: Золотой фонд -->
<a href="?floor=top&sort={{ current_sort }}" <a href="?floor=top&sort={{ current_sort }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all {% if current_floor == 'top' %}bg-emerald-600 text-white shadow-sm{% else %}text-stone-600 dark:text-stone-300 hover:bg-emerald-100/60 dark:hover:bg-emerald-950/40{% endif %}"> class="archive-tab {% if current_floor == 'top' %}archive-tab-top-active{% else %}archive-tab-top-idle{% endif %}">
<span class="w-2 h-2 rounded-full bg-emerald-400"></span> <span class="w-2 h-2 rounded-full bg-emerald-400"></span>
<span>Золотой фонд</span> <span>Золотой фонд</span>
<span class="font-mono text-[10px] opacity-75">({{ counts.top }})</span> <span class="font-mono text-[10px] opacity-75">({{ counts.top }})</span>
@@ -93,13 +92,13 @@
</div> </div>
<!-- СЕЛЕКТ СОРТИРОВКИ --> <!-- СЕЛЕКТ СОРТИРОВКИ -->
<div class="flex items-center gap-2 self-end md:self-auto"> <div class="archive-sort-wrap">
<label for="gallery-sort-select" class="text-xs font-bold text-stone-500 dark:text-stone-400 shrink-0"> <label for="gallery-sort-select" class="archive-sort-label">
Порядок: Порядок:
</label> </label>
<select id="gallery-sort-select" <select id="gallery-sort-select"
onchange="window.location.href=this.value" onchange="window.location.href=this.value"
class="text-xs font-medium bg-white dark:bg-zinc-800 text-stone-800 dark:text-stone-200 border border-stone-300 dark:border-zinc-700 rounded-xl px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500/50 cursor-pointer shadow-sm"> class="archive-sort-select">
{% for opt in sort_options %} {% for opt in sort_options %}
<option value="?floor={{ current_floor }}&sort={{ opt.id }}" {% if current_sort == opt.id %}selected{% endif %}> <option value="?floor={{ current_floor }}&sort={{ opt.id }}" {% if current_sort == opt.id %}selected{% endif %}>
{{ opt.icon }} {{ opt.title }} {{ opt.icon }} {{ opt.title }}
@@ -111,34 +110,32 @@
</div> </div>
<!-- Сетка картин (16 штук на страницу) --> <!-- Сетка картин (16 штук на страницу) -->
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6"> <div class="gallery-grid">{% for item in page_obj %}
{% for item in page_obj %} {% include "block/gallery_card.html" %}
{% include "block/gallery_card.html" %} {% empty %}<div class="archive-empty">
{% empty %} <svg class="archive-empty-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="col-span-full py-16 text-center text-stone-400 space-y-3 bg-stone-100/50 dark:bg-zinc-900/50 rounded-2xl border border-stone-200 dark:border-zinc-800">
<svg class="w-12 h-12 mx-auto opacity-30 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg> </svg>
<p class="font-medium text-sm">В этой выборке пока нет картин. Сгенерируйте шедевр прямо сейчас!</p> <p class="archive-empty-text">В этой выборке пока нет картин. Сгенерируйте шедевр прямо сейчас!</p>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<!-- Пагинация (Предыдущая фаза / Номера страниц / Следующая фаза) --> <!-- Пагинация (Предыдущая фаза / Номера страниц / Следующая фаза) -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<div class="flex items-center justify-between pt-6 border-t border-stone-200 dark:border-zinc-800"> <div class="paginator">
<div> <div>
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<a href="?floor={{ current_floor }}&sort={{ current_sort }}&page={{ page_obj.previous_page_number }}" <a href="?floor={{ current_floor }}&sort={{ current_sort }}&page={{ page_obj.previous_page_number }}"
rel="prev" rel="prev"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-700 dark:text-stone-300 transition-all hover:scale-105 active:scale-95 shadow-sm border border-stone-200 dark:border-zinc-700/60 cursor-pointer"> class="paginator-btn paginator-btn-prev">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg> </svg>
<span>Предыдущая фаза</span> <span>Предыдущая фаза</span>
</a> </a>
{% else %} {% else %}
<span class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold text-stone-400 dark:text-stone-600 bg-stone-100/50 dark:bg-zinc-900/50 border border-stone-200/50 dark:border-zinc-800/50 opacity-50 cursor-not-allowed"> <span class="paginator-btn paginator-btn-disabled">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg> </svg>
@@ -148,15 +145,15 @@
</div> </div>
<!-- Номера страниц --> <!-- Номера страниц -->
<div class="hidden sm:flex items-center gap-1.5 font-mono text-xs"> <div class="paginator-pages">
{% for p in page_obj.paginator.page_range %} {% for p in page_obj.paginator.page_range %}
{% if p == page_obj.number %} {% if p == page_obj.number %}
<span class="w-8 h-8 rounded-lg bg-purple-600 text-white font-bold flex items-center justify-center shadow-md"> <span class="paginator-page-active">
{{ p }} {{ p }}
</span> </span>
{% else %} {% else %}
<a href="?floor={{ current_floor }}&sort={{ current_sort }}&page={{ p }}" <a href="?floor={{ current_floor }}&sort={{ current_sort }}&page={{ p }}"
class="w-8 h-8 rounded-lg bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-700 dark:text-stone-300 flex items-center justify-center transition-colors"> class="paginator-page-link">
{{ p }} {{ p }}
</a> </a>
{% endif %} {% endif %}
@@ -167,14 +164,14 @@
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?floor={{ current_floor }}&sort={{ current_sort }}&page={{ page_obj.next_page_number }}" <a href="?floor={{ current_floor }}&sort={{ current_sort }}&page={{ page_obj.next_page_number }}"
rel="next" rel="next"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold bg-purple-600 hover:bg-purple-700 text-white transition-all hover:scale-105 active:scale-95 shadow-md border-0 cursor-pointer"> class="paginator-btn paginator-btn-next">
<span>Следующая фаза</span> <span>Следующая фаза</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg> </svg>
</a> </a>
{% else %} {% else %}
<span class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold text-stone-400 dark:text-stone-600 bg-stone-100/50 dark:bg-zinc-900/50 border border-stone-200/50 dark:border-zinc-800/50 opacity-50 cursor-not-allowed"> <span class="paginator-btn paginator-btn-disabled">
<span>Следующая фаза</span> <span>Следующая фаза</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
+7 -7
View File
@@ -1,11 +1,11 @@
{% extends "_base.html" %} {% extends "_base.html" %}
{% load static %} {% load static %}
{% block PAGE_TITLE %}{{ item.s_title }} — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG{% endblock %} {% block PAGE_TITLE %}{{ item.s_title_plain }} — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG{% endblock %}
{% block META_TITLE %}{{ item.s_title }} — Векторный транс #{{ item.s_hash_id }} | HypnoSVG{% endblock %} {% block META_TITLE %}{{ item.s_title_plain }} — Векторный транс #{{ item.s_hash_id }} | HypnoSVG{% endblock %}
{% block META_DESCRIPTION %}Гипнотическая векторная картина «{{ item.s_title }}» (#{{ item.s_hash_id }}). {{ svg_stats.total_oscillators }} векторных осцилляторов. Генератор анимированных SVG-халфтонов HypnoSVG.{% endblock %} {% block META_DESCRIPTION %}Гипнотическая векторная картина «{{ item.s_title_plain }}» (#{{ item.s_hash_id }}). {{ svg_stats.total_oscillators }} векторных осцилляторов. Генератор анимированных SVG-халфтонов HypnoSVG.{% endblock %}
{% block OG_TITLE %}{{ item.s_title }} — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG{% endblock %} {% block OG_TITLE %}{{ item.s_title_plain }} — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG{% endblock %}
{% block OG_DESCRIPTION %}Гипнотическая векторная картина «{{ item.s_title }}» (#{{ item.s_hash_id }}). Векторный транс в галерее HypnoSVG.{% endblock %} {% block OG_DESCRIPTION %}Гипнотическая векторная картина «{{ item.s_title_plain }}» (#{{ item.s_hash_id }}). Векторный транс в галерее HypnoSVG.{% endblock %}
{% block OG_IMAGE %}{% static 'img/og-hypn0-default.png' %}{% endblock %} {% block OG_IMAGE %}{% static 'img/og-hypn0-default.png' %}{% endblock %}
{% block TWITTER_IMAGE %}{% static 'img/og-hypn0-default.png' %}{% endblock %} {% block TWITTER_IMAGE %}{% static 'img/og-hypn0-default.png' %}{% endblock %}
@@ -77,8 +77,8 @@
<!-- Полноэкранный просмотр SVG с живой анимацией и плавающими стрелками --> <!-- Полноэкранный просмотр SVG с живой анимацией и плавающими стрелками -->
<div style="{{ item.card_bg_style }}" <div style="{{ item.card_bg_style }}"
title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG" title="«{{ item.s_title_plain }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG"
aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG" aria-label="«{{ item.s_title_plain }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG"
class="relative w-full aspect-square flex items-center justify-center rounded-2xl overflow-hidden shadow-inner border border-stone-300/80 dark:border-zinc-800/80 group select-none bg-[var(--card-bg-light)] dark:bg-[var(--card-bg-dark)]"> class="relative w-full aspect-square flex items-center justify-center rounded-2xl overflow-hidden shadow-inner border border-stone-300/80 dark:border-zinc-800/80 group select-none bg-[var(--card-bg-light)] dark:bg-[var(--card-bg-dark)]">
<!-- Плавающая кнопка: Назад --> <!-- Плавающая кнопка: Назад -->
+1 -1
View File
File diff suppressed because one or more lines are too long