feat: typewriter-эффект в логотипе dedinit() — темы блога в скобках

- layouts/partials/extend_footer.html: инлайн-скрипт (печать/пауза/стирание/мигание, prefers-reduced-motion, слова из [params.typewriter])
- assets/css/extended/typewriter.css: блочный мигающий курсор
- hugo.toml: [params.typewriter] words
- openspec/changes/header-typewriter: proposal/design/specs/tasks

Без зависимостей (Alpine не нужен), тема не модифицируется.
This commit is contained in:
2026-09-19 17:42:59 +00:00
parent a0b3749a1b
commit 2af0530f8a
7 changed files with 338 additions and 0 deletions
@@ -0,0 +1,81 @@
# Design: Анимированный логотип-печатная машинка (header typewriter)
## Context
- Тема: rDedInit (форк PaperMod), Hugo Extended 0.165.x.
- Логотип в шапке — партиал `themes/rDedInit/layouts/partials/header.html`,
ссылка `<a class="logo">` содержит текст `{{ $label_text }}` = `dedinit()`
(`site.Params.label.text`).
- Хуки темы: `layouts/partials/extend_head.html` (в `<head>`) и
`layouts/partials/extend_footer.html` (перед `</body>`). Локальные layouts/
переопределяют тему. В `footer.html` темы есть
`{{- partial "extend_footer.html" . }}` — подтверждено grep.
- CSS: тема собирает всё из `assets/css/extended/*.css` в один
`extended.css` (в `head.html`: `resources.Match "css/extended/*.css"`).
## Files
1. `layouts/partials/extend_footer.html` (НОВЫЙ) — подключает inline `<script>`.
Hugo сам добавит партиал в футер всех страниц (переопределение темы).
Скрипт маленький (~1 КБ), inline — без отдельного assets/js (меньше
запросов; для трёхстрочного эффекта отдельный бандл избыточен).
Слова передаём из Hugo: `data-words="{{ delimit (site.Params.typewriter.words | default (slice "linux" "llm" "politics" "homelab" "nix")) "," }}"`.
2. `assets/css/extended/typewriter.css` (НОВЫЙ) — курсор и вставка:
```css
#dedinit-typewriter { display:inline-block; }
#dedinit-typewriter .tw-cursor { ... блочный квадратик, animation: tw-blink 1s steps(1) infinite ... }
@keyframes tw-blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { #dedinit-typewriter .tw-cursor { animation: none; } }
```
Курсор — всегда видимый блок `currentColor`, чтобы не зависеть от схемы
(light/dark).
3. `hugo.toml` (ОПЦИОНАЛЬНО) — `[params.typewriter] words = [...]`. Если не
задан — скрипт использует фолбэк. Не обязателен для эффекта.
## JS logic (inline in extend_footer.html)
```js
(() => {
const a = document.querySelector('header a.logo');
if (!a || a.dataset.twDone) return;
const words = (a.dataset.words || 'linux,llm,politics,homelab,nix').split(',');
// вставить <span id="dedinit-typewriter"><span class="tw-cursor"></span></span>
// перед ')' в тексте логотипа; первый клик -- печать
// тайминги: type 90ms/char, hold 1400ms, erase 50ms/char, gap 400ms
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { первый word статично }
})();
```
- Символы печатаются через `textContent` — безопасно (нет HTML-инъекции из
слов; слова приходят из конфига автора).
- Слово не должно разрываться: `white-space: nowrap` на тизере.
- Курсор мигает на пустом месте между циклами (это и есть «моргает на месте»).
## Apply
```bash
# 1. создать файлы (выше), 2. собрать:
make build # hugo --minify
```
## Verify
```bash
grep -n "dedinit-typewriter" public/index.html # скрипт + span присутствуют
grep -n "tw-cursor" public/index.html # CSS-курсор
grep -n "prefers-reduced-motion" public/**.html # a11y-заглушка
# в браузере: логотип печатает/стирает слова по циклу; курсор мигает;
# с prefers-reduced-motion — первое слово статично
```
## Rollback
```bash
git rm layouts/partials/extend_footer.html assets/css/extended/typewriter.css
# (и убрать [params.typewriter] из hugo.toml, если добавлялся)
make build
```
Публикация/деплой — по согласованию с пользователем (`make deploy`).
@@ -0,0 +1,42 @@
# Proposal: Анимированный логотип-печатная машинка для dedinit()
## Why
Логотип в шапке сайта `dedinit()` — статический текст. Хочется «оживить»
брендинг: внутри круглых скобок в логотипе должен появляться блочный курсор и
«печатать» по одному символу темы/теги блога (linux, llm, politics, homelab...),
замирать на секунду, стирать слово посимвольно, мигать на пустом месте и снова
печатать следующее слово. Это декоративный эффект печатной машинки
(typewriter), который обыгрывает название блога как вызов функции
`dedinit(...)` — «инициализация» блога.
## What Changes
- Новый крошечный JS-инлайн (~1 КБ, без зависимостей, без Alpine.js):
- Находит тег `<a class="logo">` на странице, внутри текста `dedinit()`
выделяет скобки, вставляет `<span id="dedinit-typewriter">` между `( )`.
- Цикл: печать слова по 1 символу (быстро), пауза (~1.4 с), стирание по
1 символу, пауза с курсором (~0.4 с), следующее слово.
- Слова берутся из `data-words` атрибута (передаются из Hugo конфига,
`site.Params.typewriter.words`), фолбэк на массив по умолчанию.
- Уважает `prefers-reduced-motion`: при включённом — вместо анимации
показывается первое слово статически.
- Если JS выключен — логотип остаётся обычным `dedinit()` (прогрессивное
улучшение).
- Новый CSS в `assets/css/extended/typewriter.css` (тема PaperMod
автоматически подхватывает всё из `assets/css/extended/`):
- Блочный курсор в виде квадратика с миганием (CSS-анимация, не JS).
- Стили для слова-тизера в скобках.
- Хук JS через переопределение партиала `layouts/partials/extend_footer.html`
(Hugo: локальные layouts переопределяют тему; там подключаем `<script>`).
## Impact
- Затронутые файлы:
- `layouts/partials/extend_footer.html` (новый, переопределяет тему)
- `assets/css/extended/typewriter.css` (новый)
- `hugo.toml` — необязательно: `[params.typewriter] words = [...]` (опция)
- Тема `themes/rDedInit` НЕ модифицируется (правки не затрутся апдейтом темы).
- Контент постов, RSS, разметка списков — не меняются.
- Откат (rollback): удалить 2 новых файла (и, если добавляли, параметр
`typewriter` в hugo.toml), пересобрать — сайт вернётся к статическому логотипу.
@@ -0,0 +1,86 @@
# Site Header Typewriter Capability
## Purpose
Логотип `dedinit()` в шапке сайта анимирован эффектом печатной машинки:
внутри скобок блочный курсор печатает темы блога, замирает, стирает их и
переходит к следующей — обыгрывая название как вызов функции `dedinit(...)`.
## ADDED Requirements
### Requirement: Анимация «печатная машинка» в логотипе
Система SHALL анимировать текст в скобках логотипа `dedinit()` на всех
страницах сайта: блочный курсор печатает слово по одному символу, удерживает
его ~1 секунду, стирает по одному символу, мигает на пустом месте и печатает
следующее слово — бесконечным циклом по списку тем блога.
#### Scenario: Логотип печатает слова по циклу
- **GIVEN** открыта любая страница сайта (`/` или внутренняя), JS включён
- **WHEN** страница загружена
- **THEN** в `dedinit()` между скобками присутствует контейнер
`#dedinit-typewriter` с блочным мигающим курсором, и текст посимвольно
печатается из списка тем (например, `linux` → `llm` → `politics` → ...)
#### Scenario: Цикл без остановки
- **GIVEN** анимация запущена
- **WHEN** прошло более 3 слов печати/стирания
- **THEN** анимация продолжает цикл (после последнего слова переходит к
первому), не останавливаясь и не накапливая ошибок
### Requirement: Бесшовный вывод (прогрессивное улучшение)
Система SHALL оставаться декоративным улучшением: при выключенном JS логотип
остаётся обычным текстом `dedinit()`; при его включении — анимация
накладывается, не ломая вёрстку и навигацию шапки.
#### Scenario: JS отключён — логотип статичен
- **GIVEN** браузер с отключённым JavaScript
- **WHEN** страница загружена
- **THEN** в шапке отображается обычный текст `dedinit()` без скобочного
контейнера и без ошибок
### Requirement: Доступность (prefers-reduced-motion)
Система SHALL уважать системную настройку `prefers-reduced-motion: reduce`:
при ней показывать первое слово списка статически, без анимации и мигания.
#### Scenario: Уменьшение движения
- **GIVEN** у пользователя включено `prefers-reduced-motion: reduce`
- **WHEN** страница загружена
- **THEN** внутри скобок логотипа отображается первое слово списка статично,
курсор не мигает (CSS `animation: none`), JS-цикл не запускается
### Requirement: Минимальный размер и отсутствие зависимостей
Система SHALL реализовывать эффект без внешних зависимостей (без Alpine.js,
без jQuery): инлайн-скрипт и CSS суммарно не более ~2 КБ.
#### Scenario: Нет внешних библиотек
- **GIVEN** собранный сайт
- **WHEN** просматривается HTML страницы
- **THEN** в `<head>`/футере не подключено никаких сторонних JS-библиотек для
эффекта (только собственный инлайн-скрипт и CSS темы)
### Requirement: Список тем конфигурируется
Система SHALL позволять задавать список тем через `hugo.toml`
(`[params.typewriter] words = [...]`), со встроенным фолбэком по умолчанию
(`linux, llm, politics, homelab, nix`) при его отсутствии.
#### Scenario: Слова из конфига
- **GIVEN** в `hugo.toml` задан `[params.typewriter] words = ["linux","llm","politics"]`
- **WHEN** страница собрана и открыта
- **THEN** анимация печатает именно эти слова, в заданном порядке
#### Scenario: Конфиг не задан
- **GIVEN** в `hugo.toml` нет `[params.typewriter]`
- **WHEN** страница собрана и открыта
- **THEN** используется фолбэк-список по умолчанию, анимация работает
@@ -0,0 +1,37 @@
# Tasks: Header Typewriter
## 1. Реализация
- [x] 1.1 Создать `assets/css/extended/typewriter.css` — стили контейнера
`#dedinit-typewriter` и блочного курсора `.tw-cursor` (мигание через
`@keyframes`, `prefers-reduced-motion` отключает анимацию)
- [x] 1.2 Создать `layouts/partials/extend_footer.html` — инлайн-скрипт цикла
печати/стирания слов (тайминги: type ~90ms, hold ~1400ms, erase ~50ms,
gap ~400ms), вставка контейнера в логотип `dedinit()`, обработка
`prefers-reduced-motion`, фолбэк-список слов
- [x] 1.3 Проверить, что `extend_footer.html` реально вызывается темой:
`grep -n "extend_footer" themes/rDedInit/layouts/partials/footer.html`
## 2. Сборка и проверка
- [x] 2.1 Собрать сайт: `make build` — без ошибок (exit 0)
- [x] 2.2 Проверить в `public/index.html` наличие `dedinit-typewriter`
(`grep -c "dedinit-typewriter" public/index.html` >= 1)
- [x] 2.3 Проверить наличие `.tw-cursor` и `prefers-reduced-motion` в собранном CSS
(`grep -c "tw-cursor" public/**/*.css` >= 1)
- [x] 2.4 Логика цикла проверена эмуляцией (node): печать до полного слова,
пауза, стирание до пустоты, пауза-мигание, переход к следующему;
после последнего слова цикл замыкается; все слова проходят полный цикл
(см. /tmp/tw_test.js)
## 3. Git
- [ ] 3.1 Закоммитить изменения (layouts/, assets/, openspec/, при желании
hugo.toml с [params.typewriter]) и запушить в gitverse (после
согласования с пользователем)
- [ ] 3.2 Обновить `docs/STATUS.md` / `docs/TODO.md` при необходимости
## 4. Деплой (по согласованию)
- [ ] 4.1 `make deploy` (rsync public/ → kpa39l.myjino.ru:2222)
- [ ] 4.2 Проверить на https://dedinit.ru/ — логотип печатает темы