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:
@@ -0,0 +1,30 @@
|
|||||||
|
/* dedinit() typewriter — animated blog topics inside the logo parens.
|
||||||
|
Progressive enhancement: pure CSS cursor blink, JS only drives the text. */
|
||||||
|
|
||||||
|
#dedinit-typewriter {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap; /* never wrap the typed word */
|
||||||
|
min-width: 0.5ch; /* keeps parens from collapsing when empty */
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dedinit-typewriter .tw-cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.55ch;
|
||||||
|
height: 1.05em;
|
||||||
|
margin-inline-start: 0.15ch;
|
||||||
|
background: currentColor; /* follows light/dark text color */
|
||||||
|
vertical-align: -0.12em;
|
||||||
|
animation: tw-blink 1s steps(1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tw-blink {
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#dedinit-typewriter .tw-cursor {
|
||||||
|
animation: none; /* static cursor, no blink */
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,10 @@ draft = false
|
|||||||
# Полный текст постов в RSS-ленте (content:encoded)
|
# Полный текст постов в RSS-ленте (content:encoded)
|
||||||
ShowFullTextinRSS = true
|
ShowFullTextinRSS = true
|
||||||
|
|
||||||
|
# Типографика логотипа dedinit(): темы, печатаемые в скобках (typewriter)
|
||||||
|
[params.typewriter]
|
||||||
|
words = ["linux", "llm", "politics", "homelab", "nix", "devops"]
|
||||||
|
|
||||||
# Настройки PaperMod
|
# Настройки PaperMod
|
||||||
defaultTheme = "auto"
|
defaultTheme = "auto"
|
||||||
disableThemeToggle = false
|
disableThemeToggle = false
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{{- /* dedinit() typewriter: animated topics inside logo parens.
|
||||||
|
Pure JS + CSS, no Alpine. Progressive enhancement — no-JS keeps
|
||||||
|
the plain "dedinit()" logo. Words configurable via hugo.toml:
|
||||||
|
[params.typewriter] words = ["linux", "llm", "politics"] */ -}}
|
||||||
|
{{- $tw_words := site.Params.typewriter.words | default (slice "linux" "llm" "politics" "homelab" "nix") -}}
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const LOGO_SEL = 'header .logo a, .logo a';
|
||||||
|
const queue = [() => {
|
||||||
|
const a = document.querySelector(LOGO_SEL);
|
||||||
|
if (!a || a.dataset.twDone) return;
|
||||||
|
a.dataset.twDone = '1';
|
||||||
|
const text = a.textContent.trim();
|
||||||
|
const open = text.lastIndexOf('(');
|
||||||
|
const close = text.lastIndexOf(')');
|
||||||
|
if (open < 0 || close < open) return;
|
||||||
|
const words = {{ $tw_words | jsonify | safeJS }};
|
||||||
|
const box = document.createElement('span');
|
||||||
|
box.id = 'dedinit-typewriter';
|
||||||
|
box.className = 'tw';
|
||||||
|
box.textContent = '';
|
||||||
|
box.appendChild(Object.assign(document.createElement('span'), { className: 'tw-cursor' }));
|
||||||
|
// rebuild: <prefix>(<typewriter>)<suffix>
|
||||||
|
a.textContent = '';
|
||||||
|
a.appendChild(document.createTextNode(text.slice(0, open + 1)));
|
||||||
|
a.appendChild(box);
|
||||||
|
a.appendChild(document.createTextNode(text.slice(close)));
|
||||||
|
|
||||||
|
// reduced motion: static first word, no animation
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
box.firstChild.before(document.createTextNode(words[0] || ''));
|
||||||
|
box.querySelector('.tw-cursor').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE = 90, ERASE = 50, HOLD = 1400, GAP = 400;
|
||||||
|
let w = 0, i = 0, dir = 1;
|
||||||
|
box.querySelector('.tw-cursor').style.display = 'inline-block';
|
||||||
|
const tick = () => {
|
||||||
|
const word = words[w];
|
||||||
|
// keep cursor as the last child; typed text goes before it
|
||||||
|
box.replaceChildren(document.createTextNode(word.slice(0, i)), box.querySelector('.tw-cursor'));
|
||||||
|
let delay;
|
||||||
|
if (dir === 1) {
|
||||||
|
if (i < word.length) { i++; delay = TYPE; }
|
||||||
|
else { dir = -1; delay = HOLD; }
|
||||||
|
} else {
|
||||||
|
if (i > 0) { i--; delay = ERASE; }
|
||||||
|
else { dir = 1; w = (w + 1) % words.length; delay = GAP; }
|
||||||
|
}
|
||||||
|
setTimeout(tick, delay);
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
}];
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => queue.shift()());
|
||||||
|
else queue.shift()();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -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/ — логотип печатает темы
|
||||||
Reference in New Issue
Block a user