- T4 reader:html.reader 单 class(去装饰非去内容:隐辉光/图标 钮/pv 行,ICP 合规底线永留;正文字号+1px 行宽 56ch;入场动效 关);命令面板 reader 命令+r 键+localStorage;ThemeScript 预绘 防闪 - T7 基底:--flow-* 五流距变量双轨应用;暗色 hairline 降档; 页面标题 700→600(裁定 #6 减字重不减号);focus-visible ring 统一;列表行距 +2px;--paper/--radius-img/--ease-out 入库 - T8:首页 IdCard slot 落 tagline「写作者 · 读者 · 快门按压者, 拥抱开源。」;图库暗色 4% 轻衬底+圆角统一(裁定 #4 双模式分判) 验证: E2E 33/33;reader r 键触发+辉光 none+刷新保持实测;流距 computed 0.6em 实算;tagline 渲染实测 Spec: 2026-10-26_blog-5f Co-authored-by: GLM-5.3
120 lines
4.5 KiB
Plaintext
120 lines
4.5 KiB
Plaintext
---
|
||
// 键盘层(极客层 P0):? 帮助 / t 主题 / g-chords 导航
|
||
// 惯例来源:GitHub 键位(g+N、? 帮助);每项能力都有可视入口(触屏等价物原则)
|
||
// 文案全部 ui.ts 词条:locale 构建期按页面 URL 自推断(BaseLayout 不透传,同 Footer 模式)
|
||
import { t } from '../../i18n/ui';
|
||
import { localeOf } from '../../lib/locale';
|
||
const locale = localeOf(Astro.url.pathname);
|
||
// g-chords 目标随轨前缀化(en 页 g h → /en/ 而非 /)——C 车道已知边界的补刀
|
||
const pre = locale === 'en' ? '/en' : '';
|
||
const routes: Record<string, string> = {
|
||
h: `${pre}/`,
|
||
a: `${pre}/archives/`,
|
||
s: `${pre}/books/`,
|
||
g: `${pre}/gallery/`,
|
||
m: `${pre}/moments/`,
|
||
b: `${pre}/about/`,
|
||
};
|
||
---
|
||
|
||
<dialog id="kbd-help" class="kbd-help" data-routes={JSON.stringify(routes)}>
|
||
<h2>{t('kbd_title', locale)}</h2>
|
||
<table>
|
||
<tbody>
|
||
<tr><td><kbd>Ctrl</kbd> <kbd>K</kbd></td><td>{t('kbd_palette', locale)}</td></tr>
|
||
<tr><td><kbd>t</kbd></td><td>{t('kbd_theme_toggle', locale)}</td></tr>
|
||
<tr><td><kbd>?</kbd></td><td>{t('kbd_this_help', locale)}</td></tr>
|
||
<tr><td><kbd>g</kbd> <kbd>h</kbd></td><td>{t('kbd_gh_home', locale)}</td></tr>
|
||
<tr><td><kbd>g</kbd> <kbd>a</kbd></td><td>{t('kbd_ga_posts', locale)}</td></tr>
|
||
<tr><td><kbd>g</kbd> <kbd>s</kbd></td><td>{t('kbd_gs_books', locale)}</td></tr>
|
||
<tr><td><kbd>g</kbd> <kbd>g</kbd></td><td>{t('kbd_gg_gallery', locale)}</td></tr>
|
||
<tr><td><kbd>g</kbd> <kbd>m</kbd></td><td>{t('kbd_gm_moments', locale)}</td></tr>
|
||
<tr><td><kbd>/terminal</kbd></td><td>{t('kbd_terminal', locale)}</td></tr>
|
||
<tr><td><kbd>Esc</kbd></td><td>{t('kbd_esc_close', locale)}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="hint">{t('kbd_hint', locale)}</p>
|
||
</dialog>
|
||
|
||
<script>
|
||
// chord 缓冲 300ms;输入域聚焦时整体旁路;Esc 关一切浮层
|
||
// 路由表由 data-routes 注入(module 脚本里 currentScript 恒为 null,不能走内联取参)
|
||
const helpEl = document.getElementById('kbd-help') as HTMLElement & { dataset: { routes?: string } };
|
||
const routes: Record<string, string> = JSON.parse(helpEl?.dataset.routes ?? '{}');
|
||
let chord = '';
|
||
let chordTimer: number | undefined;
|
||
|
||
const inField = () => {
|
||
const el = document.activeElement;
|
||
return el instanceof HTMLElement &&
|
||
(el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
||
};
|
||
|
||
const toggleTheme = () => {
|
||
const root = document.documentElement;
|
||
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
root.dataset.theme = next;
|
||
localStorage.setItem('theme', next);
|
||
};
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
document.querySelector('dialog[open]')?.close();
|
||
return;
|
||
}
|
||
if (inField() || e.metaKey || e.ctrlKey || e.altKey) return;
|
||
|
||
if (e.key === 't') { toggleTheme(); return; }
|
||
if (e.key === 'r') {
|
||
// reader 纯文本模式(5f T4):与命令面板/持久化同源
|
||
const root = document.documentElement;
|
||
root.classList.toggle('reader');
|
||
localStorage.setItem('reader', root.classList.contains('reader') ? '1' : '0');
|
||
return;
|
||
}
|
||
if (e.key === '?') {
|
||
(document.getElementById('kbd-help') as HTMLDialogElement | null)?.showModal();
|
||
return;
|
||
}
|
||
// 路由判断必须先于 g 武装分支:g→g 自身也是合法 chord(gallery),
|
||
// 原序里第二击 g 永远重新武装、routes['g'] 不可达——Phase 1 潜伏缺陷
|
||
// (g g 全站失灵实锤于 2026-10-25 矩阵测试)
|
||
if (chord === 'g' && routes[e.key]) {
|
||
location.href = routes[e.key]!;
|
||
chord = '';
|
||
clearTimeout(chordTimer);
|
||
return;
|
||
}
|
||
if (e.key === 'g') {
|
||
chord = 'g';
|
||
clearTimeout(chordTimer);
|
||
chordTimer = window.setTimeout(() => (chord = ''), 300);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.kbd-help {
|
||
border: 1px solid var(--line);
|
||
border-radius: 10px;
|
||
background: var(--bg);
|
||
color: var(--ink-2);
|
||
padding: var(--space-4);
|
||
max-width: 340px;
|
||
}
|
||
.kbd-help::backdrop { background: rgba(0, 0, 0, 0.4); }
|
||
.kbd-help h2 { font-size: var(--text-h3); color: var(--ink-1); margin-bottom: var(--space-3); }
|
||
table { border-collapse: collapse; }
|
||
td { padding: 4px 10px 4px 0; font-size: 14px; }
|
||
kbd {
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
border: 1px solid var(--line);
|
||
border-bottom-width: 2px;
|
||
border-radius: 4px;
|
||
padding: 1px 6px;
|
||
background: var(--bg-soft);
|
||
}
|
||
.hint { margin-top: var(--space-3); font-size: 12px; color: var(--ink-3); }
|
||
</style>
|