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,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>
|
||||
Reference in New Issue
Block a user