dsh-ppt 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,8 +17,8 @@
17
17
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
18
18
  import { resolve as resolvePath, join as joinPath } from 'node:path'
19
19
  import { deflateRawSync } from 'node:zlib'
20
- import { MEDIA_LAYOUTS, MEDIA_CSS, normalizeMediaSlide, renderMediaHtml, mediaBoxes } from './deck-media.mjs'
21
- import { renderDelivery } from './deck-delivery.mjs'
20
+ import { MEDIA_LAYOUTS, MEDIA_CSS, normalizeMediaSlide, renderMediaHtml, mediaBoxes } from './deck-media.mjs'
21
+ import { renderDelivery } from './deck-delivery.mjs'
22
22
 
23
23
  /**
24
24
  * manifest 版本跟随发布包 package.json;把 skills/dsh-ppt 独立复制到其他
@@ -974,7 +974,7 @@ export function buildDeck(options = {}) {
974
974
  // 先生成三份内容再统一提交:生成阶段抛错时也不会留下半套文件。
975
975
  commitDeckArtifacts([
976
976
  { path: jsonPath, data: JSON.stringify(manifest, null, 2) + '\n', encoding: 'utf8' },
977
- { path: htmlPath, data: renderHtml(manifest, theme, language, { pptx: pptxPath, json: jsonPath }), encoding: 'utf8' },
977
+ { path: htmlPath, data: renderHtml(manifest, theme, language, { pptx: pptxPath, json: jsonPath }), encoding: 'utf8' },
978
978
  { path: pptxPath, data: buildPptx(manifest, theme, language) },
979
979
  ])
980
980
 
@@ -1000,7 +1000,7 @@ export function buildDeck(options = {}) {
1000
1000
  // HTML 网页放映
1001
1001
  // ---------------------------------------------------------------------------
1002
1002
 
1003
- export function renderHtml(manifest, theme, language, files = {}) {
1003
+ export function renderHtml(manifest, theme, language, files = {}) {
1004
1004
  const t = theme
1005
1005
  const lang = language
1006
1006
  const ui = lang.ui
@@ -1020,8 +1020,8 @@ body.motion .bullets li{
1020
1020
  renderHtmlSlide(slide, index, ui, lang.id, manifest, theme) + '</div>'
1021
1021
  }).join('\n')
1022
1022
  const themeLabel = t.name[lang.id] ?? t.name.en
1023
- const total = manifest.slides.length
1024
- const delivery = renderDelivery(manifest, files, lang.id)
1023
+ const total = manifest.slides.length
1024
+ const delivery = renderDelivery(manifest, files, lang.id)
1025
1025
 
1026
1026
  return `<!DOCTYPE html>
1027
1027
  <html lang="${lang.attr}">
@@ -1124,7 +1124,7 @@ table.deck-table{
1124
1124
  .deck-table tbody tr:nth-child(even){background:color-mix(in srgb, var(--panel) 62%, transparent)}
1125
1125
  #progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);z-index:30;transition:width .25s}
1126
1126
  #hud{
1127
- position:fixed;left:22px;right:22px;bottom:18px;z-index:50;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;align-items:center;
1127
+ position:fixed;left:22px;right:22px;bottom:18px;z-index:50;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;align-items:center;
1128
1128
  color:var(--muted);font-size:13px;letter-spacing:.08em;font-variant-numeric:tabular-nums;
1129
1129
  }
1130
1130
  #hud button{
@@ -1215,7 +1215,7 @@ body.presenting #hud:hover{opacity:1;pointer-events:auto}
1215
1215
  .kicker{font-size:13px}
1216
1216
  .deck-table{font-size:14px}
1217
1217
  .deck-table th,.deck-table td{padding:.55em .6em}
1218
- #hud{left:10px;right:10px;bottom:10px;gap:6px;font-size:12px}
1218
+ #hud{left:10px;right:10px;bottom:10px;gap:6px;font-size:12px}
1219
1219
  #hud button{padding:10px 16px;min-height:44px}
1220
1220
  #notes-panel{bottom:68px;max-height:45vh}
1221
1221
  #help-panel{right:10px;bottom:68px;left:10px;max-width:none}
@@ -1233,18 +1233,18 @@ body.presenting #hud:hover{opacity:1;pointer-events:auto}
1233
1233
  ${motion ? 'body.motion .bullets li{opacity:1 !important;animation:none !important}' : ''}
1234
1234
  #progress,#hud,#notes-panel,#help-panel,#toast{display:none !important}
1235
1235
  }
1236
- ${delivery.css}
1237
- </style>
1238
- </head>
1239
- <body class="${motion ? 'motion' : 'no-motion'}">
1240
- <div id="progress" aria-hidden="true"></div>
1241
- ${delivery.body}
1236
+ ${delivery.css}
1237
+ </style>
1238
+ </head>
1239
+ <body class="${motion ? 'motion' : 'no-motion'}">
1240
+ <div id="progress" aria-hidden="true"></div>
1241
+ ${delivery.body}
1242
1242
  <div id="stage">
1243
1243
  ${slides}
1244
1244
  </div>
1245
1245
  <div id="hud" aria-live="polite">
1246
- <span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
1247
- ${delivery.navigation}
1246
+ <span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
1247
+ ${delivery.navigation}
1248
1248
  <span id="theme-label">${ui.theme} · ${escapeHtml(themeLabel)}</span>
1249
1249
  <button id="notes-toggle" type="button" title="S · ${escapeHtml(ui.notesToggle)}" aria-controls="notes-panel" aria-expanded="false">${escapeHtml(ui.notesToggle)}</button>
1250
1250
  <button id="presenter-toggle" type="button" title="V · ${escapeHtml(ui.presenterToggle)}">${escapeHtml(ui.presenterToggle)}</button>
@@ -1283,8 +1283,8 @@ ${slides}
1283
1283
  const helpBtn = document.getElementById('help-toggle');
1284
1284
  const helpPanel = document.getElementById('help-panel');
1285
1285
  const toastEl = document.getElementById('toast');
1286
- let index = 0;
1287
- let syncDelivery = () => {};
1286
+ let index = 0;
1287
+ let syncDelivery = () => {};
1288
1288
  let presenterWin = null;
1289
1289
  let presenterInitSent = false;
1290
1290
  let toastTimer = 0;
@@ -1357,8 +1357,8 @@ ${slides}
1357
1357
  slide.classList.toggle('is-active', i === index);
1358
1358
  if (frames[i]) frames[i].classList.toggle('is-current', i === index);
1359
1359
  });
1360
- counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
1361
- syncDelivery(index);
1360
+ counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
1361
+ syncDelivery(index);
1362
1362
  progress.style.width = ((index + 1) / total * 100) + '%';
1363
1363
  document.title = (index + 1) + ' / ' + total + ' · ' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
1364
1364
  try { history.replaceState?.(null, '', '#slide-' + (index + 1)); } catch (_) { /* file:// 下个别浏览器可能拒绝 */ }
@@ -1562,9 +1562,9 @@ ${slides}
1562
1562
  document.addEventListener('keydown', (event) => {
1563
1563
  const key = event.key;
1564
1564
  const lower = key.toLowerCase();
1565
- const targetEl = event.target;
1566
- if (document.getElementById('review-panel').open || event.ctrlKey || event.metaKey || event.altKey) return;
1567
- if (key !== 'Escape' && targetEl && (['INPUT', 'SELECT', 'TEXTAREA'].includes(targetEl.tagName) || targetEl.isContentEditable === true)) return;
1565
+ const targetEl = event.target;
1566
+ if (document.getElementById('review-panel').open || event.ctrlKey || event.metaKey || event.altKey) return;
1567
+ if (key !== 'Escape' && targetEl && (['INPUT', 'SELECT', 'TEXTAREA'].includes(targetEl.tagName) || targetEl.isContentEditable === true)) return;
1568
1568
  const interactive = targetEl && (targetEl.tagName === 'BUTTON' || targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT' || targetEl.tagName === 'TEXTAREA' || targetEl.isContentEditable === true);
1569
1569
  if (key === ' ' && interactive) return;
1570
1570
  if (key === 'Escape') {
@@ -1611,20 +1611,20 @@ ${slides}
1611
1611
  });
1612
1612
 
1613
1613
  let wheelLock = 0;
1614
- document.addEventListener('wheel', (event) => {
1615
- if (document.getElementById('review-panel').open || event.target.closest?.('#hud,#delivery-bar,#notes-panel,#help-panel')) return;
1614
+ document.addEventListener('wheel', (event) => {
1615
+ if (document.getElementById('review-panel').open || event.target.closest?.('#hud,#delivery-bar,#notes-panel,#help-panel')) return;
1616
1616
  const now = Date.now();
1617
1617
  if (now - wheelLock < 550 || document.body.classList.contains('overview') || notesOpen()) return;
1618
1618
  wheelLock = now;
1619
1619
  if (Math.abs(event.deltaY) > 12) go(index + (event.deltaY > 0 ? 1 : -1));
1620
1620
  }, { passive: true });
1621
1621
 
1622
- let touchStartY = 0;
1623
- let touchOnStage = false;
1624
- document.addEventListener('touchstart', (event) => { touchOnStage = !document.getElementById('review-panel').open && !!event.target.closest?.('#stage'); touchStartY = event.touches[0].clientY; }, { passive: true });
1622
+ let touchStartY = 0;
1623
+ let touchOnStage = false;
1624
+ document.addEventListener('touchstart', (event) => { touchOnStage = !document.getElementById('review-panel').open && !!event.target.closest?.('#stage'); touchStartY = event.touches[0].clientY; }, { passive: true });
1625
1625
  document.addEventListener('touchend', (event) => {
1626
1626
  const delta = event.changedTouches[0].clientY - touchStartY;
1627
- if (touchOnStage && Math.abs(delta) > 48 && !document.body.classList.contains('overview')) go(index + (delta < 0 ? 1 : -1));
1627
+ if (touchOnStage && Math.abs(delta) > 48 && !document.body.classList.contains('overview')) go(index + (delta < 0 ? 1 : -1));
1628
1628
  }, { passive: true });
1629
1629
 
1630
1630
  fullscreenBtn.addEventListener('click', () => { void toggleFullscreen(); });
@@ -1647,8 +1647,8 @@ ${slides}
1647
1647
  window.addEventListener('resize', updateThumbScale);
1648
1648
  window.addEventListener('beforeunload', closePresenter);
1649
1649
 
1650
- ${delivery.script}
1651
- const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
1650
+ ${delivery.script}
1651
+ const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
1652
1652
  go(Number.isInteger(start) ? start - 1 : 0);
1653
1653
  })();
1654
1654
  </script>
@@ -1,104 +1,104 @@
1
- import { basename } from 'node:path'
2
-
3
- const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c])
4
-
5
- /** Offline, sibling-file links only. No credentials or application server needed. */
6
- export function renderDelivery(manifest, files, language) {
7
- const label = (zh, en) => language === 'en' ? en : language === 'bilingual' ? zh + ' · ' + en : zh
8
- const issues = manifest.quality?.issues ?? []
9
- const draft = issues.some(issue => issue.code === 'template-unfilled')
10
- const href = path => './' + encodeURIComponent(basename(path))
11
- const download = (kind, text, id) => files[kind] ? `<a id="${id}" href="${escape(href(files[kind]))}" download>${escape(text)}</a>` : ''
12
- const pptxLabel = label('下载 PPTX', 'Download PPTX')
13
- const reviewLabel = label('检查与导出', 'Review and export')
14
- const outline = manifest.slides.map((slide, i) => `<li><button type="button" data-review-slide="${i + 1}"><span>${i + 1}</span>${escape(slide.title || label('无标题', 'Untitled'))}</button></li>`).join('')
15
- const rows = issues.map(issue => `<li><button type="button" data-review-slide="${issue.slide}"><strong>${escape(label('第 ' + issue.slide + ' 页', 'Slide ' + issue.slide))} · ${escape(issue.message)}</strong><span>${escape(issue.suggestion)}</span></button></li>`).join('')
16
- const summary = draft ? label('草稿中还有待填写内容,请补齐事实后再交付。', 'This draft still contains placeholders. Complete the facts before delivery.')
17
- : manifest.quality ? label('静态检查发现 ' + issues.length + ' 项提示;请核对实际显示。', 'Static checks found ' + issues.length + ' item(s); review the actual slides.')
18
- : label('此项目没有静态质量报告,请逐页检查。', 'No static quality report is available; review every slide.')
19
- return {
20
- body: `<div id="delivery-bar">
21
- <span id="delivery-status">${escape(draft ? label('待补全草稿', 'Draft: content needed') : label('演示文稿 · 请检查后交付', 'Presentation · review before delivery'))}</span>
22
- ${download('pptx', pptxLabel, 'download-pptx')}
23
- <button id="review-toggle" type="button" aria-haspopup="dialog">${escape(reviewLabel)}</button>
24
- </div>
25
- <dialog id="review-panel" aria-labelledby="review-title">
26
- <div class="review-head"><h2 id="review-title">${escape(reviewLabel)}</h2><button id="review-close" type="button">${escape(label('关闭', 'Close'))}</button></div>
27
- <p id="review-summary">${escape(summary)}</p>
28
- <div class="review-exports">${download('pptx', pptxLabel, 'review-download-pptx')}${download('json', label('保存可编辑项目', 'Save project'), 'download-project')}</div>
29
- <p>${escape(label('用 PowerPoint / WPS 打开 PPTX 可直接修改文字、图片和图表。让 Agent 继续修改时,使用保存的项目文件。', 'Open the PPTX in PowerPoint / WPS to edit text, images and charts. Use the saved project when asking your Agent to revise it.'))}</p>
30
- <h3>${escape(label('检查提示 · 点击定位页面', 'Review items · click to open slide'))}</h3>
31
- ${issues.length ? `<ul id="review-issues">${rows}</ul>` : `<p>${escape(manifest.quality ? label('静态检查未发现问题;仍需核对实际排版和事实。', 'No static issues found; check the actual layout and facts.') : label('暂无报告。', 'No report available.'))}</p>`}
32
- <p>${escape(label('文字尺寸是静态估算;这里不能证明 PowerPoint / WPS 排版已通过验收。', 'Text fit is estimated; this does not verify the rendered PowerPoint / WPS layout.'))}</p>
33
- <h3>${escape(label('页面目录', 'Slides'))}</h3><ol id="review-outline">${outline}</ol>
34
- </dialog>`,
35
- navigation: `<button id="previous-slide" type="button" aria-label="${escape(label('上一页', 'Previous slide'))}">←</button><input id="page-number" type="number" min="1" max="${manifest.slides.length}" value="1" aria-label="${escape(label('跳到第几页', 'Go to slide'))}"><button id="next-slide" type="button" aria-label="${escape(label('下一页', 'Next slide'))}">→</button>`,
36
- css: DELIVERY_CSS,
37
- script: `syncDelivery = (${initDelivery.toString()})(go, setOverview, () => index, total);`,
38
- }
39
- }
40
-
41
- /** This function is embedded in the standalone HTML, with no module dependencies. */
42
- function initDelivery(go, setOverview, current, total) {
43
- const previous = document.getElementById('previous-slide')
44
- const next = document.getElementById('next-slide')
45
- const number = document.getElementById('page-number')
46
- const panel = document.getElementById('review-panel')
47
- const toggle = document.getElementById('review-toggle')
48
- previous.addEventListener('click', () => go(Math.max(0, current() - 1)))
49
- next.addEventListener('click', () => go(Math.min(total - 1, current() + 1)))
50
- const jump = () => {
51
- const value = Number(number.value)
52
- if (Number.isInteger(value) && value >= 1 && value <= total) go(value - 1)
53
- else number.value = String(current() + 1)
54
- }
55
- number.addEventListener('change', jump)
56
- number.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); jump() } })
57
- toggle.addEventListener('click', () => panel.showModal())
58
- document.getElementById('review-close').addEventListener('click', () => panel.close())
59
- panel.querySelectorAll('[data-review-slide]').forEach(button => button.addEventListener('click', () => {
60
- const slide = Number(button.dataset.reviewSlide)
61
- if (Number.isInteger(slide) && slide >= 1 && slide <= total) { go(slide - 1); setOverview(false); panel.close() }
62
- }))
63
- return index => {
64
- number.value = String(index + 1)
65
- previous.disabled = index === 0
66
- next.disabled = index === total - 1
67
- }
68
- }
69
-
70
- const DELIVERY_CSS = `
71
- #hud button:disabled{opacity:.4;cursor:default}
72
- #page-number{width:56px;padding:7px;border:1px solid var(--muted);border-radius:8px;background:var(--panel);color:var(--fg);font:inherit}
73
- #delivery-bar{position:fixed;top:18px;left:22px;right:22px;z-index:45;display:flex;align-items:center;gap:12px;font-size:13px}
74
- #delivery-status{flex:1;color:var(--muted)}
75
- #delivery-bar a,#delivery-bar button,.review-exports a{display:inline-block;text-decoration:none;border:1px solid var(--muted);border-radius:99px;padding:9px 14px;background:var(--panel);color:var(--fg);font:inherit;cursor:pointer}
76
- #delivery-bar a:hover,#delivery-bar button:hover,.review-exports a:hover{border-color:var(--accent);color:var(--accent)}
77
- #delivery-bar #download-pptx{background:var(--accent);color:var(--bg);border-color:var(--accent)}
78
- #review-panel{background:var(--panel);color:var(--fg);border:1px solid var(--muted);border-radius:18px;width:min(620px,calc(100vw - 24px));max-height:85vh;padding:24px;overflow:auto}
79
- #review-panel::backdrop{background:rgba(0,0,0,.65)}
80
- .review-head{display:flex;justify-content:space-between;align-items:center;gap:16px}
81
- .review-head h2{margin:0;font-size:24px}
82
- #review-close{border:1px solid var(--muted);border-radius:10px;background:transparent;color:var(--fg);padding:8px 12px;font:inherit;cursor:pointer}
83
- #review-panel p{line-height:1.65;color:var(--muted)}
84
- #review-panel h3{margin:24px 0 10px;font-size:17px}
85
- #review-panel ul,#review-panel ol{list-style:none;padding:0;margin:0}
86
- #review-panel li+li{margin-top:8px}
87
- #review-panel li button{width:100%;text-align:left;display:flex;gap:12px;align-items:center;background:var(--bg);color:var(--fg);border:1px solid transparent;border-radius:10px;padding:12px;font:inherit;cursor:pointer}
88
- #review-panel li button:hover{border-color:var(--accent)}
89
- #review-issues li button{display:block}
90
- #review-issues strong,#review-issues span{display:block;line-height:1.6}
91
- #review-issues span{color:var(--muted);font-size:13px;margin-top:4px}
92
- .review-exports{display:flex;flex-wrap:wrap;gap:8px}
93
- body.presenting #delivery-bar{display:none}
94
- button:focus-visible,a:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
95
- @media(max-width:640px){
96
- #delivery-bar{left:10px;right:10px;top:10px;gap:8px}
97
- #delivery-status{display:none}
98
- #delivery-bar a,#delivery-bar button{padding:10px 12px;min-height:44px}
99
- #hud button{padding:9px 12px}
100
- #notes-panel,#help-panel{bottom:160px}
101
- #review-panel{padding:18px}
102
- }
103
- @media print{#delivery-bar,#review-panel{display:none!important}}
104
- `
1
+ import { basename } from 'node:path'
2
+
3
+ const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c])
4
+
5
+ /** Offline, sibling-file links only. No credentials or application server needed. */
6
+ export function renderDelivery(manifest, files, language) {
7
+ const label = (zh, en) => language === 'en' ? en : language === 'bilingual' ? zh + ' · ' + en : zh
8
+ const issues = manifest.quality?.issues ?? []
9
+ const draft = issues.some(issue => issue.code === 'template-unfilled')
10
+ const href = path => './' + encodeURIComponent(basename(path))
11
+ const download = (kind, text, id) => files[kind] ? `<a id="${id}" href="${escape(href(files[kind]))}" download>${escape(text)}</a>` : ''
12
+ const pptxLabel = label('下载 PPTX', 'Download PPTX')
13
+ const reviewLabel = label('检查与导出', 'Review and export')
14
+ const outline = manifest.slides.map((slide, i) => `<li><button type="button" data-review-slide="${i + 1}"><span>${i + 1}</span>${escape(slide.title || label('无标题', 'Untitled'))}</button></li>`).join('')
15
+ const rows = issues.map(issue => `<li><button type="button" data-review-slide="${issue.slide}"><strong>${escape(label('第 ' + issue.slide + ' 页', 'Slide ' + issue.slide))} · ${escape(issue.message)}</strong><span>${escape(issue.suggestion)}</span></button></li>`).join('')
16
+ const summary = draft ? label('草稿中还有待填写内容,请补齐事实后再交付。', 'This draft still contains placeholders. Complete the facts before delivery.')
17
+ : manifest.quality ? label('静态检查发现 ' + issues.length + ' 项提示;请核对实际显示。', 'Static checks found ' + issues.length + ' item(s); review the actual slides.')
18
+ : label('此项目没有静态质量报告,请逐页检查。', 'No static quality report is available; review every slide.')
19
+ return {
20
+ body: `<div id="delivery-bar">
21
+ <span id="delivery-status">${escape(draft ? label('待补全草稿', 'Draft: content needed') : label('演示文稿 · 请检查后交付', 'Presentation · review before delivery'))}</span>
22
+ ${download('pptx', pptxLabel, 'download-pptx')}
23
+ <button id="review-toggle" type="button" aria-haspopup="dialog">${escape(reviewLabel)}</button>
24
+ </div>
25
+ <dialog id="review-panel" aria-labelledby="review-title">
26
+ <div class="review-head"><h2 id="review-title">${escape(reviewLabel)}</h2><button id="review-close" type="button">${escape(label('关闭', 'Close'))}</button></div>
27
+ <p id="review-summary">${escape(summary)}</p>
28
+ <div class="review-exports">${download('pptx', pptxLabel, 'review-download-pptx')}${download('json', label('保存可编辑项目', 'Save project'), 'download-project')}</div>
29
+ <p>${escape(label('用 PowerPoint / WPS 打开 PPTX 可直接修改文字、图片和图表。让 Agent 继续修改时,使用保存的项目文件。', 'Open the PPTX in PowerPoint / WPS to edit text, images and charts. Use the saved project when asking your Agent to revise it.'))}</p>
30
+ <h3>${escape(label('检查提示 · 点击定位页面', 'Review items · click to open slide'))}</h3>
31
+ ${issues.length ? `<ul id="review-issues">${rows}</ul>` : `<p>${escape(manifest.quality ? label('静态检查未发现问题;仍需核对实际排版和事实。', 'No static issues found; check the actual layout and facts.') : label('暂无报告。', 'No report available.'))}</p>`}
32
+ <p>${escape(label('文字尺寸是静态估算;这里不能证明 PowerPoint / WPS 排版已通过验收。', 'Text fit is estimated; this does not verify the rendered PowerPoint / WPS layout.'))}</p>
33
+ <h3>${escape(label('页面目录', 'Slides'))}</h3><ol id="review-outline">${outline}</ol>
34
+ </dialog>`,
35
+ navigation: `<button id="previous-slide" type="button" aria-label="${escape(label('上一页', 'Previous slide'))}">←</button><input id="page-number" type="number" min="1" max="${manifest.slides.length}" value="1" aria-label="${escape(label('跳到第几页', 'Go to slide'))}"><button id="next-slide" type="button" aria-label="${escape(label('下一页', 'Next slide'))}">→</button>`,
36
+ css: DELIVERY_CSS,
37
+ script: `syncDelivery = (${initDelivery.toString()})(go, setOverview, () => index, total);`,
38
+ }
39
+ }
40
+
41
+ /** This function is embedded in the standalone HTML, with no module dependencies. */
42
+ function initDelivery(go, setOverview, current, total) {
43
+ const previous = document.getElementById('previous-slide')
44
+ const next = document.getElementById('next-slide')
45
+ const number = document.getElementById('page-number')
46
+ const panel = document.getElementById('review-panel')
47
+ const toggle = document.getElementById('review-toggle')
48
+ previous.addEventListener('click', () => go(Math.max(0, current() - 1)))
49
+ next.addEventListener('click', () => go(Math.min(total - 1, current() + 1)))
50
+ const jump = () => {
51
+ const value = Number(number.value)
52
+ if (Number.isInteger(value) && value >= 1 && value <= total) go(value - 1)
53
+ else number.value = String(current() + 1)
54
+ }
55
+ number.addEventListener('change', jump)
56
+ number.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); jump() } })
57
+ toggle.addEventListener('click', () => panel.showModal())
58
+ document.getElementById('review-close').addEventListener('click', () => panel.close())
59
+ panel.querySelectorAll('[data-review-slide]').forEach(button => button.addEventListener('click', () => {
60
+ const slide = Number(button.dataset.reviewSlide)
61
+ if (Number.isInteger(slide) && slide >= 1 && slide <= total) { go(slide - 1); setOverview(false); panel.close() }
62
+ }))
63
+ return index => {
64
+ number.value = String(index + 1)
65
+ previous.disabled = index === 0
66
+ next.disabled = index === total - 1
67
+ }
68
+ }
69
+
70
+ const DELIVERY_CSS = `
71
+ #hud button:disabled{opacity:.4;cursor:default}
72
+ #page-number{width:56px;padding:7px;border:1px solid var(--muted);border-radius:8px;background:var(--panel);color:var(--fg);font:inherit}
73
+ #delivery-bar{position:fixed;top:18px;left:22px;right:22px;z-index:45;display:flex;align-items:center;gap:12px;font-size:13px}
74
+ #delivery-status{flex:1;color:var(--muted)}
75
+ #delivery-bar a,#delivery-bar button,.review-exports a{display:inline-block;text-decoration:none;border:1px solid var(--muted);border-radius:99px;padding:9px 14px;background:var(--panel);color:var(--fg);font:inherit;cursor:pointer}
76
+ #delivery-bar a:hover,#delivery-bar button:hover,.review-exports a:hover{border-color:var(--accent);color:var(--accent)}
77
+ #delivery-bar #download-pptx{background:var(--accent);color:var(--bg);border-color:var(--accent)}
78
+ #review-panel{background:var(--panel);color:var(--fg);border:1px solid var(--muted);border-radius:18px;width:min(620px,calc(100vw - 24px));max-height:85vh;padding:24px;overflow:auto}
79
+ #review-panel::backdrop{background:rgba(0,0,0,.65)}
80
+ .review-head{display:flex;justify-content:space-between;align-items:center;gap:16px}
81
+ .review-head h2{margin:0;font-size:24px}
82
+ #review-close{border:1px solid var(--muted);border-radius:10px;background:transparent;color:var(--fg);padding:8px 12px;font:inherit;cursor:pointer}
83
+ #review-panel p{line-height:1.65;color:var(--muted)}
84
+ #review-panel h3{margin:24px 0 10px;font-size:17px}
85
+ #review-panel ul,#review-panel ol{list-style:none;padding:0;margin:0}
86
+ #review-panel li+li{margin-top:8px}
87
+ #review-panel li button{width:100%;text-align:left;display:flex;gap:12px;align-items:center;background:var(--bg);color:var(--fg);border:1px solid transparent;border-radius:10px;padding:12px;font:inherit;cursor:pointer}
88
+ #review-panel li button:hover{border-color:var(--accent)}
89
+ #review-issues li button{display:block}
90
+ #review-issues strong,#review-issues span{display:block;line-height:1.6}
91
+ #review-issues span{color:var(--muted);font-size:13px;margin-top:4px}
92
+ .review-exports{display:flex;flex-wrap:wrap;gap:8px}
93
+ body.presenting #delivery-bar{display:none}
94
+ button:focus-visible,a:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
95
+ @media(max-width:640px){
96
+ #delivery-bar{left:10px;right:10px;top:10px;gap:8px}
97
+ #delivery-status{display:none}
98
+ #delivery-bar a,#delivery-bar button{padding:10px 12px;min-height:44px}
99
+ #hud button{padding:9px 12px}
100
+ #notes-panel,#help-panel{bottom:160px}
101
+ #review-panel{padding:18px}
102
+ }
103
+ @media print{#delivery-bar,#review-panel{display:none!important}}
104
+ `