# 外壳与导航契约

> 导航权威、沉浸式博客、搜索、操作、分类法、索引与页尾组合契约。

---

LLMS 索引： [llms.txt](/zh/llms.txt)

---

> [!NOTE] OINK 1.2.0 契约
> 本契约描述 v1.2.0 的正式行为。唯一的中英文契约源文件位于
> `content/docs/design/`。

## 权威来源与导航 {#authorities-and-navigation}

| 关注点 | 权威来源 |
| --- | --- |
| 全局导航 | Hugo `menus.main` |
| Docs / Book 侧栏与翻页 | 内容树或 `data/docs_nav.json` |
| 根栏目切换器 | 解析后的顶层内容根 |
| 内容发现 | 各语言的本地搜索索引 |
| 页面与命令面板操作 | 共享操作注册表 |

任何功能都不能引入另一套菜单或页面树。菜单只允许一层子项交互；更深层级会警告，
并平铺到带链接的分组标题下。外部链接使用
`target="_blank" rel="noopener noreferrer"`；内部链接保持语言与子路径感知。

顶部导航栏的桌面视图与抽屉视图投影同一棵树，每个下拉面板都是一列宽度适中的
"图标 + 标题"行——mega 面板与其 `columns` 菜单参数已退役，配置 `columns`
会发出警告并保持单列。菜单描述只是配置数据，不再渲染。链接树在任何宽度都保持居中：
lg 以上是文字链接，之下收缩为图标链接。lg 与 md 之间，右端保留搜索、版本、
语言、主题与 GitHub，没有菜单按钮；md 以下外观入口保留在搜索与抽屉入口旁，版本、语言与 GitHub 移入底栏工具组，此时首页
与显式 Landing 页在搜索旁增加一枚抽屉入口，展开完整的带标签菜单树。带侧栏的外壳页面
在这个位置打开自己的侧栏抽屉；从 md 起均不显示抽屉按钮。语言链接指向页面译文，缺少译文时
指向对应语言首页；多个语言共享主机与 base path 时保持相对链接，只有语言拥有
独立 `baseURL` 时才变成绝对链接；`hreflang` 始终使用绝对链接，而且只列出真实译文，
不把切换器提供的语言首页回退当成译文。博客分页使用每一页自己的 canonical URL；
后续分页不输出跨语言 alternate，因为不同语言归档的分页边界未必相同。
`navbar_autohide` 从 768px 起只对精细指针生效，绝不作用于触控或抽屉宽度；
隐藏的导航栏不交还占位：两种状态下布局都保留导航栏横带，固定顶栏正好占满这条
横带、下边框画在带内，显现时原地淡入、不遮挡静止内容，hero 页面忽略该策略、
保留自己的叠加导航栏。首页与 hero 页面共用同一套柔和边界：导航栏不画下边框、
滚动时不投阴影，改由栏下一小段渐隐过渡收束边缘。

侧栏与翻页共享同一个根和顺序。`manual_link`、`build.render: link`、分隔行、
隐藏节点与占位节点保留各自已定义的语义。`sidebar_icon_policy` 可取默认的 `all`、
`groups` 或 `none`；图标是一对 Font Awesome class。无效策略遵循共享的警告与
回退契约。达到 `sidebar_cache_limit` 后，两种 walker 只有在语言、导航根与实际
影响输出的有效设置均相同时才复用中性标记。没有 JavaScript 时这份标记仍然可见；
普通外壳运行时只补上 active 路径。会输出 `sidebar_headings` 的 Book 页面保持页面
专属，并绕过共享树缓存。

根候选先收集可链接、非分隔项的顶层分区，再收集 `sidebar_root_for: self` 分区，并按 URL 去重。
两种来源都遵守显式的 `sidebar_root_menu: false`；未设置或 true 时保留。
当前解析出的根即使被排除在全站候选之外，仍会追加。零入口不输出控件，单入口
输出静态链接；所有 URL 保留语言和部署路径前缀。分隔项和使用 `build.render: never`
的分区不会成为切换器链接。

`sidebar_divider` 叶子保持静态标题；分区节点在两套侧栏遍历器中保留子项，标题不跳转，
启用折叠时提供真正的展开按钮。分组自身不进入翻页序列，子页保持原有顺序。配合
`build.render: never` 可省略分区自己的输出而不隐藏后代。面包屑显示不带链接的标题，
搜索跳过分组，导航 JSON 提升其子节点，Print 保留子文档。Book 目录保留分组标题和子项链接，
但跳过未发布的分组正文标题。`toc_hide` 仍隐藏整棵子树，
不能代替分组。显式导航键使用不含语言和部署前缀的路径，实际链接则保留这两个前缀。
显式导航的 `sections` 数组为空时发出警告，并在所有导航输出中回退到内容树。
两种权威来源都剪掉 `toc_hide` 子树；导航 JSON 将 `manual_link_relref` 保留为指向
解析目标的内部链接，不把占位页自身当成页面身份。

## 侧栏运行时 {#sidebar-runtime}

> [!NOTE] 自 OINK 1.1 起提供
> OINK 1.1.0 提供本节的展开状态 API 与隐藏内容隔离；1.0.0 不提供该 API。

`window.OinkSidebar` 管理已注册的树分支及可搬迁的 TOC、反向链接和分类法分组，
不依赖它们当前的 DOM 父节点。`setExpanded(id, boolean, {source})` 对有效目标返回
true，对未知 ID 或非布尔值返回 false。`getState(id)` 返回新的 `{id, expanded}` 快照
或 null。ID 使用现有 `aria-controls` 指定的受控区域 ID，不能修改注册范围之外的元素。

来源为 `user`、`active-path`、`responsive` 和默认的 `api`。所有写入先提交
`aria-expanded`、`td-is-open`、本地化标签和区域的 inert 状态，再向 document 发送一次
`oink:sidebar-disclosure` 事件，详情为 `{id, expanded, source}`。重复写入相同状态不发
事件。API 恢复时保留当前路径祖先展开，用户仍可主动折叠这些分支。关闭含焦点的区域时，
先把焦点归还给展开按钮，再执行隔离。

`ready` 是初始活动路径补全和响应式搬迁完成后解析为 API 的 Promise；`isReady` 和
`oink:sidebar-ready` 也提供可供晚加载消费者检查的完成状态。可选的持久化由站点负责：
等待 ready，在 try/catch 中读取存储，再通过 setter 恢复有效 ID。OINK 自身保存整栏
折叠、宽度和滚动位置，不定义读者分支选择的版本/语言存储格式。

桌面折叠和移动抽屉关闭时，面板内容设为 inert，面板标记 `aria-hidden`。先移出焦点再
隔离，打开时先解除隔离再聚焦。面板本身继续充当 16px 指针感应区，外部恢复按钮保持
可用。悬浮、Escape、遮罩关闭、断点清理和滚动解锁保留原有行为。这些运行时属性不进入
无 JavaScript 的服务端回退标记。

TOC 整栏折叠时也隔离其隐藏面板。如果焦点原本位于折叠按钮，先移至可见的浮动恢复按钮；
恢复整栏后，焦点回到栏内可见的控制按钮。右侧内容搬入移动侧栏时，先清除原先的整栏
隔离，再由抽屉管理交互。抽屉的 Tab 循环只包括可见、非 inert 的控件，排除隐藏或
折叠区域中的后代。

## 沉浸式博客展示 {#immersive-blog-presentation}

OINK 没有 article 类型或第二套外壳。沉浸式阅读由普通博客外壳上的四个独立键
组成，可设在页面或分区 cascade 上；分区索引会重复它自己也需要的值：

```yaml
featured_image: hero
toc_style: flow
toc_taxonomies: false
sidebar_enabled: false
```

博客外壳默认不渲染面包屑导航——文章应作为独立作品阅读——所以这份配置不需要
相应的键。`breadcrumb` 仍是普通键，页面或 cascade 可以在任何外壳上明确打开
或关闭它。

`hero` 在单页与分区索引上把共享特色图片用作装饰性的全出血背景。没有图片时
渲染普通开场；`banner` 与 `wash` 仍只用于单页。顶部导航栏以对比遮罩叠在 hero
上，并随页面一起滚动。

`toc_style` 可取 `fixed` 或 `flow`；flow 在文章旁放置更宽的导轨，并且只在滚动
之后固定。它的静止位置与文章信息行对齐；页面没有信息行时，与描述对齐。标题
换行数无法预知，因此由 `docs-shell.js` 测量偏移；没有 JavaScript 时，导轨从
文章起点开始。`toc_taxonomies: false` 移除术语云；导轨既无 TOC 又无术语云时
完全不渲染。`notoc` 仍是页面级 TOC 退出键。这些开关不改变署名、标签、系列、
翻页顺序、feed 或页尾组合；导轨在 `xl` 断点以下消失。

## 搜索、操作与运行时 {#search-actions-and-runtime}

`params.offline_search` 选择启用各语言的本地索引。启用后默认也在 `hugo server`
期间构建；大型编辑循环可以设置 `offline_search_on_serve: false`。HTML 搜索出现
在首页、外壳页面，以及启用 `landing_search` 的落地页上。其它非外壳页面与 Print
不包含对话框、Lunr 或命令面板。

搜索元数据包括 `search_keywords`、默认值为 1 的 `search_boost`，以及
`search_exclude`。索引携带 URL、标题、分类法、摘录、小标题、description、
正文或摘要、根、分区、类型、关键词、boost、面包屑导航与图标。夹具预算为原始
2 MiB、gzip 512 KiB。站点可以通过 `hooks/search-keywords-extra.html` 返回额外
字符串。关键词用于匹配与排序；CJK 查询仅命中关键词时，结果显示页面描述或
摘录，不展示关键词列表。正文命中仍使用匹配位置附近的文字作为摘要。

内置操作 ID 包括 `copy_markdown`、`copy_link`、`open_chatgpt`、`open_claude`、
`view_markdown`、`view_history`、`edit_page`、`create_child_page`、`create_issue`、
`create_project_issue`、`print_section`、`print`、`switch_preset`、`switch_theme`、
`switch_language`、`switch_version` 与 `open_github`。分享栏之外的 `copy_link`
只出现在命令面板中。站点通过
`languages.<lang>.params.ui.command_palette.commands` 配置的命令可以打开安全
URL，或调用内置 ID，绝不能注入 JavaScript。剪贴板的旧式回退恢复此前的焦点、选区及其方向，
但复制期间其他控件已获得焦点时不再抢回。

编辑、历史与新建子页操作要求源文件具有仓库相对路径。物理文件名和站点工作目录
先统一为 `/` 分隔符，再判断包含关系。`path_base_for_github_subdir` 匹配归一化后的
路径：站点内内容使用相对工作目录的路径，外部挂载使用绝对路径。字符串正则可以
移除匹配的内容，`{from, to}` 映射可以替换它；外部来源必须显式匹配规则。
映射并整理路径后，空路径、`.`、绝对路径、带盘符的路径以及以 `..` 路径段开头的
结果都会隐藏这三项操作。文档 issue 与项目 issue 操作仍按各自的仓库配置提供。
Windows 映射应匹配 `/` 而不是 `\`；归一化不会改变文件名大小写。

命令面板有空状态、文本搜索状态与 `>` 命令状态；快捷链接来自导航。它没有历史、
语义搜索、个性化或远程回退。搜索查询留在浏览器内，默认不发送遥测。

`OinkSurfaceCoordinator` 协调命令面板、抽屉、根栏目、语言与版本菜单。各界面自行
管理焦点恢复与 Escape。键盘导航会忽略可编辑控件与模态框，Ctrl/Cmd+K 同样避让
其他已打开的 dialog，包括固定定位的 ARIA 对话框。`/`、`\`、`f`、`c`
打开搜索或命令；`j`/`k` 移动标题；`q`/`e` 翻页；`h` 改变展示方式；`l`/`y`、
`t`、`r` 分别打开语言、主题与根栏目选项。侧栏 WASD/方向键导航使用真实焦点，
不会改写 Tab 顺序。
不带链接的分隔分组按钮也参与树导航。从子页按 Left/`a` 先聚焦父分组，再按一次才折叠；
Right/`d` 展开已折叠的分组，已展开时进入第一个可见子项。上一页/下一页仍只遍历链接，
不会把分组按钮当成页面。

页面大纲从同一套标题模型与滚动容器计算后的 `scroll-padding-top` 推导光标和可见
标题范围；SVG 线条与圆点共享同一组动画值，不会漂移。合法 URL 片段会被解码；
非法百分号序列则回退到字面的标题 ID，建立链接索引和选中页尾请求的标题时遵循
同一规则。禁止增加臆测性的 DOM
修复遍历。这项跟踪始终由普通外壳运行时负责。`params.ui.scroll_spy` 与页面键
`scroll_spy` 在整个 1.x 期间都是静默兼容 no-op，不加载独立运行时；只有未来的
破坏性版本才会删除它们。

## 搜索尾部扩展 {#search-tail-extensions}

> [!NOTE] 自 OINK 1.1 起提供
> OINK 1.1.0 包含该 API，1.0.0 中不存在。

受信任的站点 JavaScript 可调用
`OinkCommandPalette.registerSearchTail({id, rows, activate})`；YAML 和操作清单仍然只接受
数据。资源包继续按本地搜索开关装配。注册要求唯一且符合 `[A-Za-z0-9][A-Za-z0-9_-]*` 的
ID，以及两个函数；非法或重复注册抛出异常。返回的注销函数可重复调用，旧句柄不能删除
复用该 ID 的新注册。面板打开时的注册变化会合并调度一次渲染，注销会取消该扩展的待完成操作。

`rows(context)` 同步返回描述符。context 是冻结的 `{query, locale, phase, pageResultCount}`
快照：query 去除首尾空白，locale 使用 HTML 的语言标签，phase 为 `results`、`empty` 或
`error`，数量只统计上限截取后的本地页面结果。仅在非空文本搜索完成后调用扩展，不在空查询、
命令、选择或加载状态调用。扩展行按注册顺序放在本地化的 Actions 组中，排在全部原生页面及
操作之后。原生空结果、错误提示与输入触发的索引重试保留。

描述符必须提供扩展内唯一、符合相同语法的 `id`，以及非空字符串 `title`。
可选的 `description`、`icon`、`disabledReason` 为字符串，`available` 为布尔值，默认 true。
OINK 复制并冻结这些字段，把显示字符串当作文本渲染。描述符非法、ID 重复、返回异步结果或
回调抛出异常时，本轮跳过整个扩展，其他扩展不受影响。不承诺回调的精确调用次数。

`activate(row, context)` 只通过普通结果行的激活路径调用，收到复制后的描述符和生成该行时
的上下文，另带 `AbortSignal` 与 `handoff()`。操作待完成时阻止其他结果行激活，包括进入原生
选择菜单。同步异常和 Promise
拒绝会释放待完成状态、保持面板打开，并播报本地化的操作失败信息。成功值被忽略；成功时关闭
面板，不从其他界面抢回焦点。关闭、重新打开、渲染的查询改变或注销会取消待完成操作，旧操作
的迟到结果不能修改新会话。

打开另一个受协调器管理的界面前，调用 `context.handoff()`。它关闭面板，但不归还焦点，也不
取消本次激活。随后由站点管理新界面的焦点与错误提示。之后的新 Palette 会话或注销仍可取消
尚未完成的操作；成功完成不会取消已经移交的操作。OINK 不强制超时。

`rows()` 必须保持纯净。这是受信任代码的契约，不是安全沙箱。默认查询仍留在本地，不内置
远程服务商或遥测。扩展的网络行为和服务商所需授权由站点负责。

## 分享 {#share}

`params.ui.share` 默认为空，可接受 16 个目标的任意有序子集：`x`、`bluesky`、
`mastodon`、`facebook`、`linkedin`、`reddit`、`hackernews`、`telegram`、
`whatsapp`、`line`、`pinterest`、`weibo`、`chatgpt`、`claude`、`email`、`copy`。
页面列表会替换继承列表；`share: false` 退出。未知项会警告并丢弃。只有普通页面
渲染分享栏；Print、Markdown 与 RSS 省略它。

目标是携带页面永久链接与标题的普通 intent 链接，外加本地 `copy_link` 按钮。
Pinterest 图片来自共享特色图片解析器。ChatGPT 与 Claude 接收构建期生成的永久
链接提示，与页面菜单里的助理操作相互独立。Discord 没有公共 intent 目标，因此
有意不提供。

分享栏不加载平台 SDK、iframe、脚本、样式表、计数器或 campaign 参数；只有读者
主动点击链接时才产生请求。它是一行带无障碍标签的字形。
`share/items.html` 解析目标，`share/bar.html` 负责渲染。

## 注记 {#annotation}

页面注记在 `annotation-items.html` 中解析描述项，再通过
`page-meta-lastmod.html` 渲染；两者都可以做窄范围覆盖。各行顺序如下：

| 行 | 条件 |
| --- | --- |
| 最后修改 | 已设置 `Lastmod` |
| 上游 | front matter 中的 `upstream_link` 非空 |
| 翻译 | 配置的权威语言存在译文，而且本页包含作者正文 |

`upstream_link` 是页面级事实；cascade 有效，`upstream_link: ""` 表示退出。
其它上游事实按站点参数 → `data/upstreams[upstream_source]` → front matter 解析：
`upstream_name`、`upstream_copyright`、`upstream_license`、`upstream_notice`，
以及可选的 `upstream_ref`、`upstream_modified`。存在链接时，前四项必填。无效或
残缺的署名会警告，而且不渲染法律声明；不支持的 URL 会被拒绝。发布门禁通过
`--panicOnWarning` 拒绝这类警告。

`upstream_modified` 改变署名动词并链接提交历史，不增加新行。notice 页面承载
完整的许可证与免责声明。翻译说明通过 `params.ui.translation_notice` 选择启用，
以页面键 `translation_notice` 参与 cascade，跳过生成页面或无正文页面；以本语言
原创的页面可以用 `translation_notice: false` 关闭。

## 作者与系列 {#authors-and-series}

博客文章页头依次为标题、信息行、术语徽章、作者署名、系列条；description 在其后
引出正文。信息行 `article-info.html` 始终包含日期；启用 `reading_time` 后再增加
字数与分钟数。Front matter 的 `upstream_link` 与注记使用同一个页面级事实，
并在共享 URL 策略保护下增加本地化的原文链接。术语行只是裸徽章组，分类法名称
位于分组标签中，不显示前缀。术语徽章静止时是浅中性底与弱化文字，前置该分类法的
term 图标；可点击徽章在 hover 或 focus 时才取得当前分区的强调色淡铺、边框与文字。
图标词汇表由 `taxonomy-icon.html` 独家拥有——每个
分类法配一对图标：整体分类法一枚、单个术语一枚（`folder-open`/`folder`、
`tags`/`tag`、`cubes`/`cube`、`users`/`user-pen`、series 用
`book-bookmark`/`book`，其余用 `shapes`）；`params.ui.taxonomy_icons` 可覆盖：
字符串同时作用于两个表面，`taxonomy`/`term` map 分别设置；无效输入警告并保留
内置。右栏词云只在云头戴整体图标：云 chip 保持"文本 + 计数"——分类法已经亮明
身份，再在每个 chip 上重复图标只是噪声。独立的分类法目录卡片会带一枚术语图标；
作者署名只放人物——头像、姓名与个人资料的一行简介——
不带标签或日期。列表行、卡片与术语归档共享同一形态的元数据行：日期、一条本地化
的作者与分区短语，以及由同一个 `reading_time` 开关控制的字数和分钟数。句子下方
是独立成行、自动换行的徽章行，按分类法字母序列出页面在全部分类法下的词条，每枚
徽章佩戴各自的 term 图标；卡片排除 `authors`——其句中已具名。

只有声明 `taxonomies: {author: authors}` 才启用作者。作者 term 页面拥有显示名称、
摘要、正文与特色图片头像；没有 profile 时，回退到链接标题、首字母与归档。
`authors-resolve.html` 在文章页头、列表行中保留 front matter 顺序，并为每位作者
生成一个 RSS `dc:creator`。没有 `authors` 时，旧 `author` 保持原样；两者同时
存在时，`authors` 无警告胜出。自定义作者分类法复数名按普通分类法处理。

只有声明 `taxonomies: {series: series}` 才启用系列。Term 页面拥有引言；不新增
参数、数据文件、封面模型或运行时。页面使用 `series: [name]` 与可选的
`series_weight`。`series-pages.html` 先按 weight 排有权重成员，再按日期升序排
无权重成员，并用 `Path` 打破平局；系列条与 term 页面共享该顺序。第一个命名系列
得到一条 HTML/Print 系列条。面板是半透明加模糊，而不是一张不透明卡片：`hero`
文章会把题图铺在这一段背后，不透明底色等于在画面上挖个洞；普通文章上这层色调
就落回页面自身的底色，所以一种处理同时服务两种场景。summary 拥有整行与末端
箭头；系列名连同它的分类法图标，仍是 summary 的兄弟链接，覆盖在一份隐藏的等宽
占位文字上，避免 summary 内出现嵌套交互控件。展开后先划一条细线，再在同一层
表面上把成员阅读顺序放进一个保持 DOM 顺序的自适应网格。每个链接都把序号纳入
点击目标，序号贴在固定方格轨道的末端，因此无论多少篇，标题都对齐在同一条边上；
窄屏保持一栏，只有当每个标题仍有可读宽度时才增加等宽栏，因此桌面面板能用满自身
宽度，也不会把一条选中背景拖过整篇正文。悬停与读者所在位置直接借用侧栏导航
处理这两种状态的同两种底色，当前篇再加上填充序号与加粗标题，不靠颜色单独表意。打印时显示同一份展开
列表，收为单栏。单篇系列与非 HTML 输出省略它。编号、交叉引用与聚合输出仍属于 Book。

默认文章分类法徽章会排除保留的 `authors` 与 `series`，因为专属界面已经展示
它们。显式设置 `params.taxonomy.page_header` 可以恢复任意一项。

## 博客索引与页面组合 {#blog-indexes-and-page-composition}

博客分区索引使用 `params.ui.blog_index`：默认的 `list` 与 `cards` 都是按最新优先
排列的一段扁平结果，共享 `blog_index_size` 分页；元数据行已经显示日期，所以不再
需要年份标题。`table` 把整个分区显示为日期、标题、标签行，不分页。卡片使用共享
首图、本地化日期/作者/分区元数据、标签与三行摘要。

分类法页（`/tags/`、`/authors/`）与其术语页共用一个页头
`shell/taxonomy-head.html`。分类法页以整体分类法图标的着色方块、本地化名称与
术语数开头；术语页以术语标题与取自 `ui_taxonomy_pages`、按当前 locale 的 CLDR
复数类别选择的页面数开头，没有渲染面包屑时标题上方再加一行
kicker，写明分类法并链回列表页——面包屑开启时它在上一行已经做了这两件事：代表
生成的分类法页的那一级面包屑借用页头同一个本地化标签，而不是 Hugo 的复数名标题。
页头之下，分类法页把术语排成单行卡片网格 `shell/taxonomy-cards.html`：使用次数
多者在前、同数按字母序（与右栏词云同序），以 `auto-fill` 填满等宽列，因此术语
很少时两张卡片也不会被拉宽到整页。一张卡片就是术语图标、术语名与页面数，整张
卡片即链接；只有作者以署名同款小头像开头，走同一个头像 partial。卡片不带描述、
不带最新一页：术语没有标题与计数之外值得一说的内容，多出的那一行只会让网格发糊。
不再有筛选芯片行与「全部」芯片：分区根已经在侧栏与顶栏里。术语页保持行列表，
作者资料页保留自己的页头。

分类法页与术语页的右栏以 `shell/taxonomy-switcher.html` 开头：声明的每种分类法
一行——整体图标、本地化名称、术语数——链向其列表页，当前分类法置于选中底色上。
这是从一种分类法的页面去另一种的路：词云芯片跳到术语页，词云头只负责折叠；只有
一种分类法的站点不渲染切换器。这一组与词云共用 `toc_taxonomies` 开关。分类法页
的词云按全站统计（`taxonomy-root.html` 对该 kind 不返回根），并省略自己那一组，
它的术语就是旁边的卡片；术语页保持分区作用域与完整的一组。

`params.ui.blog_index_toggle` 为当前分页切片渲染三种形态，并允许读者循环切换。
配置值控制首次绘制，隐藏形态不加载图片。读者存储的选择只作用于发布了全部三种
形态的索引：切换器关闭的分区只发布一种形态，并始终显示它。Front matter 或
cascade 可为每个分区覆盖站点模式。没有切换器的 table 仍是完整且不分页的归档。

`params.logo` 始终是品牌标志；`params.wordmark` 或站点标题是紧凑宽度下隐藏的
文字部分。Docs、Book、Blog 与 Swagger 共享一个外壳模型。页尾顺序为分享、反馈、
注记、翻页、评论。Docs/Book 翻页遵循侧栏前序遍历；Blog 按 weight 后接日期倒序；
`pager: false` 退出。静态输出省略翻页 UI。

每一种实际渲染的页脚形态，都会在最底层栏右侧保留纯图标工具组，顺序为版本、
语言、主题、快捷键帮助。各菜单向上展开；版本触发器不直接显示当前分支或版本名。
胖页脚的折叠箭头排在工具组之后。低于 `lg` 时，底层栏放弃版权/居中/工具组的
三列布局，改为三行全宽居中堆叠，工具组在最后一行。这些全局控件不再出现在
侧栏底部；`footer_style: none` 会移除整条底栏。

OINK 没有归档外壳、任意深度飞出菜单、第二个导航权威、查询上传，也没有针对已
移除配置的浏览器兼容 shim。反馈只通过既有 `gtag` 发出 `docs_feedback`，在本地
保存选择，而且不替代 Giscus。

## 验证 {#verification}

`bin/check-navigation-contract.py`、`bin/check-shell.py`、JavaScript 测试、输出
golden 与消费站点浏览器套件覆盖导航、语言与子路径链接、博客变体、页尾顺序、
键盘行为、无障碍与响应式布局。

## 外观控件 {#appearance-control}

顶栏与底栏共用点击或键盘展开的外观控件，Landing 手机抽屉另有带标签的入口。
`preset_menu` 允许选择时显示原生风格单选组；`dark_mode.show_menu` 开启时显示
浅色、深色、跟随系统单选组。选择立即生效，面板保持打开。Enter、Space 或向下
方向键展开；方向键在组内选择，Tab 在组间移动，Escape 关闭并归还焦点。触发按钮
图标表示实际明暗状态：浅色显示太阳，深色显示月亮，跟随系统变化时也同步更新。

英文分组标题为 Style 和 Light，中文为「风格」与「明暗」。风格选项以两列独立
按钮排列，使用带主题色的纸页、叠层、笔尖或终端图标与预设名称，不显示 Aa 预览
或实验标记。站点默认项在悬停提示与无障碍名称中注明，选择后清除保存的风格。
选中项使用淡色背景与边框，键盘焦点另有轮廓线。

桌面使用锚定触发器的非模态 dialog，外部点击或焦点离开时关闭。低于 768 px 或
从 Landing 抽屉进入时，通过 `showModal()` 在浏览器顶层打开底部表单，提供关闭
按钮和 44 px 选项目标。关闭表单保留下方抽屉。表面协调器在展开前关闭无关弹层。
`t` 快捷键继续通过 `switch_theme` 切换明暗；`switch_preset` 是独立的命令面板选项。

本地 Ink/Terminal 实验仍需显式配置；`preset_menu: true`
继续提供 Paper/Slate 与站点默认值。两者复用同一套状态、键盘、命令面板与底部表单
机制。Terminal 压紧桌面导航行，保留正文与手机触控目标尺寸。

---

反链：

- [OINK v1.1.0](/zh/blog/release/1.1.0/)
- [版本升级](/zh/docs/admin/upgrade/)
- [命令面板](/zh/docs/customize/panel/)
- [设计](/zh/docs/design/)
- [架构](/zh/docs/design/architecture/)
- [Markdown 优先创作](/zh/docs/design/decisions/authoring/)
- [视觉预设](/zh/docs/design/decisions/visual-presets/)
- [落地页](/zh/docs/design/landing/)
- [视觉预设](/zh/docs/design/proposals/visual-presets/)
- [2026-09-19 社区反馈调研](/zh/docs/design/research/2026-09-19-upstream-review/)
- [博客与文章](/zh/docs/write/blog/)
- [组织内容](/zh/docs/write/organize/)
