Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c44f2ef35 | ||
|
|
5e9d4e0902 | ||
|
|
5f53be810f | ||
|
|
29a063c806 | ||
|
|
d16a5459ad | ||
|
|
ddb0f9ebbb | ||
|
|
b86bb6c883 | ||
|
|
110c525e40 | ||
|
|
9936be03ad | ||
|
|
afb75b7cf7 | ||
|
|
abd4678e4a |
@@ -583,6 +583,7 @@ CLI больше не нужен.
|
||||
| `.env.sample` | Env template |
|
||||
| `Dockerfile` | Сборка |
|
||||
| `docker-compose.local.yml` | Dev |
|
||||
| `docker-compose.prod-test.yml` | Тестовый Prod |
|
||||
| `docker-compose.prod.yml` | Prod |
|
||||
| `config/nginx/hypn0-app--external-nginx.conf` | Nginx |
|
||||
| `.gitea/workflows/docker-publish.yaml` | CI/CD |
|
||||
|
||||
@@ -39,6 +39,11 @@ server {
|
||||
charset utf-8;
|
||||
client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок
|
||||
|
||||
# --- СЕТЕВЫЕ ОПТИМИЗАЦИИ И ЗАЩИТА ОТ ТАЙМАУТОВ ЧЕРЕЗ VPN (MTU/MSS) ---
|
||||
tcp_nopush on; # Отправляет заголовки и начало файла в одном TCP-пакете
|
||||
tcp_nodelay on; # Отключает задержку Nagle для быстрых интерактивных ответов
|
||||
ssl_buffer_size 4k; # Уменьшенный буфер TLS: решает зависания пакетов в VPN/WireGuard туннелях
|
||||
|
||||
# Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu)
|
||||
access_log /var/log/nginx/hypn0.access.log;
|
||||
error_log /var/log/nginx/hypn0.error.log;
|
||||
@@ -49,7 +54,7 @@ server {
|
||||
gzip_vary on; # Добавляет заголовок Vary: Accept-Encoding
|
||||
gzip_proxied any; # Сжимать ответы, даже если мы за прокси
|
||||
gzip_comp_level 6; # Оптимальный баланс скорость/сжатие
|
||||
gzip_min_length 1000; # Не сжимать совсем мелочь
|
||||
gzip_min_length 256; # Сжимать ответы от 256 байт
|
||||
# Типы файлов для сжатия (HTML сжимается автоматически, его писать не нужно)
|
||||
gzip_types
|
||||
text/plain
|
||||
@@ -133,6 +138,14 @@ server {
|
||||
# Даже если внутри контейнера это HTTP на 127.0.0.1:8042, для Django это должно быть HTTPS
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# --- БУФЕРИЗАЦИЯ В ПАМЯТИ (RAM) ---
|
||||
# Позволяет отдавать большие страницы (80+ КБ) целиком из RAM без записи во временные файлы /var/lib/nginx/proxy
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 8 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_temp_file_write_size 256k;
|
||||
|
||||
# Тайм-ауты (важно для долгих операций, если они есть)
|
||||
proxy_read_timeout 180s;
|
||||
proxy_connect_timeout 180s;
|
||||
|
||||
+21
-10
@@ -5,18 +5,18 @@
|
||||
|
||||
## Что здесь лежит
|
||||
|
||||
### `tailwind/` — сборка Tailwind CSS v3.4
|
||||
### `tailwind/` — сборка Tailwind CSS v4
|
||||
|
||||
```
|
||||
tailwind/
|
||||
├── package.json # Зависимости: tailwindcss@3.4, postcss, autoprefixer
|
||||
├── package.json # Зависимости: @tailwindcss/cli, tailwindcss v4
|
||||
├── package-lock.json # Фиксация версий
|
||||
└── build-tailwind.sh # ← запускается из корня проекта
|
||||
└── build-tailwind.sh # ← запускается из корня проекта (scripts/build-tailwind.sh)
|
||||
```
|
||||
|
||||
**Что делает:**
|
||||
- `build-tailwind.sh` создаёт временные файлы (`tailwind.config.js`, `postcss.config.js`, `input.css`),
|
||||
запускает `npm ci` + `npm run build`, собирает `public/static/css/tailwind.min.css`,
|
||||
- `build-tailwind.sh` создаёт временный `input.css` с директивами `@import "tailwindcss";`, `@source` и импортом `tailwind-custom.css`,
|
||||
запускает `npm install` + `npm run build` (`@tailwindcss/cli`), собирает `public/static/css/tailwind.min.css`,
|
||||
затем удаляет временные файлы.
|
||||
- Результат: `public/static/css/tailwind.min.css` — минифицированный CSS со всеми
|
||||
используемыми утилитами + кастомные стили из `hypn0/templates/css/tailwind-custom.css`.
|
||||
@@ -37,6 +37,18 @@ alpine/
|
||||
└── package-lock.json
|
||||
```
|
||||
|
||||
### `codemirror/` — сборка CodeMirror 6 для админки Django
|
||||
|
||||
```
|
||||
codemirror/
|
||||
├── package.json # Зависимости: @codemirror/*, esbuild, @uiw/codemirror-theme-solarized
|
||||
└── package-lock.json # Фиксация версий
|
||||
```
|
||||
|
||||
**Что делает:**
|
||||
- `build-codemirror.sh` генерирует точку входа `src/editor.js` с поддержкой языков (HTML, CSS, JavaScript, JSON), автоматической смены темы (Solarized Light / Dark), форматирования и двусторонней синхронизации с Django админкой (`textarea[data-codemirror-editor]`).
|
||||
- Собирает единый минифицированный IIFE-бандл через `esbuild` в `public/static/codemirror/editor.js`.
|
||||
|
||||
## Как запускать сборки
|
||||
|
||||
Из корня проекта:
|
||||
@@ -50,18 +62,17 @@ bash scripts/build-htmx.sh
|
||||
|
||||
# Alpine.js
|
||||
bash scripts/build-alpine.sh
|
||||
|
||||
# CodeMirror 6 (для админки Django)
|
||||
bash scripts/build-codemirror.sh
|
||||
```
|
||||
|
||||
Каждый скрипт сам делает всё:
|
||||
1. проверяет наличие `npm`;
|
||||
2. создаёт временные файлы;
|
||||
3. устанавливает зависимости через `npm ci`;
|
||||
3. устанавливает зависимости через `npm ci` (или `npm install`);
|
||||
4. запускает `npm run build`;
|
||||
5. кладёт готовый бандл в `public/static/...`;
|
||||
6. удаляет временные файлы.
|
||||
|
||||
В рабочем дереве не остаётся мусора от сборки.
|
||||
|
||||
## Кодовая статика
|
||||
|
||||
В будущем — возможно, сборка CodeMirror 6 для админки Django.
|
||||
|
||||
+1372
-1468
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,7 @@
|
||||
"build": "tailwindcss -i input.css -o ../../public/static/css/tailwind.min.css --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.35",
|
||||
"autoprefixer": "^10.4.17"
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ if settings.DEBUG:
|
||||
urlpatterns = [path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
|
||||
urlpatterns = [*PUBLIC_ROOT_URLPATTERNS, *urlpatterns]
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
# ==============================================================================
|
||||
# РАЗДАЧА МЕДИА-ФАЙЛОВ (/media/...)
|
||||
|
||||
@@ -878,9 +878,9 @@ class CardBgStyleTests(BaseMediaTestCase):
|
||||
self.assertIn('id="test-svg"', item.card_svg)
|
||||
|
||||
response = self.client.get(reverse("hypn0_site:index"))
|
||||
self.assertContains(response, 'template shadowrootmode="open"')
|
||||
self.assertContains(response, "hypn0LazySvg")
|
||||
self.assertContains(response, "hypn0-card-svg")
|
||||
self.assertContains(response, "--hypn0-play: running !important")
|
||||
self.assertContains(response, item.file_svg.url)
|
||||
|
||||
|
||||
class GalleryArchiveTests(BaseMediaTestCase):
|
||||
@@ -1458,7 +1458,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
("502", 502, "Контейнер потерял сознание"),
|
||||
("503", 503, "Плановый сеанс гипнотерапии"),
|
||||
("504", 504, "Разрыв астральной связи"),
|
||||
("under_reconstruction", 200, "Сектор на реконструкции"),
|
||||
("under_reconstruction", 200, "Отдел мозга на реконструкции"),
|
||||
]
|
||||
|
||||
for code, expected_status, text_fragment in expected_checks:
|
||||
|
||||
+12
-27
@@ -111,41 +111,26 @@ val=>{if(val)document.documentElement.classList.add('dark');else document.docume
|
||||
{% endblock EXTRA_LD_JSON %}
|
||||
]
|
||||
</script>
|
||||
<script src="{% static 'js/hypn0.js' %}" defer></script>
|
||||
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
||||
<script src="{% static 'js/htmx.min.js' %}" defer></script>
|
||||
<script>
|
||||
document.addEventListener('htmx:configRequest', function(evt) {
|
||||
var token = '{{ csrf_token }}';
|
||||
if (!token) {
|
||||
var match = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
if (match) token = match[1];
|
||||
}
|
||||
if (token) {
|
||||
evt.detail.headers['X-CSRFToken'] = token;
|
||||
}
|
||||
});
|
||||
|
||||
function attachShadowRoots(root) {
|
||||
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
|
||||
if (!tmpl.parentElement.shadowRoot) {
|
||||
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
|
||||
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
|
||||
shadow.appendChild(tmpl.content.cloneNode(true));
|
||||
tmpl.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() { attachShadowRoots(document); });
|
||||
document.addEventListener('htmx:afterSwap', function(evt) { attachShadowRoots(evt.detail.target); });
|
||||
</script>
|
||||
{% block EXTRA_HEAD %}{% endblock EXTRA_HEAD %}
|
||||
</head>
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="bg-stone-50 text-emerald-700 dark:bg-zinc-950 dark:text-emerald-100 font-mono">{% block ADD_CSS1 %}{% endblock %}{% block ADD_CSS2 %}{% endblock %}{% block ADD_CSS3 %}{% endblock %}{% block BODY %}
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="bg-stone-50 text-emerald-700 dark:bg-zinc-950 dark:text-emerald-100 font-mono">
|
||||
<noscript>
|
||||
<div class="sticky top-0 z-50 bg-amber-500 text-stone-950 px-4 py-2.5 text-center text-xs sm:text-sm font-bold shadow-lg border-b border-amber-600 flex items-center justify-center gap-2">
|
||||
<span>⚠️</span>
|
||||
<span>Для подчинения разума и генерации гипнотических SVG-матриц требуется включить <strong>JavaScript</strong> в вашем браузере!</span>
|
||||
</div>
|
||||
</noscript>
|
||||
{% block ADD_CSS1 %}{% endblock %}{% block ADD_CSS2 %}{% endblock %}{% block ADD_CSS3 %}{% endblock %}{% block BODY %}
|
||||
{% include "block/header.html" %}
|
||||
<main id="main-content" class="mx-auto min-h-screen max-w-7xl px-4 py-8">
|
||||
{% block CONTENT %}{% endblock CONTENT %}
|
||||
</main>
|
||||
{# {% include "blocks/footer.jinja2" %} #}{% endblock BODY %}{% if not ALLOW_TRACKING %}{# Если клиент еще не согласился на отслеживание покажем ему уведомление #}{% include "block/allow-tracking.html" %}{% else %}{# Если согласился -- отслеживаем#}
|
||||
<script src="{% static 'js/analytics.js' %}"></script>{% endif %}
|
||||
<script src="{% static 'js/analytics.js' %}"></script>
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5FVH3KMJ" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/112812760" style="position:absolute; left:-9999px;" alt="" /></div></noscript>{% endif %}
|
||||
{% block ADD_JS1 %}{% endblock ADD_JS1 %}{% block ADD_JS2 %}{% endblock ADD_JS2 %}{% block ADD_JS3 %}{% endblock ADD_JS3 %}</body>
|
||||
</html>
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>400 — Сбой ментального протокола | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 400: Некорректный запрос к матрице HypnoSVG. Сервер не распознал структуру переданного сигнала." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>401 — Требуется ментальная идентификация | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 401: Доступ к закрытому узлу HypnoSVG требует прохождения авторизации." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>403 — Отказано бюрократом 24-го уровня | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 403: Доступ к ресурсу категорически запрещен протоколами безопасности HypnoSVG." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>404 — Векторная реальность не найдена | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 404: Запрашиваемая страница HypnoSVG не существует или стёрта волей Гипножабы." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>413 — Перегрузка сенсорных каналов | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 413: Загружаемый файл превышает допустимый размер ментального буфера HypnoSVG." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>429 — Слишком много мыслей в секунду | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 429: Превышен лимит запросов к генератору HypnoSVG. Сделайте ментальную паузу." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>500 — Критический перегрев неокортекса | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 500: Внутренний сбой сервера HypnoSVG. Ведутся ментальные восстановительные работы." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>502 — Контейнер потерял сознание | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 502: Прокси-сервер не получил ответа от бэкенда HypnoSVG." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>503 — Плановый сеанс гипнотерапии | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 503: Сервис HypnoSVG временно недоступен из-за технического обслуживания или пиковой нагрузки." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>504 — Разрыв астральной связи | HypnoSVG</title>
|
||||
<meta name="description" content="Ошибка 504: Превышено время ожидания ответа от вышестоящего сервера HypnoSVG." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title>Реконструкция матрицы | HypnoSVG</title>
|
||||
<meta name="description" content="Сектор временно закрыт на квантовую реконструкцию и модернизацию алгоритмов HypnoSVG." />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/img/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/media/_error/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/media/_error/favicon.ico" />
|
||||
<script>
|
||||
(function() {
|
||||
var saved = localStorage.getItem('darkMode');
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
{# НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #}<div id="allow-tracking-popup" x-data="{ agreed: !!localStorage.getItem('hypn0_vid') }" x-show="!agreed" x-cloak class="sticky bottom-0 z-50 bg-gray-100/80 dark:bg-slate-800/70 backdrop-blur border-t border-gray-300 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 py-8 grid grid-cols-[auto_1fr] gap-x-4">
|
||||
<p class="flex-col px-6 py-6">
|
||||
{# НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #}<div id="allow-tracking-popup" x-data="{ agreed: !!localStorage.getItem('hypn0_vid') }" x-show="!agreed" x-cloak class="sticky bottom-0 z-50 bg-gray-100/95 dark:bg-slate-800/90 backdrop-blur border-t border-gray-300 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto p-3 sm:p-4 md:px-4 md:py-8 flex flex-col md:grid md:grid-cols-[1fr_auto] gap-2.5 md:gap-x-4 items-center">
|
||||
<p class="p-0 md:px-6 md:py-6 text-xs sm:text-sm md:text-base leading-snug md:leading-normal">
|
||||
<span class="md:hidden">
|
||||
Внимание! Этот сайт использует файлы-присоски (cookies) и алгоритмы ментального мерцания в соответствии с 152-<abbr title="Федеральный закон">ФЗ</abbr>, <abbr title="General Data Protection Regulation">GDPR</abbr> и <a href="/blog/security-and-privacy-policy">политикой конфи­денциаль­ности</a>.
|
||||
</span>
|
||||
<span class="hidden md:inline">
|
||||
Внимание! В соответствии с 152-<abbr title="Федеральный закон">ФЗ</abbr>
|
||||
и <abbr title="General Data Protection Regulation">GDPR</abbr> и протоколами Нового Нью-Йорка,
|
||||
этот сайт использует файлы-присоски (cookies). ВСЕ СЛАВЯТ ГИПНОЖАБУ. Ваши обезли­ченные данные
|
||||
считываются алгоритмами ментального мерцания. Оставаясь на гипно-платформе <strong>hypn0</strong>, вы добровольно
|
||||
впадаете в транс и принимаете условия нашей <a href="/blog/security-and-privacy-policy">политики
|
||||
конфи­денциаль­ности</a>. <strong>Не пытайтесь переключить вкладку!</strong>
|
||||
</span>
|
||||
</p>
|
||||
<button @click="const vid = crypto.randomUUID(); localStorage.setItem('hypn0_vid', vid); document.cookie = `hypn0_vid=${vid}; path=/; max-age=63072000; SameSite=Lax`; agreed = true" class="text-lg px-6 py-2 my-6 border-2 rounded-md bg-emerald-600 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 text-white">Подчиниться Гипножабе!</button>
|
||||
<button @click="const vid = crypto.randomUUID(); localStorage.setItem('hypn0_vid', vid); document.cookie = `hypn0_vid=${vid}; path=/; max-age=63072000; SameSite=Lax`; agreed = true" class="w-full md:w-auto text-xs sm:text-sm md:text-lg px-4 md:px-6 py-2 md:py-2 my-0 md:my-6 border md:border-2 rounded-md bg-emerald-600 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 text-white font-bold whitespace-nowrap cursor-pointer transition-colors">Подчиниться Гипножабе!</button>
|
||||
</div>
|
||||
</div>{# / НИЖНИЙ POPUP О КУКУХ И ОТСЛЕЖИВАНИИ #}
|
||||
@@ -8,32 +8,14 @@
|
||||
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"
|
||||
title="«{{ item.s_title }}» — Анимированная SVG-картина #{{ item.s_hash_id }} | HypnoSVG"
|
||||
aria-label="«{{ item.s_title }}» — Векторная картина #{{ item.s_hash_id }} в галерее HypnoSVG">
|
||||
<div 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">
|
||||
<template shadowrootmode="open">
|
||||
{# Declarative Shadow DOM: изолирует ID (<defs id="s1"..>) и стили каждой картины в галерее. #}
|
||||
{# В отличие от <img>, события мыши не блокируются, а в отличие от обычного inline SVG нет конфликтов идентификаторов. #}
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
{# В покое внутри SVG действует --hypn0-play: paused (0% нагрузки на CPU). #}
|
||||
{# При наведении мыши на хост-карточку переопределяем переменную на running, и анимация оживает. #}
|
||||
:host(:hover) svg, svg:hover {
|
||||
--hypn0-play: running !important;
|
||||
}
|
||||
</style>
|
||||
{{ item.card_svg|safe }}
|
||||
</template>
|
||||
<div x-data="hypn0LazySvg('{% if item.file_svg %}{{ item.file_svg.url }}{% endif %}')"
|
||||
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>
|
||||
</noscript>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<div class="max-w-7xl mx-auto px-2 grid grid-cols-[1fr_auto] gap-x-4">
|
||||
{# ЛОГОТИП #}{% comment %}
|
||||
border-0 — логотип оборачивается в ссылку без текста, отключаем типовое
|
||||
пунктирное подчёркивание ссылки, которое иначе появится под картинкой.{% endcomment %}<div class="flex py-4">
|
||||
<a href="/" class="border-0" title="HypnoSVG (hypn0) — Главная"><img src="{% static 'img/logo-hypn0.svg' %}" alt="HypnoSVG (hypn0) — Генератор анимированных SVG-халфтонов" title="HypnoSVG (hypn0)"></a>
|
||||
пунктирное подчёркивание ссылки, которое иначе появится под картинкой.{% endcomment %}<div class="flex items-center py-2 md:py-4">
|
||||
<a href="/" class="border-0 flex items-center" title="HypnoSVG (hypn0) — Главная"><img src="{% static 'img/logo-hypn0.svg' %}" alt="HypnoSVG (hypn0) — Генератор анимированных SVG-халфтонов" title="HypnoSVG (hypn0)" class="h-7 sm:h-8 md:h-12 w-auto"></a>
|
||||
</div>{# /ЛОГОТИП #}
|
||||
{# ПРАВОЕ МЕНЮ #}<menu class="flex items-center gap-x-4 justify-self-start text-xl font-semibold text-slate-900 dark:text-slate-100">
|
||||
<a href="{% url 'hypn0_site:gallery_archive' %}" class="px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg border-0 text-slate-900 dark:text-slate-100">галерея</a>
|
||||
<a href="{% url 'hypn0_site:blog_feed' %}" class="px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden md:block hyphens-none border-0 text-slate-900 dark:text-slate-100">блог</a>
|
||||
<a href="#" class="px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden lg:block border-0 text-slate-900 dark:text-slate-100" @click.prevent="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)" title="Переключить тему"><span x-text="darkMode?'●':'○'"></span></a>
|
||||
{# ПРАВОЕ МЕНЮ #}<menu class="flex items-center gap-x-2 md:gap-x-4 justify-self-start text-sm sm:text-base md:text-xl font-semibold text-slate-900 dark:text-slate-100">
|
||||
<a href="{% url 'hypn0_site:gallery_archive' %}" class="px-2.5 py-1 sm:px-4 sm:py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg border-0 text-slate-900 dark:text-slate-100">галерея</a>
|
||||
<a href="{% url 'hypn0_site:blog_feed' %}" class="px-2.5 py-1 sm:px-4 sm:py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden md:block hyphens-none border-0 text-slate-900 dark:text-slate-100">блог</a>
|
||||
<a href="#" class="px-2.5 py-1 sm:px-4 sm:py-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg hidden lg:block border-0 text-slate-900 dark:text-slate-100" @click.prevent="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)" title="Светлая/Темная тема"><span x-text="darkMode?'●':'○'"></span></a>
|
||||
</menu>{# /ПРАВОЕ МЕНЮ #}
|
||||
</div>
|
||||
</header>
|
||||
@@ -41,7 +41,8 @@
|
||||
<input type="hidden" name="color" value="{% if primary_color %}{{ primary_color }}{% else %}#a855ff{% endif %}">
|
||||
|
||||
<button id="like-to-gallery" type="submit" title="Отправить в галерею"
|
||||
:disabled="isPublishing"
|
||||
:disabled="isPublishing"{% if ALLOW_TRACKING %}
|
||||
{# Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'like_art')"{% endif %}
|
||||
class="group flex items-center gap-2 bg-emerald-600/90 hover:bg-emerald-600 text-white text-xs font-bold uppercase tracking-wide backdrop-blur-md px-4 py-2.5 rounded-full shadow-xl transition-all hover:scale-105 active:scale-95 disabled:opacity-60 disabled:scale-100 disabled:pointer-events-none cursor-pointer">
|
||||
<template x-if="isPublishing">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
class="inline-flex items-center">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
{% if user_voted %}disabled{% endif %}
|
||||
{% if user_voted %}disabled{% endif %}{% if ALLOW_TRACKING %}
|
||||
{# Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'like_art')"{% endif %}
|
||||
class="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-bold transition-all {% if user_voted %}bg-rose-500/20 text-rose-400 border border-rose-500/30 cursor-default{% else %}bg-stone-200 dark:bg-stone-800 hover:bg-rose-500 hover:text-white text-stone-700 dark:text-stone-300 cursor-pointer hover:scale-105 active:scale-95 shadow-sm{% endif %}">
|
||||
<svg class="w-4 h-4 {% if user_voted %}fill-rose-500 text-rose-500{% else %}fill-none stroke-current{% endif %}" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<div class="flex flex-wrap items-center gap-2.5">
|
||||
<!-- Кнопка Копировать SVG -->
|
||||
<button type="button"
|
||||
@click="navigator.clipboard.writeText(document.getElementById('raw-svg-source').value); copiedSvg = true; setTimeout(() => copiedSvg = false, 2500)"
|
||||
@click="navigator.clipboard.writeText(document.getElementById('raw-svg-source').value); copiedSvg = true; setTimeout(() => copiedSvg = false, 2500){% if ALLOW_TRACKING %}; window.ym && ym(112812760, 'reachGoal', 'copy_svg'){% endif %}"
|
||||
class="flex items-center gap-2 px-4 py-2 bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-stone-800 dark:text-stone-200 rounded-xl text-xs font-bold transition-all hover:scale-105 active:scale-95 shadow-sm cursor-pointer">
|
||||
<svg x-show="!copiedSvg" 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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
@@ -136,7 +136,8 @@
|
||||
|
||||
<!-- Кнопка Скачать SVG -->
|
||||
<a href="{% url 'hypn0_site:gallery_download' item.s_hash_id %}"
|
||||
download="hypn0-{{ item.s_hash_id }}.svg"
|
||||
download="hypn0-{{ item.s_hash_id }}.svg"{% if ALLOW_TRACKING %}
|
||||
{# Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'download_svg')"{% endif %}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold transition-all hover:scale-105 active:scale-95 shadow-md border-0 cursor-pointer">
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
|
||||
+39
-33
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
{% block CONTENT %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-8 items-start"
|
||||
<div class="flex flex-col lg:grid lg:grid-cols-[400px_1fr] gap-6 lg:gap-8 items-start"
|
||||
x-data="{
|
||||
/* Активная вкладка: 'style' (фигуры), 'params' (ползунки), 'color' (палитра) */
|
||||
activeTab: 'style',
|
||||
@@ -47,6 +47,7 @@
|
||||
/* Служебные состояния интерфейса */
|
||||
isLoading: false, /* Идет ли процесс генерации SVG (индикация, блокировка) */
|
||||
hasChanges: true, /* Флаг наличия правок в форме для активности кнопки */
|
||||
hasGenerated: false, /* Флаг завершения хотя бы одной генерации */
|
||||
hasImage: false, /* Флаг загрузки пользовательского файла */
|
||||
fileName: '', /* Имя выбранного файла */
|
||||
imagePreviewUrl: null, /* Превью загруженного файла (Blob URL) */
|
||||
@@ -106,7 +107,7 @@
|
||||
}">
|
||||
|
||||
<!-- ЛЕВАЯ ПАНЕЛЬ: НАСТРОЙКИ -->
|
||||
<aside class="space-y-6">
|
||||
<aside class="contents lg:block lg:space-y-6">
|
||||
<form hx-post="/generate"
|
||||
hx-target="#preview"
|
||||
hx-trigger="submit"
|
||||
@@ -115,8 +116,8 @@
|
||||
@change="hasChanges = true"
|
||||
@input="hasChanges = true"
|
||||
@htmx:before-request="startThinking()"
|
||||
@htmx:after-request="isLoading = false; hasChanges = false"
|
||||
class="relative bg-white dark:bg-zinc-900 p-6 rounded-2xl shadow-sm border border-stone-200 dark:border-zinc-800 overflow-hidden">
|
||||
@htmx:after-request="isLoading = false; hasChanges = false; hasGenerated = true"
|
||||
class="contents lg:block lg:relative lg:bg-white lg:dark:bg-zinc-900 lg:p-6 lg:rounded-2xl lg:shadow-sm lg:border lg:border-stone-200 lg:dark:border-zinc-800 lg:overflow-hidden">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Фоновая спираль размышления при генерации (прижата снизу на всю левую панель) -->
|
||||
@@ -127,37 +128,37 @@
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute inset-0 z-20 pointer-events-none flex items-end justify-center overflow-hidden rounded-2xl bg-white/70 dark:bg-zinc-900/75 backdrop-blur-[2px]">
|
||||
class="hidden lg:flex absolute inset-0 z-20 pointer-events-none items-end justify-center overflow-hidden rounded-2xl bg-white/70 dark:bg-zinc-900/75 backdrop-blur-[2px]">
|
||||
<img src="{% static 'img/thinking.svg' %}" alt="thinking"
|
||||
class="w-120 h-120 -mb-24 opacity-75 animate-spin [animation-duration:10s]">
|
||||
</div>
|
||||
|
||||
<!-- Загрузка файла -->
|
||||
<div class="mb-8">
|
||||
<label class="block text-sm font-bold mb-2 uppercase tracking-wider text-stone-500 dark:text-zinc-400">Источник</label>
|
||||
<div class="order-1 w-full bg-white dark:bg-zinc-900 p-3 sm:p-4 lg:p-0 rounded-2xl lg:rounded-none border border-stone-200 dark:border-zinc-800 lg:border-0 shadow-sm lg:shadow-none mb-0 lg:mb-8">
|
||||
<label class="hidden lg:block text-sm font-bold mb-2 uppercase tracking-wider text-stone-500 dark:text-zinc-400">Источник</label>
|
||||
<div class="relative group">
|
||||
<input type="file" name="image" accept="image/*"
|
||||
@change="handleFileChange($event)"
|
||||
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
|
||||
<div
|
||||
:class="hasImage ? 'border-emerald-500 bg-emerald-50/20 dark:bg-emerald-950/20' : 'border-stone-300 dark:border-zinc-700'"
|
||||
class="border-2 border-dashed group-hover:border-emerald-500 rounded-xl p-6 flex flex-col items-center justify-center text-center transition-all">
|
||||
class="border-2 border-dashed group-hover:border-emerald-500 rounded-xl p-3 sm:p-4 lg:p-6 flex flex-col items-center justify-center text-center transition-all">
|
||||
|
||||
<!-- Состояние 1: Файл не выбран -->
|
||||
<div x-show="!hasImage" class="text-stone-400 group-hover:text-emerald-500 flex flex-col items-center gap-3 transition-colors">
|
||||
<svg class="w-20 h-20 fill-current transition-colors opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<div x-show="!hasImage" class="text-stone-400 group-hover:text-emerald-500 flex flex-row lg:flex-col items-center gap-3 transition-colors">
|
||||
<svg class="w-8 h-8 sm:w-10 sm:h-10 lg:w-20 lg:h-20 shrink-0 fill-current transition-colors opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M477 174c8.812-13.708 13.948-29.993 13.948-47.466 0-48.55-39.499-88.048-88.05-88.048-32.839 0-61.516 18.084-76.651 44.803-22.773-5.031-46.356-7.586-70.424-7.586-24.003 0-47.624 2.55-70.427 7.582-15.135-26.717-43.813-44.799-76.65-44.799-48.55 0-88.048 39.498-88.048 88.049 0 17.473 5.137 33.759 13.949 47.467C11.987 204.676 0 239.016 0 274.61c0 53.849 27.194 104.194 76.573 141.763C124.677 452.97 188.399 473.125 256 473.125s131.323-20.155 179.427-56.753C484.806 378.803 512 328.457 512 274.609c0-35.59-11.986-69.935-34.822-100.219zM403.076 70.224c31.266 0 56.703 25.436 56.703 56.701s-25.437 56.702-56.703 56.702-56.701-25.436-56.701-56.702 25.436-56.701 56.701-56.701zm-294.152 0c31.266 0 56.703 25.436 56.703 56.701s-25.437 56.702-56.703 56.702-56.701-25.436-56.701-56.702 25.435-56.701 56.701-56.701zM56.629 197.71a87.57 87.57 0 0 0 52.294 17.264c48.55 0 88.05-39.498 88.05-88.049a88.25 88.25 0 0 0-1.02-13.409c19.464-4.03 39.588-6.075 60.047-6.075 20.515 0 40.613 2.041 60.048 6.068-.672 4.376-1.021 8.857-1.021 13.417 0 48.55 39.498 88.049 88.049 88.049 19.568 0 37.66-6.424 52.294-17.263 15.157 21.633 23.712 45.416 25.08 69.911-23.821 20.076-49.402 34.122-79.949 43.943-38.207 12.281-85.474 18.252-144.5 18.252s-106.292-5.971-144.499-18.253c-30.549-9.821-56.132-23.869-79.952-43.945 1.369-24.496 9.921-48.278 25.079-69.91zm199.372 244.067c-107.337 0-197.317-56.31-219.433-131.312 19.882 13.102 41.246 23.196 65.34 30.94 41.355 13.295 91.76 19.758 154.094 19.758s112.738-6.463 154.095-19.758c24.092-7.745 45.456-17.839 65.338-30.939-22.12 75.002-112.099 131.311-219.434 131.311z"/><use href="#B"/><use href="#B" x="294.153"/><path d="M219.852 256.624c-9.79 0-17.756 8.048-17.756 17.986s7.966 17.992 17.756 17.992c9.802 0 17.767-8.055 17.767-17.992s-7.965-17.986-17.767-17.986zm72.163-.14c-9.885 0-17.902 8.115-17.902 18.126s8.017 18.126 17.902 18.126c9.872 0 17.89-8.115 17.89-18.126s-8.018-18.126-17.89-18.126z"/><defs><path id="B" d="M108.931 105.926c-11.45 0-20.738 9.401-20.738 20.998s9.288 20.998 20.738 20.998c11.436 0 20.725-9.401 20.725-20.998s-9.289-20.998-20.725-20.998z"/></defs></svg>
|
||||
<span>Покорми Гипнoжабу,<br />притащи сюда img или кликни</span>
|
||||
<span class="text-xs sm:text-sm lg:text-base font-medium text-left lg:text-center">Покорми Гипнoжабу,<br class="hidden lg:inline" /> притащи сюда img или кликни</span>
|
||||
</div>
|
||||
|
||||
<!-- Состояние 2: Файл выбран -->
|
||||
<div x-show="hasImage" class="flex flex-col items-center gap-3 text-emerald-600 dark:text-emerald-400">
|
||||
<div x-show="hasImage" class="flex flex-row lg:flex-col items-center gap-3 text-emerald-600 dark:text-emerald-400 text-left lg:text-center">
|
||||
<img :src="imagePreviewUrl" alt="Превью"
|
||||
class="w-24 h-24 object-cover rounded-xl border-2 border-emerald-500 shadow-md">
|
||||
class="w-12 h-12 sm:w-14 sm:h-14 lg:w-24 lg:h-24 shrink-0 object-cover rounded-xl border-2 border-emerald-500 shadow-md">
|
||||
<div class="text-xs font-mono">
|
||||
<p class="font-bold">Гипножаба облизывается на:</p>
|
||||
<p class="truncate max-w-[260px] text-stone-700 dark:text-zinc-300 font-semibold mt-0.5" x-text="fileName"></p>
|
||||
<p class="text-[11px] text-stone-400 dark:text-zinc-500 mt-1">кликни или притащи другой файл, чтобы скормить</p>
|
||||
<p class="truncate max-w-[200px] sm:max-w-[260px] text-stone-700 dark:text-zinc-300 font-semibold mt-0.5" x-text="fileName"></p>
|
||||
<p class="text-[10px] sm:text-[11px] text-stone-400 dark:text-zinc-500 mt-0.5">кликни или притащи другой файл</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,6 +166,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок настроек: табы и параметры -->
|
||||
<div class="order-5 w-full bg-white dark:bg-zinc-900 p-4 sm:p-6 lg:p-0 rounded-2xl lg:rounded-none border border-stone-200 dark:border-zinc-800 lg:border-0 shadow-sm lg:shadow-none">
|
||||
<!-- Табы настроек -->
|
||||
<nav class="flex border-b border-stone-200 dark:border-zinc-800 mb-6 overflow-x-auto no-scrollbar">
|
||||
<button type="button" @click="activeTab = 'style'"
|
||||
@@ -419,9 +422,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Индикатор размышления / статус активности -->
|
||||
<div class="h-8 mt-2 flex items-center justify-center">
|
||||
<!-- Индикатор размышления / статус активности (только десктоп) -->
|
||||
<div class="hidden lg:flex h-8 mt-2 items-center justify-center">
|
||||
<div x-show="isLoading"
|
||||
x-transition
|
||||
class="flex items-center gap-2 px-3.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 text-xs font-bold font-mono tracking-wide animate-pulse">
|
||||
@@ -440,11 +444,12 @@
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
:disabled="isLoading || !hasChanges"
|
||||
:disabled="isLoading || !hasChanges"{% if ALLOW_TRACKING %}
|
||||
{# СТАРТ ГЛЮКОТРОН: Отслеживание Яндекс #}@click="window.ym && ym(112812760, 'reachGoal', 'start_gluco')"{% endif %}
|
||||
:class="(!hasChanges || isLoading)
|
||||
? 'bg-stone-200 dark:bg-zinc-800 text-stone-400 dark:text-zinc-600 cursor-not-allowed shadow-none'
|
||||
: 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-900/20 active:scale-95 cursor-pointer'"
|
||||
class="w-full mt-2 font-bold py-3.5 px-6 rounded-xl transition-all flex items-center justify-center gap-2">
|
||||
class="order-3 w-full mt-0 lg:mt-2 font-bold py-3.5 px-6 rounded-xl transition-all flex items-center justify-center gap-2">
|
||||
<template x-if="isLoading">
|
||||
<span class="flex items-center gap-2">
|
||||
<img src="{% static 'img/thinking.svg' %}" class="w-5 h-5 animate-spin [animation-duration:2s]">
|
||||
@@ -459,35 +464,36 @@
|
||||
</aside>
|
||||
|
||||
<!-- ПРАВАЯ ПАНЕЛЬ: ПРЕВЬЮ -->
|
||||
<section class="flex flex-col gap-6">
|
||||
<section class="contents lg:flex lg:flex-col lg:gap-6">
|
||||
<div id="preview"
|
||||
class="w-full h-[744px] bg-stone-200 dark:bg-zinc-900 rounded-3xl flex items-center justify-center border border-stone-300 dark:border-zinc-800 overflow-hidden relative group">
|
||||
:class="hasGenerated ? 'h-auto min-h-[280px] aspect-square lg:aspect-auto lg:h-[744px]' : 'h-14 sm:h-16 lg:h-[744px]'"
|
||||
class="order-2 w-full bg-stone-200 dark:bg-zinc-900 rounded-2xl lg:rounded-3xl flex items-center justify-center border border-stone-300 dark:border-zinc-800 overflow-hidden relative group transition-all duration-300">
|
||||
|
||||
<div class="text-stone-400 flex flex-col items-center gap-4 text-center px-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
<div class="text-stone-400 flex flex-row lg:flex-col items-center gap-2.5 lg:gap-4 text-center px-4 lg:px-6 py-2 lg:py-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 lg:w-16 lg:h-16 opacity-50 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
:class="isLoading ? 'animate-pulse text-emerald-500 opacity-80' : 'opacity-50'">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<p class="transition-all duration-300"
|
||||
:class="isLoading ? 'text-emerald-600 dark:text-emerald-400 font-mono text-xs font-bold uppercase tracking-wider animate-pulse' : 'font-medium opacity-50'"
|
||||
<p class="transition-all duration-300 text-xs sm:text-sm lg:text-base font-mono"
|
||||
:class="isLoading ? 'text-emerald-600 dark:text-emerald-400 font-mono text-xs font-bold uppercase tracking-wider animate-pulse' : 'font-medium opacity-60'"
|
||||
x-text="isLoading ? 'Гипножаба переваривает пиксели…' : 'Ваше искусство появится здесь'"></p>
|
||||
</div>
|
||||
</div>
|
||||
{# КНОПКИ КОПИРОВАНИЕ И СКАЧИВАНИЕ (ПОД ПРЕВЬЮ)#}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="order-4 grid grid-cols-2 gap-4 w-full">
|
||||
<button
|
||||
type="button"
|
||||
@click="copySvg()"
|
||||
class="bg-stone-800 text-white dark:bg-zinc-800 py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-stone-700 transition-colors cursor-pointer active:scale-95">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@click="copySvg(){% if ALLOW_TRACKING %}; window.ym && ym(112812760, 'reachGoal', 'copy_svg'){% endif %}"
|
||||
class="bg-stone-800 text-white dark:bg-zinc-800 py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-stone-700 transition-colors cursor-pointer active:scale-95 text-xs sm:text-sm md:text-base">
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||
<span x-text="copied ? 'СКОПИРОВАНО В БУФЕР!' : 'КОПИРОВАТЬ SVG'"></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="downloadSvg()"
|
||||
class="bg-emerald-600 text-white py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-emerald-500 transition-colors shadow-lg shadow-emerald-900/20 cursor-pointer active:scale-95">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
@click="downloadSvg(){% if ALLOW_TRACKING %}; window.ym && ym(112812760, 'reachGoal', 'download_svg'){% endif %}"
|
||||
class="bg-emerald-600 text-white py-3.5 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-emerald-500 transition-colors shadow-lg shadow-emerald-900/20 cursor-pointer active:scale-95 text-xs sm:text-sm md:text-base">
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
СКАЧАТЬ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -1,98 +1,51 @@
|
||||
// analytics.js — Аналитика и счетчики посещений для hypn0.xyz
|
||||
// Версия: 1.0 | Дата: 2026-05-15
|
||||
// Содержит: Google Analytics 4, Yandex.Metrika, Top.Mail.Ru
|
||||
// Версия: 1.2 | Дата: 2026-09-16
|
||||
// Содержит: Google Tag Manager, Yandex.Metrika
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Google Analytics 4 (GA4)
|
||||
// ID: GT-XXXXXXXX
|
||||
// Google Tag Manager (GTM)
|
||||
// ID: GTM-5FVH3KMJ
|
||||
// ============================================================================
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.src = 'https://www.googletagmanager.com/gtag/js?id=GT-XXXXXXXX';
|
||||
document.head.appendChild(script);
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
window.gtag = gtag;
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'GT-XXXXXXXX');
|
||||
})();
|
||||
(function(w, d, s, l, i) {
|
||||
w[l] = w[l] || [];
|
||||
w[l].push({
|
||||
'gtm.start': new Date().getTime(),
|
||||
event: 'gtm.js'
|
||||
});
|
||||
var f = d.getElementsByTagName(s)[0],
|
||||
j = d.createElement(s),
|
||||
dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||
j.async = true;
|
||||
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
|
||||
f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', 'GTM-5FVH3KMJ');
|
||||
|
||||
// ============================================================================
|
||||
// Yandex.Metrika (Яндекс.Метрика)
|
||||
// ID: 12345678
|
||||
// ID: 112812760
|
||||
// ============================================================================
|
||||
(function() {
|
||||
window.ym = window.ym || function(){
|
||||
(window.ym.a = window.ym.a || []).push(arguments);
|
||||
};
|
||||
window.ym.l = 1 * new Date();
|
||||
(function(m,e,t,r,i,k,a){
|
||||
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||
})(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=112812760', 'ym');
|
||||
|
||||
// Загружаем скрипт Метрики
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = 'https://mc.yandex.ru/metrika/tag.js';
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Инициализируем Метрику
|
||||
window.ym(12345678, 'init', {
|
||||
trackHash: true,
|
||||
window.ym(112812760, 'init', {
|
||||
ssr: true,
|
||||
webvisor: true,
|
||||
clickmap: true,
|
||||
ecommerce: 'dataLayer',
|
||||
referrer: document.referrer,
|
||||
url: location.href,
|
||||
accurateTrackBounce: true,
|
||||
trackLinks: true
|
||||
});
|
||||
})();
|
||||
|
||||
// ============================================================================
|
||||
// Top.Mail.Ru counter (Рейтинг@Mail.ru)
|
||||
// ID: 1234567
|
||||
// ============================================================================
|
||||
(function() {
|
||||
var _tmr = window._tmr || (window._tmr = []);
|
||||
_tmr.push({
|
||||
id: "1234567",
|
||||
type: "pageView",
|
||||
start: (new Date()).getTime()
|
||||
});
|
||||
|
||||
(function(d, w, id) {
|
||||
if (d.getElementById(id)) return;
|
||||
var ts = d.createElement("script");
|
||||
ts.type = "text/javascript";
|
||||
ts.async = true;
|
||||
ts.id = id;
|
||||
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
||||
|
||||
var f = function() {
|
||||
var s = d.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(ts, s);
|
||||
};
|
||||
|
||||
if (w.opera == "[object Opera]") {
|
||||
d.addEventListener("DOMContentLoaded", f, false);
|
||||
} else {
|
||||
f();
|
||||
}
|
||||
})(document, window, "tmr-code");
|
||||
|
||||
// Добавляем изображение для noscript
|
||||
if (!window.noScriptAdded) {
|
||||
window.noScriptAdded = true;
|
||||
var noscriptDiv = document.createElement('div');
|
||||
noscriptDiv.style.display = 'none';
|
||||
noscriptDiv.innerHTML = '<img src="https://top-fwz1.mail.ru/counter?id=1234567;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" />';
|
||||
document.body.appendChild(noscriptDiv);
|
||||
}
|
||||
})();
|
||||
|
||||
})();
|
||||
|
||||
// Экспортируем gtag в глобальный контекст для возможности использования в коде
|
||||
// window.gtag доступна через глобальную переменную
|
||||
// window.dataLayer доступна через глобальную переменную
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* hypn0.js — Основной клиентский скрипт для hypn0.xyz
|
||||
* Обеспечивает интеграцию HTMX, работу с Declarative Shadow DOM
|
||||
* и ленивую асинхронную подгрузку SVG-халфтонов через Alpine.js.
|
||||
*/
|
||||
|
||||
// Автоматическая передача CSRF-токена в AJAX-запросах HTMX
|
||||
document.addEventListener('htmx:configRequest', function(evt) {
|
||||
var csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
|
||||
var token = csrfInput ? csrfInput.value : null;
|
||||
if (!token) {
|
||||
var match = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
if (match) token = match[1];
|
||||
}
|
||||
if (token) {
|
||||
evt.detail.headers['X-CSRFToken'] = token;
|
||||
}
|
||||
});
|
||||
|
||||
// Полифилл / обработка Declarative Shadow DOM для старых браузеров и динамических вставок
|
||||
function attachShadowRoots(root) {
|
||||
(root || document).querySelectorAll('template[shadowrootmode]').forEach(function(tmpl) {
|
||||
if (!tmpl.parentElement.shadowRoot) {
|
||||
var mode = tmpl.getAttribute('shadowrootmode') || 'open';
|
||||
var shadow = tmpl.parentElement.attachShadow({ mode: mode });
|
||||
shadow.appendChild(tmpl.content.cloneNode(true));
|
||||
tmpl.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
attachShadowRoots(document);
|
||||
});
|
||||
|
||||
// Обработка HTMX swap: инициализация теневого корня и Alpine.js компонентов в новом DOM-фрагменте
|
||||
document.addEventListener('htmx:afterSwap', function(evt) {
|
||||
attachShadowRoots(evt.detail.target);
|
||||
if (window.Alpine && evt.detail && evt.detail.target) {
|
||||
window.Alpine.initTree(evt.detail.target);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* hypn0LazySvg — Alpine-компонент ленивой асинхронной загрузки SVG в Shadow DOM.
|
||||
* Защищает от лагов быстрого скролла (AbortController + debounce),
|
||||
* размазывает пиковые сетевые всплески (джиттер) и изолирует стили/ID.
|
||||
*/
|
||||
function hypn0LazySvg(svgUrl) {
|
||||
return {
|
||||
svgUrl: svgUrl || '',
|
||||
loaded: false,
|
||||
error: false,
|
||||
_observer: null,
|
||||
_controller: null,
|
||||
_timer: null,
|
||||
|
||||
init() {
|
||||
if (!this.svgUrl) return;
|
||||
var el = this.$el;
|
||||
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
this.fetchSvg(el);
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.isIntersecting) {
|
||||
var jitter = Math.floor(Math.random() * 60);
|
||||
self._timer = setTimeout(function() {
|
||||
self.fetchSvg(el);
|
||||
}, 120 + jitter);
|
||||
} else {
|
||||
if (self._timer) {
|
||||
clearTimeout(self._timer);
|
||||
self._timer = null;
|
||||
}
|
||||
if (self._controller) {
|
||||
self._controller.abort();
|
||||
self._controller = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '120px 0px 120px 0px',
|
||||
threshold: 0.01
|
||||
});
|
||||
|
||||
this._observer.observe(el);
|
||||
},
|
||||
|
||||
fetchSvg(el) {
|
||||
if (this.loaded) return;
|
||||
if (this._controller) this._controller.abort();
|
||||
this._controller = new AbortController();
|
||||
|
||||
var self = this;
|
||||
fetch(this.svgUrl, { signal: this._controller.signal })
|
||||
.then(function(resp) {
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
return resp.text();
|
||||
})
|
||||
.then(function(svgText) {
|
||||
if (!svgText) return;
|
||||
|
||||
var shadow = el.shadowRoot;
|
||||
if (!shadow) {
|
||||
shadow = el.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
var style = document.createElement('style');
|
||||
style.textContent = [
|
||||
':host {',
|
||||
' display: flex;',
|
||||
' width: 100%;',
|
||||
' height: 100%;',
|
||||
' align-items: center;',
|
||||
' justify-content: center;',
|
||||
' pointer-events: auto;',
|
||||
' opacity: 0;',
|
||||
' transition: opacity 0.35s ease-out;',
|
||||
'}',
|
||||
':host(.is-loaded), :host([data-loaded]) {',
|
||||
' opacity: 1;',
|
||||
'}',
|
||||
'svg {',
|
||||
' width: 100%;',
|
||||
' height: 100%;',
|
||||
' object-fit: contain;',
|
||||
'}',
|
||||
':host(:hover) svg, svg:hover {',
|
||||
' --hypn0-play: running !important;',
|
||||
'}'
|
||||
].join('\n');
|
||||
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(svgText, 'image/svg+xml');
|
||||
var svgEl = doc.querySelector('svg');
|
||||
|
||||
shadow.innerHTML = '';
|
||||
shadow.appendChild(style);
|
||||
if (svgEl) {
|
||||
shadow.appendChild(svgEl);
|
||||
} else {
|
||||
var container = document.createElement('div');
|
||||
container.innerHTML = svgText;
|
||||
var innerSvg = container.querySelector('svg');
|
||||
if (innerSvg) shadow.appendChild(innerSvg);
|
||||
}
|
||||
|
||||
self.loaded = true;
|
||||
self.error = false;
|
||||
el.setAttribute('data-loaded', 'true');
|
||||
el.classList.add('is-loaded');
|
||||
|
||||
if (self._observer) {
|
||||
self._observer.disconnect();
|
||||
self._observer = null;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
if (err.name === 'AbortError') return;
|
||||
self.error = true;
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this._timer) clearTimeout(this._timer);
|
||||
if (this._controller) this._controller.abort();
|
||||
if (this._observer) this._observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
+20
-48
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Сборка Tailwind CSS v3 для фронтенда HYPN0
|
||||
# Сборка Tailwind CSS v4 для фронтенда HYPN0
|
||||
# Запуск из корня проекта: bash ./scripts/build-tailwind.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TAILWIND_DIR="$PROJECT_ROOT/frontend-assembly/tailwind"
|
||||
OUTPUT_DIR="$PROJECT_ROOT/public/static/css"
|
||||
@@ -18,8 +20,6 @@ fail() {
|
||||
}
|
||||
|
||||
# cleanup() — удаляет временные файлы при любом завершении скрипта.
|
||||
# Все рабочие файлы (postcss.config.js, tailwind.config.js, input.css)
|
||||
# создаются здесь и удаляются в EXIT/INT/TERM.
|
||||
cleanup() {
|
||||
rm -rf "$TAILWIND_DIR/node_modules" \
|
||||
"$TAILWIND_DIR/postcss.config.js" \
|
||||
@@ -37,67 +37,39 @@ if [[ ! -f "$TAILWIND_DIR/package.json" ]]; then
|
||||
fail "Не найден package.json: $TAILWIND_DIR/package.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TAILWIND_DIR/package-lock.json" ]]; then
|
||||
fail "Не найден package-lock.json: $TAILWIND_DIR/package-lock.json"
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# --- tailwind.config.js ---
|
||||
log "Создаю tailwind.config.js"
|
||||
cat > "$TAILWIND_DIR/tailwind.config.js" <<'TWEOF'
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'../../hypn0/templates/**/*.html',
|
||||
'../../hypn0/hypn0_site/**/*.py',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
TWEOF
|
||||
|
||||
# --- postcss.config.js ---
|
||||
log "Создаю postcss.config.js"
|
||||
cat > "$TAILWIND_DIR/postcss.config.js" <<'PCEOF'
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
PCEOF
|
||||
|
||||
# --- input.css ---
|
||||
log "Создаю input.css"
|
||||
log "Создаю input.css для Tailwind v4"
|
||||
cat > "$TAILWIND_DIR/input.css" <<'EOF'
|
||||
/*
|
||||
input.css — точка входа для сборки prod-версии Tailwind CSS v3.
|
||||
input.css — точка входа для сборки prod-версии Tailwind CSS v4.
|
||||
|
||||
Собирается через `npm run build` (PostCSS) в
|
||||
Собирается через @tailwindcss/cli в
|
||||
файл public/static/css/tailwind.min.css, который подключается в _base.html
|
||||
для production (когда settings.DEBUG == False).
|
||||
|
||||
Здесь же подключаются кастомные стили из hypn0/templates/css/tailwind-custom.css —
|
||||
того же самого файла, который в dev-режиме подключается через {% include %} внутрь
|
||||
инлайнового <style type="text/tailwindcss"> в _base.html.
|
||||
|
||||
Это обеспечивает единый источник кастомных стилей для dev и prod.
|
||||
Здесь же подключаются кастомные стили из hypn0/templates/css/tailwind-custom.css.
|
||||
*/
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@source "../../hypn0/templates";
|
||||
@source "../../hypn0/hypn0_site";
|
||||
|
||||
@import "../../hypn0/templates/css/tailwind-custom.css";
|
||||
EOF
|
||||
|
||||
log "СОБИРАЮ Tailwind CSS v3"
|
||||
log "СОБИРАЮ Tailwind CSS v4"
|
||||
cd "$TAILWIND_DIR"
|
||||
|
||||
log 'Устанавливаю зависимости через npm ci'
|
||||
npm ci
|
||||
# Если package-lock.json отсутствует или устарел, используем npm install / npm ci
|
||||
if [[ -f "$TAILWIND_DIR/package-lock.json" ]]; then
|
||||
log 'Устанавливаю зависимости через npm install'
|
||||
npm install --no-audit --no-fund
|
||||
else
|
||||
log 'Устанавливаю зависимости через npm install'
|
||||
npm install --no-audit --no-fund
|
||||
fi
|
||||
|
||||
log 'Собираю CSS'
|
||||
npm run build
|
||||
|
||||
Reference in New Issue
Block a user