Перенос дизайна в server-rendered веб-UI (web-ui)

Новая capability web-ui: презентационный перенос готового дизайна
(семантический HTML + единый jellybit.css, тёмная тема по настройке ОС)
в html/template без шага сборки.

- Встроенная (go:embed) отдача статики под /static с Cache-Control и
  cache-busting (?v=<hash> по содержимому css/js).
- Шрифты IBM Plex self-hosted (@font-face, cyrillic+latin), без CDN.
- Наколеночный менеджер зависимостей: вендор (htmx + шрифты) не хранится
  в репо (gitignore), идемпотентно добывается `task assets` по
  web/assets.manifest с проверкой sha256; task build/run зависят от assets.
- Общие партиалы: шапка, бейдж статуса (карта всех 14 состояний),
  виджет «файл источника → раскладка» (общий для review и download).
- Страницы: список с фильтром/поиском, ревью, новая страница просмотра
  загрузки (/download/{id}). deleted скрыт по умолчанию.
- Превью раскладки берётся из единой логики internal/layout
  (buildFileRows), без дублирования правил имён в шаблонах.
- Убраны meta-refresh и инлайн-стили; copyHash на vanilla с fallback
  на execCommand и честной индикацией (целевой деплой — HTTP LAN).

Вне scope (отдельный change): живые обновления прогресса и раздел
раздачи, клиентский режим ручной раскладки файл→серия (с Alpine.js).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-06-30 19:59:19 +03:00
co-authored by Claude Opus 4.8
parent d190072647
commit 4e2593ac31
26 changed files with 1960 additions and 303 deletions
+75
View File
@@ -0,0 +1,75 @@
// Клиентская мелочь без сборки и фреймворков.
// htmx тянет серверные фрагменты, здесь — только то, что серверу знать не нужно.
// Копирование infohash в буфер обмена. На целевом деплое (домашний LAN по HTTP)
// navigator.clipboard недоступен — secure context только на HTTPS/localhost,
// поэтому есть fallback на execCommand, а индикация честная: «✓» лишь при успехе.
function copyHash(btn, full) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(full).then(
() => flashCopy(btn, true),
() => flashCopy(btn, legacyCopy(full)),
);
} else {
flashCopy(btn, legacyCopy(full));
}
}
function legacyCopy(text) {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
} catch (e) {
return false;
}
}
function flashCopy(btn, ok) {
const old = btn.dataset.label || btn.textContent;
btn.dataset.label = old;
btn.textContent = ok ? '✓ скопировано' : '✗ не вышло';
btn.classList.toggle('copied', ok);
setTimeout(() => { btn.textContent = old; btn.classList.remove('copied'); }, 1400);
}
// --- Фильтр/поиск по списку загрузок (по уже отрисованным карточкам) ---
let activeFilter = 'all';
function setFilter(el) {
document.querySelectorAll('#chips .chip').forEach((c) => c.classList.remove('active'));
el.classList.add('active');
activeFilter = el.dataset.f;
applyFilters();
}
function applyFilters() {
const searchEl = document.querySelector('#list-toolbar input[type=search]');
const q = (searchEl ? searchEl.value : '').toLowerCase().trim();
const showAllEl = document.getElementById('showAll');
const showAll = showAllEl ? showAllEl.checked : false;
let visible = 0;
document.querySelectorAll('#list .card').forEach((card) => {
const grp = card.dataset.group;
const isDeleted = card.dataset.deleted === '1';
let ok = true;
if (isDeleted && !showAll) ok = false;
if (ok && activeFilter !== 'all' && grp !== activeFilter) ok = false;
if (ok && q && !(card.dataset.text || '').includes(q)) ok = false;
card.style.display = ok ? '' : 'none';
if (ok) visible += 1;
});
const empty = document.getElementById('empty');
if (empty) empty.style.display = visible ? 'none' : '';
}
// Применить начальный фильтр (скрыть удалённые) после загрузки.
document.addEventListener('DOMContentLoaded', applyFilters);