ソースを参照

fix(docs): keep platform commands readable in MPA builds

gengruilin 1 ヶ月 前
親
コミット
7aca916679

+ 2 - 2
.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.md
-2026-09-15-docs-platform-command-tabs.md: f21fef8b7091de296ee2aab329fdc7d6362e8c14
-2026-09-15-docs-platform-command-tabs.zh.md: 2b309a4587df3fc4b727baf6aa546f07e2a51311
+2026-09-15-docs-platform-command-tabs.md: 4d44d1191a7ffc1601e15aa4d9f119a90e06b0dd
+2026-09-15-docs-platform-command-tabs.zh.md: 450765b52d1507fa9b9e6a20a1854e6861655838

+ 2 - 2
.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.md

@@ -14,7 +14,7 @@ Each of the four platform-dependent steps uses VitePress's native `code-group`:
 
 Both languages retain every command in canonical Markdown. The documentation projector preserves the groups and their code in the website's raw Markdown. Explicit anchors before each group preserve the former platform heading targets; following a platform anchor locates the group without selecting a tab.
 
-The theme adds a visible keyboard focus outline to native radio labels and displays both command blocks in search excerpts, whose tab controls are unavailable. A Markdown renderer adapter makes each native tab strip a form with submission prevented. Local search copies a section's rendered HTML, including radio names; separate form owners stop those copies from clearing the page's selection. VitePress owns selection, syntax highlighting, and copying the active code block. There is no shared selection state or stored preference.
+The theme adds a visible keyboard focus outline to native radio labels and displays both command blocks in search excerpts, whose tab controls are unavailable. A Markdown renderer adapter makes each native tab strip a form with submission prevented. Local search copies a section's rendered HTML, including radio names; separate form owners stop those copies from clearing the page's selection. VitePress owns selection, syntax highlighting, and copying the active code block. MPA builds omit those client handlers, so build-only styles display both command blocks and hide the tab strip and copy buttons. There is no shared selection state or stored preference.
 
 ## Alternatives considered
 
@@ -24,7 +24,7 @@ The theme adds a visible keyboard focus outline to native radio labels and displ
 
 ## Consequences
 
-Readers see one command version per step while repository readers and raw-Markdown consumers retain both. Windows readers select PowerShell in each group. Existing platform links still reach the corresponding step, although the platform names leave the page outline.
+SPA readers see one command version per step while MPA, repository, and raw-Markdown readers retain both. Windows readers select PowerShell in each group. Existing platform links still reach the corresponding step, although the platform names leave the page outline.
 
 Verification covers command preservation, bilingual pairing, projected and raw Markdown, and actual browser switching, copying, keyboard focus, search, themes, and narrow layouts. Framework-owned interaction is exercised on the running site; session replay is inapplicable to this documentation-only presentation.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 两种语言均在规范 Markdown 中保留每条命令。文档投影器在网站原始 Markdown 中保留分组及代码。各组前的显式锚点保留原有平台标题的链接目标;平台锚点定位到分组,但不选择标签。
 
-主题为原生单选按钮标签补充可见的键盘焦点轮廓,并在无法操作标签的搜索摘要中显示两个命令块。Markdown 渲染适配器将各原生标签栏设为禁止提交的表单。本地搜索会复制章节渲染后的 HTML,其中包含单选按钮名称;独立的所属表单可避免副本清除页面的选择。VitePress 负责选择、语法高亮和复制当前代码块。不引入共享选择状态或持久化偏好。
+主题为原生单选按钮标签补充可见的键盘焦点轮廓,并在无法操作标签的搜索摘要中显示两个命令块。Markdown 渲染适配器将各原生标签栏设为禁止提交的表单。本地搜索会复制章节渲染后的 HTML,其中包含单选按钮名称;独立的所属表单可避免副本清除页面的选择。VitePress 负责选择、语法高亮和复制当前代码块。MPA 构建不包含这些客户端处理器,因此构建专用样式显示两个命令块,并隐藏标签栏与复制按钮。不引入共享选择状态或持久化偏好。
 
 ## 考虑过的替代方案
 
@@ -24,7 +24,7 @@ Status: implemented
 
 ## 影响
 
-读者每一步只看到一个命令版本,仓库读者和原始 Markdown 消费者仍能获取两个版本。Windows 读者需要在各组中选择 PowerShell。原有平台链接仍到达对应步骤,但平台名称不再出现在页面大纲中。
+SPA 读者每一步只看到一个命令版本,MPA、仓库和原始 Markdown 读者仍能获取两个版本。Windows 读者需要在各组中选择 PowerShell。原有平台链接仍到达对应步骤,但平台名称不再出现在页面大纲中。
 
 验证覆盖命令保留、双语配对、投影和原始 Markdown,以及真实浏览器中的切换、复制、键盘焦点、搜索、主题和窄屏布局。框架负责的交互在运行中的站点验收;会话回放不适用于这项纯文档展示变更。
 

+ 2 - 2
docs/user/guide/python-sdk.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/user/guide/python-sdk.md
-python-sdk.md: 445003e980c92c5c727e51b41deb1b516681b20b
-python-sdk.zh.md: b776781022dd3ef7af2331d169c006b9c9fa3381
+python-sdk.md: a14d656171f37a7feea8c091741f2175f9617cd4
+python-sdk.zh.md: 0846ed95ef3a396b468b0f50bcc176c64a963850

+ 16 - 4
docs/user/guide/python-sdk.md

@@ -14,7 +14,10 @@ This tutorial installs the published Python SDK, runs the shipped standalone min
 
 ## Install the SDK
 
-<a id="linux-and-macos"></a><a id="windows-powershell"></a>
+<div>
+<a id="linux-and-macos"></a>
+<a id="windows-powershell"></a>
+</div>
 
 ::: code-group
 
@@ -42,7 +45,10 @@ The installation includes a matching native runtime wheel and the `dsh` command.
 
 Export the credential and, when needed, a compatible proxy endpoint:
 
-<a id="linux-and-macos-1"></a><a id="windows-powershell-1"></a>
+<div>
+<a id="linux-and-macos-1"></a>
+<a id="windows-powershell-1"></a>
+</div>
 
 ::: code-group
 
@@ -60,7 +66,10 @@ $env:DEEPSEEK_API_KEY = "sk-your-key-here"
 
 Run one task with explicit workspace and home paths:
 
-<a id="linux-and-macos-2"></a><a id="windows-powershell-2"></a>
+<div>
+<a id="linux-and-macos-2"></a>
+<a id="windows-powershell-2"></a>
+</div>
 
 ::: code-group
 
@@ -115,7 +124,10 @@ The SDK starts the bundled `dsh --profile sdk-minimal` process lazily and reuses
 
 Use `dsh plugin` for dependencies and bundle layers that should persist in this home:
 
-<a id="linux-and-macos-3"></a><a id="windows-powershell-3"></a>
+<div>
+<a id="linux-and-macos-3"></a>
+<a id="windows-powershell-3"></a>
+</div>
 
 ::: code-group
 

+ 16 - 4
docs/user/guide/python-sdk.zh.md

@@ -14,7 +14,10 @@
 
 ## 安装 SDK
 
-<a id="linux-与-macos"></a><a id="windows-powershell"></a>
+<div>
+<a id="linux-与-macos"></a>
+<a id="windows-powershell"></a>
+</div>
 
 ::: code-group
 
@@ -42,7 +45,10 @@ python -m pip install deepseek-harness-sdk
 
 导出凭据;使用兼容代理时再设置 endpoint:
 
-<a id="linux-与-macos-1"></a><a id="windows-powershell-1"></a>
+<div>
+<a id="linux-与-macos-1"></a>
+<a id="windows-powershell-1"></a>
+</div>
 
 ::: code-group
 
@@ -60,7 +66,10 @@ $env:DEEPSEEK_API_KEY = "sk-your-key-here"
 
 使用显式 workspace 与 home 路径运行一个任务:
 
-<a id="linux-与-macos-2"></a><a id="windows-powershell-2"></a>
+<div>
+<a id="linux-与-macos-2"></a>
+<a id="windows-powershell-2"></a>
+</div>
 
 ::: code-group
 
@@ -115,7 +124,10 @@ SDK 会延迟启动内置的 `dsh --profile sdk-minimal` 进程,并复用到
 
 需要在该 home 中持久保存依赖与 bundle 层时,使用 `dsh plugin`:
 
-<a id="linux-与-macos-3"></a><a id="windows-powershell-3"></a>
+<div>
+<a id="linux-与-macos-3"></a>
+<a id="windows-powershell-3"></a>
+</div>
 
 ::: code-group
 

+ 13 - 1
website/.vitepress/code-groups.ts

@@ -1,6 +1,18 @@
 /** Isolates native code-group radios from copies rendered in local search. */
 
-import type { MarkdownRenderer } from 'vitepress'
+import type { HeadConfig, MarkdownRenderer } from 'vitepress'
+
+/**
+ * Keeps all code-group commands readable when MPA output omits the theme's client handlers.
+ * @param mpa - Whether the resolved site uses VitePress's MPA build.
+ * @returns MPA-only styles that expose every block and hide inactive controls.
+ */
+export function codeGroupFallbackHead(mpa: boolean | undefined): HeadConfig[] {
+  return mpa ? [['style', {}, `
+.vp-code-group .tabs, .vp-code-group button.copy { display: none; }
+.vp-code-group .blocks > div { display: block; }
+`]] : []
+}
 
 /**
  * Gives each native tab strip its own form so search excerpts cannot clear the page's selection.

+ 2 - 1
website/.vitepress/config.ts

@@ -5,7 +5,7 @@ import { resolve } from 'node:path'
 import type { DefaultTheme, PageData, SiteConfig } from 'vitepress'
 import type { ViteDevServer } from 'vite'
 import { withMermaid } from 'vitepress-plugin-mermaid'
-import { isolateCodeGroupRadios } from './code-groups.ts'
+import { codeGroupFallbackHead, isolateCodeGroupRadios } from './code-groups.ts'
 import { landingLink, localeCollections, orderedPages, routeLink, sectionSpec, type DocsLocale, type DocsPage, type DocsSidebar } from '../docs.ts'
 import { docsSourceFiles, emitRawMarkdownPages, llmsTxt, projectDocs, rawMarkdownRoute } from '../../scripts/project-doc-site.ts'
 
@@ -296,6 +296,7 @@ export default withMermaid({
   title: siteIdentity.title,
   description: siteIdentity.description,
   base,
+  transformHead: ({ siteConfig }) => codeGroupFallbackHead(siteConfig.mpa),
   /** Emit the raw-Markdown twin of every route plus llms.txt beside the rendered site. */
   buildEnd(siteConfig: SiteConfig) {
     emitRawMarkdownPages(siteConfig.outDir)

+ 1 - 1
website/AGENTS.md

@@ -18,4 +18,4 @@ Run `pnpm docs:check` after changing this subtree; the gate rejects additional n
 
 The default-theme extension owns the Mermaid fullscreen viewer. Keep its enhancements separate from Markdown projection and preserve the original SVG. Route, language, theme, and rendered-SVG changes close the active view; theme disposal releases every observer, listener, and scroll lock. The [viewer decision](../.agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.md) explains SVG isolation and verification.
 
-Native code groups keep their radio controls in separate forms so copied search excerpts cannot clear the page's selected platform. Search excerpts display every command block because their tab controls are unavailable; the [platform-tab decision](../.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.md) owns the interaction and verification.
+Native code groups keep their radio controls in separate forms so copied search excerpts cannot clear the page's selected platform. Search excerpts and MPA builds display every command block because tab switching is unavailable; the [platform-tab decision](../.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.md) owns the interaction and verification.

+ 41 - 5
website/tests/code-groups.spec.ts

@@ -1,21 +1,30 @@
 /** Native code-group markup remains isolated when local search copies a section. */
 
 import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
 import { JSDOM } from 'jsdom'
 import { createMarkdownRenderer } from 'vitepress'
 import { expect, it } from 'vitest'
-import { isolateCodeGroupRadios } from '../.vitepress/code-groups.ts'
+import { codeGroupFallbackHead, isolateCodeGroupRadios } from '../.vitepress/code-groups.ts'
+
+const source = '::: code-group\n\n```sh [Linux/macOS]\n. .venv/bin/activate\n```\n\n```powershell [Windows PowerShell]\n.venv\\Scripts\\Activate.ps1\n```\n\n:::\n'
 
 it('keeps the page selection when a search excerpt selects the same named radio', async () => {
   const md = await createMarkdownRenderer(import.meta.dirname, { config: isolateCodeGroupRadios })
-  const html = md.render('::: code-group\n\n```sh [Linux/macOS]\n. .venv/bin/activate\n```\n\n```powershell [Windows PowerShell]\n.venv\\Scripts\\Activate.ps1\n```\n\n:::\n')
-  const dom = new JSDOM(`<main>${html}</main><aside>${html}</aside>`)
+  const html = md.render(source)
+  const dom = new JSDOM(`<main>${html}</main><aside></aside>`)
   try {
     const { document } = dom.window
     const pageRadios = document.querySelectorAll<HTMLInputElement>('main input')
-    const excerptRadio = document.querySelector<HTMLInputElement>('aside input')
-    assert(pageRadios[0] && pageRadios[1] && excerptRadio)
+    const excerpt = document.querySelector('aside')
+    assert(pageRadios[0] && pageRadios[1] && excerpt)
     pageRadios[1].checked = true
+
+    // Search remounts the compiled page, reusing its generated radio names and IDs.
+    excerpt.innerHTML = html
+    expect(pageRadios[1].checked).toBe(true)
+    const excerptRadio = excerpt.querySelector<HTMLInputElement>('input')
+    assert(excerptRadio)
     excerptRadio.checked = true
 
     expect(pageRadios[0].checked).toBe(false)
@@ -33,3 +42,30 @@ it('keeps the page selection when a search excerpt selects the same named radio'
     dom.window.close()
   }
 })
+
+it('exposes both commands without client handlers in MPA output', async () => {
+  const md = await createMarkdownRenderer(import.meta.dirname, { config: isolateCodeGroupRadios })
+  const nativeStyles = readFileSync(new URL('../node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css', import.meta.url), 'utf8')
+  const fallback = codeGroupFallbackHead(true)
+  const dom = new JSDOM(`<style>${nativeStyles}</style>${md.render(source)}`)
+  try {
+    const { document } = dom.window
+    const blocks = [...document.querySelectorAll<HTMLElement>('.blocks > div')]
+    expect(blocks.map(block => dom.window.getComputedStyle(block).display)).toEqual(['block', 'none'])
+    for (const [tag, , content] of fallback) {
+      const element = document.createElement(tag)
+      element.textContent = content ?? ''
+      document.head.append(element)
+    }
+    expect(blocks.map(block => dom.window.getComputedStyle(block).display)).toEqual(['block', 'block'])
+    const controls = [...document.querySelectorAll<HTMLElement>('.tabs, button.copy')]
+    expect(controls).toHaveLength(3)
+    expect(controls.every(control => dom.window.getComputedStyle(control).display === 'none')).toBe(true)
+    expect(blocks.map(block => block.querySelector('code')?.textContent)).toEqual([
+      '. .venv/bin/activate', '.venv\\Scripts\\Activate.ps1',
+    ])
+    expect(codeGroupFallbackHead(false)).toEqual([])
+  } finally {
+    dom.window.close()
+  }
+})