@dongfangw/study-kit 0.1.2 → 0.1.3
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.
- package/README.md +46 -2
- package/ext/adoc-convert/index.mjs +62 -62
- package/lib/client.js +1858 -160
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +30 -15
- package/lib/index.js.map +1 -1
- package/lib/libPath.d.ts +28 -0
- package/lib/libPath.d.ts.map +1 -0
- package/lib/libPath.js +36 -0
- package/lib/libPath.js.map +1 -0
- package/lib/pdf.d.ts +31 -0
- package/lib/pdf.d.ts.map +1 -0
- package/lib/pdf.js +174 -0
- package/lib/pdf.js.map +1 -0
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +58 -1
- package/lib/render.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +8 -0
- package/lib/tools.js.map +1 -1
- package/lib/types/client/LibraryWorkspace.d.ts.map +1 -1
- package/lib/types/client/PdfViewer.d.ts +15 -0
- package/lib/types/client/PdfViewer.d.ts.map +1 -0
- package/lib/types/client/annotations.d.ts +26 -2
- package/lib/types/client/annotations.d.ts.map +1 -1
- package/lib/types/client/docEnhance.d.ts.map +1 -1
- package/lib/types/client/docKey.d.ts +8 -0
- package/lib/types/client/docKey.d.ts.map +1 -1
- package/lib/types/client/pdfAnno.d.ts +44 -0
- package/lib/types/client/pdfAnno.d.ts.map +1 -0
- package/lib/types/client/pdfPos.d.ts +27 -0
- package/lib/types/client/pdfPos.d.ts.map +1 -0
- package/lib/types/client/pdfjs.d.ts +87 -0
- package/lib/types/client/pdfjs.d.ts.map +1 -0
- package/lib/types/client/styles.module.css.gen.d.ts.map +1 -1
- package/package.json +140 -138
- package/skills/aero-concept-qa/SKILL.md +59 -59
- package/skills/aero-learning-path/SKILL.md +55 -55
- package/skills/aero-study-guide/SKILL.md +47 -47
- package/skills/aero-think/SKILL.md +59 -59
package/lib/client.js
CHANGED
|
@@ -6,8 +6,8 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
-
// \0aero-inline-css:
|
|
10
|
-
const css$2 = "/* ===========================================================================\n * 学习资料阅读器 · AsciiDoc 书籍版式主题(全局注入,仅作用于 .aero-doc)\n *\n * 背景:知识库(knowledge-base/正文)与教案(lesson-plan)正文都是 AsciiDoc,\n * 由宿主用 @asciidoctor/core 渲染成 HTML。AsciiDoc 的默认样式表\n * (asciidoctor-default.css)为大量「书本级」构造提供了配套样式——admonition\n * 提示块、example/sidebar 卡片、quote/verse 题引、图表题注、三线表/网格表、\n * callout 编号、描述列表、footnote、TOC 侧栏、内建 role(text-center 等)。\n * 本项目此前只给 .aero-doc 写了极简样式,于是这些构造全部退化成裸 HTML:\n * · admonition 的内层 <table> 被通用表格规则套上边框,图标列是空的\n * (:icons: font 输出 <i class=\"fa icon-note\">,但没有 Font Awesome);\n * · image::路径[图注] 的「图注五要素」落在 alt 属性里,页面上根本看不见;\n * · 「Figure 1.」这类自动题注号与作者手写的「图 C05.9-1」重复(中英混排)。\n * 本文件补齐这套版式,并叠加中文书籍的排版习惯:版心宽度、两端对齐、\n * 首行缩进 2 字符、章节页式标题、居中图注、三线表。\n *\n * 约束与依赖:\n * · 颜色一律走 DSH 主题 token(--dsw-alias-*),明暗两套自动跟随;\n * 见 dsh-client-ui-theme(深色由 body[data-ds-dark-theme] 切换)。\n * · 字号基准用 --dsh-content-font-size(用户在设置里调的字号)。\n * · 本文件以 `?inline` 由 doc-theme.ts 作为纯文本导入,不过 lightningcss,\n * 因此可以放心使用 mask-image / color-mix / :has 等现代特性。\n * =========================================================================== */\n\n/* ---------------------------------------------------------------------------\n * 0. 局部变量(含无 token 时的兜底,保证脱离 DSH 外壳(如离线预览)也成篇)\n * ------------------------------------------------------------------------- */\n.aero-doc {\n /* 每个 token 先看外层 `-ov` 覆盖(阅读设置的「底色」由 [data-aero-doc-wrap][data-paper]\n 注入,见本文件 §15),没有覆盖才回落到 DSH 主题 token,最后是内置兜底。\n 这样正文卡片与右侧「本页目录」能共用同一套墨色,深色纸面上不会出现黑字压暗底。 */\n --aero-ink: var(--aero-ink-ov, var(--dsw-alias-label-primary, #1f2328));\n --aero-ink-2: var(--aero-ink-2-ov, var(--dsw-alias-label-secondary, #57606a));\n --aero-ink-3: var(--aero-ink-3-ov, var(--dsw-alias-label-tertiary, #818b98));\n --aero-rule: var(--aero-rule-ov, var(--dsw-alias-border-l2, rgba(31, 35, 40, 0.12)));\n --aero-rule-strong: var(--aero-rule-strong-ov, var(--dsw-alias-border-l3, rgba(31, 35, 40, 0.22)));\n --aero-paper: var(--aero-paper-ov, var(--dsw-alias-bg-layer-1, #ffffff));\n --aero-soft: var(--aero-soft-ov, var(--dsw-alias-bg-module-platform, rgba(31, 35, 40, 0.035)));\n --aero-hover: var(--aero-hover-ov, var(--dsw-alias-interactive-bg-hover, rgba(31, 35, 40, 0.05)));\n --aero-accent: var(--aero-accent-ov, var(--dsw-alias-link, #4176e6));\n --aero-accent-ink: var(--aero-accent-ink-ov, var(--dsw-alias-state-business-primary, #2f6feb));\n --aero-mono: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);\n --aero-sans: var(--dsw-font-family, -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);\n --aero-serif: Georgia, \"Nimbus Roman\", \"Times New Roman\", \"Noto Serif SC\", \"Noto Serif CJK SC\",\n \"Source Han Serif SC\", \"Source Han Serif CN\", \"Songti SC\", STSong, SimSun, serif;\n /* 字号基准 = DSH 全局字号 × 阅读设置的字号倍率(--aero-scale 由 DocViewer 内联写入,\n 倍率表见 readerPrefs.ts 的 FONT_STEPS;缺省 1 即 100%)。 */\n --aero-base: calc(var(--dsh-content-font-size, 15px) * var(--aero-scale, 1));\n --aero-measure: 42em;\n --aero-shadow: var(--aero-shadow-ov, 0 1px 2px rgba(15, 25, 50, 0.04), 0 18px 40px -32px rgba(15, 25, 50, 0.45));\n}\n\n/* ---------------------------------------------------------------------------\n * 1. 版心:居中限宽的书页。窄屏退化为整幅,宽屏不出现超长行(阅读体验第一)。\n * ------------------------------------------------------------------------- */\n.aero-doc {\n box-sizing: border-box;\n max-width: var(--aero-measure);\n margin: 0 auto;\n padding: clamp(18px, 3.2vw, 34px) clamp(16px, 3.6vw, 44px) clamp(36px, 5vw, 60px);\n border: 1px solid var(--aero-rule);\n border-radius: 14px;\n background: var(--aero-paper);\n box-shadow: var(--aero-shadow);\n color: var(--aero-ink);\n font-family: var(--aero-serif);\n font-size: calc(var(--aero-base) * 1.07);\n line-height: 1.9;\n /* 中文书籍排版的三个关键:两端对齐、标点挤压、按 CJK 规则断行 */\n text-align: justify;\n text-justify: inter-ideograph;\n line-break: strict;\n overflow-wrap: break-word;\n word-break: normal;\n hyphens: none;\n font-variant-numeric: tabular-nums;\n -webkit-font-smoothing: antialiased;\n}\n/* 「网页版式」:无衬线、左对齐、不缩进、不要纸面卡片(长文档快速扫读更合适) */\n.aero-doc--web {\n --aero-measure: 56em;\n padding: 4px clamp(2px, 1vw, 10px) 32px;\n border: none;\n border-radius: 0;\n background: transparent;\n box-shadow: none;\n font-family: var(--aero-sans);\n font-size: var(--aero-base);\n line-height: 1.8;\n text-align: left;\n text-justify: auto;\n}\n\n/* ---------------------------------------------------------------------------\n * 2. 标题:章标题居中成「章首页」,节标题用细线分隔,三级标题带竖标\n * ------------------------------------------------------------------------- */\n.aero-doc h1,\n.aero-doc h2,\n.aero-doc h3,\n.aero-doc h4,\n.aero-doc h5,\n.aero-doc h6 {\n font-family: inherit;\n font-weight: 600;\n line-height: 1.35;\n color: var(--aero-ink);\n scroll-margin-top: 12px;\n}\n.aero-doc h1 {\n margin: 0.2em 0 0.1em;\n font-size: 1.62em;\n font-weight: 700;\n letter-spacing: 0.01em;\n text-align: center;\n}\n/* 章标题下的短装饰线(书籍章首页的「题花」位置) */\n.aero-doc h1::after {\n content: \"\";\n display: block;\n width: 2.6em;\n height: 2px;\n margin: 0.75em auto 0;\n border-radius: 2px;\n background: var(--aero-accent);\n opacity: 0.75;\n}\n.aero-doc h2 {\n margin: 2.2em 0 0.7em;\n padding-top: 0.85em;\n font-size: 1.28em;\n border-top: 1px solid var(--aero-rule);\n}\n.aero-doc h3 {\n display: flex;\n align-items: baseline;\n gap: 0.5em;\n margin: 1.8em 0 0.5em;\n font-size: 1.12em;\n}\n.aero-doc h3::before {\n content: \"\";\n flex: none;\n width: 3px;\n height: 0.95em;\n border-radius: 2px;\n background: var(--aero-accent);\n transform: translateY(0.08em);\n}\n.aero-doc h4,\n.aero-doc h5,\n.aero-doc h6 {\n margin: 1.4em 0 0.4em;\n font-size: 1em;\n color: var(--aero-ink-2);\n}\n/* 网页版式:标题不做居中/装饰,保持文档工具感 */\n.aero-doc--web h1 {\n margin: 0.4em 0 0.6em;\n font-size: 1.5em;\n text-align: left;\n}\n.aero-doc--web h1::after { display: none; }\n.aero-doc--web h2 {\n margin: 1.6em 0 0.5em;\n padding-top: 0;\n font-size: 1.26em;\n border-top: none;\n}\n.aero-doc--web h3 { display: block; margin: 1.3em 0 0.4em; font-size: 1.1em; }\n.aero-doc--web h3::before { display: none; }\n\n/* 节标题编号「1 / 1.1 / 1.1.1」(阅读器端写入,见 src/client/docEnhance.ts):\n 等宽数字 + 强调色,只做路标,不加大字号、不抢标题本身。 */\n.aero-doc .aero-secnum {\n flex: none;\n margin-right: 0.2em;\n font-variant-numeric: tabular-nums;\n font-weight: 600;\n color: var(--aero-accent);\n}\n/* h3 是 flex(竖标 + 文字),编号作为 flex 项已由 gap 提供间距,不再叠 margin */\n.aero-doc h3 > .aero-secnum { margin-right: 0; }\n\n/* ---------------------------------------------------------------------------\n * 3. 段落:书籍体首行缩进 2 字符;网页体用段间距\n * ------------------------------------------------------------------------- */\n.aero-doc p { margin: 0.55em 0; orphans: 2; widows: 2; }\n/* 中文书籍体例:每一段(含节首段)都首行缩进 2 字符 */\n.aero-doc .paragraph > p { text-indent: 2em; }\n/* 缩进只给「正文段」:列表、表格、图注、卡片内的段落一律不缩进 */\n.aero-doc li p,\n.aero-doc dd p,\n.aero-doc td p,\n.aero-doc th p,\n.aero-doc .admonitionblock p,\n.aero-doc .exampleblock p,\n.aero-doc .sidebarblock p,\n.aero-doc .quoteblock p,\n.aero-doc .aero-caption p,\n.aero-doc .aero-doc-meta p,\n.aero-doc blockquote p,\n.aero-doc #toc p,\n.aero-doc #footnotes p { text-indent: 0; }\n.aero-doc--web .paragraph > p { text-indent: 0; }\n\n/* 导语 / 摘要:略大字号、弱色,作为章首的引子 */\n.aero-doc .paragraph.lead > p,\n.aero-doc .exampleblock.lead > .content > .paragraph > p,\n.aero-doc .exampleblock.lead p {\n font-size: 1.04em;\n color: var(--aero-ink-2);\n}\n.aero-doc .quoteblock.abstract {\n margin: 1.4em 0;\n padding: 0.9em 1.2em 0.9em 1.8em;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background: var(--aero-soft);\n color: var(--aero-ink-2);\n font-size: 0.96em;\n}\n.aero-doc .quoteblock.abstract blockquote { padding: 0; border: none; }\n\n/* ---------------------------------------------------------------------------\n * 4. 列表:无序/有序标记、描述列表、横向列表、checklist、callout 清单\n * ------------------------------------------------------------------------- */\n.aero-doc ul,\n.aero-doc ol { margin: 0.6em 0; padding-left: 1.9em; }\n.aero-doc li { margin: 0.22em 0; }\n.aero-doc li > p { margin: 0.2em 0; }\n.aero-doc ul.disc > li,\n.aero-doc ul:not([class]) > li { list-style-type: disc; }\n.aero-doc ul.circle > li { list-style-type: circle; }\n.aero-doc ul.square > li { list-style-type: square; }\n.aero-doc ul.none,\n.aero-doc ul.no-bullet { list-style: none; padding-left: 0.2em; }\n.aero-doc ol.arabic { list-style-type: decimal; }\n.aero-doc ol.loweralpha { list-style-type: lower-alpha; }\n.aero-doc ol.upperalpha { list-style-type: upper-alpha; }\n.aero-doc ol.lowerroman { list-style-type: lower-roman; }\n.aero-doc ol.upperroman { list-style-type: upper-roman; }\n.aero-doc .ulist .ulist,\n.aero-doc .olist .olist { margin: 0.2em 0; }\n\n.aero-doc .dlist > dl > dt,\n.aero-doc .dlist > dl > dd { margin: 0; }\n.aero-doc .dlist .hdlist1 {\n margin-top: 0.7em;\n font-weight: 600;\n color: var(--aero-ink);\n}\n.aero-doc .dlist > dl > dd { padding-left: 1.2em; color: var(--aero-ink-2); }\n.aero-doc .dlist > dl > dd::before { content: \"— \"; color: var(--aero-ink-3); }\n\n/* 横向描述列表([horizontal])在 Asciidoctor 里输出内层表格 */\n.aero-doc .hdlist > table { border: none; border-collapse: collapse; }\n.aero-doc .hdlist > table td { border: none; padding: 0.25em 0; vertical-align: top; }\n.aero-doc .hdlist td.hdlist1 {\n width: 9em;\n padding-right: 1em;\n font-weight: 600;\n white-space: nowrap;\n}\n.aero-doc .hdlist td.hdlist1::after { content: \"\"; }\n.aero-doc .hdlist td.hdlist2 { color: var(--aero-ink-2); }\n\n/* checklist([checklist] 语法,icons: font 输出 Font Awesome 方框) */\n.aero-doc .ulist.checklist ul { list-style: none; padding-left: 0.2em; }\n.aero-doc .ulist.checklist li { padding-left: 1.6em; text-indent: -1.6em; }\n.aero-doc .ulist.checklist .fa::before {\n display: inline-block;\n width: 1.25em;\n color: var(--aero-accent-ink);\n font-style: normal;\n text-align: left;\n}\n.aero-doc .ulist.checklist .fa-check-square-o::before,\n.aero-doc .ulist.checklist .fa-check-square::before { content: \"\\2611\\FE0E\"; }\n.aero-doc .ulist.checklist .fa-square-o::before { content: \"\\2610\\FE0E\"; }\n\n/* ---------------------------------------------------------------------------\n * 5. 表格:frame/grid/stripes 全组合 + 单元格对齐 + 题注;宽表横向滚动\n * ------------------------------------------------------------------------- */\n.aero-doc .aero-tablewrap {\n max-width: 100%;\n margin: 1.5em 0;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n.aero-doc table.tableblock {\n border-collapse: collapse;\n font-size: 0.94em;\n line-height: 1.65;\n font-family: inherit;\n}\n.aero-doc table.tableblock.stretch { width: 100%; }\n.aero-doc table.tableblock.fit-content { width: auto; }\n.aero-doc table.tableblock > caption,\n.aero-doc table.tableblock > .title {\n caption-side: top;\n padding: 0 0 0.55em;\n font-family: inherit;\n font-size: 0.95em;\n font-weight: 600;\n color: var(--aero-ink-2);\n text-align: center;\n}\n.aero-doc table.tableblock th,\n.aero-doc table.tableblock td {\n padding: 0.42em 0.7em;\n border: 1px solid transparent;\n text-align: left;\n vertical-align: top;\n}\n.aero-doc table.tableblock thead th {\n background: var(--aero-soft);\n font-weight: 600;\n color: var(--aero-ink);\n}\n.aero-doc table.tableblock tfoot td {\n background: var(--aero-soft);\n color: var(--aero-ink-2);\n font-size: 0.96em;\n}\n.aero-doc table.tableblock th > p,\n.aero-doc table.tableblock td > p { margin: 0; }\n\n/* frame-*:外框 */\n.aero-doc table.tableblock.frame-all,\n.aero-doc table.tableblock.frame-sides,\n.aero-doc table.tableblock.frame-none,\n.aero-doc table.tableblock.frame-topbot,\n.aero-doc table.tableblock.frame-ends { border: none; }\n.aero-doc table.tableblock.frame-all th,\n.aero-doc table.tableblock.frame-all td { border: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.frame-sides th:first-child,\n.aero-doc table.tableblock.frame-sides td:first-child { border-left: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.frame-sides th:last-child,\n.aero-doc table.tableblock.frame-sides td:last-child { border-right: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.frame-topbot thead th,\n.aero-doc table.tableblock.frame-ends thead th { border-top: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.frame-topbot tbody tr:last-child td,\n.aero-doc table.tableblock.frame-ends tbody tr:last-child td,\n.aero-doc table.tableblock.frame-topbot tfoot tr:last-child td,\n.aero-doc table.tableblock.frame-ends tfoot tr:last-child td { border-bottom: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.frame-none thead th { border-bottom: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.frame-none tfoot td { border-top: 1px solid var(--aero-rule); }\n\n/* grid-*:内部网格 */\n.aero-doc table.tableblock.grid-all th,\n.aero-doc table.tableblock.grid-all td { border-top: 1px solid var(--aero-rule); border-left: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-all th:last-child,\n.aero-doc table.tableblock.grid-all td:last-child { border-right: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-all tr:last-child td { border-bottom: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-rows th:not(:first-child),\n.aero-doc table.tableblock.grid-rows td:not(:first-child) { border-left: none; }\n.aero-doc table.tableblock.grid-rows th,\n.aero-doc table.tableblock.grid-rows td { border-top: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-cols th,\n.aero-doc table.tableblock.grid-cols td { border-left: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-cols th:first-child,\n.aero-doc table.tableblock.grid-cols td:first-child { border-left: none; }\n.aero-doc table.tableblock.grid-none th,\n.aero-doc table.tableblock.grid-none td { border-left: none; border-right: none; }\n.aero-doc table.tableblock.grid-none thead th { border-bottom: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.grid-none tfoot td { border-top: 1px solid var(--aero-rule); }\n\n/* 三线表(frame-topbot/grid-rows/grid-none 这类学术表)与网格表共用上面的规则;\n 以下补 header 分隔线,使任何 frame 下表头都有一条压线 */\n.aero-doc table.tableblock thead th { border-bottom: 1px solid var(--aero-rule-strong); }\n\n/* stripes-*:隔行底纹(作者显式要求时才出现) */\n.aero-doc table.tableblock.stripes-even tbody tr:nth-child(even),\n.aero-doc table.tableblock.stripes-all tbody tr:nth-child(even),\n.aero-doc table.tableblock.stripes-odd tbody tr:nth-child(odd) { background: var(--aero-soft); }\n.aero-doc table.tableblock.stripes-hover tbody tr:hover { background: var(--aero-hover); }\n\n/* 单元格对齐(halign-*/valign-*) */\n.aero-doc table.tableblock .halign-left { text-align: left; }\n.aero-doc table.tableblock .halign-center { text-align: center; }\n.aero-doc table.tableblock .halign-right { text-align: right; }\n.aero-doc table.tableblock .valign-top { vertical-align: top; }\n.aero-doc table.tableblock .valign-middle { vertical-align: middle; }\n.aero-doc table.tableblock .valign-bottom { vertical-align: bottom; }\n\n/* ---------------------------------------------------------------------------\n * 6. 图:题注(作者写在 image::[图注] 里 → alt 属性)升级为可见图注,\n * 分「图号 / 对象 / 条件→结论」三层;点击放大。\n * ------------------------------------------------------------------------- */\n.aero-doc .imageblock {\n margin: 1.5em 0;\n text-align: center;\n}\n.aero-doc .imageblock > .content {\n display: block;\n line-height: 0;\n}\n.aero-doc .imageblock img {\n max-width: 100%;\n height: auto;\n border-radius: 4px;\n}\n.aero-doc .imageblock.aero-zoomable img { cursor: zoom-in; }\n.aero-doc .imageblock.aero-zoomable img:hover { outline: 2px solid color-mix(in srgb, var(--aero-accent) 45%, transparent); outline-offset: 2px; }\n.aero-doc .imageblock.text-center,\n.aero-doc .imageblock.text-center > .content { text-align: center; }\n.aero-doc .imageblock.text-left { text-align: left; }\n.aero-doc .imageblock.text-right { text-align: right; }\n.aero-doc .imageblock.left,\n.aero-doc .imageblock.right {\n max-width: 48%;\n margin: 0.4em 1.2em 0.8em;\n}\n.aero-doc .imageblock.left { float: left; margin-left: 0; }\n.aero-doc .imageblock.right { float: right; margin-right: 0; }\n.aero-doc .imageblock.thumb img { border: 1px solid var(--aero-rule); }\n\n/* 图注:居中、小字号、弱色;「图 C05.9-1」加粗,「。——条件→结论」另起一行更弱 */\n.aero-doc .aero-caption,\n.aero-doc .imageblock > .title {\n margin: 0.6em auto 0;\n max-width: 40em;\n font-size: 0.88em;\n line-height: 1.7;\n color: var(--aero-ink-2);\n text-align: center;\n text-indent: 0;\n}\n.aero-doc .aero-caption .aero-cap-no {\n margin-right: 0.4em;\n font-weight: 600;\n color: var(--aero-ink);\n white-space: nowrap;\n}\n.aero-doc .aero-caption .aero-cap-tail {\n display: block;\n margin-top: 0.1em;\n font-size: 0.96em;\n color: var(--aero-ink-3);\n}\n.aero-doc img.aero-inline-img { vertical-align: middle; }\n\n/* ---------------------------------------------------------------------------\n * 7. admonition:五种提示块。内层 <table> 必须去表格化(旧样式会把它变成带框表格)\n * ------------------------------------------------------------------------- */\n.aero-doc .admonitionblock {\n --aero-adm: var(--aero-accent);\n margin: 1.4em 0;\n padding: 0;\n border: 1px solid color-mix(in srgb, var(--aero-adm) 26%, transparent);\n border-left: 4px solid var(--aero-adm);\n border-radius: 10px;\n background: color-mix(in srgb, var(--aero-adm) 6%, transparent);\n}\n.aero-doc .admonitionblock.note { --aero-adm: var(--dsw-alias-state-business-primary, #2f6feb); }\n.aero-doc .admonitionblock.tip { --aero-adm: var(--dsw-alias-state-success-primary, #2f9e44); }\n.aero-doc .admonitionblock.important { --aero-adm: var(--dsw-alias-state-warn-label, #e8590c); }\n.aero-doc .admonitionblock.warning { --aero-adm: var(--dsw-alias-state-warn-primary, #f08c00); }\n.aero-doc .admonitionblock.caution { --aero-adm: var(--dsw-alias-state-error-primary, #e03131); }\n\n.aero-doc .admonitionblock > table {\n display: block;\n width: auto;\n margin: 0;\n border: none;\n border-collapse: separate;\n}\n.aero-doc .admonitionblock > table > tbody { display: block; }\n.aero-doc .admonitionblock > table > tbody > tr {\n display: flex;\n align-items: stretch;\n gap: 0;\n}\n.aero-doc .admonitionblock > table td {\n display: block;\n border: none;\n background: none;\n padding: 0.85em 1em 0.9em 0;\n vertical-align: top;\n}\n.aero-doc .admonitionblock > table td.icon {\n flex: none;\n width: 2.9em;\n padding: 0.95em 0 0 0.95em;\n color: var(--aero-adm);\n text-align: center;\n}\n.aero-doc .admonitionblock > table td.content {\n flex: 1 1 auto;\n min-width: 0;\n padding-right: 1.1em;\n color: var(--aero-ink);\n}\n.aero-doc .admonitionblock > table td.content > :first-child { margin-top: 0; }\n.aero-doc .admonitionblock > table td.content > :last-child { margin-bottom: 0; }\n/* 图标:DSH 主题没有 Font Awesome,用内联 SVG 蒙版自绘(随文字色,明暗自适应) */\n.aero-doc .admonitionblock td.icon .fa {\n display: inline-block;\n width: 1.35em;\n height: 1.35em;\n background-color: currentColor;\n -webkit-mask: var(--aero-adm-icon) center / contain no-repeat;\n mask: var(--aero-adm-icon) center / contain no-repeat;\n}\n.aero-doc .admonitionblock.note td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M8 7.3v4.2'/%3E%3Cpath d='M8 4.7h.01'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.tip td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 1.9a4.1 4.1 0 0 0-2.4 7.4c.4.3.6.7.6 1.2v.4h3.6v-.4c0-.5.2-.9.6-1.2A4.1 4.1 0 0 0 8 1.9z'/%3E%3Cpath d='M6.5 13.1h3'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.important td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M8 4.6v4.6'/%3E%3Cpath d='M8 11.6h.01'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.warning td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2.2 14.5 13.5H1.5z'/%3E%3Cpath d='M8 6.4v3.3'/%3E%3Cpath d='M8 11.8h.01'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.caution td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M4.4 11.6 11.6 4.4'/%3E%3C/svg%3E\");\n}\n/* 提示块标题:作者用 `.历史注记` 换行 `[NOTE]` 的写法,标题在 content 里 */\n.aero-doc .admonitionblock td.content > .title {\n margin: 0 0 0.3em;\n padding: 0;\n font-size: 0.95em;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--aero-adm);\n text-align: left;\n}\n.aero-doc .admonitionblock td.content > .title::before {\n content: \"\";\n display: inline-block;\n width: 0.2em;\n height: 0.85em;\n margin-right: 0.5em;\n border-radius: 2px;\n background: currentColor;\n vertical-align: -0.05em;\n}\n\n/* ---------------------------------------------------------------------------\n * 8. example / sidebar / quote / verse / open:AsciiDoc 的「书本卡片」\n * ------------------------------------------------------------------------- */\n/* 例题 / 示例:带边框的卡片,题注做成左上角标签 */\n.aero-doc .exampleblock {\n margin: 1.5em 0;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background: var(--aero-paper);\n overflow: hidden;\n}\n.aero-doc .exampleblock > .title {\n margin: 0;\n padding: 0.45em 1em;\n border-bottom: 1px solid var(--aero-rule);\n background: var(--aero-soft);\n font-size: 0.92em;\n font-weight: 600;\n color: var(--aero-ink-2);\n text-align: left;\n}\n.aero-doc .exampleblock > .content { padding: 0.85em 1.1em 0.95em; }\n.aero-doc .exampleblock > .content > :first-child { margin-top: 0; }\n.aero-doc .exampleblock > .content > :last-child { margin-bottom: 0; }\n\n/* 侧栏([sidebar]):书籍里最常见的「插叙」块 */\n.aero-doc .sidebarblock {\n margin: 1.6em 0;\n padding: 0.95em 1.15em 1em;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background:\n linear-gradient(180deg, color-mix(in srgb, var(--aero-accent) 5%, transparent), transparent 60%),\n var(--aero-soft);\n font-size: 0.96em;\n}\n.aero-doc .sidebarblock > .content > .title {\n margin: 0 0 0.5em;\n padding-bottom: 0.35em;\n border-bottom: 1px dashed var(--aero-rule-strong);\n font-size: 0.95em;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--aero-accent-ink);\n text-align: left;\n}\n.aero-doc .sidebarblock > .content > :first-child { margin-top: 0; }\n.aero-doc .sidebarblock > .content > :last-child { margin-bottom: 0; }\n\n/* 引文([quote]):大引号 + 出处右对齐 */\n.aero-doc .quoteblock {\n margin: 1.7em 0;\n padding: 0.2em 0 0.2em 2em;\n border-left: 3px solid color-mix(in srgb, var(--aero-accent) 45%, transparent);\n color: var(--aero-ink-2);\n}\n.aero-doc .quoteblock blockquote {\n position: relative;\n margin: 0;\n padding: 0;\n border: none;\n quotes: \"\\201C\" \"\\201D\" \"\\2018\" \"\\2019\";\n}\n/* 装饰引号用绝对定位压在左侧留白里。\n 注意:伪元素的 em 一律按**伪元素自身**的 font-size 解析,所以不能靠\n font-size:2.3em + left:-1.05em 来定位(那会甩到块外一个引号的距离);\n 这里保持 1em 排版尺寸,用 transform 放大,定位才与父级字号一致。 */\n.aero-doc .quoteblock blockquote::before {\n content: \"\\201C\";\n position: absolute;\n top: -0.16em;\n left: -1.05em;\n font-family: var(--aero-serif);\n font-size: 1em;\n line-height: 1;\n transform: scale(2.3);\n transform-origin: left top;\n color: color-mix(in srgb, var(--aero-accent) 55%, transparent);\n pointer-events: none;\n}\n.aero-doc .quoteblock .attribution,\n.aero-doc .verseblock .attribution {\n margin-top: 0.5em;\n font-size: 0.9em;\n font-style: normal;\n color: var(--aero-ink-3);\n text-align: right;\n}\n.aero-doc .quoteblock .attribution br,\n.aero-doc .verseblock .attribution br { display: none; }\n.aero-doc .quoteblock .attribution::before,\n.aero-doc .verseblock .attribution::before { content: \"\\2014\\00A0\"; }\n.aero-doc .quoteblock .attribution cite,\n.aero-doc .verseblock .attribution cite { font-style: italic; }\n.aero-doc .quoteblock .attribution cite::before { content: \"\\300A\"; font-style: normal; }\n.aero-doc .quoteblock .attribution cite::after { content: \"\\300B\"; font-style: normal; }\n\n/* 题诗([verse]):居中、保留换行 */\n.aero-doc .verseblock {\n margin: 1.8em 0;\n padding: 0.6em 1em;\n font-size: 1em;\n}\n.aero-doc .verseblock pre {\n margin: 0;\n padding: 0;\n border: none;\n background: none;\n font-family: var(--aero-serif);\n font-size: 1em;\n line-height: 2;\n text-align: center;\n white-space: pre-wrap;\n color: var(--aero-ink-2);\n}\n\n/* 开放块([open])只做留白,不加装饰 */\n.aero-doc .openblock { margin: 1.2em 0; }\n.aero-doc .openblock > .title,\n.aero-doc .literalblock > .title,\n.aero-doc .listingblock > .title {\n margin: 0 0 0.45em;\n font-size: 0.9em;\n font-weight: 600;\n color: var(--aero-ink-2);\n text-align: left;\n}\n\n/* ---------------------------------------------------------------------------\n * 9. 代码:listing(含语言角标、callout 编号)/ literal / colist 清单\n * ------------------------------------------------------------------------- */\n.aero-doc .listingblock,\n.aero-doc .literalblock { margin: 1.3em 0; }\n.aero-doc .listingblock > .content { position: relative; }\n.aero-doc .listingblock pre,\n.aero-doc .literalblock pre {\n margin: 0;\n padding: 0.85em 1em;\n border: 1px solid var(--aero-rule);\n border-radius: 8px;\n background: var(--dsw-alias-markdown-code-block, var(--aero-soft));\n overflow-x: auto;\n font-family: var(--aero-mono);\n font-size: 0.86em;\n line-height: 1.7;\n text-align: left;\n text-indent: 0;\n white-space: pre;\n tab-size: 4;\n}\n.aero-doc .listingblock code,\n.aero-doc .literalblock code { font-family: inherit; }\n.aero-doc .listingblock code[data-lang]::before {\n content: attr(data-lang);\n position: absolute;\n top: 0.45em;\n right: 0.7em;\n font-family: var(--aero-mono);\n font-size: 0.72em;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--aero-ink-3);\n pointer-events: none;\n}\n/* callout 编号(<i class=\"conum\" data-value=\"1\">) */\n.aero-doc .conum[data-value] {\n display: inline-block;\n width: 1.35em;\n height: 1.35em;\n margin: 0 0.15em;\n border-radius: 50%;\n background: var(--aero-accent-ink);\n color: var(--aero-paper);\n font-family: var(--aero-sans);\n font-size: 0.72em;\n font-weight: 600;\n font-style: normal;\n line-height: 1.35em;\n text-align: center;\n vertical-align: 0.15em;\n}\n.aero-doc .conum[data-value]::after { content: attr(data-value); }\n.aero-doc .conum[data-value] + b { display: none; }\n.aero-doc .colist { margin: 0.7em 0 1.3em; font-size: 0.92em; }\n.aero-doc .colist > table { border: none; border-collapse: collapse; }\n.aero-doc .colist > table td { border: none; padding: 0.15em 0.5em 0.15em 0; vertical-align: top; }\n.aero-doc .colist > table td:first-child { width: 1.8em; }\n\n/* ---------------------------------------------------------------------------\n * 10. 行内:代码、链接、强调、上下标、role(text-center/line-through/mark…)\n * ------------------------------------------------------------------------- */\n.aero-doc code,\n.aero-doc kbd,\n.aero-doc samp {\n font-family: var(--aero-mono);\n font-size: 0.88em;\n}\n.aero-doc :not(pre) > code {\n padding: 0.12em 0.38em;\n border: 1px solid var(--aero-rule);\n border-radius: 4px;\n background: var(--dsw-alias-markdown-inline-code, var(--aero-soft));\n overflow-wrap: break-word;\n}\n.aero-doc a,\n.aero-doc a:visited {\n color: var(--aero-accent);\n text-decoration: none;\n border-bottom: 1px solid color-mix(in srgb, var(--aero-accent) 35%, transparent);\n}\n.aero-doc a:hover { border-bottom-color: currentColor; }\n.aero-doc a[data-kbdoc] { cursor: pointer; }\n.aero-doc strong,\n.aero-doc b { font-weight: 600; color: var(--aero-ink); }\n.aero-doc em,\n.aero-doc i { font-style: italic; }\n.aero-doc sup,\n.aero-doc sub { font-size: 0.72em; line-height: 0; }\n.aero-doc .mark {\n padding: 0 0.15em;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0b429) 30%, transparent);\n color: inherit;\n}\n.aero-doc .line-through { text-decoration: line-through; }\n.aero-doc .underline { text-decoration: underline; }\n.aero-doc .overline { text-decoration: overline; }\n.aero-doc .big { font-size: 1.15em; }\n.aero-doc .small { font-size: 0.86em; }\n.aero-doc .text-center { text-align: center; }\n.aero-doc .text-right { text-align: right; }\n.aero-doc .text-left { text-align: left; }\n.aero-doc .text-justify { text-align: justify; }\n.aero-doc .nowrap { white-space: nowrap; }\n.aero-doc .hide { display: none; }\n.aero-doc .menuseq,\n.aero-doc .keyseq { white-space: nowrap; }\n.aero-doc .menuseq b.caret { font-weight: 400; color: var(--aero-ink-3); }\n/* footnote:正文里的上标编号 + 文末脚注区 */\n.aero-doc .footnote { font-size: 0.82em; vertical-align: super; line-height: 0; }\n.aero-doc .footnote a { border-bottom: none; }\n.aero-doc #footnotes {\n margin-top: 2.4em;\n padding-top: 0.9em;\n border-top: 1px solid var(--aero-rule);\n font-size: 0.88em;\n color: var(--aero-ink-2);\n}\n.aero-doc #footnotes .footnote { font-size: 1em; vertical-align: baseline; }\n.aero-doc #footnotes hr { display: none; }\n\n/* ---------------------------------------------------------------------------\n * 11. 目录(:toc: + toc::[]):书本目录的引导点与层级缩进\n * ------------------------------------------------------------------------- */\n.aero-doc .toc,\n.aero-doc #toc {\n margin: 1.6em 0 2.2em;\n padding: 1em 1.2em;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background: var(--aero-soft);\n font-size: 0.94em;\n}\n.aero-doc #toc > .title,\n.aero-doc .toc > .title {\n margin: 0 0 0.6em;\n font-size: 1em;\n font-weight: 600;\n letter-spacing: 0.08em;\n color: var(--aero-ink-2);\n text-align: center;\n}\n.aero-doc .toc ul { margin: 0; padding: 0; list-style: none; }\n.aero-doc .toc ul ul { padding-left: 1.2em; }\n.aero-doc .toc li { margin: 0.2em 0; }\n.aero-doc .toc a { border-bottom: none; color: var(--aero-ink-2); }\n.aero-doc .toc a:hover { color: var(--aero-accent); }\n\n/* ---------------------------------------------------------------------------\n * 12. 知识库固定小节(本节小结 / 本节来源 / 练习指引):给「书末固定栏目」做卡片\n * 由 DocViewer 的 DOM 增强给 .sect1 打上 aero-sect-* 类\n * ------------------------------------------------------------------------- */\n.aero-doc .sect1.aero-sect-summary > .sectionbody {\n padding: 0.9em 1.15em 1em;\n border: 1px solid var(--aero-rule);\n border-left: 4px solid var(--aero-accent);\n border-radius: 10px;\n background: var(--aero-soft);\n}\n.aero-doc .sect1.aero-sect-source > .sectionbody { font-size: 0.92em; color: var(--aero-ink-2); }\n.aero-doc .sect1.aero-sect-practice > .sectionbody {\n padding: 0.9em 1.15em 1em;\n border: 1px dashed var(--aero-rule-strong);\n border-radius: 10px;\n}\n.aero-doc .sect1.aero-sect-source .tableblock { font-size: 0.95em; }\n\n/* ---------------------------------------------------------------------------\n * 13. 其它块级与主题细节\n * ------------------------------------------------------------------------- */\n.aero-doc hr {\n margin: 1.8em auto;\n border: none;\n border-top: 1px solid var(--aero-rule);\n width: 60%;\n}\n.aero-doc .aero-math-block {\n margin: 1.2em 0;\n overflow-x: auto;\n overflow-y: hidden;\n text-align: center;\n}\n.aero-doc .aero-math-block .katex-display { margin: 0.2em 0; }\n.aero-doc .aero-math-inline { white-space: nowrap; }\n.aero-doc .aero-math-inline .katex { font-size: 1.03em; }\n.aero-doc .sect1 + .sect1 { margin-top: 0.6em; }\n.aero-doc .sect2 + .sect2 { margin-top: 0.4em; }\n.aero-doc .videoblock,\n.aero-doc .audioblock { margin: 1.4em 0; text-align: center; }\n\n/* ---------------------------------------------------------------------------\n * 14. Markdown 文档(教材《空气动力学基础》章、英文参考书、2025 教案)通用回退\n *\n * 同一个 .aero-doc 也承载 markdown-it 的输出,而它的结构与 AsciiDoc 不同:\n * 裸 <p>/<img>/<table>/<pre>/<blockquote> 直接挂在 .aero-doc-body 下(没有\n * .paragraph/.imageblock/.tableblock 包装)。这里只用**直接子元素**选择器补回退,\n * 以免误伤 AsciiDoc 的结构性内层表格(admonition / colist / hdlist 的内层\n * <table> 都没有 .tableblock 类,用 `table:not(.tableblock)` 会连它们一起套上边框,\n * 把提示块又变回带框表格)。\n * ------------------------------------------------------------------------- */\n/* 段落缩进:markdown 的顶层段落没有 .paragraph 包装,按版式单独给缩进 */\n.aero-doc--book .aero-doc-body > p { text-indent: 2em; }\n.aero-doc--web .aero-doc-body > p { text-indent: 0; }\n.aero-doc img {\n max-width: 100%;\n height: auto;\n border-radius: 4px;\n}\n/* 整段只有图(或「图 + 图注同段」)→ 当作插图块:居中 + 可放大 + 图注分层 */\n.aero-doc .aero-md-figure {\n margin: 1.4em 0;\n text-align: center;\n text-indent: 0;\n}\n.aero-doc .aero-md-figure img { cursor: zoom-in; }\n.aero-doc .aero-md-figure img:hover {\n outline: 2px solid color-mix(in srgb, var(--aero-accent) 45%, transparent);\n outline-offset: 2px;\n}\n.aero-doc-body > pre {\n margin: 1.2em 0;\n padding: 0.85em 1em;\n border: 1px solid var(--aero-rule);\n border-radius: 8px;\n background: var(--dsw-alias-markdown-code-block, var(--aero-soft));\n overflow-x: auto;\n font-family: var(--aero-mono);\n font-size: 0.86em;\n line-height: 1.7;\n text-align: left;\n text-indent: 0;\n tab-size: 4;\n}\n.aero-doc-body > pre code { font-family: inherit; background: none; border: none; padding: 0; }\n.aero-doc-body > blockquote {\n margin: 1.2em 0;\n padding: 0.1em 0 0.1em 1em;\n border-left: 3px solid color-mix(in srgb, var(--aero-accent) 40%, transparent);\n color: var(--aero-ink-2);\n}\n.aero-doc-body > blockquote > p { text-indent: 0; }\n/* markdown 表格(无 .tableblock 类):等价于 AsciiDoc 的 frame-all grid-all */\n.aero-doc-body > table {\n border-collapse: collapse;\n margin: 1.4em 0;\n font-size: 0.94em;\n line-height: 1.65;\n}\n.aero-doc-body > table th,\n.aero-doc-body > table td {\n padding: 0.4em 0.65em;\n border: 1px solid var(--aero-rule);\n text-align: left;\n vertical-align: top;\n}\n.aero-doc-body > table th {\n background: var(--aero-soft);\n font-weight: 600;\n}\n.aero-doc-body > table code { white-space: nowrap; }\n\n/* ---------------------------------------------------------------------------\n * 16. 用户阅读偏好(由「阅读设置」面板写入 data-* 属性,取值定义见 readerPrefs.ts)\n *\n * 选择器一律带属性,特异性高于 .aero-doc / .aero-doc--web 与各构造规则,\n * 因此不依赖书写顺序。取值为 auto / theme / 100 / normal 时不输出规则,\n * 保持该版式的原生默认(例如「标准」行距 = 书籍 1.9、网页 1.8)。\n * ------------------------------------------------------------------------- */\n\n/* 16.1 字号倍率:与 DSH 全局字号相乘而不是覆盖,用户在 DSH 设置里改字号仍生效。\n 倍率由 DocViewer 以 --aero-scale 内联写入(FONT_STEPS 的取值,见 readerPrefs.ts),\n §0 的 --aero-base 直接乘它——**不在这里逐档枚举规则**:早先的写法只覆盖\n data-scale 90/110/125/140/160 五档,档位表一旦加档(如 180/200/250/300)\n 新档就没有样式,表现为「按 A+ 显示 180% 但字号纹丝不动」。 */\n\n/* 16.2 字体族:两族一起改,书籍版式与网页版式都听用户的 */\n.aero-doc[data-font='serif'] {\n --aero-serif: Georgia, \"Nimbus Roman\", \"Times New Roman\", \"Noto Serif SC\", \"Source Han Serif SC\",\n \"Source Han Serif CN\", \"Songti SC\", STSong, SimSun, serif;\n --aero-sans: Georgia, \"Nimbus Roman\", \"Times New Roman\", \"Noto Serif SC\", \"Source Han Serif SC\",\n \"Source Han Serif CN\", \"Songti SC\", STSong, SimSun, serif;\n}\n.aero-doc[data-font='sans'] {\n --aero-serif: -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans SC\", sans-serif;\n --aero-sans: -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans SC\", sans-serif;\n}\n.aero-doc[data-font='kai'] {\n --aero-serif: KaiTi, STKaiti, \"Kaiti SC\", \"KaiTi_GB2312\", \"Noto Serif SC\", \"Songti SC\", serif;\n --aero-sans: KaiTi, STKaiti, \"Kaiti SC\", \"KaiTi_GB2312\", \"Noto Serif SC\", \"Songti SC\", serif;\n}\n\n/* 16.3 行距 */\n.aero-doc[data-leading='tight'] { line-height: 1.65; }\n.aero-doc[data-leading='loose'] { line-height: 2.25; }\n\n/* 16.4 版心:auto 不写规则 → 书籍 42em / 网页 56em 的原生默认 */\n.aero-doc[data-measure='narrow'] { --aero-measure: 34em; }\n.aero-doc[data-measure='wide'] { --aero-measure: 64em; }\n.aero-doc[data-measure='full'] { --aero-measure: none; }\n\n/* 16.5 底色:纸面与墨色成套写到外层 wrapper 的 -ov 变量上,正文卡片与右侧目录共用;\n wrapper 自身也铺同色底,于是正文两侧留白与页脚一起进入纸面,\n 而不是\"白底上浮一张色卡\"。注意这几段也定义了右侧目录(.docOutline)要用的墨色。 */\n[data-aero-doc-wrap][data-paper='white'] {\n --aero-paper-ov: #ffffff;\n --aero-ink-ov: #17181a;\n --aero-ink-2-ov: #4a4f57;\n --aero-ink-3-ov: #7b828c;\n --aero-rule-ov: rgba(20, 25, 35, 0.12);\n --aero-rule-strong-ov: rgba(20, 25, 35, 0.24);\n --aero-soft-ov: rgba(20, 25, 35, 0.04);\n --aero-hover-ov: rgba(20, 25, 35, 0.06);\n --aero-shadow-ov: 0 1px 2px rgba(15, 25, 50, 0.05), 0 18px 40px -32px rgba(15, 25, 50, 0.4);\n background: #ffffff;\n}\n[data-aero-doc-wrap][data-paper='sepia'] {\n --aero-paper-ov: #f7efdd;\n --aero-ink-ov: #3a3126;\n --aero-ink-2-ov: #6a5c46;\n --aero-ink-3-ov: #8d7f68;\n --aero-rule-ov: rgba(96, 76, 48, 0.2);\n --aero-rule-strong-ov: rgba(96, 76, 48, 0.34);\n --aero-soft-ov: rgba(120, 95, 55, 0.07);\n --aero-hover-ov: rgba(120, 95, 55, 0.11);\n --aero-shadow-ov: 0 1px 2px rgba(90, 70, 40, 0.06), 0 18px 40px -32px rgba(90, 70, 40, 0.4);\n background: #f7efdd;\n}\n[data-aero-doc-wrap][data-paper='green'] {\n --aero-paper-ov: #e7efe2;\n --aero-ink-ov: #22301d;\n --aero-ink-2-ov: #4e5c46;\n --aero-ink-3-ov: #77866e;\n --aero-rule-ov: rgba(60, 90, 45, 0.2);\n --aero-rule-strong-ov: rgba(60, 90, 45, 0.34);\n --aero-soft-ov: rgba(70, 110, 60, 0.07);\n --aero-hover-ov: rgba(70, 110, 60, 0.12);\n --aero-shadow-ov: 0 1px 2px rgba(50, 80, 40, 0.06), 0 18px 40px -32px rgba(50, 80, 40, 0.4);\n background: #e7efe2;\n}\n[data-aero-doc-wrap][data-paper='dark'] {\n --aero-paper-ov: #191c22;\n --aero-ink-ov: #dfe3e8;\n --aero-ink-2-ov: #a8b0bb;\n --aero-ink-3-ov: #7c8593;\n --aero-rule-ov: rgba(255, 255, 255, 0.14);\n --aero-rule-strong-ov: rgba(255, 255, 255, 0.26);\n --aero-soft-ov: rgba(255, 255, 255, 0.055);\n --aero-hover-ov: rgba(255, 255, 255, 0.09);\n --aero-accent-ov: #7aa2f7;\n --aero-accent-ink-ov: #9ec1ff;\n --aero-shadow-ov: 0 1px 2px rgba(0, 0, 0, 0.45), 0 18px 40px -32px rgba(0, 0, 0, 0.95);\n background: #191c22;\n}\n\n/* 网页版式默认不要纸面卡片;一旦选了非「跟随主题」的底色,补回正文内边距,\n 否则文字会贴着纸面边缘。 */\n[data-aero-doc-wrap]:not([data-paper='theme']) .aero-doc--web {\n padding: clamp(14px, 2.4vw, 28px) clamp(14px, 3vw, 34px) clamp(28px, 4vw, 48px);\n}\n\n/* ---------------------------------------------------------------------------\n * 17. 打印:A4 书页(Ctrl+P 直接当讲义印)\n * ------------------------------------------------------------------------- */\n@media print {\n .aero-doc {\n max-width: none;\n padding: 0;\n border: none;\n box-shadow: none;\n font-size: 10.5pt;\n line-height: 1.7;\n }\n .aero-doc h1 { font-size: 18pt; }\n .aero-doc h2 { font-size: 14pt; }\n .aero-doc .admonitionblock,\n .aero-doc .exampleblock,\n .aero-doc .sidebarblock,\n .aero-doc table.tableblock,\n .aero-doc .imageblock { break-inside: avoid; }\n .aero-doc img { max-height: 8.5cm; }\n}\n\n/* ---------------------------------------------------------------------------\n * 18. 阅读标注:荧光笔 / 重点线(客户端写入 <mark class=\"aero-anno\">)\n *\n * 学生要求「像微信读书那样用不同荧光色标重点、画重点线」。三条做法上的决定:\n * ① **荧光笔用「下半截染色带」而不是整块底色**:真荧光笔是划过字行下半部的,\n * 整块底色会把字形压进色块里,深色纸面(夜间)上尤其糊;染色带只覆盖行高的\n * 中下部,字形上缘仍落在纸面上,明暗两套纸色都读得清。\n * ② 颜色 `--aero-mark-*` 一律**写在 mark 自己身上**(§18.1/§18.2):自定义属性里的\n * var() 是在**声明它的元素**上解析的,若把染色带写在 `.aero-doc` 上用\n * `var(--aero-mark)`,解析时 mark 上的颜色还不存在 → 整个属性变成无效值,\n * 表现就是「DOM 里有 mark、屏幕上没有颜色」(本轮实测踩过这个坑)。\n * ③ 重点线(underline)不用 text-decoration:跨行时它会连到行尾空白,而我们按文本\n * 节点分片绘制,用 inset box-shadow 才是逐片的短线。\n *\n * 与交互的关系:mark 上的点击由 DocViewer 接管(打开编辑浮层),因此这里只负责\n * 「看起来像荧光笔」,并给 hover/定位闪烁留出视觉反馈。\n * ------------------------------------------------------------------------- */\n\n/* 18.1 四种笔芯:wash = 染色带(半透明),line = 重点线(实色、比 wash 深一档) */\nmark.aero-anno[data-color='yellow'] { --aero-mark-wash: #ffe066a8; --aero-mark-line: #e8b400; }\nmark.aero-anno[data-color='green'] { --aero-mark-wash: #a6e3a1a0; --aero-mark-line: #3f9e4d; }\nmark.aero-anno[data-color='blue'] { --aero-mark-wash: #93c9f7a0; --aero-mark-line: #2b7cd3; }\nmark.aero-anno[data-color='pink'] { --aero-mark-wash: #f7a8c4a8; --aero-mark-line: #d94f8a; }\n\nmark.aero-anno {\n /* 染色带:行高 42%→92% 之间染色,上下留白(正文字形一般落在 10%→85%) */\n background: transparent;\n background-image: linear-gradient(\n to bottom,\n transparent 0%,\n transparent 42%,\n var(--aero-mark-wash) 42%,\n var(--aero-mark-wash) 92%,\n transparent 92%\n );\n color: inherit;\n padding: 0;\n border-radius: 2px;\n cursor: pointer;\n /* 跨行断开的每一片各自染色,不连成一条横贯整行的色带 */\n -webkit-box-decoration-break: clone;\n box-decoration-break: clone;\n transition: filter 0.12s ease;\n}\n\n/* 重点线:逐片的粗下划线(跨行时每片各有一道,视觉上连成一条) */\nmark.aero-anno[data-style='underline'] {\n background-image: none;\n box-shadow: inset 0 -0.16em 0 var(--aero-mark-line);\n}\n\nmark.aero-anno:hover {\n filter: saturate(1.25) contrast(1.05);\n}\n\n/* 面板「定位」的闪烁:外描边,1.6 s 后由脚本摘掉 */\nmark.aero-anno[data-flash='1'] {\n outline: 2px solid color-mix(in srgb, var(--aero-accent-ink, #2f6feb) 80%, transparent);\n outline-offset: 1px;\n border-radius: 3px;\n}\n\n/* 18.2 夜间纸面:换成低亮度笔芯(浅色压在浅字上会糊成一片) */\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='yellow'] { --aero-mark-wash: #b58f00b8; --aero-mark-line: #e8c34a; }\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='green'] { --aero-mark-wash: #3f7a3aa8; --aero-mark-line: #7fd18b; }\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='blue'] { --aero-mark-wash: #2f5f8ab8; --aero-mark-line: #7ab6ee; }\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='pink'] { --aero-mark-wash: #8a3f5fb8; --aero-mark-line: #f095b8; }\n\n/* 打印:标记要跟着正文印出来(讲义上就能看见自己划的重点),但定位闪烁不要。 */\n@media print {\n mark.aero-anno {\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n mark.aero-anno[data-flash='1'] {\n outline: none;\n }\n}\n";
|
|
9
|
+
// \0aero-inline-css:L21udC9kYXRhL2Flcm8vZHNoLWFlcm8vYWVyby1zdHVkeS1raXQvc3JjL2NsaWVudC9kb2MtdGhlbWUuY3Nz
|
|
10
|
+
const css$2 = "/* ===========================================================================\n * 学习资料阅读器 · AsciiDoc 书籍版式主题(全局注入,仅作用于 .aero-doc)\n *\n * 背景:知识库(knowledge-base/正文)与教案(lesson-plan)正文都是 AsciiDoc,\n * 由宿主用 @asciidoctor/core 渲染成 HTML。AsciiDoc 的默认样式表\n * (asciidoctor-default.css)为大量「书本级」构造提供了配套样式——admonition\n * 提示块、example/sidebar 卡片、quote/verse 题引、图表题注、三线表/网格表、\n * callout 编号、描述列表、footnote、TOC 侧栏、内建 role(text-center 等)。\n * 本项目此前只给 .aero-doc 写了极简样式,于是这些构造全部退化成裸 HTML:\n * · admonition 的内层 <table> 被通用表格规则套上边框,图标列是空的\n * (:icons: font 输出 <i class=\"fa icon-note\">,但没有 Font Awesome);\n * · image::路径[图注] 的「图注五要素」落在 alt 属性里,页面上根本看不见;\n * · 「Figure 1.」这类自动题注号与作者手写的「图 C05.9-1」重复(中英混排)。\n * 本文件补齐这套版式,并叠加中文书籍的排版习惯:版心宽度、两端对齐、\n * 首行缩进 2 字符、章节页式标题、居中图注、三线表。\n *\n * 约束与依赖:\n * · 颜色一律走 DSH 主题 token(--dsw-alias-*),明暗两套自动跟随;\n * 见 dsh-client-ui-theme(深色由 body[data-ds-dark-theme] 切换)。\n * · 字号基准用 --dsh-content-font-size(用户在设置里调的字号)。\n * · 本文件以 `?inline` 由 doc-theme.ts 作为纯文本导入,不过 lightningcss,\n * 因此可以放心使用 mask-image / color-mix / :has 等现代特性。\n * =========================================================================== */\n\n/* ---------------------------------------------------------------------------\n * 0. 局部变量(含无 token 时的兜底,保证脱离 DSH 外壳(如离线预览)也成篇)\n * ------------------------------------------------------------------------- */\n.aero-doc {\n /* 每个 token 先看外层 `-ov` 覆盖(阅读设置的「底色」由 [data-aero-doc-wrap][data-paper]\n 注入,见本文件 §15),没有覆盖才回落到 DSH 主题 token,最后是内置兜底。\n 这样正文卡片与右侧「本页目录」能共用同一套墨色,深色纸面上不会出现黑字压暗底。 */\n --aero-ink: var(--aero-ink-ov, var(--dsw-alias-label-primary, #1f2328));\n --aero-ink-2: var(--aero-ink-2-ov, var(--dsw-alias-label-secondary, #57606a));\n --aero-ink-3: var(--aero-ink-3-ov, var(--dsw-alias-label-tertiary, #818b98));\n --aero-rule: var(--aero-rule-ov, var(--dsw-alias-border-l2, rgba(31, 35, 40, 0.12)));\n --aero-rule-strong: var(--aero-rule-strong-ov, var(--dsw-alias-border-l3, rgba(31, 35, 40, 0.22)));\n --aero-paper: var(--aero-paper-ov, var(--dsw-alias-bg-layer-1, #ffffff));\n --aero-soft: var(--aero-soft-ov, var(--dsw-alias-bg-module-platform, rgba(31, 35, 40, 0.035)));\n --aero-hover: var(--aero-hover-ov, var(--dsw-alias-interactive-bg-hover, rgba(31, 35, 40, 0.05)));\n --aero-accent: var(--aero-accent-ov, var(--dsw-alias-link, #4176e6));\n --aero-accent-ink: var(--aero-accent-ink-ov, var(--dsw-alias-state-business-primary, #2f6feb));\n --aero-mono: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);\n --aero-sans: var(--dsw-font-family, -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);\n --aero-serif: Georgia, \"Nimbus Roman\", \"Times New Roman\", \"Noto Serif SC\", \"Noto Serif CJK SC\",\n \"Source Han Serif SC\", \"Source Han Serif CN\", \"Songti SC\", STSong, SimSun, serif;\n /* 字号基准 = DSH 全局字号 × 阅读设置的字号倍率(--aero-scale 由 DocViewer 内联写入,\n 倍率表见 readerPrefs.ts 的 FONT_STEPS;缺省 1 即 100%)。 */\n --aero-base: calc(var(--dsh-content-font-size, 15px) * var(--aero-scale, 1));\n --aero-measure: 42em;\n --aero-shadow: var(--aero-shadow-ov, 0 1px 2px rgba(15, 25, 50, 0.04), 0 18px 40px -32px rgba(15, 25, 50, 0.45));\n}\n\n/* ---------------------------------------------------------------------------\n * 1. 版心:居中限宽的书页。窄屏退化为整幅,宽屏不出现超长行(阅读体验第一)。\n * ------------------------------------------------------------------------- */\n.aero-doc {\n box-sizing: border-box;\n max-width: var(--aero-measure);\n margin: 0 auto;\n padding: clamp(18px, 3.2vw, 34px) clamp(16px, 3.6vw, 44px) clamp(36px, 5vw, 60px);\n border: 1px solid var(--aero-rule);\n border-radius: 14px;\n background: var(--aero-paper);\n box-shadow: var(--aero-shadow);\n color: var(--aero-ink);\n font-family: var(--aero-serif);\n font-size: calc(var(--aero-base) * 1.07);\n line-height: 1.9;\n /* 中文书籍排版的三个关键:两端对齐、标点挤压、按 CJK 规则断行 */\n text-align: justify;\n text-justify: inter-ideograph;\n line-break: strict;\n overflow-wrap: break-word;\n word-break: normal;\n hyphens: none;\n font-variant-numeric: tabular-nums;\n -webkit-font-smoothing: antialiased;\n}\n/* 「网页版式」:无衬线、左对齐、不缩进、不要纸面卡片(长文档快速扫读更合适) */\n.aero-doc--web {\n --aero-measure: 56em;\n padding: 4px clamp(2px, 1vw, 10px) 32px;\n border: none;\n border-radius: 0;\n background: transparent;\n box-shadow: none;\n font-family: var(--aero-sans);\n font-size: var(--aero-base);\n line-height: 1.8;\n text-align: left;\n text-justify: auto;\n}\n\n/* ---------------------------------------------------------------------------\n * 2. 标题:章标题居中成「章首页」,节标题用细线分隔,三级标题带竖标\n * ------------------------------------------------------------------------- */\n.aero-doc h1,\n.aero-doc h2,\n.aero-doc h3,\n.aero-doc h4,\n.aero-doc h5,\n.aero-doc h6 {\n font-family: inherit;\n font-weight: 600;\n line-height: 1.35;\n color: var(--aero-ink);\n scroll-margin-top: 12px;\n}\n.aero-doc h1 {\n margin: 0.2em 0 0.1em;\n font-size: 1.62em;\n font-weight: 700;\n letter-spacing: 0.01em;\n text-align: center;\n}\n/* 章标题下的短装饰线(书籍章首页的「题花」位置) */\n.aero-doc h1::after {\n content: \"\";\n display: block;\n width: 2.6em;\n height: 2px;\n margin: 0.75em auto 0;\n border-radius: 2px;\n background: var(--aero-accent);\n opacity: 0.75;\n}\n.aero-doc h2 {\n margin: 2.2em 0 0.7em;\n padding-top: 0.85em;\n font-size: 1.28em;\n border-top: 1px solid var(--aero-rule);\n}\n.aero-doc h3 {\n display: flex;\n align-items: baseline;\n gap: 0.5em;\n margin: 1.8em 0 0.5em;\n font-size: 1.12em;\n}\n.aero-doc h3::before {\n content: \"\";\n flex: none;\n width: 3px;\n height: 0.95em;\n border-radius: 2px;\n background: var(--aero-accent);\n transform: translateY(0.08em);\n}\n.aero-doc h4,\n.aero-doc h5,\n.aero-doc h6 {\n margin: 1.4em 0 0.4em;\n font-size: 1em;\n color: var(--aero-ink-2);\n}\n/* 网页版式:标题不做居中/装饰,保持文档工具感 */\n.aero-doc--web h1 {\n margin: 0.4em 0 0.6em;\n font-size: 1.5em;\n text-align: left;\n}\n.aero-doc--web h1::after { display: none; }\n.aero-doc--web h2 {\n margin: 1.6em 0 0.5em;\n padding-top: 0;\n font-size: 1.26em;\n border-top: none;\n}\n.aero-doc--web h3 { display: block; margin: 1.3em 0 0.4em; font-size: 1.1em; }\n.aero-doc--web h3::before { display: none; }\n\n/* 节标题编号「1 / 1.1 / 1.1.1」(阅读器端写入,见 src/client/docEnhance.ts):\n 等宽数字 + 强调色,只做路标,不加大字号、不抢标题本身。 */\n.aero-doc .aero-secnum {\n flex: none;\n margin-right: 0.2em;\n font-variant-numeric: tabular-nums;\n font-weight: 600;\n color: var(--aero-accent);\n}\n/* h3 是 flex(竖标 + 文字),编号作为 flex 项已由 gap 提供间距,不再叠 margin */\n.aero-doc h3 > .aero-secnum { margin-right: 0; }\n\n/* ---------------------------------------------------------------------------\n * 3. 段落:书籍体首行缩进 2 字符;网页体用段间距\n * ------------------------------------------------------------------------- */\n.aero-doc p { margin: 0.55em 0; orphans: 2; widows: 2; }\n/* 中文书籍体例:每一段(含节首段)都首行缩进 2 字符 */\n.aero-doc .paragraph > p { text-indent: 2em; }\n/* 缩进只给「正文段」:列表、表格、图注、卡片内的段落一律不缩进 */\n.aero-doc li p,\n.aero-doc dd p,\n.aero-doc td p,\n.aero-doc th p,\n.aero-doc .admonitionblock p,\n.aero-doc .exampleblock p,\n.aero-doc .sidebarblock p,\n.aero-doc .quoteblock p,\n.aero-doc .aero-caption p,\n.aero-doc .aero-doc-meta p,\n.aero-doc blockquote p,\n.aero-doc #toc p,\n.aero-doc #footnotes p { text-indent: 0; }\n.aero-doc--web .paragraph > p { text-indent: 0; }\n\n/* 导语 / 摘要:略大字号、弱色,作为章首的引子 */\n.aero-doc .paragraph.lead > p,\n.aero-doc .exampleblock.lead > .content > .paragraph > p,\n.aero-doc .exampleblock.lead p {\n font-size: 1.04em;\n color: var(--aero-ink-2);\n}\n.aero-doc .quoteblock.abstract {\n margin: 1.4em 0;\n padding: 0.9em 1.2em 0.9em 1.8em;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background: var(--aero-soft);\n color: var(--aero-ink-2);\n font-size: 0.96em;\n}\n.aero-doc .quoteblock.abstract blockquote { padding: 0; border: none; }\n\n/* ---------------------------------------------------------------------------\n * 4. 列表:无序/有序标记、描述列表、横向列表、checklist、callout 清单\n * ------------------------------------------------------------------------- */\n.aero-doc ul,\n.aero-doc ol { margin: 0.6em 0; padding-left: 1.9em; }\n.aero-doc li { margin: 0.22em 0; }\n.aero-doc li > p { margin: 0.2em 0; }\n.aero-doc ul.disc > li,\n.aero-doc ul:not([class]) > li { list-style-type: disc; }\n.aero-doc ul.circle > li { list-style-type: circle; }\n.aero-doc ul.square > li { list-style-type: square; }\n.aero-doc ul.none,\n.aero-doc ul.no-bullet { list-style: none; padding-left: 0.2em; }\n.aero-doc ol.arabic { list-style-type: decimal; }\n.aero-doc ol.loweralpha { list-style-type: lower-alpha; }\n.aero-doc ol.upperalpha { list-style-type: upper-alpha; }\n.aero-doc ol.lowerroman { list-style-type: lower-roman; }\n.aero-doc ol.upperroman { list-style-type: upper-roman; }\n.aero-doc .ulist .ulist,\n.aero-doc .olist .olist { margin: 0.2em 0; }\n\n.aero-doc .dlist > dl > dt,\n.aero-doc .dlist > dl > dd { margin: 0; }\n.aero-doc .dlist .hdlist1 {\n margin-top: 0.7em;\n font-weight: 600;\n color: var(--aero-ink);\n}\n.aero-doc .dlist > dl > dd { padding-left: 1.2em; color: var(--aero-ink-2); }\n.aero-doc .dlist > dl > dd::before { content: \"— \"; color: var(--aero-ink-3); }\n\n/* 横向描述列表([horizontal])在 Asciidoctor 里输出内层表格 */\n.aero-doc .hdlist > table { border: none; border-collapse: collapse; }\n.aero-doc .hdlist > table td { border: none; padding: 0.25em 0; vertical-align: top; }\n.aero-doc .hdlist td.hdlist1 {\n width: 9em;\n padding-right: 1em;\n font-weight: 600;\n white-space: nowrap;\n}\n.aero-doc .hdlist td.hdlist1::after { content: \"\"; }\n.aero-doc .hdlist td.hdlist2 { color: var(--aero-ink-2); }\n\n/* checklist([checklist] 语法,icons: font 输出 Font Awesome 方框) */\n.aero-doc .ulist.checklist ul { list-style: none; padding-left: 0.2em; }\n.aero-doc .ulist.checklist li { padding-left: 1.6em; text-indent: -1.6em; }\n.aero-doc .ulist.checklist .fa::before {\n display: inline-block;\n width: 1.25em;\n color: var(--aero-accent-ink);\n font-style: normal;\n text-align: left;\n}\n.aero-doc .ulist.checklist .fa-check-square-o::before,\n.aero-doc .ulist.checklist .fa-check-square::before { content: \"\\2611\\FE0E\"; }\n.aero-doc .ulist.checklist .fa-square-o::before { content: \"\\2610\\FE0E\"; }\n\n/* ---------------------------------------------------------------------------\n * 5. 表格:frame/grid/stripes 全组合 + 单元格对齐 + 题注;宽表横向滚动\n * ------------------------------------------------------------------------- */\n.aero-doc .aero-tablewrap {\n max-width: 100%;\n margin: 1.5em 0;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n.aero-doc table.tableblock {\n border-collapse: collapse;\n font-size: 0.94em;\n line-height: 1.65;\n font-family: inherit;\n}\n.aero-doc table.tableblock.stretch { width: 100%; }\n.aero-doc table.tableblock.fit-content { width: auto; }\n.aero-doc table.tableblock > caption,\n.aero-doc table.tableblock > .title {\n caption-side: top;\n padding: 0 0 0.55em;\n font-family: inherit;\n font-size: 0.95em;\n font-weight: 600;\n color: var(--aero-ink-2);\n text-align: center;\n}\n.aero-doc table.tableblock th,\n.aero-doc table.tableblock td {\n padding: 0.42em 0.7em;\n border: 1px solid transparent;\n text-align: left;\n vertical-align: top;\n}\n.aero-doc table.tableblock thead th {\n background: var(--aero-soft);\n font-weight: 600;\n color: var(--aero-ink);\n}\n.aero-doc table.tableblock tfoot td {\n background: var(--aero-soft);\n color: var(--aero-ink-2);\n font-size: 0.96em;\n}\n.aero-doc table.tableblock th > p,\n.aero-doc table.tableblock td > p { margin: 0; }\n\n/* frame-*:外框 */\n.aero-doc table.tableblock.frame-all,\n.aero-doc table.tableblock.frame-sides,\n.aero-doc table.tableblock.frame-none,\n.aero-doc table.tableblock.frame-topbot,\n.aero-doc table.tableblock.frame-ends { border: none; }\n.aero-doc table.tableblock.frame-all th,\n.aero-doc table.tableblock.frame-all td { border: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.frame-sides th:first-child,\n.aero-doc table.tableblock.frame-sides td:first-child { border-left: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.frame-sides th:last-child,\n.aero-doc table.tableblock.frame-sides td:last-child { border-right: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.frame-topbot thead th,\n.aero-doc table.tableblock.frame-ends thead th { border-top: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.frame-topbot tbody tr:last-child td,\n.aero-doc table.tableblock.frame-ends tbody tr:last-child td,\n.aero-doc table.tableblock.frame-topbot tfoot tr:last-child td,\n.aero-doc table.tableblock.frame-ends tfoot tr:last-child td { border-bottom: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.frame-none thead th { border-bottom: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.frame-none tfoot td { border-top: 1px solid var(--aero-rule); }\n\n/* grid-*:内部网格 */\n.aero-doc table.tableblock.grid-all th,\n.aero-doc table.tableblock.grid-all td { border-top: 1px solid var(--aero-rule); border-left: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-all th:last-child,\n.aero-doc table.tableblock.grid-all td:last-child { border-right: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-all tr:last-child td { border-bottom: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-rows th:not(:first-child),\n.aero-doc table.tableblock.grid-rows td:not(:first-child) { border-left: none; }\n.aero-doc table.tableblock.grid-rows th,\n.aero-doc table.tableblock.grid-rows td { border-top: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-cols th,\n.aero-doc table.tableblock.grid-cols td { border-left: 1px solid var(--aero-rule); }\n.aero-doc table.tableblock.grid-cols th:first-child,\n.aero-doc table.tableblock.grid-cols td:first-child { border-left: none; }\n.aero-doc table.tableblock.grid-none th,\n.aero-doc table.tableblock.grid-none td { border-left: none; border-right: none; }\n.aero-doc table.tableblock.grid-none thead th { border-bottom: 1.5px solid var(--aero-rule-strong); }\n.aero-doc table.tableblock.grid-none tfoot td { border-top: 1px solid var(--aero-rule); }\n\n/* 三线表(frame-topbot/grid-rows/grid-none 这类学术表)与网格表共用上面的规则;\n 以下补 header 分隔线,使任何 frame 下表头都有一条压线 */\n.aero-doc table.tableblock thead th { border-bottom: 1px solid var(--aero-rule-strong); }\n\n/* stripes-*:隔行底纹(作者显式要求时才出现) */\n.aero-doc table.tableblock.stripes-even tbody tr:nth-child(even),\n.aero-doc table.tableblock.stripes-all tbody tr:nth-child(even),\n.aero-doc table.tableblock.stripes-odd tbody tr:nth-child(odd) { background: var(--aero-soft); }\n.aero-doc table.tableblock.stripes-hover tbody tr:hover { background: var(--aero-hover); }\n\n/* 单元格对齐(halign-*/valign-*) */\n.aero-doc table.tableblock .halign-left { text-align: left; }\n.aero-doc table.tableblock .halign-center { text-align: center; }\n.aero-doc table.tableblock .halign-right { text-align: right; }\n.aero-doc table.tableblock .valign-top { vertical-align: top; }\n.aero-doc table.tableblock .valign-middle { vertical-align: middle; }\n.aero-doc table.tableblock .valign-bottom { vertical-align: bottom; }\n\n/* ---------------------------------------------------------------------------\n * 6. 图:题注(作者写在 image::[图注] 里 → alt 属性)升级为可见图注,\n * 分「图号 / 对象 / 条件→结论」三层;点击放大。\n * ------------------------------------------------------------------------- */\n.aero-doc .imageblock {\n margin: 1.5em 0;\n text-align: center;\n}\n.aero-doc .imageblock > .content {\n display: block;\n line-height: 0;\n}\n.aero-doc .imageblock img {\n max-width: 100%;\n height: auto;\n border-radius: 4px;\n}\n.aero-doc .imageblock.aero-zoomable img { cursor: zoom-in; }\n.aero-doc .imageblock.aero-zoomable img:hover { outline: 2px solid color-mix(in srgb, var(--aero-accent) 45%, transparent); outline-offset: 2px; }\n.aero-doc .imageblock.text-center,\n.aero-doc .imageblock.text-center > .content { text-align: center; }\n.aero-doc .imageblock.text-left { text-align: left; }\n.aero-doc .imageblock.text-right { text-align: right; }\n.aero-doc .imageblock.left,\n.aero-doc .imageblock.right {\n max-width: 48%;\n margin: 0.4em 1.2em 0.8em;\n}\n.aero-doc .imageblock.left { float: left; margin-left: 0; }\n.aero-doc .imageblock.right { float: right; margin-right: 0; }\n.aero-doc .imageblock.thumb img { border: 1px solid var(--aero-rule); }\n\n/* 图注:居中、小字号、弱色;「图 C05.9-1」加粗,「。——条件→结论」另起一行更弱 */\n.aero-doc .aero-caption,\n.aero-doc .imageblock > .title,\n.aero-doc .videoblock > .title {\n margin: 0.6em auto 0;\n max-width: 40em;\n font-size: 0.88em;\n line-height: 1.7;\n color: var(--aero-ink-2);\n text-align: center;\n text-indent: 0;\n}\n.aero-doc .aero-caption .aero-cap-no {\n margin-right: 0.4em;\n font-weight: 600;\n color: var(--aero-ink);\n white-space: nowrap;\n}\n.aero-doc .aero-caption .aero-cap-tail {\n display: block;\n margin-top: 0.1em;\n font-size: 0.96em;\n color: var(--aero-ink-3);\n}\n.aero-doc img.aero-inline-img { vertical-align: middle; }\n\n/* ---------------------------------------------------------------------------\n * 6.5 视频与媒体卡:教案里的「课堂播放」与「打开就上手」两件事\n *\n * 1) 本地/直链 mp4 走 AsciiDoc 原生 video:: 宏 → 原生 <video preload=none poster>:\n * 浏览器本来就在点击时才取流,不用 JS;`start=,end=` 由宏写成 `#t=a,b` 媒体片段。\n * 2) B 站播放器与仿真面板走 `.aero-embed`(宿主只放行 data-*,点击时才由阅读器建 iframe):\n * 卡片先给封面 + 标题 + 一句说明,点「▶」才加载——长文档里不会一进来就预跑几个仿真。\n * 展开只是**追加**播放区,卡片自身的文字节点不动,正文标注的字符偏移因此不会漂。\n * ------------------------------------------------------------------------- */\n.aero-doc .videoblock {\n margin: 1.35em 0 1.5em;\n text-align: center;\n}\n.aero-doc .videoblock > .content { display: block; }\n.aero-doc .videoblock video {\n max-width: 100%;\n max-height: 72vh;\n border-radius: 10px;\n background: #000;\n box-shadow: 0 1px 2px rgba(15, 25, 50, 0.18), 0 14px 30px -24px rgba(15, 25, 50, 0.55);\n}\n\n.aero-doc .aero-embed {\n margin: 1.35em 0 1.5em;\n border: 1px solid var(--aero-rule);\n border-radius: 12px;\n background: var(--aero-soft);\n overflow: hidden;\n}\n.aero-doc .aero-embed-card {\n display: flex;\n align-items: center;\n gap: 14px;\n width: 100%;\n padding: 12px 14px;\n border: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n text-indent: 0;\n cursor: pointer;\n}\n.aero-doc .aero-embed-card:hover { background: var(--aero-hover); }\n.aero-doc .aero-embed-card.is-open { cursor: default; }\n.aero-doc .aero-embed-card.is-open:hover { background: transparent; }\n.aero-doc .aero-embed-poster {\n flex: 0 0 168px;\n width: 168px;\n aspect-ratio: 16 / 9;\n object-fit: cover;\n border-radius: 8px;\n border: 1px solid var(--aero-rule);\n background: #000;\n}\n.aero-doc .aero-embed-meta {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: 5px;\n min-width: 0;\n}\n.aero-doc .aero-embed-kicker {\n font-size: 0.74em;\n letter-spacing: 0.09em;\n color: var(--aero-ink-3);\n}\n.aero-doc .aero-embed-title {\n font-weight: 600;\n line-height: 1.5;\n color: var(--aero-ink);\n}\n.aero-doc .aero-embed-note {\n font-size: 0.86em;\n line-height: 1.65;\n color: var(--aero-ink-3);\n}\n.aero-doc .aero-embed-play {\n align-self: flex-start;\n margin-top: 3px;\n padding: 4px 14px;\n border: 1px solid color-mix(in srgb, var(--aero-accent) 42%, transparent);\n border-radius: 999px;\n background: color-mix(in srgb, var(--aero-accent) 9%, transparent);\n color: var(--aero-accent-ink);\n font-size: 0.86em;\n line-height: 1.9;\n white-space: nowrap;\n transition: background 0.15s ease, border-color 0.15s ease;\n}\n.aero-doc .aero-embed-card:hover .aero-embed-play {\n background: color-mix(in srgb, var(--aero-accent) 18%, transparent);\n border-color: color-mix(in srgb, var(--aero-accent) 60%, transparent);\n}\n.aero-doc .aero-embed-card.is-open .aero-embed-play { display: none; }\n.aero-doc .aero-embed-stage {\n border-top: 1px solid var(--aero-rule);\n background: #000;\n}\n.aero-doc .aero-embed-frame {\n display: block;\n width: 100%;\n aspect-ratio: 16 / 9;\n border: 0;\n background: #000;\n}\n.aero-doc .aero-embed-open {\n display: block;\n padding: 5px 12px;\n background: var(--aero-soft);\n color: var(--aero-ink-3);\n font-size: 0.8em;\n text-align: right;\n text-indent: 0;\n}\n/* 打印/PDF:点不动的按钮不要印出来,但标题与说明留着——纸上靠「在哪能找到它」 */\n@media print {\n .aero-doc .aero-embed-play { display: none; }\n .aero-doc .aero-embed-card { cursor: auto; }\n}\n\n\n/* ---------------------------------------------------------------------------\n * 7. admonition:五种提示块。内层 <table> 必须去表格化(旧样式会把它变成带框表格)\n * ------------------------------------------------------------------------- */\n.aero-doc .admonitionblock {\n --aero-adm: var(--aero-accent);\n margin: 1.4em 0;\n padding: 0;\n border: 1px solid color-mix(in srgb, var(--aero-adm) 26%, transparent);\n border-left: 4px solid var(--aero-adm);\n border-radius: 10px;\n background: color-mix(in srgb, var(--aero-adm) 6%, transparent);\n}\n.aero-doc .admonitionblock.note { --aero-adm: var(--dsw-alias-state-business-primary, #2f6feb); }\n.aero-doc .admonitionblock.tip { --aero-adm: var(--dsw-alias-state-success-primary, #2f9e44); }\n.aero-doc .admonitionblock.important { --aero-adm: var(--dsw-alias-state-warn-label, #e8590c); }\n.aero-doc .admonitionblock.warning { --aero-adm: var(--dsw-alias-state-warn-primary, #f08c00); }\n.aero-doc .admonitionblock.caution { --aero-adm: var(--dsw-alias-state-error-primary, #e03131); }\n\n.aero-doc .admonitionblock > table {\n display: block;\n width: auto;\n margin: 0;\n border: none;\n border-collapse: separate;\n}\n.aero-doc .admonitionblock > table > tbody { display: block; }\n.aero-doc .admonitionblock > table > tbody > tr {\n display: flex;\n align-items: stretch;\n gap: 0;\n}\n.aero-doc .admonitionblock > table td {\n display: block;\n border: none;\n background: none;\n padding: 0.85em 1em 0.9em 0;\n vertical-align: top;\n}\n.aero-doc .admonitionblock > table td.icon {\n flex: none;\n width: 2.9em;\n padding: 0.95em 0 0 0.95em;\n color: var(--aero-adm);\n text-align: center;\n}\n.aero-doc .admonitionblock > table td.content {\n flex: 1 1 auto;\n min-width: 0;\n padding-right: 1.1em;\n color: var(--aero-ink);\n}\n.aero-doc .admonitionblock > table td.content > :first-child { margin-top: 0; }\n.aero-doc .admonitionblock > table td.content > :last-child { margin-bottom: 0; }\n/* 图标:DSH 主题没有 Font Awesome,用内联 SVG 蒙版自绘(随文字色,明暗自适应) */\n.aero-doc .admonitionblock td.icon .fa {\n display: inline-block;\n width: 1.35em;\n height: 1.35em;\n background-color: currentColor;\n -webkit-mask: var(--aero-adm-icon) center / contain no-repeat;\n mask: var(--aero-adm-icon) center / contain no-repeat;\n}\n.aero-doc .admonitionblock.note td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M8 7.3v4.2'/%3E%3Cpath d='M8 4.7h.01'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.tip td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 1.9a4.1 4.1 0 0 0-2.4 7.4c.4.3.6.7.6 1.2v.4h3.6v-.4c0-.5.2-.9.6-1.2A4.1 4.1 0 0 0 8 1.9z'/%3E%3Cpath d='M6.5 13.1h3'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.important td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M8 4.6v4.6'/%3E%3Cpath d='M8 11.6h.01'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.warning td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2.2 14.5 13.5H1.5z'/%3E%3Cpath d='M8 6.4v3.3'/%3E%3Cpath d='M8 11.8h.01'/%3E%3C/svg%3E\");\n}\n.aero-doc .admonitionblock.caution td.icon .fa {\n --aero-adm-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cpath d='M4.4 11.6 11.6 4.4'/%3E%3C/svg%3E\");\n}\n/* 提示块标题:作者用 `.历史注记` 换行 `[NOTE]` 的写法,标题在 content 里 */\n.aero-doc .admonitionblock td.content > .title {\n margin: 0 0 0.3em;\n padding: 0;\n font-size: 0.95em;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--aero-adm);\n text-align: left;\n}\n.aero-doc .admonitionblock td.content > .title::before {\n content: \"\";\n display: inline-block;\n width: 0.2em;\n height: 0.85em;\n margin-right: 0.5em;\n border-radius: 2px;\n background: currentColor;\n vertical-align: -0.05em;\n}\n\n/* ---------------------------------------------------------------------------\n * 8. example / sidebar / quote / verse / open:AsciiDoc 的「书本卡片」\n * ------------------------------------------------------------------------- */\n/* 例题 / 示例:带边框的卡片,题注做成左上角标签 */\n.aero-doc .exampleblock {\n margin: 1.5em 0;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background: var(--aero-paper);\n overflow: hidden;\n}\n.aero-doc .exampleblock > .title {\n margin: 0;\n padding: 0.45em 1em;\n border-bottom: 1px solid var(--aero-rule);\n background: var(--aero-soft);\n font-size: 0.92em;\n font-weight: 600;\n color: var(--aero-ink-2);\n text-align: left;\n}\n.aero-doc .exampleblock > .content { padding: 0.85em 1.1em 0.95em; }\n.aero-doc .exampleblock > .content > :first-child { margin-top: 0; }\n.aero-doc .exampleblock > .content > :last-child { margin-bottom: 0; }\n\n/* 侧栏([sidebar]):书籍里最常见的「插叙」块 */\n.aero-doc .sidebarblock {\n margin: 1.6em 0;\n padding: 0.95em 1.15em 1em;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background:\n linear-gradient(180deg, color-mix(in srgb, var(--aero-accent) 5%, transparent), transparent 60%),\n var(--aero-soft);\n font-size: 0.96em;\n}\n.aero-doc .sidebarblock > .content > .title {\n margin: 0 0 0.5em;\n padding-bottom: 0.35em;\n border-bottom: 1px dashed var(--aero-rule-strong);\n font-size: 0.95em;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--aero-accent-ink);\n text-align: left;\n}\n.aero-doc .sidebarblock > .content > :first-child { margin-top: 0; }\n.aero-doc .sidebarblock > .content > :last-child { margin-bottom: 0; }\n\n/* 引文([quote]):大引号 + 出处右对齐 */\n.aero-doc .quoteblock {\n margin: 1.7em 0;\n padding: 0.2em 0 0.2em 2em;\n border-left: 3px solid color-mix(in srgb, var(--aero-accent) 45%, transparent);\n color: var(--aero-ink-2);\n}\n.aero-doc .quoteblock blockquote {\n position: relative;\n margin: 0;\n padding: 0;\n border: none;\n quotes: \"\\201C\" \"\\201D\" \"\\2018\" \"\\2019\";\n}\n/* 装饰引号用绝对定位压在左侧留白里。\n 注意:伪元素的 em 一律按**伪元素自身**的 font-size 解析,所以不能靠\n font-size:2.3em + left:-1.05em 来定位(那会甩到块外一个引号的距离);\n 这里保持 1em 排版尺寸,用 transform 放大,定位才与父级字号一致。 */\n.aero-doc .quoteblock blockquote::before {\n content: \"\\201C\";\n position: absolute;\n top: -0.16em;\n left: -1.05em;\n font-family: var(--aero-serif);\n font-size: 1em;\n line-height: 1;\n transform: scale(2.3);\n transform-origin: left top;\n color: color-mix(in srgb, var(--aero-accent) 55%, transparent);\n pointer-events: none;\n}\n.aero-doc .quoteblock .attribution,\n.aero-doc .verseblock .attribution {\n margin-top: 0.5em;\n font-size: 0.9em;\n font-style: normal;\n color: var(--aero-ink-3);\n text-align: right;\n}\n.aero-doc .quoteblock .attribution br,\n.aero-doc .verseblock .attribution br { display: none; }\n.aero-doc .quoteblock .attribution::before,\n.aero-doc .verseblock .attribution::before { content: \"\\2014\\00A0\"; }\n.aero-doc .quoteblock .attribution cite,\n.aero-doc .verseblock .attribution cite { font-style: italic; }\n.aero-doc .quoteblock .attribution cite::before { content: \"\\300A\"; font-style: normal; }\n.aero-doc .quoteblock .attribution cite::after { content: \"\\300B\"; font-style: normal; }\n\n/* 题诗([verse]):居中、保留换行 */\n.aero-doc .verseblock {\n margin: 1.8em 0;\n padding: 0.6em 1em;\n font-size: 1em;\n}\n.aero-doc .verseblock pre {\n margin: 0;\n padding: 0;\n border: none;\n background: none;\n font-family: var(--aero-serif);\n font-size: 1em;\n line-height: 2;\n text-align: center;\n white-space: pre-wrap;\n color: var(--aero-ink-2);\n}\n\n/* 开放块([open])只做留白,不加装饰 */\n.aero-doc .openblock { margin: 1.2em 0; }\n.aero-doc .openblock > .title,\n.aero-doc .literalblock > .title,\n.aero-doc .listingblock > .title {\n margin: 0 0 0.45em;\n font-size: 0.9em;\n font-weight: 600;\n color: var(--aero-ink-2);\n text-align: left;\n}\n\n/* ---------------------------------------------------------------------------\n * 9. 代码:listing(含语言角标、callout 编号)/ literal / colist 清单\n * ------------------------------------------------------------------------- */\n.aero-doc .listingblock,\n.aero-doc .literalblock { margin: 1.3em 0; }\n.aero-doc .listingblock > .content { position: relative; }\n.aero-doc .listingblock pre,\n.aero-doc .literalblock pre {\n margin: 0;\n padding: 0.85em 1em;\n border: 1px solid var(--aero-rule);\n border-radius: 8px;\n background: var(--dsw-alias-markdown-code-block, var(--aero-soft));\n overflow-x: auto;\n font-family: var(--aero-mono);\n font-size: 0.86em;\n line-height: 1.7;\n text-align: left;\n text-indent: 0;\n white-space: pre;\n tab-size: 4;\n}\n.aero-doc .listingblock code,\n.aero-doc .literalblock code { font-family: inherit; }\n.aero-doc .listingblock code[data-lang]::before {\n content: attr(data-lang);\n position: absolute;\n top: 0.45em;\n right: 0.7em;\n font-family: var(--aero-mono);\n font-size: 0.72em;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--aero-ink-3);\n pointer-events: none;\n}\n/* callout 编号(<i class=\"conum\" data-value=\"1\">) */\n.aero-doc .conum[data-value] {\n display: inline-block;\n width: 1.35em;\n height: 1.35em;\n margin: 0 0.15em;\n border-radius: 50%;\n background: var(--aero-accent-ink);\n color: var(--aero-paper);\n font-family: var(--aero-sans);\n font-size: 0.72em;\n font-weight: 600;\n font-style: normal;\n line-height: 1.35em;\n text-align: center;\n vertical-align: 0.15em;\n}\n.aero-doc .conum[data-value]::after { content: attr(data-value); }\n.aero-doc .conum[data-value] + b { display: none; }\n.aero-doc .colist { margin: 0.7em 0 1.3em; font-size: 0.92em; }\n.aero-doc .colist > table { border: none; border-collapse: collapse; }\n.aero-doc .colist > table td { border: none; padding: 0.15em 0.5em 0.15em 0; vertical-align: top; }\n.aero-doc .colist > table td:first-child { width: 1.8em; }\n\n/* ---------------------------------------------------------------------------\n * 10. 行内:代码、链接、强调、上下标、role(text-center/line-through/mark…)\n * ------------------------------------------------------------------------- */\n.aero-doc code,\n.aero-doc kbd,\n.aero-doc samp {\n font-family: var(--aero-mono);\n font-size: 0.88em;\n}\n.aero-doc :not(pre) > code {\n padding: 0.12em 0.38em;\n border: 1px solid var(--aero-rule);\n border-radius: 4px;\n background: var(--dsw-alias-markdown-inline-code, var(--aero-soft));\n overflow-wrap: break-word;\n}\n.aero-doc a,\n.aero-doc a:visited {\n color: var(--aero-accent);\n text-decoration: none;\n border-bottom: 1px solid color-mix(in srgb, var(--aero-accent) 35%, transparent);\n}\n.aero-doc a:hover { border-bottom-color: currentColor; }\n.aero-doc a[data-kbdoc] { cursor: pointer; }\n.aero-doc strong,\n.aero-doc b { font-weight: 600; color: var(--aero-ink); }\n.aero-doc em,\n.aero-doc i { font-style: italic; }\n.aero-doc sup,\n.aero-doc sub { font-size: 0.72em; line-height: 0; }\n.aero-doc .mark {\n padding: 0 0.15em;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f0b429) 30%, transparent);\n color: inherit;\n}\n.aero-doc .line-through { text-decoration: line-through; }\n.aero-doc .underline { text-decoration: underline; }\n.aero-doc .overline { text-decoration: overline; }\n.aero-doc .big { font-size: 1.15em; }\n.aero-doc .small { font-size: 0.86em; }\n.aero-doc .text-center { text-align: center; }\n.aero-doc .text-right { text-align: right; }\n.aero-doc .text-left { text-align: left; }\n.aero-doc .text-justify { text-align: justify; }\n.aero-doc .nowrap { white-space: nowrap; }\n.aero-doc .hide { display: none; }\n.aero-doc .menuseq,\n.aero-doc .keyseq { white-space: nowrap; }\n.aero-doc .menuseq b.caret { font-weight: 400; color: var(--aero-ink-3); }\n/* footnote:正文里的上标编号 + 文末脚注区 */\n.aero-doc .footnote { font-size: 0.82em; vertical-align: super; line-height: 0; }\n.aero-doc .footnote a { border-bottom: none; }\n.aero-doc #footnotes {\n margin-top: 2.4em;\n padding-top: 0.9em;\n border-top: 1px solid var(--aero-rule);\n font-size: 0.88em;\n color: var(--aero-ink-2);\n}\n.aero-doc #footnotes .footnote { font-size: 1em; vertical-align: baseline; }\n.aero-doc #footnotes hr { display: none; }\n\n/* ---------------------------------------------------------------------------\n * 11. 目录(:toc: + toc::[]):书本目录的引导点与层级缩进\n * ------------------------------------------------------------------------- */\n.aero-doc .toc,\n.aero-doc #toc {\n margin: 1.6em 0 2.2em;\n padding: 1em 1.2em;\n border: 1px solid var(--aero-rule);\n border-radius: 10px;\n background: var(--aero-soft);\n font-size: 0.94em;\n}\n.aero-doc #toc > .title,\n.aero-doc .toc > .title {\n margin: 0 0 0.6em;\n font-size: 1em;\n font-weight: 600;\n letter-spacing: 0.08em;\n color: var(--aero-ink-2);\n text-align: center;\n}\n.aero-doc .toc ul { margin: 0; padding: 0; list-style: none; }\n.aero-doc .toc ul ul { padding-left: 1.2em; }\n.aero-doc .toc li { margin: 0.2em 0; }\n.aero-doc .toc a { border-bottom: none; color: var(--aero-ink-2); }\n.aero-doc .toc a:hover { color: var(--aero-accent); }\n\n/* ---------------------------------------------------------------------------\n * 12. 知识库固定小节(本节小结 / 本节来源 / 练习指引):给「书末固定栏目」做卡片\n * 由 DocViewer 的 DOM 增强给 .sect1 打上 aero-sect-* 类\n * ------------------------------------------------------------------------- */\n.aero-doc .sect1.aero-sect-summary > .sectionbody {\n padding: 0.9em 1.15em 1em;\n border: 1px solid var(--aero-rule);\n border-left: 4px solid var(--aero-accent);\n border-radius: 10px;\n background: var(--aero-soft);\n}\n.aero-doc .sect1.aero-sect-source > .sectionbody { font-size: 0.92em; color: var(--aero-ink-2); }\n.aero-doc .sect1.aero-sect-practice > .sectionbody {\n padding: 0.9em 1.15em 1em;\n border: 1px dashed var(--aero-rule-strong);\n border-radius: 10px;\n}\n.aero-doc .sect1.aero-sect-source .tableblock { font-size: 0.95em; }\n\n/* ---------------------------------------------------------------------------\n * 13. 其它块级与主题细节\n * ------------------------------------------------------------------------- */\n.aero-doc hr {\n margin: 1.8em auto;\n border: none;\n border-top: 1px solid var(--aero-rule);\n width: 60%;\n}\n.aero-doc .aero-math-block {\n margin: 1.2em 0;\n overflow-x: auto;\n overflow-y: hidden;\n text-align: center;\n}\n.aero-doc .aero-math-block .katex-display { margin: 0.2em 0; }\n.aero-doc .aero-math-inline { white-space: nowrap; }\n.aero-doc .aero-math-inline .katex { font-size: 1.03em; }\n.aero-doc .sect1 + .sect1 { margin-top: 0.6em; }\n.aero-doc .sect2 + .sect2 { margin-top: 0.4em; }\n.aero-doc .videoblock,\n.aero-doc .audioblock { margin: 1.4em 0; text-align: center; }\n\n/* ---------------------------------------------------------------------------\n * 14. Markdown 文档(教材《空气动力学基础》章、英文参考书、2025 教案)通用回退\n *\n * 同一个 .aero-doc 也承载 markdown-it 的输出,而它的结构与 AsciiDoc 不同:\n * 裸 <p>/<img>/<table>/<pre>/<blockquote> 直接挂在 .aero-doc-body 下(没有\n * .paragraph/.imageblock/.tableblock 包装)。这里只用**直接子元素**选择器补回退,\n * 以免误伤 AsciiDoc 的结构性内层表格(admonition / colist / hdlist 的内层\n * <table> 都没有 .tableblock 类,用 `table:not(.tableblock)` 会连它们一起套上边框,\n * 把提示块又变回带框表格)。\n * ------------------------------------------------------------------------- */\n/* 段落缩进:markdown 的顶层段落没有 .paragraph 包装,按版式单独给缩进 */\n.aero-doc--book .aero-doc-body > p { text-indent: 2em; }\n.aero-doc--web .aero-doc-body > p { text-indent: 0; }\n.aero-doc img {\n max-width: 100%;\n height: auto;\n border-radius: 4px;\n}\n/* 整段只有图(或「图 + 图注同段」)→ 当作插图块:居中 + 可放大 + 图注分层 */\n.aero-doc .aero-md-figure {\n margin: 1.4em 0;\n text-align: center;\n text-indent: 0;\n}\n.aero-doc .aero-md-figure img { cursor: zoom-in; }\n.aero-doc .aero-md-figure img:hover {\n outline: 2px solid color-mix(in srgb, var(--aero-accent) 45%, transparent);\n outline-offset: 2px;\n}\n.aero-doc-body > pre {\n margin: 1.2em 0;\n padding: 0.85em 1em;\n border: 1px solid var(--aero-rule);\n border-radius: 8px;\n background: var(--dsw-alias-markdown-code-block, var(--aero-soft));\n overflow-x: auto;\n font-family: var(--aero-mono);\n font-size: 0.86em;\n line-height: 1.7;\n text-align: left;\n text-indent: 0;\n tab-size: 4;\n}\n.aero-doc-body > pre code { font-family: inherit; background: none; border: none; padding: 0; }\n.aero-doc-body > blockquote {\n margin: 1.2em 0;\n padding: 0.1em 0 0.1em 1em;\n border-left: 3px solid color-mix(in srgb, var(--aero-accent) 40%, transparent);\n color: var(--aero-ink-2);\n}\n.aero-doc-body > blockquote > p { text-indent: 0; }\n/* markdown 表格(无 .tableblock 类):等价于 AsciiDoc 的 frame-all grid-all */\n.aero-doc-body > table {\n border-collapse: collapse;\n margin: 1.4em 0;\n font-size: 0.94em;\n line-height: 1.65;\n}\n.aero-doc-body > table th,\n.aero-doc-body > table td {\n padding: 0.4em 0.65em;\n border: 1px solid var(--aero-rule);\n text-align: left;\n vertical-align: top;\n}\n.aero-doc-body > table th {\n background: var(--aero-soft);\n font-weight: 600;\n}\n.aero-doc-body > table code { white-space: nowrap; }\n\n/* ---------------------------------------------------------------------------\n * 16. 用户阅读偏好(由「阅读设置」面板写入 data-* 属性,取值定义见 readerPrefs.ts)\n *\n * 选择器一律带属性,特异性高于 .aero-doc / .aero-doc--web 与各构造规则,\n * 因此不依赖书写顺序。取值为 auto / theme / 100 / normal 时不输出规则,\n * 保持该版式的原生默认(例如「标准」行距 = 书籍 1.9、网页 1.8)。\n * ------------------------------------------------------------------------- */\n\n/* 16.1 字号倍率:与 DSH 全局字号相乘而不是覆盖,用户在 DSH 设置里改字号仍生效。\n 倍率由 DocViewer 以 --aero-scale 内联写入(FONT_STEPS 的取值,见 readerPrefs.ts),\n §0 的 --aero-base 直接乘它——**不在这里逐档枚举规则**:早先的写法只覆盖\n data-scale 90/110/125/140/160 五档,档位表一旦加档(如 180/200/250/300)\n 新档就没有样式,表现为「按 A+ 显示 180% 但字号纹丝不动」。 */\n\n/* 16.2 字体族:两族一起改,书籍版式与网页版式都听用户的 */\n.aero-doc[data-font='serif'] {\n --aero-serif: Georgia, \"Nimbus Roman\", \"Times New Roman\", \"Noto Serif SC\", \"Source Han Serif SC\",\n \"Source Han Serif CN\", \"Songti SC\", STSong, SimSun, serif;\n --aero-sans: Georgia, \"Nimbus Roman\", \"Times New Roman\", \"Noto Serif SC\", \"Source Han Serif SC\",\n \"Source Han Serif CN\", \"Songti SC\", STSong, SimSun, serif;\n}\n.aero-doc[data-font='sans'] {\n --aero-serif: -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans SC\", sans-serif;\n --aero-sans: -apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans SC\", sans-serif;\n}\n.aero-doc[data-font='kai'] {\n --aero-serif: KaiTi, STKaiti, \"Kaiti SC\", \"KaiTi_GB2312\", \"Noto Serif SC\", \"Songti SC\", serif;\n --aero-sans: KaiTi, STKaiti, \"Kaiti SC\", \"KaiTi_GB2312\", \"Noto Serif SC\", \"Songti SC\", serif;\n}\n\n/* 16.3 行距 */\n.aero-doc[data-leading='tight'] { line-height: 1.65; }\n.aero-doc[data-leading='loose'] { line-height: 2.25; }\n\n/* 16.4 版心:auto 不写规则 → 书籍 42em / 网页 56em 的原生默认 */\n.aero-doc[data-measure='narrow'] { --aero-measure: 34em; }\n.aero-doc[data-measure='wide'] { --aero-measure: 64em; }\n.aero-doc[data-measure='full'] { --aero-measure: none; }\n\n/* 16.5 底色:纸面与墨色成套写到外层 wrapper 的 -ov 变量上,正文卡片与右侧目录共用;\n wrapper 自身也铺同色底,于是正文两侧留白与页脚一起进入纸面,\n 而不是\"白底上浮一张色卡\"。注意这几段也定义了右侧目录(.docOutline)要用的墨色。 */\n[data-aero-doc-wrap][data-paper='white'] {\n --aero-paper-ov: #ffffff;\n --aero-ink-ov: #17181a;\n --aero-ink-2-ov: #4a4f57;\n --aero-ink-3-ov: #7b828c;\n --aero-rule-ov: rgba(20, 25, 35, 0.12);\n --aero-rule-strong-ov: rgba(20, 25, 35, 0.24);\n --aero-soft-ov: rgba(20, 25, 35, 0.04);\n --aero-hover-ov: rgba(20, 25, 35, 0.06);\n --aero-shadow-ov: 0 1px 2px rgba(15, 25, 50, 0.05), 0 18px 40px -32px rgba(15, 25, 50, 0.4);\n background: #ffffff;\n}\n[data-aero-doc-wrap][data-paper='sepia'] {\n --aero-paper-ov: #f7efdd;\n --aero-ink-ov: #3a3126;\n --aero-ink-2-ov: #6a5c46;\n --aero-ink-3-ov: #8d7f68;\n --aero-rule-ov: rgba(96, 76, 48, 0.2);\n --aero-rule-strong-ov: rgba(96, 76, 48, 0.34);\n --aero-soft-ov: rgba(120, 95, 55, 0.07);\n --aero-hover-ov: rgba(120, 95, 55, 0.11);\n --aero-shadow-ov: 0 1px 2px rgba(90, 70, 40, 0.06), 0 18px 40px -32px rgba(90, 70, 40, 0.4);\n background: #f7efdd;\n}\n[data-aero-doc-wrap][data-paper='green'] {\n --aero-paper-ov: #e7efe2;\n --aero-ink-ov: #22301d;\n --aero-ink-2-ov: #4e5c46;\n --aero-ink-3-ov: #77866e;\n --aero-rule-ov: rgba(60, 90, 45, 0.2);\n --aero-rule-strong-ov: rgba(60, 90, 45, 0.34);\n --aero-soft-ov: rgba(70, 110, 60, 0.07);\n --aero-hover-ov: rgba(70, 110, 60, 0.12);\n --aero-shadow-ov: 0 1px 2px rgba(50, 80, 40, 0.06), 0 18px 40px -32px rgba(50, 80, 40, 0.4);\n background: #e7efe2;\n}\n[data-aero-doc-wrap][data-paper='dark'] {\n --aero-paper-ov: #191c22;\n --aero-ink-ov: #dfe3e8;\n --aero-ink-2-ov: #a8b0bb;\n --aero-ink-3-ov: #7c8593;\n --aero-rule-ov: rgba(255, 255, 255, 0.14);\n --aero-rule-strong-ov: rgba(255, 255, 255, 0.26);\n --aero-soft-ov: rgba(255, 255, 255, 0.055);\n --aero-hover-ov: rgba(255, 255, 255, 0.09);\n --aero-accent-ov: #7aa2f7;\n --aero-accent-ink-ov: #9ec1ff;\n --aero-shadow-ov: 0 1px 2px rgba(0, 0, 0, 0.45), 0 18px 40px -32px rgba(0, 0, 0, 0.95);\n background: #191c22;\n}\n\n/* 网页版式默认不要纸面卡片;一旦选了非「跟随主题」的底色,补回正文内边距,\n 否则文字会贴着纸面边缘。 */\n[data-aero-doc-wrap]:not([data-paper='theme']) .aero-doc--web {\n padding: clamp(14px, 2.4vw, 28px) clamp(14px, 3vw, 34px) clamp(28px, 4vw, 48px);\n}\n\n/* ---------------------------------------------------------------------------\n * 17. 打印:A4 书页(Ctrl+P 直接当讲义印)\n * ------------------------------------------------------------------------- */\n@media print {\n .aero-doc {\n max-width: none;\n padding: 0;\n border: none;\n box-shadow: none;\n font-size: 10.5pt;\n line-height: 1.7;\n }\n .aero-doc h1 { font-size: 18pt; }\n .aero-doc h2 { font-size: 14pt; }\n .aero-doc .admonitionblock,\n .aero-doc .exampleblock,\n .aero-doc .sidebarblock,\n .aero-doc table.tableblock,\n .aero-doc .imageblock { break-inside: avoid; }\n .aero-doc img { max-height: 8.5cm; }\n}\n\n/* ---------------------------------------------------------------------------\n * 18. 阅读标注:荧光笔 / 重点线(客户端写入 <mark class=\"aero-anno\">)\n *\n * 学生要求「像微信读书那样用不同荧光色标重点、画重点线」。三条做法上的决定:\n * ① **荧光笔用「下半截染色带」而不是整块底色**:真荧光笔是划过字行下半部的,\n * 整块底色会把字形压进色块里,深色纸面(夜间)上尤其糊;染色带只覆盖行高的\n * 中下部,字形上缘仍落在纸面上,明暗两套纸色都读得清。\n * ② 颜色 `--aero-mark-*` 一律**写在 mark 自己身上**(§18.1/§18.2):自定义属性里的\n * var() 是在**声明它的元素**上解析的,若把染色带写在 `.aero-doc` 上用\n * `var(--aero-mark)`,解析时 mark 上的颜色还不存在 → 整个属性变成无效值,\n * 表现就是「DOM 里有 mark、屏幕上没有颜色」(本轮实测踩过这个坑)。\n * ③ 重点线(underline)不用 text-decoration:跨行时它会连到行尾空白,而我们按文本\n * 节点分片绘制,用 inset box-shadow 才是逐片的短线。\n *\n * 与交互的关系:mark 上的点击由 DocViewer 接管(打开编辑浮层),因此这里只负责\n * 「看起来像荧光笔」,并给 hover/定位闪烁留出视觉反馈。\n * ------------------------------------------------------------------------- */\n\n/* 18.1 四种笔芯:wash = 染色带(半透明),line = 重点线(实色、比 wash 深一档) */\nmark.aero-anno[data-color='yellow'] { --aero-mark-wash: #ffe066a8; --aero-mark-line: #e8b400; }\nmark.aero-anno[data-color='green'] { --aero-mark-wash: #a6e3a1a0; --aero-mark-line: #3f9e4d; }\nmark.aero-anno[data-color='blue'] { --aero-mark-wash: #93c9f7a0; --aero-mark-line: #2b7cd3; }\nmark.aero-anno[data-color='pink'] { --aero-mark-wash: #f7a8c4a8; --aero-mark-line: #d94f8a; }\n\nmark.aero-anno {\n /* 染色带:行高 42%→92% 之间染色,上下留白(正文字形一般落在 10%→85%) */\n background: transparent;\n background-image: linear-gradient(\n to bottom,\n transparent 0%,\n transparent 42%,\n var(--aero-mark-wash) 42%,\n var(--aero-mark-wash) 92%,\n transparent 92%\n );\n color: inherit;\n padding: 0;\n border-radius: 2px;\n cursor: pointer;\n /* 跨行断开的每一片各自染色,不连成一条横贯整行的色带 */\n -webkit-box-decoration-break: clone;\n box-decoration-break: clone;\n transition: filter 0.12s ease;\n}\n\n/* 重点线:逐片的粗下划线(跨行时每片各有一道,视觉上连成一条) */\nmark.aero-anno[data-style='underline'] {\n background-image: none;\n box-shadow: inset 0 -0.16em 0 var(--aero-mark-line);\n}\n\nmark.aero-anno:hover {\n filter: saturate(1.25) contrast(1.05);\n}\n\n/* 面板「定位」的闪烁:外描边,1.6 s 后由脚本摘掉 */\nmark.aero-anno[data-flash='1'] {\n outline: 2px solid color-mix(in srgb, var(--aero-accent-ink, #2f6feb) 80%, transparent);\n outline-offset: 1px;\n border-radius: 3px;\n}\n\n/* 18.2 夜间纸面:换成低亮度笔芯(浅色压在浅字上会糊成一片) */\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='yellow'] { --aero-mark-wash: #b58f00b8; --aero-mark-line: #e8c34a; }\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='green'] { --aero-mark-wash: #3f7a3aa8; --aero-mark-line: #7fd18b; }\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='blue'] { --aero-mark-wash: #2f5f8ab8; --aero-mark-line: #7ab6ee; }\n[data-aero-doc-wrap][data-paper='dark'] .aero-doc mark.aero-anno[data-color='pink'] { --aero-mark-wash: #8a3f5fb8; --aero-mark-line: #f095b8; }\n\n/* 打印:标记要跟着正文印出来(讲义上就能看见自己划的重点),但定位闪烁不要。 */\n@media print {\n mark.aero-anno {\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n mark.aero-anno[data-flash='1'] {\n outline: none;\n }\n}\n";
|
|
11
11
|
const id = "@dongfangw/study-kit/doc-theme.css";
|
|
12
12
|
if (typeof document !== "undefined" && document.querySelector("style[data-inline-css=\"" + id + "\"]") === null) {
|
|
13
13
|
const tag = document.createElement("style");
|
|
@@ -359,10 +359,161 @@ window.__ModuleLoader__.load({
|
|
|
359
359
|
enhanceBlockTitles(root);
|
|
360
360
|
enhanceFigures(root);
|
|
361
361
|
enhanceMarkdownFigures(root);
|
|
362
|
+
enhanceVideos(root);
|
|
363
|
+
enhanceMediaCards(root);
|
|
362
364
|
markSpecialSections(root);
|
|
363
365
|
enhanceExternalLinks(root);
|
|
364
366
|
if (numbers !== void 0 && numbers !== null) numberSectionHeadings(root, numbers);
|
|
365
367
|
}
|
|
368
|
+
/** 允许被内嵌到正文里的外链播放器域名(白名单;不在名单里的 data-src 一律不渲染)。 */
|
|
369
|
+
const EMBED_HOSTS = /* @__PURE__ */ new Set([
|
|
370
|
+
"player.bilibili.com",
|
|
371
|
+
"www.youtube.com",
|
|
372
|
+
"www.youtube-nocookie.com"
|
|
373
|
+
]);
|
|
374
|
+
const EMBED_KIND_LABEL = {
|
|
375
|
+
sim: "▶ 运行仿真",
|
|
376
|
+
video: "▶ 播放视频",
|
|
377
|
+
page: "▶ 打开页面"
|
|
378
|
+
};
|
|
379
|
+
const EMBED_KIND_KICKER = {
|
|
380
|
+
sim: "交互式仿真",
|
|
381
|
+
video: "视频",
|
|
382
|
+
page: "嵌入页面"
|
|
383
|
+
};
|
|
384
|
+
/** 校验并归一化嵌入地址:站内绝对路径,或白名单内的 https 播放器。 */
|
|
385
|
+
function embedSrc(raw) {
|
|
386
|
+
const s = raw.trim();
|
|
387
|
+
if (s === "") return null;
|
|
388
|
+
if (s.startsWith("/") && !s.startsWith("//")) return s;
|
|
389
|
+
try {
|
|
390
|
+
const u = new URL(s);
|
|
391
|
+
return u.protocol === "https:" && EMBED_HOSTS.has(u.hostname) ? u.toString() : null;
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** 媒体片段 `#t=a[,b]`:返回起点(秒)与终点(秒,缺省 null)。 */
|
|
397
|
+
const MEDIA_FRAGMENT_RE = /#t=(\d+(?:\.\d+)?)(?:,(\d+(?:\.\d+)?))?$/;
|
|
398
|
+
/**
|
|
399
|
+
* 原生 <video> 的轻量增强:
|
|
400
|
+
* · 补 playsinline / controls / preload=none(作者没写时);
|
|
401
|
+
* · 尊重 `#t=a,b`:载入后跳到 a,播到 b 自动暂停(课堂只看片段,不必每次手动拖)。
|
|
402
|
+
* 不在 src 上动手脚,宿主已把相对路径改写成资源路由。
|
|
403
|
+
*/
|
|
404
|
+
function enhanceVideos(root) {
|
|
405
|
+
for (const video of Array.from(root.querySelectorAll("video"))) {
|
|
406
|
+
if (video.getAttribute("data-aero-media") === "1") continue;
|
|
407
|
+
video.setAttribute("data-aero-media", "1");
|
|
408
|
+
if (!video.hasAttribute("playsinline")) video.setAttribute("playsinline", "");
|
|
409
|
+
if (!video.hasAttribute("controls")) video.setAttribute("controls", "");
|
|
410
|
+
if (!video.hasAttribute("preload")) video.setAttribute("preload", "none");
|
|
411
|
+
const src = video.getAttribute("src") ?? "";
|
|
412
|
+
const m = MEDIA_FRAGMENT_RE.exec(src);
|
|
413
|
+
if (m === null) continue;
|
|
414
|
+
const start = Number(m[1]);
|
|
415
|
+
const end = m[2] === void 0 ? null : Number(m[2]);
|
|
416
|
+
if (!Number.isFinite(start)) continue;
|
|
417
|
+
let pausedAtEnd = false;
|
|
418
|
+
video.addEventListener("loadedmetadata", () => {
|
|
419
|
+
if (start > 0 && Math.abs(video.currentTime - start) > .4) video.currentTime = start;
|
|
420
|
+
});
|
|
421
|
+
if (end !== null && Number.isFinite(end)) {
|
|
422
|
+
video.addEventListener("timeupdate", () => {
|
|
423
|
+
if (pausedAtEnd) {
|
|
424
|
+
if (video.currentTime < end - .5) pausedAtEnd = false;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (video.currentTime >= end) {
|
|
428
|
+
video.pause();
|
|
429
|
+
pausedAtEnd = true;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
video.addEventListener("play", () => {
|
|
433
|
+
if (pausedAtEnd && video.currentTime >= end - .3) {
|
|
434
|
+
pausedAtEnd = false;
|
|
435
|
+
video.currentTime = start;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** 把一张媒体卡换成「标题 + 说明 + 按钮」,点击后才把 iframe 追加进来。 */
|
|
442
|
+
function enhanceMediaCards(root) {
|
|
443
|
+
for (const box of Array.from(root.querySelectorAll(".aero-embed"))) {
|
|
444
|
+
if (box.getAttribute("data-aero-embed") === "ready") continue;
|
|
445
|
+
const url = embedSrc(box.getAttribute("data-src") ?? "");
|
|
446
|
+
if (url === null) continue;
|
|
447
|
+
const kind = (box.getAttribute("data-kind") ?? "page").toLowerCase();
|
|
448
|
+
const title = (box.getAttribute("data-title") ?? "").trim();
|
|
449
|
+
const note = (box.getAttribute("data-note") ?? "").trim();
|
|
450
|
+
const poster = (box.getAttribute("data-poster") ?? "").trim();
|
|
451
|
+
const aspect = (box.getAttribute("data-aspect") ?? "").trim();
|
|
452
|
+
const card = document.createElement("button");
|
|
453
|
+
card.type = "button";
|
|
454
|
+
card.className = "aero-embed-card";
|
|
455
|
+
card.setAttribute("aria-expanded", "false");
|
|
456
|
+
if (poster !== "") {
|
|
457
|
+
const img = document.createElement("img");
|
|
458
|
+
img.className = "aero-embed-poster";
|
|
459
|
+
img.src = poster;
|
|
460
|
+
img.alt = "";
|
|
461
|
+
img.loading = "lazy";
|
|
462
|
+
img.addEventListener("error", () => img.remove());
|
|
463
|
+
card.appendChild(img);
|
|
464
|
+
}
|
|
465
|
+
const meta = document.createElement("span");
|
|
466
|
+
meta.className = "aero-embed-meta";
|
|
467
|
+
const kicker = document.createElement("span");
|
|
468
|
+
kicker.className = "aero-embed-kicker";
|
|
469
|
+
kicker.textContent = EMBED_KIND_KICKER[kind] ?? EMBED_KIND_KICKER.page ?? "嵌入";
|
|
470
|
+
meta.appendChild(kicker);
|
|
471
|
+
if (title !== "") {
|
|
472
|
+
const head = document.createElement("span");
|
|
473
|
+
head.className = "aero-embed-title";
|
|
474
|
+
head.textContent = title;
|
|
475
|
+
meta.appendChild(head);
|
|
476
|
+
}
|
|
477
|
+
if (note !== "") {
|
|
478
|
+
const sub = document.createElement("span");
|
|
479
|
+
sub.className = "aero-embed-note";
|
|
480
|
+
sub.textContent = note;
|
|
481
|
+
meta.appendChild(sub);
|
|
482
|
+
}
|
|
483
|
+
const play = document.createElement("span");
|
|
484
|
+
play.className = "aero-embed-play";
|
|
485
|
+
play.textContent = EMBED_KIND_LABEL[kind] ?? EMBED_KIND_LABEL.page ?? "▶ 打开";
|
|
486
|
+
meta.appendChild(play);
|
|
487
|
+
card.appendChild(meta);
|
|
488
|
+
card.addEventListener("click", () => {
|
|
489
|
+
if (box.querySelector(".aero-embed-stage") !== null) return;
|
|
490
|
+
const stage = document.createElement("div");
|
|
491
|
+
stage.className = "aero-embed-stage";
|
|
492
|
+
const frame = document.createElement("iframe");
|
|
493
|
+
frame.className = "aero-embed-frame";
|
|
494
|
+
frame.src = url;
|
|
495
|
+
frame.title = title !== "" ? title : "嵌入内容";
|
|
496
|
+
frame.loading = "eager";
|
|
497
|
+
frame.setAttribute("allow", "fullscreen; autoplay; encrypted-media; picture-in-picture");
|
|
498
|
+
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
499
|
+
if (aspect !== "") frame.style.aspectRatio = aspect;
|
|
500
|
+
stage.appendChild(frame);
|
|
501
|
+
const open = document.createElement("a");
|
|
502
|
+
open.className = "aero-embed-open";
|
|
503
|
+
open.href = url;
|
|
504
|
+
open.target = "_blank";
|
|
505
|
+
open.rel = "noreferrer noopener";
|
|
506
|
+
open.textContent = "在新窗口打开";
|
|
507
|
+
stage.appendChild(open);
|
|
508
|
+
card.classList.add("is-open");
|
|
509
|
+
card.setAttribute("aria-expanded", "true");
|
|
510
|
+
box.appendChild(stage);
|
|
511
|
+
});
|
|
512
|
+
box.textContent = "";
|
|
513
|
+
box.appendChild(card);
|
|
514
|
+
box.setAttribute("data-aero-embed", "ready");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
366
517
|
const ATTR_LINE_RE = /^:([A-Za-z0-9_][A-Za-z0-9_-]*):\s*(.*)$/;
|
|
367
518
|
/**
|
|
368
519
|
* 解析 AsciiDoc 文件头部:文档标题(`= …`)与紧随其后的 `:属性: 值` 行。
|
|
@@ -477,7 +628,7 @@ window.__ModuleLoader__.load({
|
|
|
477
628
|
} catch {}
|
|
478
629
|
return fallback;
|
|
479
630
|
}
|
|
480
|
-
function persist$
|
|
631
|
+
function persist$3(key, value) {
|
|
481
632
|
try {
|
|
482
633
|
window.localStorage.setItem(key, value);
|
|
483
634
|
} catch {}
|
|
@@ -537,7 +688,7 @@ window.__ModuleLoader__.load({
|
|
|
537
688
|
const sidebarStore = createStore(true);
|
|
538
689
|
/** 切换版式(写入 localStorage)。 */
|
|
539
690
|
function setDocLayout(next) {
|
|
540
|
-
persist$
|
|
691
|
+
persist$3(LAYOUT_KEY, next);
|
|
541
692
|
layoutStore.set(next);
|
|
542
693
|
}
|
|
543
694
|
/** 当前字号档索引。 */
|
|
@@ -547,7 +698,7 @@ window.__ModuleLoader__.load({
|
|
|
547
698
|
/** 设置字号档;越界自动夹到 [0, FONT_STEP_MAX]。 */
|
|
548
699
|
function setFontStep(next) {
|
|
549
700
|
const clamped = Math.max(0, Math.min(FONT_STEP_MAX, Math.round(next)));
|
|
550
|
-
persist$
|
|
701
|
+
persist$3(FONT_STEP_KEY, String(clamped));
|
|
551
702
|
fontStepStore.set(clamped);
|
|
552
703
|
}
|
|
553
704
|
/** 字号加/减一档(快捷键与 A- / A+ 按钮共用)。 */
|
|
@@ -559,19 +710,19 @@ window.__ModuleLoader__.load({
|
|
|
559
710
|
setFontStep(1);
|
|
560
711
|
}
|
|
561
712
|
function setDocFont(next) {
|
|
562
|
-
persist$
|
|
713
|
+
persist$3(FONT_KEY, next);
|
|
563
714
|
fontStore.set(next);
|
|
564
715
|
}
|
|
565
716
|
function setDocLeading(next) {
|
|
566
|
-
persist$
|
|
717
|
+
persist$3(LEADING_KEY, next);
|
|
567
718
|
leadingStore.set(next);
|
|
568
719
|
}
|
|
569
720
|
function setDocMeasure(next) {
|
|
570
|
-
persist$
|
|
721
|
+
persist$3(MEASURE_KEY, next);
|
|
571
722
|
measureStore.set(next);
|
|
572
723
|
}
|
|
573
724
|
function setDocPaper(next) {
|
|
574
|
-
persist$
|
|
725
|
+
persist$3(PAPER_KEY, next);
|
|
575
726
|
paperStore.set(next);
|
|
576
727
|
}
|
|
577
728
|
/** 本页目录是否展开。 */
|
|
@@ -579,7 +730,7 @@ window.__ModuleLoader__.load({
|
|
|
579
730
|
return outlineStore.get();
|
|
580
731
|
}
|
|
581
732
|
function setOutlineOpen(open) {
|
|
582
|
-
persist$
|
|
733
|
+
persist$3(OUTLINE_KEY, open ? "1" : "0");
|
|
583
734
|
outlineStore.set(open);
|
|
584
735
|
}
|
|
585
736
|
/** 专注阅读:隐藏左侧文件树与底部提问栏,正文独占。仅本会话有效(刷新即回到常规视图)。 */
|
|
@@ -606,7 +757,7 @@ window.__ModuleLoader__.load({
|
|
|
606
757
|
};
|
|
607
758
|
}
|
|
608
759
|
// src/client/readingPos.ts
|
|
609
|
-
const STORE_KEY$
|
|
760
|
+
const STORE_KEY$2 = "aero.pos.v1";
|
|
610
761
|
/**
|
|
611
762
|
* 最多保留多少份文档的位置。
|
|
612
763
|
*
|
|
@@ -614,18 +765,18 @@ window.__ModuleLoader__.load({
|
|
|
614
765
|
* 本插件已有标注库占着)。位置记录只有一两百字节,400 份 ≈ 80KB,够用;
|
|
615
766
|
* 超出的按「最久没读」淘汰——被淘汰的文档下次打开回到开头,与从没读过一致。
|
|
616
767
|
*/
|
|
617
|
-
const MAX_DOCS = 400;
|
|
618
|
-
function emptyDb$
|
|
768
|
+
const MAX_DOCS$1 = 400;
|
|
769
|
+
function emptyDb$2() {
|
|
619
770
|
return {
|
|
620
771
|
v: 1,
|
|
621
772
|
docs: {}
|
|
622
773
|
};
|
|
623
774
|
}
|
|
624
|
-
function finiteOr(value, fallback) {
|
|
775
|
+
function finiteOr$1(value, fallback) {
|
|
625
776
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
626
777
|
}
|
|
627
778
|
/** 逐条净化:手改过/旧版本的数据不能让整库报废(一条坏数据只丢它自己)。 */
|
|
628
|
-
function sanitize$
|
|
779
|
+
function sanitize$2(raw) {
|
|
629
780
|
if (typeof raw !== "object" || raw === null) return null;
|
|
630
781
|
const p = raw;
|
|
631
782
|
if (typeof p.anchor !== "string") return null;
|
|
@@ -633,23 +784,23 @@ window.__ModuleLoader__.load({
|
|
|
633
784
|
if (anchor.length > 200) return null;
|
|
634
785
|
return {
|
|
635
786
|
anchor,
|
|
636
|
-
offset: Math.max(0, finiteOr(p.offset, 0)),
|
|
637
|
-
progress: Math.min(1, Math.max(0, finiteOr(p.progress, 0))),
|
|
787
|
+
offset: Math.max(0, finiteOr$1(p.offset, 0)),
|
|
788
|
+
progress: Math.min(1, Math.max(0, finiteOr$1(p.progress, 0))),
|
|
638
789
|
label: typeof p.label === "string" ? p.label.slice(0, 160) : "",
|
|
639
|
-
at: Math.max(0, finiteOr(p.at, 0))
|
|
790
|
+
at: Math.max(0, finiteOr$1(p.at, 0))
|
|
640
791
|
};
|
|
641
792
|
}
|
|
642
|
-
function loadDb$
|
|
793
|
+
function loadDb$2() {
|
|
643
794
|
try {
|
|
644
|
-
const raw = window.localStorage.getItem(STORE_KEY$
|
|
645
|
-
if (raw === null) return emptyDb$
|
|
795
|
+
const raw = window.localStorage.getItem(STORE_KEY$2);
|
|
796
|
+
if (raw === null) return emptyDb$2();
|
|
646
797
|
const parsed = JSON.parse(raw);
|
|
647
|
-
if (typeof parsed !== "object" || parsed === null) return emptyDb$
|
|
798
|
+
if (typeof parsed !== "object" || parsed === null) return emptyDb$2();
|
|
648
799
|
const docsRaw = parsed.docs;
|
|
649
|
-
if (typeof docsRaw !== "object" || docsRaw === null) return emptyDb$
|
|
800
|
+
if (typeof docsRaw !== "object" || docsRaw === null) return emptyDb$2();
|
|
650
801
|
const docs = {};
|
|
651
802
|
for (const [key, value] of Object.entries(docsRaw)) {
|
|
652
|
-
const pos = sanitize$
|
|
803
|
+
const pos = sanitize$2(value);
|
|
653
804
|
if (pos !== null) docs[key] = pos;
|
|
654
805
|
}
|
|
655
806
|
return {
|
|
@@ -657,49 +808,49 @@ window.__ModuleLoader__.load({
|
|
|
657
808
|
docs
|
|
658
809
|
};
|
|
659
810
|
} catch {
|
|
660
|
-
return emptyDb$
|
|
811
|
+
return emptyDb$2();
|
|
661
812
|
}
|
|
662
813
|
}
|
|
663
|
-
let db$
|
|
664
|
-
function persist$
|
|
814
|
+
let db$2 = loadDb$2();
|
|
815
|
+
function persist$2() {
|
|
665
816
|
try {
|
|
666
|
-
window.localStorage.setItem(STORE_KEY$
|
|
817
|
+
window.localStorage.setItem(STORE_KEY$2, JSON.stringify(db$2));
|
|
667
818
|
} catch {}
|
|
668
819
|
}
|
|
669
820
|
/** 超过上限时按 `at` 从旧到新淘汰。只在写入路径调用(读路径不改库)。 */
|
|
670
|
-
function prune(records) {
|
|
821
|
+
function prune$1(records) {
|
|
671
822
|
const keys = Object.keys(records);
|
|
672
|
-
if (keys.length <= MAX_DOCS) return records;
|
|
673
|
-
const doomed = keys.sort((a, b) => (records[a]?.at ?? 0) - (records[b]?.at ?? 0)).slice(0, keys.length - MAX_DOCS);
|
|
823
|
+
if (keys.length <= MAX_DOCS$1) return records;
|
|
824
|
+
const doomed = keys.sort((a, b) => (records[a]?.at ?? 0) - (records[b]?.at ?? 0)).slice(0, keys.length - MAX_DOCS$1);
|
|
674
825
|
const next = { ...records };
|
|
675
826
|
for (const key of doomed) delete next[key];
|
|
676
827
|
return next;
|
|
677
828
|
}
|
|
678
829
|
/** 取某文档上次的位置;没读过返回 null。 */
|
|
679
830
|
function loadPos(key) {
|
|
680
|
-
return db$
|
|
831
|
+
return db$2.docs[key] ?? null;
|
|
681
832
|
}
|
|
682
833
|
/** 记下某文档的位置(每次滚动节流后写一次)。 */
|
|
683
834
|
function savePos(key, pos) {
|
|
684
|
-
db$
|
|
835
|
+
db$2 = {
|
|
685
836
|
v: 1,
|
|
686
|
-
docs: prune({
|
|
687
|
-
...db$
|
|
837
|
+
docs: prune$1({
|
|
838
|
+
...db$2.docs,
|
|
688
839
|
[key]: pos
|
|
689
840
|
})
|
|
690
841
|
};
|
|
691
|
-
persist$
|
|
842
|
+
persist$2();
|
|
692
843
|
}
|
|
693
844
|
/** 忘掉某文档的位置(「从头读」)。 */
|
|
694
845
|
function clearPos(key) {
|
|
695
|
-
if (db$
|
|
696
|
-
const next = { ...db$
|
|
846
|
+
if (db$2.docs[key] === void 0) return;
|
|
847
|
+
const next = { ...db$2.docs };
|
|
697
848
|
delete next[key];
|
|
698
|
-
db$
|
|
849
|
+
db$2 = {
|
|
699
850
|
v: 1,
|
|
700
851
|
docs: next
|
|
701
852
|
};
|
|
702
|
-
persist$
|
|
853
|
+
persist$2();
|
|
703
854
|
}
|
|
704
855
|
function clamp(value, min, max) {
|
|
705
856
|
return Math.min(max, Math.max(min, value));
|
|
@@ -784,9 +935,20 @@ window.__ModuleLoader__.load({
|
|
|
784
935
|
function deckKeyOf(relPath) {
|
|
785
936
|
return `deck:${relPath}`;
|
|
786
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* PDF 键:与文档/课件都区分开。
|
|
940
|
+
*
|
|
941
|
+
* 为什么不能直接用 relPath:PDF 的标注锚点是「第几页 + 页内文字偏移」,
|
|
942
|
+
* 与 md/adoc 的「整篇文字偏移」不是同一个坐标系;键分开后,同一个文件名的
|
|
943
|
+
* `x.md` 与 `x.pdf` 也不会互相污染标注与阅读位置。
|
|
944
|
+
*/
|
|
945
|
+
function pdfKeyOf(relPath) {
|
|
946
|
+
return `pdf:${relPath}`;
|
|
947
|
+
}
|
|
787
948
|
/** 键 → 人类可读标签(导出与面板标题用)。 */
|
|
788
949
|
function labelOfKey(key) {
|
|
789
950
|
if (key.startsWith("deck:")) return key.slice(5);
|
|
951
|
+
if (key.startsWith("pdf:")) return key.slice(4);
|
|
790
952
|
const hash = key.lastIndexOf("#");
|
|
791
953
|
if (hash > 0 && /^\d+$/.test(key.slice(hash + 1))) return key.slice(0, hash);
|
|
792
954
|
return key;
|
|
@@ -828,8 +990,8 @@ window.__ModuleLoader__.load({
|
|
|
828
990
|
};
|
|
829
991
|
/** 上下文长度(前缀/后缀各取多少字)。 */
|
|
830
992
|
const CONTEXT_CHARS = 24;
|
|
831
|
-
const STORE_KEY = "aero.anno.v1";
|
|
832
|
-
function emptyDb() {
|
|
993
|
+
const STORE_KEY$1 = "aero.anno.v1";
|
|
994
|
+
function emptyDb$1() {
|
|
833
995
|
return {
|
|
834
996
|
v: 1,
|
|
835
997
|
docs: {}
|
|
@@ -839,7 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
839
1001
|
return typeof value === "string" && ANNO_COLORS.includes(value);
|
|
840
1002
|
}
|
|
841
1003
|
/** 逐条净化:旧版本/手改过的数据不能让整库报废(一条坏数据只丢它自己)。 */
|
|
842
|
-
function sanitize(raw) {
|
|
1004
|
+
function sanitize$1(raw) {
|
|
843
1005
|
if (typeof raw !== "object" || raw === null) return null;
|
|
844
1006
|
const a = raw;
|
|
845
1007
|
const id = typeof a.id === "string" && a.id !== "" ? a.id : null;
|
|
@@ -861,20 +1023,20 @@ window.__ModuleLoader__.load({
|
|
|
861
1023
|
updatedAt: typeof a.updatedAt === "number" ? a.updatedAt : Date.now()
|
|
862
1024
|
};
|
|
863
1025
|
}
|
|
864
|
-
function loadDb() {
|
|
1026
|
+
function loadDb$1() {
|
|
865
1027
|
try {
|
|
866
|
-
const raw = window.localStorage.getItem(STORE_KEY);
|
|
867
|
-
if (raw === null) return emptyDb();
|
|
1028
|
+
const raw = window.localStorage.getItem(STORE_KEY$1);
|
|
1029
|
+
if (raw === null) return emptyDb$1();
|
|
868
1030
|
const parsed = JSON.parse(raw);
|
|
869
|
-
if (typeof parsed !== "object" || parsed === null) return emptyDb();
|
|
1031
|
+
if (typeof parsed !== "object" || parsed === null) return emptyDb$1();
|
|
870
1032
|
const docsRaw = parsed.docs;
|
|
871
|
-
if (typeof docsRaw !== "object" || docsRaw === null) return emptyDb();
|
|
1033
|
+
if (typeof docsRaw !== "object" || docsRaw === null) return emptyDb$1();
|
|
872
1034
|
const docs = {};
|
|
873
1035
|
for (const [key, list] of Object.entries(docsRaw)) {
|
|
874
1036
|
if (!Array.isArray(list)) continue;
|
|
875
1037
|
const clean = [];
|
|
876
1038
|
for (const item of list) {
|
|
877
|
-
const anno = sanitize(item);
|
|
1039
|
+
const anno = sanitize$1(item);
|
|
878
1040
|
if (anno !== null) clean.push(anno);
|
|
879
1041
|
}
|
|
880
1042
|
if (clean.length > 0) docs[key] = clean.sort((x, y) => x.start - y.start);
|
|
@@ -884,16 +1046,16 @@ window.__ModuleLoader__.load({
|
|
|
884
1046
|
docs
|
|
885
1047
|
};
|
|
886
1048
|
} catch {
|
|
887
|
-
return emptyDb();
|
|
1049
|
+
return emptyDb$1();
|
|
888
1050
|
}
|
|
889
1051
|
}
|
|
890
|
-
let db = loadDb();
|
|
1052
|
+
let db$1 = loadDb$1();
|
|
891
1053
|
const listeners = /* @__PURE__ */ new Set();
|
|
892
1054
|
/** 每个文档键的只读数组缓存:useSyncExternalStore 要求同一状态返回同一引用。 */
|
|
893
1055
|
const snapshots = /* @__PURE__ */ new Map();
|
|
894
|
-
function persist() {
|
|
1056
|
+
function persist$1() {
|
|
895
1057
|
try {
|
|
896
|
-
window.localStorage.setItem(STORE_KEY, JSON.stringify(db));
|
|
1058
|
+
window.localStorage.setItem(STORE_KEY$1, JSON.stringify(db$1));
|
|
897
1059
|
} catch {}
|
|
898
1060
|
}
|
|
899
1061
|
function emit() {
|
|
@@ -909,14 +1071,14 @@ window.__ModuleLoader__.load({
|
|
|
909
1071
|
function snapshot(doc) {
|
|
910
1072
|
const cached = snapshots.get(doc);
|
|
911
1073
|
if (cached !== void 0) return cached;
|
|
912
|
-
const list = db.docs[doc] ?? [];
|
|
1074
|
+
const list = db$1.docs[doc] ?? [];
|
|
913
1075
|
snapshots.set(doc, list);
|
|
914
1076
|
return list;
|
|
915
1077
|
}
|
|
916
1078
|
/** 跨标签页/窗口同步(同一浏览器开了两个「学习资料」时,一边标注另一边也刷新)。 */
|
|
917
1079
|
if (typeof window !== "undefined") window.addEventListener("storage", (e) => {
|
|
918
|
-
if (e.key !== STORE_KEY) return;
|
|
919
|
-
db = loadDb();
|
|
1080
|
+
if (e.key !== STORE_KEY$1) return;
|
|
1081
|
+
db$1 = loadDb$1();
|
|
920
1082
|
emit();
|
|
921
1083
|
});
|
|
922
1084
|
/** 订阅某文档的标注(组件内使用)。 */
|
|
@@ -935,21 +1097,21 @@ window.__ModuleLoader__.load({
|
|
|
935
1097
|
createdAt: now,
|
|
936
1098
|
updatedAt: now
|
|
937
1099
|
};
|
|
938
|
-
const list = db.docs[doc] ?? [];
|
|
939
|
-
const dup = list.find((a) => a.start === anno.start && a.end === anno.end && a.style === anno.style);
|
|
1100
|
+
const list = db$1.docs[doc] ?? [];
|
|
1101
|
+
const dup = list.find((a) => a.page === anno.page && a.start === anno.start && a.end === anno.end && a.style === anno.style);
|
|
940
1102
|
const next = dup === void 0 ? [...list, anno] : list.map((a) => a.id === dup.id ? {
|
|
941
1103
|
...a,
|
|
942
1104
|
...init,
|
|
943
1105
|
updatedAt: now
|
|
944
1106
|
} : a);
|
|
945
|
-
db = {
|
|
1107
|
+
db$1 = {
|
|
946
1108
|
v: 1,
|
|
947
1109
|
docs: {
|
|
948
|
-
...db.docs,
|
|
1110
|
+
...db$1.docs,
|
|
949
1111
|
[doc]: next.sort((x, y) => x.start - y.start)
|
|
950
1112
|
}
|
|
951
1113
|
};
|
|
952
|
-
persist();
|
|
1114
|
+
persist$1();
|
|
953
1115
|
emit();
|
|
954
1116
|
return dup === void 0 ? anno : {
|
|
955
1117
|
...dup,
|
|
@@ -1011,9 +1173,45 @@ window.__ModuleLoader__.load({
|
|
|
1011
1173
|
page
|
|
1012
1174
|
});
|
|
1013
1175
|
}
|
|
1176
|
+
/** 空白归一(PDF 文字层/课件 DOM 的换行与断行差异不影响「这是同一段原文」的判断)。 */
|
|
1177
|
+
function normalizeSpace(s) {
|
|
1178
|
+
return s.replace(/\s+/g, " ").trim();
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* 落一条 PDF 标注(页 + 页内偏移)。
|
|
1182
|
+
*
|
|
1183
|
+
* 去重规则(同一处只留一条):同页同起止,或同页同原文(空白归一后相等)都算同一条——
|
|
1184
|
+
* 后者用于「缩放/换版本后偏移变了、原文没变」的情形,否则同一段文字会越标越多条。
|
|
1185
|
+
*/
|
|
1186
|
+
function paintPageTextAnnotation(doc, anchor, color, style, note) {
|
|
1187
|
+
const needle = normalizeSpace(anchor.quote);
|
|
1188
|
+
const existing = snapshot(doc).find((a) => a.page === anchor.page && a.style === style && (a.start === anchor.start && a.end === anchor.end || normalizeSpace(a.quote) === needle));
|
|
1189
|
+
if (existing !== void 0) {
|
|
1190
|
+
updateAnnotation(doc, existing.id, {
|
|
1191
|
+
color,
|
|
1192
|
+
...note.trim() !== "" ? { note } : {}
|
|
1193
|
+
});
|
|
1194
|
+
return {
|
|
1195
|
+
...existing,
|
|
1196
|
+
color,
|
|
1197
|
+
...note.trim() !== "" ? { note } : {}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
return addAnnotation(doc, {
|
|
1201
|
+
start: anchor.start,
|
|
1202
|
+
end: anchor.end,
|
|
1203
|
+
quote: anchor.quote,
|
|
1204
|
+
prefix: anchor.prefix,
|
|
1205
|
+
suffix: anchor.suffix,
|
|
1206
|
+
color,
|
|
1207
|
+
style,
|
|
1208
|
+
note,
|
|
1209
|
+
page: anchor.page
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1014
1212
|
/** 修改一条标注(改色 / 切换形态 / 改便签)。 */
|
|
1015
1213
|
function updateAnnotation(doc, id, patch) {
|
|
1016
|
-
const list = db.docs[doc];
|
|
1214
|
+
const list = db$1.docs[doc];
|
|
1017
1215
|
if (list === void 0) return;
|
|
1018
1216
|
let touched = false;
|
|
1019
1217
|
const next = list.map((a) => {
|
|
@@ -1028,29 +1226,29 @@ window.__ModuleLoader__.load({
|
|
|
1028
1226
|
};
|
|
1029
1227
|
});
|
|
1030
1228
|
if (!touched) return;
|
|
1031
|
-
db = {
|
|
1229
|
+
db$1 = {
|
|
1032
1230
|
v: 1,
|
|
1033
1231
|
docs: {
|
|
1034
|
-
...db.docs,
|
|
1232
|
+
...db$1.docs,
|
|
1035
1233
|
[doc]: next
|
|
1036
1234
|
}
|
|
1037
1235
|
};
|
|
1038
|
-
persist();
|
|
1236
|
+
persist$1();
|
|
1039
1237
|
emit();
|
|
1040
1238
|
}
|
|
1041
1239
|
/** 删除一条标注。 */
|
|
1042
1240
|
function removeAnnotation(doc, id) {
|
|
1043
|
-
const list = db.docs[doc];
|
|
1241
|
+
const list = db$1.docs[doc];
|
|
1044
1242
|
if (list === void 0) return;
|
|
1045
1243
|
const next = list.filter((a) => a.id !== id);
|
|
1046
|
-
const docs = { ...db.docs };
|
|
1244
|
+
const docs = { ...db$1.docs };
|
|
1047
1245
|
if (next.length === 0) delete docs[doc];
|
|
1048
1246
|
else docs[doc] = next;
|
|
1049
|
-
db = {
|
|
1247
|
+
db$1 = {
|
|
1050
1248
|
v: 1,
|
|
1051
1249
|
docs
|
|
1052
1250
|
};
|
|
1053
|
-
persist();
|
|
1251
|
+
persist$1();
|
|
1054
1252
|
emit();
|
|
1055
1253
|
}
|
|
1056
1254
|
function colorDot(color) {
|
|
@@ -1065,11 +1263,11 @@ window.__ModuleLoader__.load({
|
|
|
1065
1263
|
* 导出为 Markdown。不传 `docs` 时导出全部文档;`docTitle` 用于把路径写成人类标题。
|
|
1066
1264
|
*/
|
|
1067
1265
|
function exportMarkdown(docs, titleOf) {
|
|
1068
|
-
const keys = docs ?? Object.keys(db.docs);
|
|
1266
|
+
const keys = docs ?? Object.keys(db$1.docs);
|
|
1069
1267
|
const lines = ["# 我的阅读标记与提问", ""];
|
|
1070
1268
|
let count = 0;
|
|
1071
1269
|
for (const key of keys) {
|
|
1072
|
-
const list = db.docs[key];
|
|
1270
|
+
const list = db$1.docs[key];
|
|
1073
1271
|
if (list === void 0 || list.length === 0) continue;
|
|
1074
1272
|
const title = titleOf === void 0 ? labelOfKey(key) : titleOf(key);
|
|
1075
1273
|
lines.push(`## ${title}`, "");
|
|
@@ -1182,13 +1380,13 @@ window.__ModuleLoader__.load({
|
|
|
1182
1380
|
for (const parent of parents) if (parent.nodeType === 1) parent.normalize();
|
|
1183
1381
|
}
|
|
1184
1382
|
/** 公共前后文长度(模糊定位打分)。 */
|
|
1185
|
-
function commonSuffixLength(a, b) {
|
|
1383
|
+
function commonSuffixLength$1(a, b) {
|
|
1186
1384
|
let n = 0;
|
|
1187
1385
|
const max = Math.min(a.length, b.length);
|
|
1188
1386
|
while (n < max && a[a.length - 1 - n] === b[b.length - 1 - n]) n++;
|
|
1189
1387
|
return n;
|
|
1190
1388
|
}
|
|
1191
|
-
function commonPrefixLength(a, b) {
|
|
1389
|
+
function commonPrefixLength$1(a, b) {
|
|
1192
1390
|
let n = 0;
|
|
1193
1391
|
const max = Math.min(a.length, b.length);
|
|
1194
1392
|
while (n < max && a[n] === b[n]) n++;
|
|
@@ -1206,7 +1404,7 @@ window.__ModuleLoader__.load({
|
|
|
1206
1404
|
tries++;
|
|
1207
1405
|
const before = text.slice(Math.max(0, at - anno.prefix.length), at);
|
|
1208
1406
|
const after = text.slice(at + anno.quote.length, at + anno.quote.length + anno.suffix.length);
|
|
1209
|
-
const score = commonSuffixLength(before, anno.prefix) + commonPrefixLength(after, anno.suffix);
|
|
1407
|
+
const score = commonSuffixLength$1(before, anno.prefix) + commonPrefixLength$1(after, anno.suffix);
|
|
1210
1408
|
if (best === null || score > best.score) best = {
|
|
1211
1409
|
start: at,
|
|
1212
1410
|
score
|
|
@@ -1709,7 +1907,7 @@ window.__ModuleLoader__.load({
|
|
|
1709
1907
|
* 由 scripts/gen-css-modules.mjs 从 src/client/styles.module.css 生成,**请勿手改**。
|
|
1710
1908
|
* 改样式请改源 CSS,然后跑:node scripts/gen-css-modules.mjs
|
|
1711
1909
|
*/
|
|
1712
|
-
const css$1 = "._0K34_a_root{flex-direction:column;flex:1 1 0;width:100%;min-height:0;display:flex;overflow:hidden}._0K34_a_topbar{background:#7f7f7f0a;border-bottom:1px solid #80808033;flex:none;align-items:center;gap:8px;min-height:33px;padding:4px 10px;font-size:13px;display:flex}._0K34_a_topbarTitle{flex:none;font-size:13px;font-weight:600}._0K34_a_crumbs{flex:1;align-items:center;gap:2px;min-width:0;display:flex;overflow:hidden}._0K34_a_crumb{color:#608cdcf2;white-space:nowrap}._0K34_a_crumbLast{color:inherit;font-weight:600}._0K34_a_crumbSep{color:#7f7f7fb3;flex:none;padding:0 1px}._0K34_a_tbBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:6px;flex:none;align-items:center;gap:5px;padding:4px 9px;font-size:12px;display:inline-flex}._0K34_a_tbBtn:hover{background:#7f7f7f1f}._0K34_a_tbBtn:focus-visible{border-color:#608cdccc;outline:none}._0K34_a_tweakWrap{flex:none;position:relative}._0K34_a_tweakMenu{z-index:30;background:#fff;border:1px solid #80808059;border-radius:10px;flex-direction:column;gap:2px;width:min(360px,80vw);max-height:60vh;padding:6px;display:flex;position:absolute;top:calc(100% + 6px);right:0;overflow:auto;box-shadow:0 10px 28px #0f19322e}._0K34_a_tweakItem{width:100%;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;align-items:center;gap:8px;padding:5px 8px;font-size:12px;display:flex}._0K34_a_tweakItem:hover:not(:disabled){background:#608cdc1f}._0K34_a_tweakItem:disabled{opacity:.55;cursor:default}._0K34_a_tweakItemId{color:#2a5cb0f2;flex:none;min-width:34px;font-weight:600}._0K34_a_tweakItemTitle{text-overflow:ellipsis;white-space:nowrap;opacity:.85;flex:1;min-width:0;overflow:hidden}._0K34_a_tweakMenuHint{color:#6e6e78e6;padding:4px 8px 2px;font-size:11px}._0K34_a_body{flex:1 1 0;min-height:0;display:flex}._0K34_a_sidebar{background:#7f7f7f0d;border-right:1px solid #80808033;flex-direction:column;flex:none;width:280px;min-height:0;display:flex}._0K34_a_sidebarHeader{border-bottom:1px solid #80808033;flex:none;padding:8px}._0K34_a_search{box-sizing:border-box;width:100%;color:inherit;background:0 0;border:1px solid #80808059;border-radius:6px;padding:6px 10px;font-size:13px}._0K34_a_search:focus{border-color:#608cdccc;outline:none}._0K34_a_tree{flex:1;min-height:0;padding:6px;overflow-y:auto}._0K34_a_group{margin:0}._0K34_a_groupLabel{cursor:pointer;color:inherit;user-select:none;border-radius:5px;align-items:center;gap:5px;padding:5px 6px;font-size:13px;font-weight:600;list-style:none;display:flex}._0K34_a_groupLabel::-webkit-details-marker{display:none}._0K34_a_groupLabel:hover{background:#7f7f7f1f}._0K34_a_chevron{color:#7f7f7fe6;flex:none;transition:transform .12s;display:inline-flex}._0K34_a_group[open]>._0K34_a_groupLabel ._0K34_a_chevron{transform:rotate(90deg)}._0K34_a_groupChildren{border-left:1px solid #80808026;margin-left:9px;padding-left:12px}._0K34_a_leaf{text-align:left;width:100%;color:inherit;cursor:pointer;background:0 0;border:none;border-radius:5px;align-items:center;gap:6px;margin:1px 0;padding:4px 6px;font-size:13px;display:flex;position:relative}._0K34_a_leaf:hover{background:#7f7f7f1f}._0K34_a_leafActive{background:#608cdc2e}._0K34_a_leafActive:before{content:\"\";background:#608cdce6;border-radius:2px;width:2.5px;position:absolute;top:5px;bottom:5px;left:0}._0K34_a_leaf:focus-visible{outline:none;box-shadow:inset 0 0 0 1px #608cdcb3}._0K34_a_leafText{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}._0K34_a_fileChip{text-align:center;letter-spacing:.02em;box-sizing:border-box;border-radius:4px;flex:none;min-width:24px;padding:0 2px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9px;font-weight:700;line-height:1.5}._0K34_a_chipMd{color:#608cdc;background:#608cdc38}._0K34_a_chipAdoc{color:#d29922;background:#d2992238}._0K34_a_chipYml{color:#966ec8;background:#966ec838}._0K34_a_chipTxt{color:#7f7f7ff2;background:#7f7f7f38}._0K34_a_chipSlides{color:#dca05a;background:#dca05a40}._0K34_a_chipPdf{color:#dc5a5a;background:#dc5a5a38}._0K34_a_chipOther{color:#7f7f7ff2;background:#7f7f7f2e}._0K34_a_flatList{flex-direction:column;display:flex}._0K34_a_main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}._0K34_a_content{flex:1;min-height:0;overflow:hidden}._0K34_a_empty{color:#7f7f7fcc;text-align:center;justify-content:center;align-items:center;height:100%;padding:24px;font-size:14px;display:flex}._0K34_a_gateBox{text-align:left;max-width:min(760px,100%)}._0K34_a_cmdList{flex-direction:column;gap:6px;margin-top:12px;display:flex}._0K34_a_cmdRow{background:#0000000f;border-radius:8px;align-items:center;gap:8px;padding:6px 8px;display:flex}._0K34_a_cmdLabel{color:#7f7f7ff2;flex:none;width:88px;font-size:11px;line-height:1.4}._0K34_a_cmdCode{min-width:0;color:inherit;word-break:break-all;user-select:all;cursor:text;flex:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.5}._0K34_a_cmdNote{opacity:.8;margin-top:10px;font-size:12px;line-height:1.6}._0K34_a_cmdNote code{word-break:break-all;background:#00000014;border-radius:4px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._0K34_a_loadingDot{background:currentColor;border-radius:50%;width:8px;height:8px;margin-right:8px;animation:1s ease-in-out infinite _0K34_a_aero-loading-pulse}@keyframes _0K34_a_aero-loading-pulse{0%,to{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}._0K34_a_docRendering{color:#7f7f7fbf;text-align:center;margin:12px 0 0;font-size:13px}._0K34_a_docSegBar{z-index:3;background:color-mix(in srgb, currentColor 6%, transparent);backdrop-filter:blur(6px);border:1px solid #7f7f7f40;border-radius:8px;flex-wrap:wrap;align-items:center;gap:8px;margin:-6px 0 12px;padding:6px 10px;font-size:12px;display:flex;position:sticky;top:0}._0K34_a_docSegBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #7f7f7f59;border-radius:6px;padding:3px 10px;font-size:12px}._0K34_a_docSegBtn:hover:not(:disabled){background:color-mix(in srgb, currentColor 8%, transparent);border-color:#7f7f7f99}._0K34_a_docSegBtn:disabled{opacity:.4;cursor:default}._0K34_a_docSegInfo{text-overflow:ellipsis;white-space:nowrap;opacity:.85;flex:auto;min-width:0;overflow:hidden}._0K34_a_docSegSelect{max-width:42%;color:inherit;background:0 0;border:1px solid #7f7f7f59;border-radius:6px;padding:3px 6px;font-size:12px}._0K34_a_docScroll{scroll-behavior:smooth;box-sizing:border-box;flex:auto;min-width:0;height:100%;min-height:0;padding:16px clamp(12px,2vw,26px) 40px;overflow-y:auto}._0K34_a_docWrap{align-items:stretch;height:100%;min-height:0;display:flex;position:relative;container-type:inline-size}._0K34_a_docMeta{text-align:center;margin:0 0 1.3em}._0K34_a_docMetaKicker{font-family:var(--dsw-font-family,inherit);letter-spacing:.2em;color:var(--aero-ink-3,#818b98);font-size:11px;font-weight:500}._0K34_a_docMetaChips{flex-wrap:wrap;justify-content:center;gap:6px 8px;margin-top:.6em;display:flex}._0K34_a_docMetaChip{border:1px solid var(--aero-rule,#8080804d);font-family:var(--dsw-font-family,inherit);color:var(--aero-ink-2,#57606a);white-space:nowrap;border-radius:999px;align-items:center;gap:5px;padding:1px 9px;font-size:11.5px;line-height:1.7;display:inline-flex}._0K34_a_docMetaChipLabel{color:var(--aero-ink-3,#818b98)}._0K34_a_docMetaChip[data-tone=draft]{border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f0b429) 45%, transparent);color:var(--dsw-alias-state-warn-label,#b7791f)}._0K34_a_docMetaChip[data-tone=reviewed]{border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary,#2f6feb) 45%, transparent);color:var(--dsw-alias-state-business-primary,#2f6feb)}._0K34_a_docMetaChip[data-tone=done]{border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary,#2f9e44) 45%, transparent);color:var(--dsw-alias-state-success-primary,#2f9e44)}._0K34_a_docMetaKeywords{font-family:var(--dsw-font-family,inherit);color:var(--aero-ink-3,#818b98);flex-wrap:wrap;justify-content:center;gap:8px;margin-top:.55em;font-size:11px;display:flex}._0K34_a_docMetaKeyword:before{content:\"#\";opacity:.7}._0K34_a_docOutline{border-left:1px solid var(--dsw-alias-border-l2,#80808033);background:color-mix(in srgb, var(--dsw-alias-bg-module-platform,#7f7f7f0f) 55%, transparent);box-sizing:border-box;flex:none;width:206px;height:100%;min-height:0;padding:12px 8px 32px 10px;font-size:12px;overflow-y:auto}._0K34_a_docOutlineHead{letter-spacing:.12em;color:var(--dsw-alias-label-tertiary,#818b98);align-items:center;gap:5px;padding:0 8px 8px;font-size:11px;font-weight:600;display:flex}._0K34_a_docOutlineList{flex-direction:column;gap:1px;margin:0;padding:0;list-style:none;display:flex}._0K34_a_docOutlineItem[data-level=\"1\"]{margin-top:8px}._0K34_a_docOutlineLink{width:100%;color:var(--dsw-alias-label-secondary,inherit);font:inherit;text-align:left;cursor:pointer;white-space:nowrap;text-overflow:ellipsis;background:0 0;border:none;border-radius:6px;padding:4px 8px;display:block;overflow:hidden}._0K34_a_docOutlineItem[data-level=\"2\"] ._0K34_a_docOutlineLink{padding-left:12px}._0K34_a_docOutlineItem[data-level=\"3\"] ._0K34_a_docOutlineLink{padding-left:22px}._0K34_a_docOutlineItem[data-level=\"4\"] ._0K34_a_docOutlineLink{padding-left:32px}._0K34_a_docOutlineNo{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#818b98);margin-right:3px;font-weight:600}._0K34_a_docOutlineLink:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);color:var(--dsw-alias-label-primary,inherit)}._0K34_a_docOutlineLinkActive{background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 14%, transparent);color:var(--dsw-alias-link,#4176e6);font-weight:600}@container (width<=860px){._0K34_a_docOutline{display:none}}._0K34_a_docLightbox{z-index:40;box-sizing:border-box;cursor:zoom-out;background:#0b0f17cc;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:22px;animation:.14s ease-out _0K34_a_aeroLightboxIn;display:flex;position:absolute;inset:0}@keyframes _0K34_a_aeroLightboxIn{0%{opacity:0}to{opacity:1}}._0K34_a_docLightboxFigure{flex-direction:column;align-items:center;gap:10px;max-width:100%;min-height:0;max-height:100%;margin:0;display:flex}._0K34_a_docLightboxFigure img{object-fit:contain;background:#fff;border-radius:6px;max-width:100%;min-height:0;max-height:100%;box-shadow:0 24px 64px -24px #000000b3}._0K34_a_docLightboxCap{color:#ffffffe0;text-align:center;max-width:min(880px,82%);font-size:12.5px;line-height:1.65}._0K34_a_docLightboxHint{color:#fff9;flex:none;align-items:center;gap:5px;font-size:11.5px;display:flex}._0K34_a_slideRoot{flex-direction:column;height:100%;min-height:0;display:flex}._0K34_a_slideStage{box-sizing:border-box;flex:1;justify-content:center;align-items:flex-start;min-height:0;padding:20px 28px;display:flex;overflow:auto}._0K34_a_slideBar{border-top:1px solid #80808033;flex:none;justify-content:center;align-items:center;gap:12px;padding:8px;display:flex}._0K34_a_slideCounter{color:#7f7f7fe6;text-align:center;min-width:60px;font-size:13px}._0K34_a_contextBar{background:#7f7f7f0a;border-top:1px solid #80808033;flex:none;align-items:center;gap:8px;padding:8px;display:flex}._0K34_a_contextInfo{flex:none;align-items:center;gap:6px;min-width:0;max-width:260px;display:flex}._0K34_a_contextDot{background:#5ac878f2;border-radius:50%;flex:none;width:8px;height:8px}._0K34_a_contextText{white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}._0K34_a_contextMuted{color:#7f7f7fcc;font-size:12px}._0K34_a_questionInput{min-width:0;color:inherit;background:0 0;border:1px solid #80808059;border-radius:6px;flex:1;padding:6px 10px;font-size:13px}._0K34_a_questionInput:focus{border-color:#608cdccc;outline:none}._0K34_a_btn,._0K34_a_btnPrimary{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:6px;flex:none;padding:6px 12px;font-size:13px}._0K34_a_btn:hover{background:#7f7f7f1f}._0K34_a_btn:disabled,._0K34_a_btnPrimary:disabled{opacity:.45;cursor:not-allowed}._0K34_a_btnPrimary{color:#fff;background:#608cdce6;border-color:#608cdce6}._0K34_a_btnPrimary:hover:not(:disabled){background:#608cdc}._0K34_a_deckRoot{flex-direction:column;flex:1 1 0;width:100%;height:100%;min-height:0;display:flex}._0K34_a_deckToolbar{border-bottom:1px solid #80808038;flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:7px 10px;display:flex}._0K34_a_deckTitle{text-overflow:ellipsis;white-space:nowrap;max-width:42%;font-size:13px;font-weight:600;overflow:hidden}._0K34_a_deckStatus{color:#7f7f7ff2;align-items:center;gap:6px;font-size:12px;display:inline-flex}._0K34_a_deckHint{color:#7f7f7fbf;margin-left:2px;font-size:11px}._0K34_a_deckSpacer{flex:1 1 0}._0K34_a_deckBody{flex:1 1 0;min-height:0;position:relative}._0K34_a_deckFrame{background:#fff;border:0;width:100%;height:100%;display:block}._0K34_a_deckNotice{color:#7f7f7ff2;flex-direction:column;justify-content:center;align-items:center;gap:10px;height:100%;padding:24px;font-size:13px;display:flex;overflow:auto}._0K34_a_deckErrorTitle{color:inherit;font-size:14px;font-weight:600}._0K34_a_deckErrorMsg{text-align:center;max-width:640px}._0K34_a_deckLog{text-align:left;white-space:pre-wrap;word-break:break-all;background:#0000000f;border-radius:8px;max-width:min(720px,100%);max-height:220px;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;overflow:auto}._0K34_a_spinner{border:2px solid #7f7f7f59;border-top-color:#608cdce6;border-radius:50%;flex:none;width:14px;height:14px;animation:.9s linear infinite _0K34_a_aeroSpin;display:inline-block}@keyframes _0K34_a_aeroSpin{to{transform:rotate(360deg)}}._0K34_a_deckNoticeScroll{box-sizing:border-box;color:#7f7f7ff2;flex-direction:column;align-items:center;gap:12px;height:100%;padding:20px 24px 32px;font-size:13px;display:flex;overflow-y:auto}._0K34_a_deckNoticeHead{text-align:left;width:100%;max-width:720px;line-height:1.6}._0K34_a_deckNoticeBtns{align-items:center;gap:8px;width:100%;max-width:720px;display:flex}._0K34_a_envRoot{box-sizing:border-box;width:100%;max-width:720px}._0K34_a_envCard{text-align:left;background:#7f7f7f0d;border:1px solid #80808047;border-radius:10px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}._0K34_a_envHead{align-items:center;gap:8px;display:flex}._0K34_a_envTitle{color:inherit;font-size:13px;font-weight:600}._0K34_a_envSummaryOk,._0K34_a_envSummaryBad{font-size:13px;font-weight:600;line-height:1.5}._0K34_a_envSummaryOk{color:#46aa6ef2}._0K34_a_envSummaryBad{color:#d75f5ff2}._0K34_a_envSummaryDetail{color:#7f7f7ff2;word-break:break-all;margin-top:4px;font-size:12px;font-weight:400}._0K34_a_envMuted{color:#7f7f7fd9;align-items:center;gap:6px;font-size:12px;display:flex}._0K34_a_envError{color:#d75f5ff2;word-break:break-all;font-size:12px}._0K34_a_envList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}._0K34_a_envItem{align-items:flex-start;gap:8px;display:flex}._0K34_a_envDotOk,._0K34_a_envDotWarn,._0K34_a_envDotFail{color:#fff;border-radius:50%;flex:none;justify-content:center;align-items:center;width:16px;height:16px;margin-top:1px;font-size:10px;font-weight:700;display:inline-flex}._0K34_a_envDotOk{background:#46aa6ee6}._0K34_a_envDotWarn{background:#dca550f2}._0K34_a_envDotFail{background:#d75f5ff2}._0K34_a_envItemBody{flex-direction:column;gap:2px;min-width:0;display:flex}._0K34_a_envItemLabel{color:inherit;align-items:center;gap:6px;font-size:12.5px;font-weight:600;display:flex}._0K34_a_envItemStatus{color:#7f7f7ff2;border:1px solid #80808059;border-radius:999px;padding:1px 5px;font-size:10px;font-weight:500}._0K34_a_envItemDetail{color:#7f7f7ff2;word-break:break-all;font-size:12px}._0K34_a_envItemHint{color:#7f7f7fd9;word-break:break-all;font-size:12px}._0K34_a_envActions{border-top:1px dashed #8080804d;flex-direction:column;gap:8px;padding-top:10px;display:flex}._0K34_a_envActionsTitle{color:inherit;font-size:12.5px;font-weight:600}._0K34_a_envAction{background:#7f7f7f0a;border:1px solid #8080803d;border-radius:8px;align-items:center;gap:10px;padding:8px 10px;display:flex}._0K34_a_envActionInfo{flex-direction:column;flex:auto;gap:3px;min-width:0;display:flex}._0K34_a_envActionLabel{color:inherit;font-size:12.5px;font-weight:600}._0K34_a_envActionCmd{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._0K34_a_envActionCmd code{word-break:break-all;background:#00000014;border-radius:5px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._0K34_a_envCopy{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:5px;flex:none;align-items:center;gap:4px;padding:2px 7px;font-size:11px;display:inline-flex}._0K34_a_envCopy:hover{background:#7f7f7f1f}._0K34_a_envDetails summary{cursor:pointer;color:#7f7f7ff2;font-size:12px}._0K34_a_prepContent{flex:1;min-height:0;display:flex}._0K34_a_prepPane{flex-direction:column;flex:1 1 0;min-width:0;min-height:0;display:flex;position:relative}._0K34_a_prepPane+._0K34_a_prepPane{border-left:1px solid #80808038}._0K34_a_prepPaneHead{color:#7f7f7ff2;background:#7f7f7f0a;border-bottom:1px solid #8080802e;flex:none;align-items:center;gap:8px;padding:5px 10px;font-size:12px;display:flex}._0K34_a_prepPanePath{text-overflow:ellipsis;white-space:nowrap;opacity:.85;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;overflow:hidden}._0K34_a_prepEditor{box-sizing:border-box;resize:none;width:100%;min-height:0;color:inherit;tab-size:4;white-space:pre;background:0 0;border:0;outline:none;flex:1;padding:12px 14px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.6;overflow:auto}._0K34_a_prepDocPane{flex:1;min-height:0;display:flex}._0K34_a_prepDocPane>._0K34_a_docWrap{flex:auto;height:auto}._0K34_a_prepSlidesScroll{box-sizing:border-box;flex-direction:column;flex:1;align-items:center;gap:14px;min-height:0;padding:16px;display:flex;overflow-y:auto}._0K34_a_prepSlide{box-sizing:border-box;background:#7f7f7f0d;border:1px solid #80808040;border-radius:10px;width:min(760px,100%);padding:20px 26px;box-shadow:0 1px 3px #0000001f}._0K34_a_prepSlideNo{color:#7f7f7fb3;margin-bottom:6px;font-size:11px}._0K34_a_prepCmdBar{background:#7f7f7f0a;border-top:1px solid #80808033;flex-direction:column;flex:none;gap:6px;padding:7px 10px;display:flex}._0K34_a_prepSkills{flex:none;gap:6px;display:flex;overflow-x:auto}._0K34_a_skillBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:999px;flex:none;align-items:center;gap:5px;padding:4px 10px;font-size:12px;display:inline-flex}._0K34_a_skillBtn:hover:not(:disabled){background:#7f7f7f1f;border-color:#608cdcb3}._0K34_a_skillBtn:disabled{opacity:.4;cursor:not-allowed}._0K34_a_prepCmdRow{align-items:center;gap:8px;display:flex}._0K34_a_prepMode{border:1px solid #80808059;border-radius:6px;flex:none;display:inline-flex;overflow:hidden}._0K34_a_mode,._0K34_a_modeActive{color:inherit;cursor:pointer;background:0 0;border:none;padding:4px 9px;font-size:12px}._0K34_a_mode:hover{background:#7f7f7f1f}._0K34_a_modeActive{color:#fff;background:#608cdcd9}._0K34_a_saveDot{border-radius:50%;flex:none;width:8px;height:8px}._0K34_a_dotIdle{background:#7f7f7f80}._0K34_a_dotDirty{background:#dca05af2}._0K34_a_dotSaving{background:#608cdce6}._0K34_a_dotSaved{background:#5ac878f2}._0K34_a_dotError{background:#dc5a5af2}._0K34_a_saveLabel{white-space:nowrap;text-overflow:ellipsis;color:#7f7f7ff2;max-width:340px;font-size:12px;overflow:hidden}._0K34_a_tbBtnOn{color:#2a5cb0fa;background:#608cdc24;border-color:#608cdcbf}._0K34_a_readerWrap{flex:none;position:relative}._0K34_a_menuBackdrop{z-index:29;position:fixed;inset:0}._0K34_a_readerMenu{z-index:30;background:var(--dsw-alias-bg-layer-1,#fff);width:min(328px,86vw);color:var(--dsw-alias-label-primary,inherit);border:1px solid #80808059;border-radius:10px;flex-direction:column;gap:8px;padding:10px;display:flex;position:absolute;top:calc(100% + 6px);right:0;box-shadow:0 10px 28px #0f193238}._0K34_a_readerMenuHead{letter-spacing:.04em;opacity:.9;align-items:center;gap:6px;padding:0 2px 2px;font-size:12px;font-weight:600;display:flex}._0K34_a_readerRow{align-items:center;gap:8px;display:flex}._0K34_a_readerRowLabel{opacity:.75;flex:none;width:34px;font-size:12px}._0K34_a_readerSeg{flex-wrap:wrap;flex:1;align-items:center;gap:3px;min-width:0;display:flex}._0K34_a_readerSegBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808052;border-radius:6px;padding:3px 8px;font-size:12px;line-height:1.5}._0K34_a_readerSegBtn:hover:not(:disabled){background:#7f7f7f1f}._0K34_a_readerSegBtn:disabled{opacity:.45;cursor:default}._0K34_a_readerSegBtnOn{color:#2a5cb0fa;background:#608cdc29;border-color:#608cdccc;font-weight:600}._0K34_a_readerFontValue{text-align:center;font-variant-numeric:tabular-nums;opacity:.85;min-width:46px;font-size:12px}._0K34_a_readerHint{opacity:.7;border-top:1px dashed #8080804d;padding:4px 2px 0;font-size:11px;line-height:1.6}._0K34_a_root[data-zen=\"1\"] ._0K34_a_sidebar,._0K34_a_root[data-zen=\"1\"] ._0K34_a_contextBar,._0K34_a_root[data-zen=\"1\"] ._0K34_a_prepCmdBar,._0K34_a_root[data-sidebar=\"0\"] ._0K34_a_sidebar{display:none}._0K34_a_root:fullscreen{background:var(--dsw-alias-bg-layer-1,#fff);height:100vh}._0K34_a_root[data-fullscreen=\"1\"]{position:relative}._0K34_a_root[data-fullscreen=\"1\"] ._0K34_a_topbar{z-index:45;background:var(--dsw-alias-bg-layer-1,#fff);transition:transform .18s,opacity .18s;position:absolute;top:0;left:0;right:0;box-shadow:0 6px 16px -12px #0f193280}._0K34_a_root[data-fullscreen=\"1\"][data-fsbar=hidden] ._0K34_a_topbar{opacity:0;transform:translateY(-100%)}._0K34_a_root[data-fullscreen=\"1\"][data-fsbar=hidden]:after{content:\"\";pointer-events:none;z-index:46;background:linear-gradient(#608cdc59,#0000);height:3px;position:absolute;top:0;left:0;right:0}@media (hover:none){._0K34_a_root[data-fullscreen=\"1\"][data-fsbar=hidden] ._0K34_a_topbar{opacity:1;transform:none}}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutline{border-left-color:var(--aero-rule-ov,#80808033);background:color-mix(in srgb, var(--aero-paper-ov,transparent) 78%, transparent)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineHead{color:var(--aero-ink-3-ov,#818b98)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineLink{color:var(--aero-ink-2-ov,inherit)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineLink:hover{background:var(--aero-hover-ov,#7f7f7f1f);color:var(--aero-ink-ov,inherit)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineLinkActive{background:color-mix(in srgb, var(--aero-accent-ink-ov,#4176e6) 18%, transparent);color:var(--aero-accent-ink-ov,#4176e6)}@media (width<=520px){._0K34_a_readerMenu{width:min(300px,92vw)}._0K34_a_readerRowLabel{width:28px}}._0K34_a_annoColors{align-items:center;gap:5px;display:inline-flex}._0K34_a_annoColorBtn{cursor:pointer;background:#ffe066;border:1px solid #80808073;border-radius:50%;width:18px;height:18px;padding:0;transition:transform .1s;box-shadow:inset 0 0 0 2px #ffffff73}._0K34_a_annoColorBtn[data-color=yellow]{background:#ffe066}._0K34_a_annoColorBtn[data-color=green]{background:#a6e3a1}._0K34_a_annoColorBtn[data-color=blue]{background:#93c9f7}._0K34_a_annoColorBtn[data-color=pink]{background:#f7a8c4}._0K34_a_annoColorBtn:hover{transform:scale(1.14)}._0K34_a_annoColorBtn[data-on=\"1\"]{border-color:var(--dsw-alias-link,#4176e6);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-link,#4176e6) 45%, transparent)}._0K34_a_annoBar{z-index:46;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;border:1px solid #80808052;border-radius:10px;align-items:center;gap:6px;padding:6px 8px;font-size:12px;display:flex;position:fixed;box-shadow:0 10px 26px #0f19323d}._0K34_a_annoBarSep{background:#8080804d;width:1px;height:18px}._0K34_a_annoBarBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;align-items:center;gap:4px;padding:3px 7px;font-size:12px;line-height:1.5;display:inline-flex}._0K34_a_annoBarBtn:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);border-color:#80808052}._0K34_a_annoBarIcon{width:22px;height:22px;color:inherit;opacity:.6;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}._0K34_a_annoBarIcon:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_annoPop{z-index:47;background:var(--dsw-alias-bg-layer-1,#fff);width:340px;max-width:calc(100vw - 16px);color:var(--dsw-alias-label-primary,inherit);border:1px solid #80808052;border-radius:12px;flex-direction:column;gap:8px;padding:10px;font-size:12px;display:flex;position:fixed;box-shadow:0 16px 40px #0f193242}._0K34_a_annoPopHead{align-items:center;gap:6px;display:flex}._0K34_a_annoPopDot{background:#ffe066;border-radius:50%;flex:none;width:10px;height:10px;box-shadow:0 0 0 1px #80808059}._0K34_a_annoPopDot[data-color=yellow]{background:#ffe066}._0K34_a_annoPopDot[data-color=green]{background:#a6e3a1}._0K34_a_annoPopDot[data-color=blue]{background:#93c9f7}._0K34_a_annoPopDot[data-color=pink]{background:#f7a8c4}._0K34_a_annoPopKicker{letter-spacing:.02em;font-weight:600}._0K34_a_annoPopSpacer{flex:1 1 0}._0K34_a_annoPopIcon{width:22px;height:22px;color:inherit;opacity:.6;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}._0K34_a_annoPopIcon:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_annoPopQuote{background:var(--dsw-alias-bg-module-platform,#7f7f7f12);opacity:.92;border-left:3px solid #80808059;border-radius:4px;max-height:96px;margin:0;padding:7px 9px;font-size:12px;line-height:1.65;overflow-y:auto}._0K34_a_annoPopRow{align-items:center;gap:10px;display:flex}._0K34_a_annoPopSeg{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808052;border-radius:6px;align-items:center;gap:4px;margin-left:auto;padding:3px 8px;font-size:12px;display:inline-flex}._0K34_a_annoPopSegOn{border-color:var(--dsw-alias-link,#4176e6);background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 14%, transparent)}._0K34_a_annoPopNote{box-sizing:border-box;width:100%;color:inherit;font:inherit;resize:vertical;background:0 0;border:1px solid #80808052;border-radius:8px;min-height:62px;padding:7px 9px;font-size:12.5px;line-height:1.65}._0K34_a_annoPopNote:focus{border-color:var(--dsw-alias-link,#4176e6);outline:none}._0K34_a_annoPopFoot{align-items:center;gap:8px;display:flex}._0K34_a_annoPopPrimary{background:var(--dsw-alias-link,#4176e6);color:#fff;cursor:pointer;border:1px solid #0000;border-radius:7px;padding:4px 14px;font-size:12px}._0K34_a_annoPopPrimary:hover{filter:brightness(1.06)}._0K34_a_annoPopBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808052;border-radius:7px;align-items:center;gap:4px;padding:4px 10px;font-size:12px;display:inline-flex}._0K34_a_annoPopBtn:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_annoPopHint{opacity:.55;font-size:11px}._0K34_a_annoFab{z-index:30;background:var(--dsw-alias-bg-layer-1,#fff);width:38px;height:38px;color:var(--dsw-alias-label-secondary,inherit);cursor:pointer;border:1px solid #8080804d;border-radius:50%;justify-content:center;align-items:center;padding:0;transition:transform .12s,color .12s;display:inline-flex;position:absolute;bottom:16px;box-shadow:0 8px 22px #0f193233}._0K34_a_annoFab:hover{color:var(--dsw-alias-label-primary,inherit);transform:translateY(-1px)}._0K34_a_annoFabOn{border-color:var(--dsw-alias-link,#4176e6);color:var(--dsw-alias-link,#4176e6)}._0K34_a_annoFabBadge{box-sizing:border-box;color:#fff;text-align:center;font-variant-numeric:tabular-nums;background:#f0a020;border-radius:8px;min-width:16px;height:16px;padding:0 3px;font-size:10px;line-height:16px;position:absolute;top:-5px;right:-4px}._0K34_a_annoFabDot{width:7px;height:7px;box-shadow:0 0 0 1.5px var(--dsw-alias-bg-layer-1,#fff);background:#d94f8a;border-radius:50%;position:absolute;bottom:3px;right:3px}._0K34_a_annoPins{z-index:4;pointer-events:none;position:absolute;inset:0}._0K34_a_annoPin{color:#3c2d00e6;font-variant-numeric:tabular-nums;cursor:pointer;pointer-events:auto;background:#ffe066;border:1px solid #ffffffd9;border-radius:50%;width:19px;height:19px;padding:0;font-size:10px;line-height:1;position:absolute;box-shadow:0 2px 6px #0f193238}._0K34_a_annoPin[data-color=yellow]{color:#4a3800;background:#ffdf5c}._0K34_a_annoPin[data-color=green]{color:#1f3d18;background:#a6e3a1}._0K34_a_annoPin[data-color=blue]{color:#10334f;background:#93c9f7}._0K34_a_annoPin[data-color=pink]{color:#4a1330;background:#f7a8c4}._0K34_a_annoPin:hover{transform:scale(1.12)}._0K34_a_docMarks{box-sizing:border-box;border-left:1px solid var(--dsw-alias-border-l2,#80808033);background:color-mix(in srgb, var(--dsw-alias-bg-module-platform,#7f7f7f0f) 55%, transparent);flex-direction:column;flex:none;width:300px;height:100%;min-height:0;padding:10px 10px 8px;font-size:12px;display:flex}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docMarks{border-left-color:var(--aero-rule-ov,#80808033);background:color-mix(in srgb, var(--aero-paper-ov,transparent) 82%, transparent);color:var(--aero-ink-ov,inherit)}._0K34_a_docMarksHead{letter-spacing:.1em;color:var(--dsw-alias-label-tertiary,#818b98);align-items:center;gap:6px;padding:0 0 6px;font-size:11px;font-weight:600;display:flex}._0K34_a_docMarksCount{background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 18%, transparent);color:var(--dsw-alias-link,#4176e6);letter-spacing:0;border-radius:8px;padding:0 6px;font-size:10.5px}._0K34_a_docMarksHead ._0K34_a_annoPopIcon{margin-left:auto}._0K34_a_docMarksTitle{text-overflow:ellipsis;white-space:nowrap;opacity:.9;padding:0 0 8px;font-size:12px;font-weight:600;overflow:hidden}._0K34_a_docMarksFilters{border-bottom:1px dashed #8080804d;flex-wrap:wrap;gap:4px;padding-bottom:8px;display:flex}._0K34_a_docMarksChip{color:inherit;cursor:pointer;opacity:.8;background:0 0;border:1px solid #80808047;border-radius:999px;padding:2px 8px;font-size:11px}._0K34_a_docMarksChip:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_docMarksChipOn{border-color:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 65%, transparent);background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 14%, transparent);color:var(--dsw-alias-link,#4176e6);opacity:1}._0K34_a_docMarksList{flex-direction:column;flex:auto;gap:6px;min-height:0;padding:8px 2px 8px 0;display:flex;overflow-y:auto}._0K34_a_docMarksEmpty{opacity:.65;padding:10px 6px;font-size:11.5px;line-height:1.75}._0K34_a_docMarksItem{border:1px solid var(--dsw-alias-border-l2,#80808038);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#fff) 70%, transparent);border-left-width:3px;border-radius:8px;flex-direction:column;gap:4px;padding:6px 8px;display:flex}._0K34_a_docMarksItem[data-color=yellow]{border-left-color:#f0c400}._0K34_a_docMarksItem[data-color=green]{border-left-color:#4caf50}._0K34_a_docMarksItem[data-color=blue]{border-left-color:#2b7cd3}._0K34_a_docMarksItem[data-color=pink]{border-left-color:#d94f8a}._0K34_a_docMarksQuoteBtn{color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;flex-direction:column;gap:3px;padding:0;display:flex}._0K34_a_docMarksQuoteBtn:hover ._0K34_a_docMarksQuote{color:var(--dsw-alias-link,#4176e6)}._0K34_a_docMarksItemHead{align-items:center;gap:5px;display:flex}._0K34_a_docMarksItemKind{letter-spacing:.04em;opacity:.6;font-size:10.5px}._0K34_a_docMarksQuote{-webkit-line-clamp:3;-webkit-box-orient:vertical;font-size:12px;line-height:1.6;display:-webkit-box;overflow:hidden}._0K34_a_docMarksNote{-webkit-line-clamp:3;background:#f7a8c42e;border-radius:4px;-webkit-box-orient:vertical;margin-top:2px;padding:3px 6px;font-size:11.5px;line-height:1.6;display:-webkit-box;overflow:hidden}._0K34_a_docMarksItemFoot{justify-content:flex-end;gap:6px;display:flex}._0K34_a_docMarksMini{color:inherit;cursor:pointer;opacity:.8;background:0 0;border:1px solid #8080804d;border-radius:5px;padding:1px 7px;font-size:10.5px}._0K34_a_docMarksMini:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_docMarksFoot{border-top:1px dashed #8080804d;align-items:center;gap:6px;padding-top:6px;display:flex}._0K34_a_docMarksFootBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #8080804d;border-radius:6px;padding:3px 9px;font-size:11px}._0K34_a_docMarksFootBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_docMarksFootBtn:disabled{opacity:.4;cursor:default}._0K34_a_docMarksFootHint{opacity:.55;margin-left:auto;font-size:10.5px}._0K34_a_deckBody[data-panel=\"1\"]{align-items:stretch;display:flex}._0K34_a_deckBody[data-panel=\"1\"] ._0K34_a_deckFrame{flex:auto;width:auto;min-width:0}._0K34_a_deckBody[data-panel=\"1\"]>aside{flex:none}._0K34_a_docResume{z-index:7;border:1px solid var(--aero-rule,var(--dsw-alias-border-l2,#8080804d));background:var(--dsw-alias-bg-layer-1,#fff);max-width:min(600px,100% - 28px);color:var(--aero-ink,inherit);font-family:var(--dsw-font-family,inherit);border-radius:999px;align-items:center;gap:6px;padding:5px 6px 5px 12px;font-size:12px;line-height:1.4;animation:.16s ease-out _0K34_a_aero-resume-in;display:flex;position:absolute;top:10px;left:50%;transform:translate(-50%);box-shadow:0 6px 18px #0000001a}._0K34_a_docWrap[data-segbar=\"1\"] ._0K34_a_docResume{top:54px}._0K34_a_docWrap[data-paper=sepia] ._0K34_a_docResume{color:#3a3126;background:#f7efdd;border-color:#604c3047}._0K34_a_docWrap[data-paper=green] ._0K34_a_docResume{color:#22301d;background:#e7efe2;border-color:#3c5a2d47}._0K34_a_docWrap[data-paper=dark] ._0K34_a_docResume{color:#dfe3e8;background:#232830;border-color:#ffffff29}._0K34_a_docResumeText{white-space:nowrap;text-overflow:ellipsis;opacity:.92;align-items:baseline;gap:6px;min-width:0;display:inline-flex;overflow:hidden}._0K34_a_docResumeName{text-overflow:ellipsis;font-weight:600;overflow:hidden}._0K34_a_docResumeAge{opacity:.6;flex:none}._0K34_a_docResumeBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #7f7f7f59;border-radius:999px;flex:none;padding:2px 9px;font-family:inherit;font-size:11.5px}._0K34_a_docResumeBtn:hover{background:color-mix(in srgb, currentColor 8%, transparent);border-color:#7f7f7f99}._0K34_a_docResumeClose{width:18px;height:18px;color:inherit;opacity:.55;cursor:pointer;background:0 0;border:0;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._0K34_a_docResumeClose:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}@keyframes _0K34_a_aero-resume-in{0%{opacity:0;transform:translate(-50%,-5px)}to{opacity:1;transform:translate(-50%)}}@container (width<=860px){._0K34_a_docMarks{width:252px}}@media print{._0K34_a_annoFab,._0K34_a_annoPins,._0K34_a_docMarks,._0K34_a_annoBar,._0K34_a_annoPop,._0K34_a_docResume{display:none!important}}";
|
|
1910
|
+
const css$1 = "._0K34_a_root{flex-direction:column;flex:1 1 0;width:100%;min-height:0;display:flex;overflow:hidden}._0K34_a_topbar{background:#7f7f7f0a;border-bottom:1px solid #80808033;flex:none;align-items:center;gap:8px;min-height:33px;padding:4px 10px;font-size:13px;display:flex}._0K34_a_topbarTitle{flex:none;font-size:13px;font-weight:600}._0K34_a_crumbs{flex:1;align-items:center;gap:2px;min-width:0;display:flex;overflow:hidden}._0K34_a_crumb{color:#608cdcf2;white-space:nowrap}._0K34_a_crumbLast{color:inherit;font-weight:600}._0K34_a_crumbSep{color:#7f7f7fb3;flex:none;padding:0 1px}._0K34_a_tbBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:6px;flex:none;align-items:center;gap:5px;padding:4px 9px;font-size:12px;display:inline-flex}._0K34_a_tbBtn:hover{background:#7f7f7f1f}._0K34_a_tbBtn:focus-visible{border-color:#608cdccc;outline:none}._0K34_a_tweakWrap{flex:none;position:relative}._0K34_a_tweakMenu{z-index:30;background:#fff;border:1px solid #80808059;border-radius:10px;flex-direction:column;gap:2px;width:min(360px,80vw);max-height:60vh;padding:6px;display:flex;position:absolute;top:calc(100% + 6px);right:0;overflow:auto;box-shadow:0 10px 28px #0f19322e}._0K34_a_tweakItem{width:100%;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;align-items:center;gap:8px;padding:5px 8px;font-size:12px;display:flex}._0K34_a_tweakItem:hover:not(:disabled){background:#608cdc1f}._0K34_a_tweakItem:disabled{opacity:.55;cursor:default}._0K34_a_tweakItemId{color:#2a5cb0f2;flex:none;min-width:34px;font-weight:600}._0K34_a_tweakItemTitle{text-overflow:ellipsis;white-space:nowrap;opacity:.85;flex:1;min-width:0;overflow:hidden}._0K34_a_tweakMenuHint{color:#6e6e78e6;padding:4px 8px 2px;font-size:11px}._0K34_a_body{flex:1 1 0;min-height:0;display:flex}._0K34_a_sidebar{background:#7f7f7f0d;border-right:1px solid #80808033;flex-direction:column;flex:none;width:280px;min-height:0;display:flex}._0K34_a_sidebarHeader{border-bottom:1px solid #80808033;flex:none;padding:8px}._0K34_a_search{box-sizing:border-box;width:100%;color:inherit;background:0 0;border:1px solid #80808059;border-radius:6px;padding:6px 10px;font-size:13px}._0K34_a_search:focus{border-color:#608cdccc;outline:none}._0K34_a_tree{flex:1;min-height:0;padding:6px;overflow-y:auto}._0K34_a_group{margin:0}._0K34_a_groupLabel{cursor:pointer;color:inherit;user-select:none;border-radius:5px;align-items:center;gap:5px;padding:5px 6px;font-size:13px;font-weight:600;list-style:none;display:flex}._0K34_a_groupLabel::-webkit-details-marker{display:none}._0K34_a_groupLabel:hover{background:#7f7f7f1f}._0K34_a_chevron{color:#7f7f7fe6;flex:none;transition:transform .12s;display:inline-flex}._0K34_a_group[open]>._0K34_a_groupLabel ._0K34_a_chevron{transform:rotate(90deg)}._0K34_a_groupChildren{border-left:1px solid #80808026;margin-left:9px;padding-left:12px}._0K34_a_leaf{text-align:left;width:100%;color:inherit;cursor:pointer;background:0 0;border:none;border-radius:5px;align-items:center;gap:6px;margin:1px 0;padding:4px 6px;font-size:13px;display:flex;position:relative}._0K34_a_leaf:hover{background:#7f7f7f1f}._0K34_a_leafActive{background:#608cdc2e}._0K34_a_leafActive:before{content:\"\";background:#608cdce6;border-radius:2px;width:2.5px;position:absolute;top:5px;bottom:5px;left:0}._0K34_a_leaf:focus-visible{outline:none;box-shadow:inset 0 0 0 1px #608cdcb3}._0K34_a_leafText{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}._0K34_a_fileChip{text-align:center;letter-spacing:.02em;box-sizing:border-box;border-radius:4px;flex:none;min-width:24px;padding:0 2px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9px;font-weight:700;line-height:1.5}._0K34_a_chipMd{color:#608cdc;background:#608cdc38}._0K34_a_chipAdoc{color:#d29922;background:#d2992238}._0K34_a_chipYml{color:#966ec8;background:#966ec838}._0K34_a_chipTxt{color:#7f7f7ff2;background:#7f7f7f38}._0K34_a_chipSlides{color:#dca05a;background:#dca05a40}._0K34_a_chipPdf{color:#dc5a5a;background:#dc5a5a38}._0K34_a_chipOther{color:#7f7f7ff2;background:#7f7f7f2e}._0K34_a_flatList{flex-direction:column;display:flex}._0K34_a_main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}._0K34_a_content{flex:1;min-height:0;overflow:hidden}._0K34_a_empty{color:#7f7f7fcc;text-align:center;justify-content:center;align-items:center;height:100%;padding:24px;font-size:14px;display:flex}._0K34_a_gateBox{text-align:left;max-width:min(760px,100%)}._0K34_a_cmdList{flex-direction:column;gap:6px;margin-top:12px;display:flex}._0K34_a_cmdRow{background:#0000000f;border-radius:8px;align-items:center;gap:8px;padding:6px 8px;display:flex}._0K34_a_cmdLabel{color:#7f7f7ff2;flex:none;width:88px;font-size:11px;line-height:1.4}._0K34_a_cmdCode{min-width:0;color:inherit;word-break:break-all;user-select:all;cursor:text;flex:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.5}._0K34_a_cmdNote{opacity:.8;margin-top:10px;font-size:12px;line-height:1.6}._0K34_a_cmdNote code{word-break:break-all;background:#00000014;border-radius:4px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._0K34_a_loadingDot{background:currentColor;border-radius:50%;width:8px;height:8px;margin-right:8px;animation:1s ease-in-out infinite _0K34_a_aero-loading-pulse}@keyframes _0K34_a_aero-loading-pulse{0%,to{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}._0K34_a_docRendering{color:#7f7f7fbf;text-align:center;margin:12px 0 0;font-size:13px}._0K34_a_docSegBar{z-index:3;background:color-mix(in srgb, currentColor 6%, transparent);backdrop-filter:blur(6px);border:1px solid #7f7f7f40;border-radius:8px;flex-wrap:wrap;align-items:center;gap:8px;margin:-6px 0 12px;padding:6px 10px;font-size:12px;display:flex;position:sticky;top:0}._0K34_a_docSegBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #7f7f7f59;border-radius:6px;padding:3px 10px;font-size:12px}._0K34_a_docSegBtn:hover:not(:disabled){background:color-mix(in srgb, currentColor 8%, transparent);border-color:#7f7f7f99}._0K34_a_docSegBtn:disabled{opacity:.4;cursor:default}._0K34_a_docSegInfo{text-overflow:ellipsis;white-space:nowrap;opacity:.85;flex:auto;min-width:0;overflow:hidden}._0K34_a_docSegSelect{max-width:42%;color:inherit;background:0 0;border:1px solid #7f7f7f59;border-radius:6px;padding:3px 6px;font-size:12px}._0K34_a_docScroll{scroll-behavior:smooth;box-sizing:border-box;flex:auto;min-width:0;height:100%;min-height:0;padding:16px clamp(12px,2vw,26px) 40px;overflow-y:auto}._0K34_a_docWrap{align-items:stretch;height:100%;min-height:0;display:flex;position:relative;container-type:inline-size}._0K34_a_docMeta{text-align:center;margin:0 0 1.3em}._0K34_a_docMetaKicker{font-family:var(--dsw-font-family,inherit);letter-spacing:.2em;color:var(--aero-ink-3,#818b98);font-size:11px;font-weight:500}._0K34_a_docMetaChips{flex-wrap:wrap;justify-content:center;gap:6px 8px;margin-top:.6em;display:flex}._0K34_a_docMetaChip{border:1px solid var(--aero-rule,#8080804d);font-family:var(--dsw-font-family,inherit);color:var(--aero-ink-2,#57606a);white-space:nowrap;border-radius:999px;align-items:center;gap:5px;padding:1px 9px;font-size:11.5px;line-height:1.7;display:inline-flex}._0K34_a_docMetaChipLabel{color:var(--aero-ink-3,#818b98)}._0K34_a_docMetaChip[data-tone=draft]{border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f0b429) 45%, transparent);color:var(--dsw-alias-state-warn-label,#b7791f)}._0K34_a_docMetaChip[data-tone=reviewed]{border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary,#2f6feb) 45%, transparent);color:var(--dsw-alias-state-business-primary,#2f6feb)}._0K34_a_docMetaChip[data-tone=done]{border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary,#2f9e44) 45%, transparent);color:var(--dsw-alias-state-success-primary,#2f9e44)}._0K34_a_docMetaKeywords{font-family:var(--dsw-font-family,inherit);color:var(--aero-ink-3,#818b98);flex-wrap:wrap;justify-content:center;gap:8px;margin-top:.55em;font-size:11px;display:flex}._0K34_a_docMetaKeyword:before{content:\"#\";opacity:.7}._0K34_a_docOutline{border-left:1px solid var(--dsw-alias-border-l2,#80808033);background:color-mix(in srgb, var(--dsw-alias-bg-module-platform,#7f7f7f0f) 55%, transparent);box-sizing:border-box;flex:none;width:206px;height:100%;min-height:0;padding:12px 8px 32px 10px;font-size:12px;overflow-y:auto}._0K34_a_docOutlineHead{letter-spacing:.12em;color:var(--dsw-alias-label-tertiary,#818b98);align-items:center;gap:5px;padding:0 8px 8px;font-size:11px;font-weight:600;display:flex}._0K34_a_docOutlineList{flex-direction:column;gap:1px;margin:0;padding:0;list-style:none;display:flex}._0K34_a_docOutlineItem[data-level=\"1\"]{margin-top:8px}._0K34_a_docOutlineLink{width:100%;color:var(--dsw-alias-label-secondary,inherit);font:inherit;text-align:left;cursor:pointer;white-space:nowrap;text-overflow:ellipsis;background:0 0;border:none;border-radius:6px;padding:4px 8px;display:block;overflow:hidden}._0K34_a_docOutlineItem[data-level=\"2\"] ._0K34_a_docOutlineLink{padding-left:12px}._0K34_a_docOutlineItem[data-level=\"3\"] ._0K34_a_docOutlineLink{padding-left:22px}._0K34_a_docOutlineItem[data-level=\"4\"] ._0K34_a_docOutlineLink{padding-left:32px}._0K34_a_docOutlineNo{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#818b98);margin-right:3px;font-weight:600}._0K34_a_docOutlineLink:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);color:var(--dsw-alias-label-primary,inherit)}._0K34_a_docOutlineLinkActive{background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 14%, transparent);color:var(--dsw-alias-link,#4176e6);font-weight:600}@container (width<=860px){._0K34_a_docOutline{display:none}}._0K34_a_docLightbox{z-index:40;box-sizing:border-box;cursor:zoom-out;background:#0b0f17cc;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:22px;animation:.14s ease-out _0K34_a_aeroLightboxIn;display:flex;position:absolute;inset:0}@keyframes _0K34_a_aeroLightboxIn{0%{opacity:0}to{opacity:1}}._0K34_a_docLightboxFigure{flex-direction:column;align-items:center;gap:10px;max-width:100%;min-height:0;max-height:100%;margin:0;display:flex}._0K34_a_docLightboxFigure img{object-fit:contain;background:#fff;border-radius:6px;max-width:100%;min-height:0;max-height:100%;box-shadow:0 24px 64px -24px #000000b3}._0K34_a_docLightboxCap{color:#ffffffe0;text-align:center;max-width:min(880px,82%);font-size:12.5px;line-height:1.65}._0K34_a_docLightboxHint{color:#fff9;flex:none;align-items:center;gap:5px;font-size:11.5px;display:flex}._0K34_a_slideRoot{flex-direction:column;height:100%;min-height:0;display:flex}._0K34_a_slideStage{box-sizing:border-box;flex:1;justify-content:center;align-items:flex-start;min-height:0;padding:20px 28px;display:flex;overflow:auto}._0K34_a_slideBar{border-top:1px solid #80808033;flex:none;justify-content:center;align-items:center;gap:12px;padding:8px;display:flex}._0K34_a_slideCounter{color:#7f7f7fe6;text-align:center;min-width:60px;font-size:13px}._0K34_a_contextBar{background:#7f7f7f0a;border-top:1px solid #80808033;flex:none;align-items:center;gap:8px;padding:8px;display:flex}._0K34_a_contextInfo{flex:none;align-items:center;gap:6px;min-width:0;max-width:260px;display:flex}._0K34_a_contextDot{background:#5ac878f2;border-radius:50%;flex:none;width:8px;height:8px}._0K34_a_contextText{white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}._0K34_a_contextMuted{color:#7f7f7fcc;font-size:12px}._0K34_a_questionInput{min-width:0;color:inherit;background:0 0;border:1px solid #80808059;border-radius:6px;flex:1;padding:6px 10px;font-size:13px}._0K34_a_questionInput:focus{border-color:#608cdccc;outline:none}._0K34_a_btn,._0K34_a_btnPrimary{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:6px;flex:none;padding:6px 12px;font-size:13px}._0K34_a_btn:hover{background:#7f7f7f1f}._0K34_a_btn:disabled,._0K34_a_btnPrimary:disabled{opacity:.45;cursor:not-allowed}._0K34_a_btnPrimary{color:#fff;background:#608cdce6;border-color:#608cdce6}._0K34_a_btnPrimary:hover:not(:disabled){background:#608cdc}._0K34_a_deckRoot{flex-direction:column;flex:1 1 0;width:100%;height:100%;min-height:0;display:flex}._0K34_a_deckToolbar{border-bottom:1px solid #80808038;flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:7px 10px;display:flex}._0K34_a_deckTitle{text-overflow:ellipsis;white-space:nowrap;max-width:42%;font-size:13px;font-weight:600;overflow:hidden}._0K34_a_deckStatus{color:#7f7f7ff2;align-items:center;gap:6px;font-size:12px;display:inline-flex}._0K34_a_deckHint{color:#7f7f7fbf;margin-left:2px;font-size:11px}._0K34_a_deckSpacer{flex:1 1 0}._0K34_a_deckBody{flex:1 1 0;min-height:0;position:relative}._0K34_a_deckFrame{background:#fff;border:0;width:100%;height:100%;display:block}._0K34_a_deckNotice{color:#7f7f7ff2;flex-direction:column;justify-content:center;align-items:center;gap:10px;height:100%;padding:24px;font-size:13px;display:flex;overflow:auto}._0K34_a_deckErrorTitle{color:inherit;font-size:14px;font-weight:600}._0K34_a_deckErrorMsg{text-align:center;max-width:640px}._0K34_a_deckLog{text-align:left;white-space:pre-wrap;word-break:break-all;background:#0000000f;border-radius:8px;max-width:min(720px,100%);max-height:220px;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;overflow:auto}._0K34_a_spinner{border:2px solid #7f7f7f59;border-top-color:#608cdce6;border-radius:50%;flex:none;width:14px;height:14px;animation:.9s linear infinite _0K34_a_aeroSpin;display:inline-block}@keyframes _0K34_a_aeroSpin{to{transform:rotate(360deg)}}._0K34_a_deckNoticeScroll{box-sizing:border-box;color:#7f7f7ff2;flex-direction:column;align-items:center;gap:12px;height:100%;padding:20px 24px 32px;font-size:13px;display:flex;overflow-y:auto}._0K34_a_deckNoticeHead{text-align:left;width:100%;max-width:720px;line-height:1.6}._0K34_a_deckNoticeBtns{align-items:center;gap:8px;width:100%;max-width:720px;display:flex}._0K34_a_envRoot{box-sizing:border-box;width:100%;max-width:720px}._0K34_a_envCard{text-align:left;background:#7f7f7f0d;border:1px solid #80808047;border-radius:10px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}._0K34_a_envHead{align-items:center;gap:8px;display:flex}._0K34_a_envTitle{color:inherit;font-size:13px;font-weight:600}._0K34_a_envSummaryOk,._0K34_a_envSummaryBad{font-size:13px;font-weight:600;line-height:1.5}._0K34_a_envSummaryOk{color:#46aa6ef2}._0K34_a_envSummaryBad{color:#d75f5ff2}._0K34_a_envSummaryDetail{color:#7f7f7ff2;word-break:break-all;margin-top:4px;font-size:12px;font-weight:400}._0K34_a_envMuted{color:#7f7f7fd9;align-items:center;gap:6px;font-size:12px;display:flex}._0K34_a_envError{color:#d75f5ff2;word-break:break-all;font-size:12px}._0K34_a_envList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}._0K34_a_envItem{align-items:flex-start;gap:8px;display:flex}._0K34_a_envDotOk,._0K34_a_envDotWarn,._0K34_a_envDotFail{color:#fff;border-radius:50%;flex:none;justify-content:center;align-items:center;width:16px;height:16px;margin-top:1px;font-size:10px;font-weight:700;display:inline-flex}._0K34_a_envDotOk{background:#46aa6ee6}._0K34_a_envDotWarn{background:#dca550f2}._0K34_a_envDotFail{background:#d75f5ff2}._0K34_a_envItemBody{flex-direction:column;gap:2px;min-width:0;display:flex}._0K34_a_envItemLabel{color:inherit;align-items:center;gap:6px;font-size:12.5px;font-weight:600;display:flex}._0K34_a_envItemStatus{color:#7f7f7ff2;border:1px solid #80808059;border-radius:999px;padding:1px 5px;font-size:10px;font-weight:500}._0K34_a_envItemDetail{color:#7f7f7ff2;word-break:break-all;font-size:12px}._0K34_a_envItemHint{color:#7f7f7fd9;word-break:break-all;font-size:12px}._0K34_a_envActions{border-top:1px dashed #8080804d;flex-direction:column;gap:8px;padding-top:10px;display:flex}._0K34_a_envActionsTitle{color:inherit;font-size:12.5px;font-weight:600}._0K34_a_envAction{background:#7f7f7f0a;border:1px solid #8080803d;border-radius:8px;align-items:center;gap:10px;padding:8px 10px;display:flex}._0K34_a_envActionInfo{flex-direction:column;flex:auto;gap:3px;min-width:0;display:flex}._0K34_a_envActionLabel{color:inherit;font-size:12.5px;font-weight:600}._0K34_a_envActionCmd{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._0K34_a_envActionCmd code{word-break:break-all;background:#00000014;border-radius:5px;padding:2px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._0K34_a_envCopy{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:5px;flex:none;align-items:center;gap:4px;padding:2px 7px;font-size:11px;display:inline-flex}._0K34_a_envCopy:hover{background:#7f7f7f1f}._0K34_a_envDetails summary{cursor:pointer;color:#7f7f7ff2;font-size:12px}._0K34_a_prepContent{flex:1;min-height:0;display:flex}._0K34_a_prepPane{flex-direction:column;flex:1 1 0;min-width:0;min-height:0;display:flex;position:relative}._0K34_a_prepPane+._0K34_a_prepPane{border-left:1px solid #80808038}._0K34_a_prepPaneHead{color:#7f7f7ff2;background:#7f7f7f0a;border-bottom:1px solid #8080802e;flex:none;align-items:center;gap:8px;padding:5px 10px;font-size:12px;display:flex}._0K34_a_prepPanePath{text-overflow:ellipsis;white-space:nowrap;opacity:.85;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;overflow:hidden}._0K34_a_prepEditor{box-sizing:border-box;resize:none;width:100%;min-height:0;color:inherit;tab-size:4;white-space:pre;background:0 0;border:0;outline:none;flex:1;padding:12px 14px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.6;overflow:auto}._0K34_a_prepDocPane{flex:1;min-height:0;display:flex}._0K34_a_prepDocPane>._0K34_a_docWrap{flex:auto;height:auto}._0K34_a_prepSlidesScroll{box-sizing:border-box;flex-direction:column;flex:1;align-items:center;gap:14px;min-height:0;padding:16px;display:flex;overflow-y:auto}._0K34_a_prepSlide{box-sizing:border-box;background:#7f7f7f0d;border:1px solid #80808040;border-radius:10px;width:min(760px,100%);padding:20px 26px;box-shadow:0 1px 3px #0000001f}._0K34_a_prepSlideNo{color:#7f7f7fb3;margin-bottom:6px;font-size:11px}._0K34_a_prepCmdBar{background:#7f7f7f0a;border-top:1px solid #80808033;flex-direction:column;flex:none;gap:6px;padding:7px 10px;display:flex}._0K34_a_prepSkills{flex:none;gap:6px;display:flex;overflow-x:auto}._0K34_a_skillBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808059;border-radius:999px;flex:none;align-items:center;gap:5px;padding:4px 10px;font-size:12px;display:inline-flex}._0K34_a_skillBtn:hover:not(:disabled){background:#7f7f7f1f;border-color:#608cdcb3}._0K34_a_skillBtn:disabled{opacity:.4;cursor:not-allowed}._0K34_a_prepCmdRow{align-items:center;gap:8px;display:flex}._0K34_a_prepMode{border:1px solid #80808059;border-radius:6px;flex:none;display:inline-flex;overflow:hidden}._0K34_a_mode,._0K34_a_modeActive{color:inherit;cursor:pointer;background:0 0;border:none;padding:4px 9px;font-size:12px}._0K34_a_mode:hover{background:#7f7f7f1f}._0K34_a_modeActive{color:#fff;background:#608cdcd9}._0K34_a_saveDot{border-radius:50%;flex:none;width:8px;height:8px}._0K34_a_dotIdle{background:#7f7f7f80}._0K34_a_dotDirty{background:#dca05af2}._0K34_a_dotSaving{background:#608cdce6}._0K34_a_dotSaved{background:#5ac878f2}._0K34_a_dotError{background:#dc5a5af2}._0K34_a_saveLabel{white-space:nowrap;text-overflow:ellipsis;color:#7f7f7ff2;max-width:340px;font-size:12px;overflow:hidden}._0K34_a_tbBtnOn{color:#2a5cb0fa;background:#608cdc24;border-color:#608cdcbf}._0K34_a_readerWrap{flex:none;position:relative}._0K34_a_menuBackdrop{z-index:29;position:fixed;inset:0}._0K34_a_readerMenu{z-index:30;background:var(--dsw-alias-bg-layer-1,#fff);width:min(328px,86vw);color:var(--dsw-alias-label-primary,inherit);border:1px solid #80808059;border-radius:10px;flex-direction:column;gap:8px;padding:10px;display:flex;position:absolute;top:calc(100% + 6px);right:0;box-shadow:0 10px 28px #0f193238}._0K34_a_readerMenuHead{letter-spacing:.04em;opacity:.9;align-items:center;gap:6px;padding:0 2px 2px;font-size:12px;font-weight:600;display:flex}._0K34_a_readerRow{align-items:center;gap:8px;display:flex}._0K34_a_readerRowLabel{opacity:.75;flex:none;width:34px;font-size:12px}._0K34_a_readerSeg{flex-wrap:wrap;flex:1;align-items:center;gap:3px;min-width:0;display:flex}._0K34_a_readerSegBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808052;border-radius:6px;padding:3px 8px;font-size:12px;line-height:1.5}._0K34_a_readerSegBtn:hover:not(:disabled){background:#7f7f7f1f}._0K34_a_readerSegBtn:disabled{opacity:.45;cursor:default}._0K34_a_readerSegBtnOn{color:#2a5cb0fa;background:#608cdc29;border-color:#608cdccc;font-weight:600}._0K34_a_readerFontValue{text-align:center;font-variant-numeric:tabular-nums;opacity:.85;min-width:46px;font-size:12px}._0K34_a_readerHint{opacity:.7;border-top:1px dashed #8080804d;padding:4px 2px 0;font-size:11px;line-height:1.6}._0K34_a_root[data-zen=\"1\"] ._0K34_a_sidebar,._0K34_a_root[data-zen=\"1\"] ._0K34_a_contextBar,._0K34_a_root[data-zen=\"1\"] ._0K34_a_prepCmdBar,._0K34_a_root[data-sidebar=\"0\"] ._0K34_a_sidebar{display:none}._0K34_a_root:fullscreen{background:var(--dsw-alias-bg-layer-1,#fff);height:100vh}._0K34_a_root[data-fullscreen=\"1\"]{position:relative}._0K34_a_root[data-fullscreen=\"1\"] ._0K34_a_topbar{z-index:45;background:var(--dsw-alias-bg-layer-1,#fff);transition:transform .18s,opacity .18s;position:absolute;top:0;left:0;right:0;box-shadow:0 6px 16px -12px #0f193280}._0K34_a_root[data-fullscreen=\"1\"][data-fsbar=hidden] ._0K34_a_topbar{opacity:0;transform:translateY(-100%)}._0K34_a_root[data-fullscreen=\"1\"][data-fsbar=hidden]:after{content:\"\";pointer-events:none;z-index:46;background:linear-gradient(#608cdc59,#0000);height:3px;position:absolute;top:0;left:0;right:0}@media (hover:none){._0K34_a_root[data-fullscreen=\"1\"][data-fsbar=hidden] ._0K34_a_topbar{opacity:1;transform:none}}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutline{border-left-color:var(--aero-rule-ov,#80808033);background:color-mix(in srgb, var(--aero-paper-ov,transparent) 78%, transparent)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineHead{color:var(--aero-ink-3-ov,#818b98)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineLink{color:var(--aero-ink-2-ov,inherit)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineLink:hover{background:var(--aero-hover-ov,#7f7f7f1f);color:var(--aero-ink-ov,inherit)}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docOutlineLinkActive{background:color-mix(in srgb, var(--aero-accent-ink-ov,#4176e6) 18%, transparent);color:var(--aero-accent-ink-ov,#4176e6)}@media (width<=520px){._0K34_a_readerMenu{width:min(300px,92vw)}._0K34_a_readerRowLabel{width:28px}}._0K34_a_annoColors{align-items:center;gap:5px;display:inline-flex}._0K34_a_annoColorBtn{cursor:pointer;background:#ffe066;border:1px solid #80808073;border-radius:50%;width:18px;height:18px;padding:0;transition:transform .1s;box-shadow:inset 0 0 0 2px #ffffff73}._0K34_a_annoColorBtn[data-color=yellow]{background:#ffe066}._0K34_a_annoColorBtn[data-color=green]{background:#a6e3a1}._0K34_a_annoColorBtn[data-color=blue]{background:#93c9f7}._0K34_a_annoColorBtn[data-color=pink]{background:#f7a8c4}._0K34_a_annoColorBtn:hover{transform:scale(1.14)}._0K34_a_annoColorBtn[data-on=\"1\"]{border-color:var(--dsw-alias-link,#4176e6);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-link,#4176e6) 45%, transparent)}._0K34_a_annoBar{z-index:46;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;border:1px solid #80808052;border-radius:10px;align-items:center;gap:6px;padding:6px 8px;font-size:12px;display:flex;position:fixed;box-shadow:0 10px 26px #0f19323d}._0K34_a_annoBarSep{background:#8080804d;width:1px;height:18px}._0K34_a_annoBarBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;align-items:center;gap:4px;padding:3px 7px;font-size:12px;line-height:1.5;display:inline-flex}._0K34_a_annoBarBtn:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);border-color:#80808052}._0K34_a_annoBarIcon{width:22px;height:22px;color:inherit;opacity:.6;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}._0K34_a_annoBarIcon:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_annoPop{z-index:47;background:var(--dsw-alias-bg-layer-1,#fff);width:340px;max-width:calc(100vw - 16px);color:var(--dsw-alias-label-primary,inherit);border:1px solid #80808052;border-radius:12px;flex-direction:column;gap:8px;padding:10px;font-size:12px;display:flex;position:fixed;box-shadow:0 16px 40px #0f193242}._0K34_a_annoPopHead{align-items:center;gap:6px;display:flex}._0K34_a_annoPopDot{background:#ffe066;border-radius:50%;flex:none;width:10px;height:10px;box-shadow:0 0 0 1px #80808059}._0K34_a_annoPopDot[data-color=yellow]{background:#ffe066}._0K34_a_annoPopDot[data-color=green]{background:#a6e3a1}._0K34_a_annoPopDot[data-color=blue]{background:#93c9f7}._0K34_a_annoPopDot[data-color=pink]{background:#f7a8c4}._0K34_a_annoPopKicker{letter-spacing:.02em;font-weight:600}._0K34_a_annoPopSpacer{flex:1 1 0}._0K34_a_annoPopIcon{width:22px;height:22px;color:inherit;opacity:.6;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}._0K34_a_annoPopIcon:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_annoPopQuote{background:var(--dsw-alias-bg-module-platform,#7f7f7f12);opacity:.92;border-left:3px solid #80808059;border-radius:4px;max-height:96px;margin:0;padding:7px 9px;font-size:12px;line-height:1.65;overflow-y:auto}._0K34_a_annoPopRow{align-items:center;gap:10px;display:flex}._0K34_a_annoPopSeg{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808052;border-radius:6px;align-items:center;gap:4px;margin-left:auto;padding:3px 8px;font-size:12px;display:inline-flex}._0K34_a_annoPopSegOn{border-color:var(--dsw-alias-link,#4176e6);background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 14%, transparent)}._0K34_a_annoPopNote{box-sizing:border-box;width:100%;color:inherit;font:inherit;resize:vertical;background:0 0;border:1px solid #80808052;border-radius:8px;min-height:62px;padding:7px 9px;font-size:12.5px;line-height:1.65}._0K34_a_annoPopNote:focus{border-color:var(--dsw-alias-link,#4176e6);outline:none}._0K34_a_annoPopFoot{align-items:center;gap:8px;display:flex}._0K34_a_annoPopPrimary{background:var(--dsw-alias-link,#4176e6);color:#fff;cursor:pointer;border:1px solid #0000;border-radius:7px;padding:4px 14px;font-size:12px}._0K34_a_annoPopPrimary:hover{filter:brightness(1.06)}._0K34_a_annoPopBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #80808052;border-radius:7px;align-items:center;gap:4px;padding:4px 10px;font-size:12px;display:inline-flex}._0K34_a_annoPopBtn:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_annoPopHint{opacity:.55;font-size:11px}._0K34_a_annoFab{z-index:30;background:var(--dsw-alias-bg-layer-1,#fff);width:38px;height:38px;color:var(--dsw-alias-label-secondary,inherit);cursor:pointer;border:1px solid #8080804d;border-radius:50%;justify-content:center;align-items:center;padding:0;transition:transform .12s,color .12s;display:inline-flex;position:absolute;bottom:16px;box-shadow:0 8px 22px #0f193233}._0K34_a_annoFab:hover{color:var(--dsw-alias-label-primary,inherit);transform:translateY(-1px)}._0K34_a_annoFabOn{border-color:var(--dsw-alias-link,#4176e6);color:var(--dsw-alias-link,#4176e6)}._0K34_a_annoFabBadge{box-sizing:border-box;color:#fff;text-align:center;font-variant-numeric:tabular-nums;background:#f0a020;border-radius:8px;min-width:16px;height:16px;padding:0 3px;font-size:10px;line-height:16px;position:absolute;top:-5px;right:-4px}._0K34_a_annoFabDot{width:7px;height:7px;box-shadow:0 0 0 1.5px var(--dsw-alias-bg-layer-1,#fff);background:#d94f8a;border-radius:50%;position:absolute;bottom:3px;right:3px}._0K34_a_annoPins{z-index:4;pointer-events:none;position:absolute;inset:0}._0K34_a_annoPin{color:#3c2d00e6;font-variant-numeric:tabular-nums;cursor:pointer;pointer-events:auto;background:#ffe066;border:1px solid #ffffffd9;border-radius:50%;width:19px;height:19px;padding:0;font-size:10px;line-height:1;position:absolute;box-shadow:0 2px 6px #0f193238}._0K34_a_annoPin[data-color=yellow]{color:#4a3800;background:#ffdf5c}._0K34_a_annoPin[data-color=green]{color:#1f3d18;background:#a6e3a1}._0K34_a_annoPin[data-color=blue]{color:#10334f;background:#93c9f7}._0K34_a_annoPin[data-color=pink]{color:#4a1330;background:#f7a8c4}._0K34_a_annoPin:hover{transform:scale(1.12)}._0K34_a_docMarks{box-sizing:border-box;border-left:1px solid var(--dsw-alias-border-l2,#80808033);background:color-mix(in srgb, var(--dsw-alias-bg-module-platform,#7f7f7f0f) 55%, transparent);flex-direction:column;flex:none;width:300px;height:100%;min-height:0;padding:10px 10px 8px;font-size:12px;display:flex}._0K34_a_docWrap:not([data-paper=theme]) ._0K34_a_docMarks{border-left-color:var(--aero-rule-ov,#80808033);background:color-mix(in srgb, var(--aero-paper-ov,transparent) 82%, transparent);color:var(--aero-ink-ov,inherit)}._0K34_a_docMarksHead{letter-spacing:.1em;color:var(--dsw-alias-label-tertiary,#818b98);align-items:center;gap:6px;padding:0 0 6px;font-size:11px;font-weight:600;display:flex}._0K34_a_docMarksCount{background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 18%, transparent);color:var(--dsw-alias-link,#4176e6);letter-spacing:0;border-radius:8px;padding:0 6px;font-size:10.5px}._0K34_a_docMarksHead ._0K34_a_annoPopIcon{margin-left:auto}._0K34_a_docMarksTitle{text-overflow:ellipsis;white-space:nowrap;opacity:.9;padding:0 0 8px;font-size:12px;font-weight:600;overflow:hidden}._0K34_a_docMarksFilters{border-bottom:1px dashed #8080804d;flex-wrap:wrap;gap:4px;padding-bottom:8px;display:flex}._0K34_a_docMarksChip{color:inherit;cursor:pointer;opacity:.8;background:0 0;border:1px solid #80808047;border-radius:999px;padding:2px 8px;font-size:11px}._0K34_a_docMarksChip:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_docMarksChipOn{border-color:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 65%, transparent);background:color-mix(in srgb, var(--dsw-alias-link,#4176e6) 14%, transparent);color:var(--dsw-alias-link,#4176e6);opacity:1}._0K34_a_docMarksList{flex-direction:column;flex:auto;gap:6px;min-height:0;padding:8px 2px 8px 0;display:flex;overflow-y:auto}._0K34_a_docMarksEmpty{opacity:.65;padding:10px 6px;font-size:11.5px;line-height:1.75}._0K34_a_docMarksItem{border:1px solid var(--dsw-alias-border-l2,#80808038);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#fff) 70%, transparent);border-left-width:3px;border-radius:8px;flex-direction:column;gap:4px;padding:6px 8px;display:flex}._0K34_a_docMarksItem[data-color=yellow]{border-left-color:#f0c400}._0K34_a_docMarksItem[data-color=green]{border-left-color:#4caf50}._0K34_a_docMarksItem[data-color=blue]{border-left-color:#2b7cd3}._0K34_a_docMarksItem[data-color=pink]{border-left-color:#d94f8a}._0K34_a_docMarksQuoteBtn{color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;flex-direction:column;gap:3px;padding:0;display:flex}._0K34_a_docMarksQuoteBtn:hover ._0K34_a_docMarksQuote{color:var(--dsw-alias-link,#4176e6)}._0K34_a_docMarksItemHead{align-items:center;gap:5px;display:flex}._0K34_a_docMarksItemKind{letter-spacing:.04em;opacity:.6;font-size:10.5px}._0K34_a_docMarksQuote{-webkit-line-clamp:3;-webkit-box-orient:vertical;font-size:12px;line-height:1.6;display:-webkit-box;overflow:hidden}._0K34_a_docMarksNote{-webkit-line-clamp:3;background:#f7a8c42e;border-radius:4px;-webkit-box-orient:vertical;margin-top:2px;padding:3px 6px;font-size:11.5px;line-height:1.6;display:-webkit-box;overflow:hidden}._0K34_a_docMarksItemFoot{justify-content:flex-end;gap:6px;display:flex}._0K34_a_docMarksMini{color:inherit;cursor:pointer;opacity:.8;background:0 0;border:1px solid #8080804d;border-radius:5px;padding:1px 7px;font-size:10.5px}._0K34_a_docMarksMini:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_docMarksFoot{border-top:1px dashed #8080804d;align-items:center;gap:6px;padding-top:6px;display:flex}._0K34_a_docMarksFootBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #8080804d;border-radius:6px;padding:3px 9px;font-size:11px}._0K34_a_docMarksFootBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}._0K34_a_docMarksFootBtn:disabled{opacity:.4;cursor:default}._0K34_a_docMarksFootHint{opacity:.55;margin-left:auto;font-size:10.5px}._0K34_a_deckBody[data-panel=\"1\"]{align-items:stretch;display:flex}._0K34_a_deckBody[data-panel=\"1\"] ._0K34_a_deckFrame{flex:auto;width:auto;min-width:0}._0K34_a_deckBody[data-panel=\"1\"]>aside{flex:none}._0K34_a_docResume{z-index:7;border:1px solid var(--aero-rule,var(--dsw-alias-border-l2,#8080804d));background:var(--dsw-alias-bg-layer-1,#fff);max-width:min(600px,100% - 28px);color:var(--aero-ink,inherit);font-family:var(--dsw-font-family,inherit);border-radius:999px;align-items:center;gap:6px;padding:5px 6px 5px 12px;font-size:12px;line-height:1.4;animation:.16s ease-out _0K34_a_aero-resume-in;display:flex;position:absolute;top:10px;left:50%;transform:translate(-50%);box-shadow:0 6px 18px #0000001a}._0K34_a_docWrap[data-segbar=\"1\"] ._0K34_a_docResume{top:54px}._0K34_a_docWrap[data-paper=sepia] ._0K34_a_docResume{color:#3a3126;background:#f7efdd;border-color:#604c3047}._0K34_a_docWrap[data-paper=green] ._0K34_a_docResume{color:#22301d;background:#e7efe2;border-color:#3c5a2d47}._0K34_a_docWrap[data-paper=dark] ._0K34_a_docResume{color:#dfe3e8;background:#232830;border-color:#ffffff29}._0K34_a_docResumeText{white-space:nowrap;text-overflow:ellipsis;opacity:.92;align-items:baseline;gap:6px;min-width:0;display:inline-flex;overflow:hidden}._0K34_a_docResumeName{text-overflow:ellipsis;font-weight:600;overflow:hidden}._0K34_a_docResumeAge{opacity:.6;flex:none}._0K34_a_docResumeBtn{color:inherit;cursor:pointer;background:0 0;border:1px solid #7f7f7f59;border-radius:999px;flex:none;padding:2px 9px;font-family:inherit;font-size:11.5px}._0K34_a_docResumeBtn:hover{background:color-mix(in srgb, currentColor 8%, transparent);border-color:#7f7f7f99}._0K34_a_docResumeClose{width:18px;height:18px;color:inherit;opacity:.55;cursor:pointer;background:0 0;border:0;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._0K34_a_docResumeClose:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}@keyframes _0K34_a_aero-resume-in{0%{opacity:0;transform:translate(-50%,-5px)}to{opacity:1;transform:translate(-50%)}}@container (width<=860px){._0K34_a_docMarks{width:252px}}@media print{._0K34_a_annoFab,._0K34_a_annoPins,._0K34_a_docMarks,._0K34_a_annoBar,._0K34_a_annoPop,._0K34_a_docResume{display:none!important}}._0K34_a_pdfWrap{flex-direction:column;height:100%;min-height:0;display:flex;position:relative}._0K34_a_pdfToolbar{border-bottom:1px solid var(--dsw-alias-border-l2,#80808033);white-space:nowrap;flex:none;align-items:center;gap:6px;padding:6px 10px;font-size:12px;display:flex;overflow-x:auto}._0K34_a_pdfBtn{border:1px solid var(--dsw-alias-border-l2,#8080804d);color:inherit;cursor:pointer;background:0 0;border-radius:6px;flex:none;padding:3px 8px;font-size:12px}._0K34_a_pdfBtn:hover:not(:disabled){background:color-mix(in srgb, currentColor 8%, transparent)}._0K34_a_pdfBtn:disabled{opacity:.4;cursor:default}._0K34_a_pdfBtnOn{border-color:var(--dsw-alias-border-accent,#608cdc);background:#608cdc29}._0K34_a_pdfPageInput{border:1px solid var(--dsw-alias-border-l2,#8080804d);width:44px;color:inherit;text-align:center;font-variant-numeric:tabular-nums;background:0 0;border-radius:6px;flex:none;padding:2px 4px;font-size:12px}._0K34_a_pdfPageTotal,._0K34_a_pdfZoom{color:color-mix(in srgb, currentColor 60%, transparent);font-variant-numeric:tabular-nums}._0K34_a_pdfZoom{text-align:right;min-width:38px}._0K34_a_pdfSpacer{flex:auto}._0K34_a_pdfHint{color:color-mix(in srgb, currentColor 55%, transparent);flex:none;font-size:11px}._0K34_a_pdfHint b{color:color-mix(in srgb, currentColor 80%, transparent);font-weight:600}._0K34_a_pdfBody{flex:auto;align-items:stretch;min-width:0;min-height:0;display:flex}._0K34_a_pdfScroll{scrollbar-gutter:stable;background:color-mix(in srgb, var(--dsw-alias-bg-module-platform,#7f7f7f1a) 70%, #0000001f);flex:auto;min-width:0;min-height:0;overflow:auto}._0K34_a_pdfPages{flex-direction:column;align-items:center;gap:18px;padding:12px 0 28px;display:flex}._0K34_a_pdfPage{background:#fff;flex:none;position:relative;box-shadow:0 1px 3px #0f19323d,0 6px 18px #0f19321f}._0K34_a_pdfPage[data-rendered=\"0\"]{background:#efefefa1}._0K34_a_pdfCanvas{border-radius:1px;width:100%;height:100%;display:block}._0K34_a_pdfMarks{z-index:2;pointer-events:none;position:absolute;inset:0}._0K34_a_pdfMarks i{mix-blend-mode:multiply;background:var(--aero-pdf-wash,#ffe066a8);border-radius:2px;display:block;position:absolute}._0K34_a_pdfMarks i[data-style=underline]{border-bottom:3px solid var(--aero-pdf-line,#e8b400);mix-blend-mode:normal;background:0 0;border-radius:0}._0K34_a_pdfMarks i[data-color=yellow]{--aero-pdf-wash:#ffe066a8;--aero-pdf-line:#e8b400}._0K34_a_pdfMarks i[data-color=green]{--aero-pdf-wash:#a6e3a1a0;--aero-pdf-line:#3f9e4d}._0K34_a_pdfMarks i[data-color=blue]{--aero-pdf-wash:#93c9f7a0;--aero-pdf-line:#2b7cd3}._0K34_a_pdfMarks i[data-color=pink]{--aero-pdf-wash:#f7a8c4a8;--aero-pdf-line:#d94f8a}._0K34_a_pdfTextLayer{z-index:3;text-align:initial;-webkit-text-size-adjust:none;text-size-adjust:none;forced-color-adjust:none;transform-origin:0 0;caret-color:#0000;line-height:1;position:absolute;inset:0;overflow:clip}._0K34_a_pdfTextLayer :is(span,br){color:#0000;white-space:pre;cursor:text;transform-origin:0 0;position:absolute}._0K34_a_pdfTextLayer>:not(._0K34_a_markedContent),._0K34_a_pdfTextLayer ._0K34_a_markedContent span:not(._0K34_a_markedContent){z-index:1}._0K34_a_pdfTextLayer span._0K34_a_markedContent{height:0;top:0}._0K34_a_pdfTextLayer ::selection{background:#5082ff47}._0K34_a_pdfPins{z-index:4;pointer-events:none;position:absolute;inset:0}._0K34_a_pdfPageIdle{color:#4a55688c;pointer-events:none;justify-content:center;align-items:center;font-size:13px;display:flex;position:absolute;inset:0}._0K34_a_pdfNotice{min-height:260px;color:color-mix(in srgb, currentColor 75%, transparent);text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:32px 24px;font-size:13px;display:flex}._0K34_a_pdfNoticeHint{max-width:46em;color:color-mix(in srgb, currentColor 55%, transparent);font-size:11.5px;line-height:1.6}";
|
|
1713
1911
|
const styleId$1 = "@dongfangw/study-kit/styles.module.css";
|
|
1714
1912
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + styleId$1 + "\"]") === null) {
|
|
1715
1913
|
const tag = document.createElement("style");
|
|
@@ -1886,9 +2084,30 @@ window.__ModuleLoader__.load({
|
|
|
1886
2084
|
"leafText": "_0K34_a_leafText",
|
|
1887
2085
|
"loadingDot": "_0K34_a_loadingDot",
|
|
1888
2086
|
"main": "_0K34_a_main",
|
|
2087
|
+
"markedContent": "_0K34_a_markedContent",
|
|
1889
2088
|
"menuBackdrop": "_0K34_a_menuBackdrop",
|
|
1890
2089
|
"mode": "_0K34_a_mode",
|
|
1891
2090
|
"modeActive": "_0K34_a_modeActive",
|
|
2091
|
+
"pdfBody": "_0K34_a_pdfBody",
|
|
2092
|
+
"pdfBtn": "_0K34_a_pdfBtn",
|
|
2093
|
+
"pdfBtnOn": "_0K34_a_pdfBtnOn",
|
|
2094
|
+
"pdfCanvas": "_0K34_a_pdfCanvas",
|
|
2095
|
+
"pdfHint": "_0K34_a_pdfHint",
|
|
2096
|
+
"pdfMarks": "_0K34_a_pdfMarks",
|
|
2097
|
+
"pdfNotice": "_0K34_a_pdfNotice",
|
|
2098
|
+
"pdfNoticeHint": "_0K34_a_pdfNoticeHint",
|
|
2099
|
+
"pdfPage": "_0K34_a_pdfPage",
|
|
2100
|
+
"pdfPageIdle": "_0K34_a_pdfPageIdle",
|
|
2101
|
+
"pdfPageInput": "_0K34_a_pdfPageInput",
|
|
2102
|
+
"pdfPages": "_0K34_a_pdfPages",
|
|
2103
|
+
"pdfPageTotal": "_0K34_a_pdfPageTotal",
|
|
2104
|
+
"pdfPins": "_0K34_a_pdfPins",
|
|
2105
|
+
"pdfScroll": "_0K34_a_pdfScroll",
|
|
2106
|
+
"pdfSpacer": "_0K34_a_pdfSpacer",
|
|
2107
|
+
"pdfTextLayer": "_0K34_a_pdfTextLayer",
|
|
2108
|
+
"pdfToolbar": "_0K34_a_pdfToolbar",
|
|
2109
|
+
"pdfWrap": "_0K34_a_pdfWrap",
|
|
2110
|
+
"pdfZoom": "_0K34_a_pdfZoom",
|
|
1892
2111
|
"prepCmdBar": "_0K34_a_prepCmdBar",
|
|
1893
2112
|
"prepCmdRow": "_0K34_a_prepCmdRow",
|
|
1894
2113
|
"prepContent": "_0K34_a_prepContent",
|
|
@@ -2438,7 +2657,7 @@ window.__ModuleLoader__.load({
|
|
|
2438
2657
|
* localStorage 又会把整库 JSON 序列化几十次/秒。200ms ≈ 每秒 5 次,
|
|
2439
2658
|
* 丢也最多丢半行,滚动一停最新位置立刻在库里。
|
|
2440
2659
|
*/
|
|
2441
|
-
const POS_SAVE_THROTTLE_MS = 200;
|
|
2660
|
+
const POS_SAVE_THROTTLE_MS$1 = 200;
|
|
2442
2661
|
/**
|
|
2443
2662
|
* 恢复后自我纠偏的时间窗(ms)。
|
|
2444
2663
|
*
|
|
@@ -2447,7 +2666,7 @@ window.__ModuleLoader__.load({
|
|
|
2447
2666
|
*/
|
|
2448
2667
|
const POS_CORRECTION_WINDOW_MS = 3e3;
|
|
2449
2668
|
/** 「已回到上次读到处」提示条的存活时间(ms):够看清一行字,又不会挡着读书。 */
|
|
2450
|
-
const RESUME_HINT_MS = 9e3;
|
|
2669
|
+
const RESUME_HINT_MS$1 = 9e3;
|
|
2451
2670
|
/**
|
|
2452
2671
|
* 正文分片注入的粒度(每片大致节点数)。
|
|
2453
2672
|
*
|
|
@@ -2783,7 +3002,7 @@ window.__ModuleLoader__.load({
|
|
|
2783
3002
|
};
|
|
2784
3003
|
const onScroll = () => {
|
|
2785
3004
|
if (restoringRef.current) return;
|
|
2786
|
-
const wait = POS_SAVE_THROTTLE_MS - (Date.now() - lastAt);
|
|
3005
|
+
const wait = POS_SAVE_THROTTLE_MS$1 - (Date.now() - lastAt);
|
|
2787
3006
|
if (wait <= 0) {
|
|
2788
3007
|
write();
|
|
2789
3008
|
return;
|
|
@@ -2825,7 +3044,7 @@ window.__ModuleLoader__.load({
|
|
|
2825
3044
|
}, [renderedHtml, html]);
|
|
2826
3045
|
(0, react.useEffect)(() => {
|
|
2827
3046
|
if (resume === null) return;
|
|
2828
|
-
const timer = window.setTimeout(() => setResume(null), RESUME_HINT_MS);
|
|
3047
|
+
const timer = window.setTimeout(() => setResume(null), RESUME_HINT_MS$1);
|
|
2829
3048
|
return () => window.clearTimeout(timer);
|
|
2830
3049
|
}, [resume]);
|
|
2831
3050
|
/** 「从头读」:忘掉这份文档的位置并回到开头(提示条同时收起)。 */
|
|
@@ -3360,95 +3579,1559 @@ window.__ModuleLoader__.load({
|
|
|
3360
3579
|
]
|
|
3361
3580
|
});
|
|
3362
3581
|
}
|
|
3363
|
-
// src/client/
|
|
3582
|
+
// src/client/pdfAnno.ts
|
|
3364
3583
|
/**
|
|
3365
|
-
*
|
|
3584
|
+
* PDF 页内锚定与绘制:把「一次选区」变成「页 + 页内文字偏移」,再把标注画回页面上。
|
|
3366
3585
|
*
|
|
3367
|
-
*
|
|
3368
|
-
*
|
|
3369
|
-
*
|
|
3370
|
-
*
|
|
3586
|
+
* 坐标系(与 `annotations.ts` 的文档标注刻意分开):
|
|
3587
|
+
* · 文档标注的偏移从**整篇正文**开头算;
|
|
3588
|
+
* · PDF 的偏移从**这一页的文字层根节点**开头算 —— pdf.js 的文字层是一片按视口坐标
|
|
3589
|
+
* 绝对定位的 `<span>`,页内文字顺序与缩放无关,因此「页号 + 页内偏移」稳定。
|
|
3590
|
+
*
|
|
3591
|
+
* 为什么画色块而不是像 md 那样往文字里插 `<mark>`:文字层的每个 span 都带
|
|
3592
|
+
* `font-size: calc(… * var(--scale-factor))` 与 `transform` 定位,往里插元素会改变断行、
|
|
3593
|
+
* 让文字与底图(canvas)错位。因此标注只画在**叠加层**上:量出选区的每一行矩形,
|
|
3594
|
+
* 把半透明色块贴上去(与课件播放器同一手法,见 SlidevDeckViewer 的 paintDeck)。
|
|
3371
3595
|
*/
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
fail: "缺失"
|
|
3376
|
-
};
|
|
3377
|
-
async function fetchEnv(relPath, cwdParam, force) {
|
|
3378
|
-
const qs = `?p=${encodeURIComponent(relPath)}${cwdParam}${force ? "&force=1" : ""}`;
|
|
3379
|
-
const r = await fetch(`/aero-kb/slidev/env${qs}`);
|
|
3380
|
-
const body = await r.json();
|
|
3381
|
-
if (!r.ok || !body.ok || body.report === void 0) throw new Error(body.error !== void 0 && body.error !== "" ? body.error : `HTTP ${r.status}`);
|
|
3382
|
-
return body.report;
|
|
3596
|
+
/** 页内纯文本索引(坐标系 = 该页文字层内所有文本节点按文档序拼接)。 */
|
|
3597
|
+
function pageIndex(layer) {
|
|
3598
|
+
return buildTextIndex(layer);
|
|
3383
3599
|
}
|
|
3384
|
-
/**
|
|
3385
|
-
function
|
|
3386
|
-
return
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3600
|
+
/** 一次 DOM 选区(已裁到单页内)→ 页内锚点;空选区 / 过长 / 全是空白返回 null。 */
|
|
3601
|
+
function anchorInPage(layer, range, page) {
|
|
3602
|
+
if (range.collapsed) return null;
|
|
3603
|
+
const index = pageIndex(layer);
|
|
3604
|
+
const probe = layer.ownerDocument.createRange();
|
|
3605
|
+
probe.selectNodeContents(layer);
|
|
3606
|
+
try {
|
|
3607
|
+
probe.setEnd(range.startContainer, range.startOffset);
|
|
3608
|
+
} catch {
|
|
3609
|
+
return null;
|
|
3610
|
+
}
|
|
3611
|
+
const start = probe.toString().length;
|
|
3612
|
+
try {
|
|
3613
|
+
probe.setEnd(range.endContainer, range.endOffset);
|
|
3614
|
+
} catch {
|
|
3615
|
+
return null;
|
|
3616
|
+
}
|
|
3617
|
+
const end = probe.toString().length;
|
|
3618
|
+
if (end <= start) return null;
|
|
3619
|
+
if (end - start > 2400) return null;
|
|
3620
|
+
const quote = index.text.slice(start, end);
|
|
3621
|
+
if (quote.trim() === "") return null;
|
|
3622
|
+
return {
|
|
3623
|
+
page,
|
|
3624
|
+
start,
|
|
3625
|
+
end,
|
|
3626
|
+
quote,
|
|
3627
|
+
prefix: index.text.slice(Math.max(0, start - 24), start).replace(/\s+/g, " "),
|
|
3628
|
+
suffix: index.text.slice(end, end + 24).replace(/\s+/g, " ")
|
|
3629
|
+
};
|
|
3394
3630
|
}
|
|
3395
|
-
function
|
|
3396
|
-
const
|
|
3397
|
-
const
|
|
3398
|
-
const
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3631
|
+
function normalizeIndex(text) {
|
|
3632
|
+
const chars = [];
|
|
3633
|
+
const from = [];
|
|
3634
|
+
const to = [];
|
|
3635
|
+
let i = 0;
|
|
3636
|
+
let lastSpace = true;
|
|
3637
|
+
while (i < text.length) {
|
|
3638
|
+
const ch = text[i] ?? "";
|
|
3639
|
+
if (/\s/.test(ch)) {
|
|
3640
|
+
let j = i + 1;
|
|
3641
|
+
while (j < text.length && /\s/.test(text[j] ?? "")) j++;
|
|
3642
|
+
if (!lastSpace) {
|
|
3643
|
+
chars.push(" ");
|
|
3644
|
+
from.push(i);
|
|
3645
|
+
to.push(j);
|
|
3646
|
+
lastSpace = true;
|
|
3647
|
+
}
|
|
3648
|
+
i = j;
|
|
3649
|
+
continue;
|
|
3412
3650
|
}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3651
|
+
chars.push(ch);
|
|
3652
|
+
from.push(i);
|
|
3653
|
+
to.push(i + 1);
|
|
3654
|
+
lastSpace = false;
|
|
3655
|
+
i++;
|
|
3656
|
+
}
|
|
3657
|
+
if (chars.length > 0 && chars[chars.length - 1] === " ") {
|
|
3658
|
+
chars.pop();
|
|
3659
|
+
from.pop();
|
|
3660
|
+
to.pop();
|
|
3661
|
+
}
|
|
3662
|
+
return {
|
|
3663
|
+
text: chars.join(""),
|
|
3664
|
+
from: Int32Array.from(from),
|
|
3665
|
+
to: Int32Array.from(to)
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
function commonPrefixLength(a, b) {
|
|
3669
|
+
const max = Math.min(a.length, b.length);
|
|
3670
|
+
let n = 0;
|
|
3671
|
+
while (n < max && a[n] === b[n]) n++;
|
|
3672
|
+
return n;
|
|
3673
|
+
}
|
|
3674
|
+
function commonSuffixLength(a, b) {
|
|
3675
|
+
const max = Math.min(a.length, b.length);
|
|
3676
|
+
let n = 0;
|
|
3677
|
+
while (n < max && a[a.length - 1 - n] === b[b.length - 1 - n]) n++;
|
|
3678
|
+
return n;
|
|
3679
|
+
}
|
|
3680
|
+
/** 归一后按「原文 + 前后文」找最佳落点;找不到返回 null。 */
|
|
3681
|
+
function relocateNormalized(norm, anno) {
|
|
3682
|
+
const needle = normalizeSpace(anno.quote);
|
|
3683
|
+
if (needle === "") return null;
|
|
3684
|
+
const prefix = normalizeSpace(anno.prefix);
|
|
3685
|
+
const suffix = normalizeSpace(anno.suffix);
|
|
3686
|
+
let best = null;
|
|
3687
|
+
let at = 0;
|
|
3688
|
+
for (let tries = 0; tries < 40; tries++) {
|
|
3689
|
+
const hit = norm.text.indexOf(needle, at);
|
|
3690
|
+
if (hit < 0) break;
|
|
3691
|
+
at = hit + 1;
|
|
3692
|
+
const before = norm.text.slice(Math.max(0, hit - prefix.length), hit);
|
|
3693
|
+
const after = norm.text.slice(hit + needle.length, hit + needle.length + suffix.length);
|
|
3694
|
+
const score = commonSuffixLength(before, prefix) + commonPrefixLength(after, suffix);
|
|
3695
|
+
if (best === null || score > best.score) best = {
|
|
3696
|
+
start: norm.from[hit] ?? 0,
|
|
3697
|
+
end: norm.to[hit + needle.length - 1] ?? 0,
|
|
3698
|
+
score
|
|
3421
3699
|
};
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3700
|
+
}
|
|
3701
|
+
return best === null ? null : [best.start, best.end];
|
|
3702
|
+
}
|
|
3703
|
+
function lookupOf(layer) {
|
|
3704
|
+
const index = pageIndex(layer);
|
|
3705
|
+
return {
|
|
3706
|
+
index,
|
|
3707
|
+
norm: normalizeIndex(index.text)
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
/** 标注 → 本页的 Range(偏移命中优先,否则按归一原文定位)。 */
|
|
3711
|
+
function rangeOf(lookup, layer, anno) {
|
|
3712
|
+
const doc = layer.ownerDocument;
|
|
3713
|
+
const { text } = lookup.index;
|
|
3714
|
+
if (anno.start >= 0 && anno.end > anno.start && anno.end <= text.length) {
|
|
3715
|
+
if (normalizeSpace(text.slice(anno.start, anno.end)) === normalizeSpace(anno.quote)) {
|
|
3716
|
+
const range = rangeFromOffsets(lookup.index, anno.start, anno.end, doc);
|
|
3717
|
+
if (range !== null) return range;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
const found = relocateNormalized(lookup.norm, anno);
|
|
3721
|
+
if (found === null) return null;
|
|
3722
|
+
return rangeFromOffsets(lookup.index, found[0], found[1], doc);
|
|
3723
|
+
}
|
|
3724
|
+
/** 选区矩形 → 页内坐标(丢掉零宽/零高与画到页外的碎片)。 */
|
|
3725
|
+
function rectsInPage(layer, range) {
|
|
3726
|
+
const base = layer.getBoundingClientRect();
|
|
3727
|
+
const out = [];
|
|
3728
|
+
for (const rect of Array.from(range.getClientRects())) {
|
|
3729
|
+
if (rect.width < 1 || rect.height < 1) continue;
|
|
3730
|
+
out.push({
|
|
3731
|
+
left: rect.left - base.left,
|
|
3732
|
+
top: rect.top - base.top,
|
|
3733
|
+
width: rect.width,
|
|
3734
|
+
height: rect.height
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
return out;
|
|
3738
|
+
}
|
|
3739
|
+
/** 已上笔的色块(overlay → [{id, rect}]):点击命中测试用,见 `hitTestAnnotations`。 */
|
|
3740
|
+
const rectCache = /* @__PURE__ */ new WeakMap();
|
|
3741
|
+
/**
|
|
3742
|
+
* 把本页的标注画成色块(幂等:先清空叠加层再重画)。
|
|
3743
|
+
*
|
|
3744
|
+
* 返回真正落上笔的标注 id:调用方用它判断「这条标注在这一页上还找得到吗」。
|
|
3745
|
+
*/
|
|
3746
|
+
function paintPageAnnotations(overlay, layer, annos) {
|
|
3747
|
+
overlay.textContent = "";
|
|
3748
|
+
rectCache.delete(overlay);
|
|
3749
|
+
if (annos.length === 0) return [];
|
|
3750
|
+
const lookup = lookupOf(layer);
|
|
3751
|
+
const painted = [];
|
|
3752
|
+
const boxes = [];
|
|
3753
|
+
for (const anno of annos) {
|
|
3754
|
+
const range = rangeOf(lookup, layer, anno);
|
|
3755
|
+
if (range === null) continue;
|
|
3756
|
+
const rects = rectsInPage(layer, range);
|
|
3757
|
+
if (rects.length === 0) continue;
|
|
3758
|
+
painted.push(anno.id);
|
|
3759
|
+
for (const rect of rects) {
|
|
3760
|
+
const box = overlay.ownerDocument.createElement("i");
|
|
3761
|
+
box.dataset.anno = anno.id;
|
|
3762
|
+
box.dataset.color = anno.color;
|
|
3763
|
+
box.dataset.style = anno.style;
|
|
3764
|
+
box.style.left = `${rect.left.toFixed(1)}px`;
|
|
3765
|
+
box.style.top = `${rect.top.toFixed(1)}px`;
|
|
3766
|
+
box.style.width = `${rect.width.toFixed(1)}px`;
|
|
3767
|
+
box.style.height = `${rect.height.toFixed(1)}px`;
|
|
3768
|
+
overlay.appendChild(box);
|
|
3769
|
+
boxes.push({
|
|
3770
|
+
id: anno.id,
|
|
3771
|
+
rect
|
|
3441
3772
|
});
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
rectCache.set(overlay, boxes);
|
|
3776
|
+
return painted;
|
|
3777
|
+
}
|
|
3778
|
+
/** 某条标注在本页的**第一个**色块矩形(页内坐标;弹层定位用)。 */
|
|
3779
|
+
function firstAnnoRect(overlay, id) {
|
|
3780
|
+
const box = overlay.querySelector(`i[data-anno="${CSS.escape(id)}"]`);
|
|
3781
|
+
if (box === null) return null;
|
|
3782
|
+
return {
|
|
3783
|
+
left: Number.parseFloat(box.style.left),
|
|
3784
|
+
top: Number.parseFloat(box.style.top),
|
|
3785
|
+
width: Number.parseFloat(box.style.width),
|
|
3786
|
+
height: Number.parseFloat(box.style.height)
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* 点在某条色块上吗?(返回该标注 id,否则 null)
|
|
3791
|
+
*
|
|
3792
|
+
* 色块本身 `pointer-events: none`——它压在文字层下面/上面的中间层,只要能接收鼠标就会把
|
|
3793
|
+
* 拖选拦住(学生选不中高亮过的字)。所以点击靠**矩形反查**:绘制时把每块的位置记在
|
|
3794
|
+
* WeakMap 里,点击时用页内坐标命中测试。
|
|
3795
|
+
*/
|
|
3796
|
+
function hitTestAnnotations(overlay, x, y) {
|
|
3797
|
+
const boxes = rectCache.get(overlay);
|
|
3798
|
+
if (boxes === void 0) return null;
|
|
3799
|
+
for (let i = boxes.length - 1; i >= 0; i--) {
|
|
3800
|
+
const box = boxes[i];
|
|
3801
|
+
if (box === void 0) continue;
|
|
3802
|
+
const { rect } = box;
|
|
3803
|
+
if (x >= rect.left && x <= rect.left + rect.width && y >= rect.top && y <= rect.top + rect.height) return box.id;
|
|
3804
|
+
}
|
|
3805
|
+
return null;
|
|
3806
|
+
}
|
|
3807
|
+
// src/client/pdfjs.ts
|
|
3808
|
+
/**
|
|
3809
|
+
* pdf.js 运行时的按需加载(客户端)。
|
|
3810
|
+
*
|
|
3811
|
+
* 为什么不在 bundle 里 `import 'pdfjs-dist'`:
|
|
3812
|
+
* · pdf.js 需要**独立的 worker 文件**(`new Worker(url)`),打包进客户端 bundle 反而拿不到;
|
|
3813
|
+
* · 主库 350KB + worker 1.4MB,而 PDF 只占学习资料的一小部分——不该让不用 PDF 的人付这份体积;
|
|
3814
|
+
* · 资源 URL 必须带版本号(升级后不能命中旧 worker),版本号由宿主注入(见宿主 `src/pdf.ts`)。
|
|
3815
|
+
*
|
|
3816
|
+
* 因此这里只做一件事:注入宿主的 loader 脚本(`/aero-kb/pdfjs/loader.js`),等它把
|
|
3817
|
+
* `window.__aeroPdfjs` 挂好;失败时把**具体原因**抛给阅读器展示(404 / CSP / 协议不支持)。
|
|
3818
|
+
*
|
|
3819
|
+
* 说明:本文件的类型是**本地最小声明**,不从 `pdfjs-dist` 引类型——该包的 `.d.ts` 内部
|
|
3820
|
+
* 交叉引用(`./display/api` 等)在 npm 包里并不齐全,`skipLibCheck` 下宁可自己声明用到的
|
|
3821
|
+
* 那几个成员,也不让构建依赖它的类型解析运气。
|
|
3822
|
+
*/
|
|
3823
|
+
/** pdf.js 运行时目录(宿主 `/kb/pdfjs` 的别名前缀,见宿主 `src/pdf.ts`)。 */
|
|
3824
|
+
const PDFJS_BASE = "/aero-kb/pdfjs/";
|
|
3825
|
+
/** 加载等待上限(本地资源,正常 <1s;超时说明脚本被拦或路由没起来)。 */
|
|
3826
|
+
const LOAD_TIMEOUT_MS = 2e4;
|
|
3827
|
+
let api = null;
|
|
3828
|
+
let pending = null;
|
|
3829
|
+
function win() {
|
|
3830
|
+
return window;
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* 取(必要时加载)pdf.js 运行时。
|
|
3834
|
+
*
|
|
3835
|
+
* 幂等:并发调用共用同一个 Promise;已加载过直接返回。失败后**允许重试**(清掉 pending,
|
|
3836
|
+
* 下次调用重新注入 loader),因为失败往往是一次性的(服务刚重启、脚本被临时拦)。
|
|
3837
|
+
*/
|
|
3838
|
+
function loadPdfjs() {
|
|
3839
|
+
if (api !== null) return Promise.resolve(api);
|
|
3840
|
+
if (pending !== null) return pending;
|
|
3841
|
+
const w = win();
|
|
3842
|
+
if (w.__aeroPdfjs !== void 0) {
|
|
3843
|
+
api = w.__aeroPdfjs;
|
|
3844
|
+
return Promise.resolve(api);
|
|
3845
|
+
}
|
|
3846
|
+
const task = new Promise((resolve, reject) => {
|
|
3847
|
+
let settled = false;
|
|
3848
|
+
let timer = 0;
|
|
3849
|
+
const cleanup = () => {
|
|
3850
|
+
window.removeEventListener("aero-pdfjs-ready", onReady);
|
|
3851
|
+
window.removeEventListener("aero-pdfjs-failed", onFail);
|
|
3852
|
+
window.clearTimeout(timer);
|
|
3853
|
+
};
|
|
3854
|
+
const finish = (value) => {
|
|
3855
|
+
if (settled) return;
|
|
3856
|
+
settled = true;
|
|
3857
|
+
cleanup();
|
|
3858
|
+
pending = null;
|
|
3859
|
+
if (value instanceof Error) reject(value);
|
|
3860
|
+
else {
|
|
3861
|
+
api = value;
|
|
3862
|
+
resolve(value);
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
3865
|
+
const onReady = () => {
|
|
3866
|
+
const loaded = w.__aeroPdfjs;
|
|
3867
|
+
if (loaded === void 0) finish(/* @__PURE__ */ new Error("pdf.js 已报告就绪,但运行时没有挂到 window.__aeroPdfjs"));
|
|
3868
|
+
else finish(loaded);
|
|
3869
|
+
};
|
|
3870
|
+
const onFail = () => {
|
|
3871
|
+
finish(new Error(w.__aeroPdfjsError ?? "pdf.js 加载失败(原因未知)"));
|
|
3872
|
+
};
|
|
3873
|
+
timer = window.setTimeout(() => {
|
|
3874
|
+
finish(/* @__PURE__ */ new Error(`pdf.js 加载超时(${String(LOAD_TIMEOUT_MS / 1e3)}s):请确认插件宿主已重启,且 ${PDFJS_BASE}loader.js 可访问`));
|
|
3875
|
+
}, LOAD_TIMEOUT_MS);
|
|
3876
|
+
window.addEventListener("aero-pdfjs-ready", onReady);
|
|
3877
|
+
window.addEventListener("aero-pdfjs-failed", onFail);
|
|
3878
|
+
w.__aeroPdfjsError = void 0;
|
|
3879
|
+
for (const stale of document.querySelectorAll("script[data-aero-pdfjs-loader]")) stale.remove();
|
|
3880
|
+
const script = document.createElement("script");
|
|
3881
|
+
script.dataset.aeroPdfjsLoader = "1";
|
|
3882
|
+
script.src = `${PDFJS_BASE}loader.js`;
|
|
3883
|
+
script.onerror = () => {
|
|
3884
|
+
finish(/* @__PURE__ */ new Error(`加载 ${PDFJS_BASE}loader.js 失败:宿主 PDF 路由未就绪,或脚本被 CSP 拦截`));
|
|
3885
|
+
};
|
|
3886
|
+
document.head.appendChild(script);
|
|
3887
|
+
});
|
|
3888
|
+
pending = task;
|
|
3889
|
+
return task;
|
|
3890
|
+
}
|
|
3891
|
+
// src/client/pdfPos.ts
|
|
3892
|
+
const STORE_KEY = "aero.pos.pdf.v1";
|
|
3893
|
+
/** 最多保留多少份 PDF 的位置(位置记录很小,与阅读位置同一量级的配额预算)。 */
|
|
3894
|
+
const MAX_DOCS = 400;
|
|
3895
|
+
function emptyDb() {
|
|
3896
|
+
return {
|
|
3897
|
+
v: 1,
|
|
3898
|
+
docs: {}
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function finiteOr(value, fallback) {
|
|
3902
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
3903
|
+
}
|
|
3904
|
+
function sanitize(raw) {
|
|
3905
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
3906
|
+
const p = raw;
|
|
3907
|
+
const page = Math.floor(finiteOr(p.page, 0));
|
|
3908
|
+
if (page < 1) return null;
|
|
3909
|
+
return {
|
|
3910
|
+
page,
|
|
3911
|
+
frac: Math.min(1, Math.max(0, finiteOr(p.frac, 0))),
|
|
3912
|
+
at: Math.max(0, finiteOr(p.at, 0))
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
function loadDb() {
|
|
3916
|
+
try {
|
|
3917
|
+
const raw = window.localStorage.getItem(STORE_KEY);
|
|
3918
|
+
if (raw === null) return emptyDb();
|
|
3919
|
+
const parsed = JSON.parse(raw);
|
|
3920
|
+
if (typeof parsed !== "object" || parsed === null) return emptyDb();
|
|
3921
|
+
const docsRaw = parsed.docs;
|
|
3922
|
+
if (typeof docsRaw !== "object" || docsRaw === null) return emptyDb();
|
|
3923
|
+
const docs = {};
|
|
3924
|
+
for (const [key, value] of Object.entries(docsRaw)) {
|
|
3925
|
+
const pos = sanitize(value);
|
|
3926
|
+
if (pos !== null) docs[key] = pos;
|
|
3927
|
+
}
|
|
3928
|
+
return {
|
|
3929
|
+
v: 1,
|
|
3930
|
+
docs
|
|
3931
|
+
};
|
|
3932
|
+
} catch {
|
|
3933
|
+
return emptyDb();
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
let db = loadDb();
|
|
3937
|
+
function persist() {
|
|
3938
|
+
try {
|
|
3939
|
+
window.localStorage.setItem(STORE_KEY, JSON.stringify(db));
|
|
3940
|
+
} catch {}
|
|
3941
|
+
}
|
|
3942
|
+
function prune(records) {
|
|
3943
|
+
const keys = Object.keys(records);
|
|
3944
|
+
if (keys.length <= MAX_DOCS) return records;
|
|
3945
|
+
const doomed = keys.sort((a, b) => (records[a]?.at ?? 0) - (records[b]?.at ?? 0)).slice(0, keys.length - MAX_DOCS);
|
|
3946
|
+
const next = { ...records };
|
|
3947
|
+
for (const key of doomed) delete next[key];
|
|
3948
|
+
return next;
|
|
3949
|
+
}
|
|
3950
|
+
/** 取某份 PDF 上次的位置;没读过返回 null。 */
|
|
3951
|
+
function loadPdfPos(key) {
|
|
3952
|
+
return db.docs[key] ?? null;
|
|
3953
|
+
}
|
|
3954
|
+
/** 记下某份 PDF 的位置。 */
|
|
3955
|
+
function savePdfPos(key, pos) {
|
|
3956
|
+
db = {
|
|
3957
|
+
v: 1,
|
|
3958
|
+
docs: prune({
|
|
3959
|
+
...db.docs,
|
|
3960
|
+
[key]: pos
|
|
3961
|
+
})
|
|
3962
|
+
};
|
|
3963
|
+
persist();
|
|
3964
|
+
}
|
|
3965
|
+
/** 忘掉某份 PDF 的位置(「从头读」)。 */
|
|
3966
|
+
function clearPdfPos(key) {
|
|
3967
|
+
if (db.docs[key] === void 0) return;
|
|
3968
|
+
const next = { ...db.docs };
|
|
3969
|
+
delete next[key];
|
|
3970
|
+
db = {
|
|
3971
|
+
v: 1,
|
|
3972
|
+
docs: next
|
|
3973
|
+
};
|
|
3974
|
+
persist();
|
|
3975
|
+
}
|
|
3976
|
+
// src/client/PdfViewer.tsx
|
|
3977
|
+
/**
|
|
3978
|
+
* PDF 阅读器:pdf.js 渲染 + 选中文字取色标注 / 写批注 / 问 AI。
|
|
3979
|
+
*
|
|
3980
|
+
* 与 DocViewer 的分工:md/adoc 由宿主渲染成 HTML(`/kb/doc`),阅读器只管排版与标注;
|
|
3981
|
+
* PDF 反过来——宿主只送字节(`/kb/pdf`),**渲染、文字层、标注落笔全在浏览器里**。
|
|
3982
|
+
*
|
|
3983
|
+
* 三个关键设计(改之前先读):
|
|
3984
|
+
*
|
|
3985
|
+
* 1. **按页锚定,不按整篇偏移。** 每一页的坐标系是「页号 + 该页文字层内的字符偏移」,
|
|
3986
|
+
* 见 `pdfAnno.ts`。缩放只改字号/矩形,不改页内文字顺序,所以换缩放、换窗口宽度后
|
|
3987
|
+
* 标注依然贴在同一段话上。
|
|
3988
|
+
* 2. **色块画在叠加层,不动文字层。** 文字层每个 span 都带 `calc(… * var(--scale-factor))`
|
|
3989
|
+
* 的字号与 transform 定位,往里插 `<mark>` 会改变断行、让文字与底图错位。因此标注是
|
|
3990
|
+
* 画在 canvas 与文字层之间的半透明色块(`paintPageAnnotations`);文字层在上、
|
|
3991
|
+
* 选区高亮再压在它上面,于是「底下的荧光色不会被盖住,拖选也不会被色块拦住」。
|
|
3992
|
+
* 点色块靠矩形反查(色块自身不接收鼠标,见 `hitTestAnnotations`)。
|
|
3993
|
+
* 3. **只渲染视口附近的页。** 一页 canvas 在 1000×1400 CSS px 下约占 5–14MB 内存,
|
|
3994
|
+
* 整本一次渲完会拖垮长文档;每页由 IntersectionObserver 决定「渲 / 拆」,离视口两屏
|
|
3995
|
+
* 以外就拆掉画布(占位块保留尺寸,滚回来再渲)。
|
|
3996
|
+
*/
|
|
3997
|
+
/** 页与页之间的间距(px)与页面上边距:滚动定位按它们累计,改 CSS 的 `.pdfPages` 要同步改这里。 */
|
|
3998
|
+
const PAGE_GAP = 18;
|
|
3999
|
+
const PAGE_TOP_PAD = 12;
|
|
4000
|
+
/** 渲染精度上限:Retina 上 2 倍够用,再高只是白烧内存。 */
|
|
4001
|
+
const DPR_CAP = 2;
|
|
4002
|
+
/** 缩放档位(1 = 100%);`适宽` 会算出不在表里的值。 */
|
|
4003
|
+
const ZOOM_STEPS = [
|
|
4004
|
+
.5,
|
|
4005
|
+
.67,
|
|
4006
|
+
.8,
|
|
4007
|
+
.9,
|
|
4008
|
+
1,
|
|
4009
|
+
1.1,
|
|
4010
|
+
1.25,
|
|
4011
|
+
1.5,
|
|
4012
|
+
1.75,
|
|
4013
|
+
2,
|
|
4014
|
+
2.5,
|
|
4015
|
+
3
|
|
4016
|
+
];
|
|
4017
|
+
/** 阅读位置落库的节流间隔(ms)。 */
|
|
4018
|
+
const POS_SAVE_THROTTLE_MS = 400;
|
|
4019
|
+
/** 「已回到上次读到处」提示条存活时间(ms)。 */
|
|
4020
|
+
const RESUME_HINT_MS = 9e3;
|
|
4021
|
+
/** 空标注表(稳定引用:避免父组件每次渲染都给子页传一个新数组,逼它重画) */
|
|
4022
|
+
const NO_ANNOS = [];
|
|
4023
|
+
function clampPage(value, max) {
|
|
4024
|
+
return Math.min(Math.max(1, Math.round(value)), Math.max(1, max));
|
|
4025
|
+
}
|
|
4026
|
+
/** 把窗口选区裁到某一页的文字层里;不重叠返回 null。 */
|
|
4027
|
+
function clipRangeToLayer(range, layer) {
|
|
4028
|
+
if (!range.intersectsNode(layer)) return null;
|
|
4029
|
+
const doc = layer.ownerDocument;
|
|
4030
|
+
const host = doc.createRange();
|
|
4031
|
+
host.selectNodeContents(layer);
|
|
4032
|
+
const startAfterHost = range.compareBoundaryPoints(Range.START_TO_START, host) > 0;
|
|
4033
|
+
const endBeforeHost = range.compareBoundaryPoints(Range.END_TO_END, host) < 0;
|
|
4034
|
+
const sub = doc.createRange();
|
|
4035
|
+
sub.setStart(startAfterHost ? range.startContainer : host.startContainer, startAfterHost ? range.startOffset : host.startOffset);
|
|
4036
|
+
sub.setEnd(endBeforeHost ? range.endContainer : host.endContainer, endBeforeHost ? range.endOffset : host.endOffset);
|
|
4037
|
+
return sub.collapsed ? null : sub;
|
|
4038
|
+
}
|
|
4039
|
+
const PdfPage = (0, react.memo)(function PdfPage({ api, doc, pageNo, size, scale, root, annos, noteOrder, register, onOpenAnno, onTextLayer }) {
|
|
4040
|
+
const hostRef = (0, react.useRef)(null);
|
|
4041
|
+
const canvasRef = (0, react.useRef)(null);
|
|
4042
|
+
const marksRef = (0, react.useRef)(null);
|
|
4043
|
+
const layerRef = (0, react.useRef)(null);
|
|
4044
|
+
/** 视口附近才渲染:离得远就把画布拆掉(占位块保留尺寸,滚回来再渲)。 */
|
|
4045
|
+
const [near, setNear] = (0, react.useState)(false);
|
|
4046
|
+
/** 有想法的标注 → 页内圆点。 */
|
|
4047
|
+
const [pins, setPins] = (0, react.useState)([]);
|
|
4048
|
+
/** 文字层重建代数:**只增不减**,保证每次重建后绘制 effect 一定重跑(见下方 cleanup 的注释)。 */
|
|
4049
|
+
const [layerGen, setLayerGen] = (0, react.useState)(0);
|
|
4050
|
+
const width = Math.max(1, size.width * scale);
|
|
4051
|
+
const height = Math.max(1, size.height * scale);
|
|
4052
|
+
(0, react.useEffect)(() => {
|
|
4053
|
+
const host = hostRef.current;
|
|
4054
|
+
if (host === null) return;
|
|
4055
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
4056
|
+
setNear(true);
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
const observer = new IntersectionObserver((entries) => {
|
|
4060
|
+
const entry = entries[entries.length - 1];
|
|
4061
|
+
if (entry !== void 0) setNear(entry.isIntersecting);
|
|
4062
|
+
}, {
|
|
4063
|
+
root,
|
|
4064
|
+
rootMargin: "1200px 0px",
|
|
4065
|
+
threshold: 0
|
|
4066
|
+
});
|
|
4067
|
+
observer.observe(host);
|
|
4068
|
+
return () => observer.disconnect();
|
|
4069
|
+
}, [root]);
|
|
4070
|
+
(0, react.useEffect)(() => {
|
|
4071
|
+
if (!near) return;
|
|
4072
|
+
const host = hostRef.current;
|
|
4073
|
+
const canvas = canvasRef.current;
|
|
4074
|
+
const layer = layerRef.current;
|
|
4075
|
+
if (host === null || canvas === null || layer === null) return;
|
|
4076
|
+
let cancelled = false;
|
|
4077
|
+
let task = null;
|
|
4078
|
+
setLayerGen((gen) => gen + 1);
|
|
4079
|
+
(async () => {
|
|
4080
|
+
try {
|
|
4081
|
+
const page = await doc.getPage(pageNo);
|
|
4082
|
+
if (cancelled) return;
|
|
4083
|
+
const viewport = page.getViewport({ scale });
|
|
4084
|
+
const dpr = Math.min(window.devicePixelRatio || 1, DPR_CAP);
|
|
4085
|
+
canvas.width = Math.max(1, Math.floor(viewport.width * dpr));
|
|
4086
|
+
canvas.height = Math.max(1, Math.floor(viewport.height * dpr));
|
|
4087
|
+
canvas.style.width = `${viewport.width.toFixed(2)}px`;
|
|
4088
|
+
canvas.style.height = `${viewport.height.toFixed(2)}px`;
|
|
4089
|
+
const ctx = canvas.getContext("2d");
|
|
4090
|
+
if (ctx === null) return;
|
|
4091
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4092
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
4093
|
+
const renderTask = page.render(dpr === 1 ? {
|
|
4094
|
+
canvasContext: ctx,
|
|
4095
|
+
viewport
|
|
4096
|
+
} : {
|
|
4097
|
+
canvasContext: ctx,
|
|
4098
|
+
viewport,
|
|
4099
|
+
transform: [
|
|
4100
|
+
dpr,
|
|
4101
|
+
0,
|
|
4102
|
+
0,
|
|
4103
|
+
dpr,
|
|
4104
|
+
0,
|
|
4105
|
+
0
|
|
4106
|
+
]
|
|
4107
|
+
});
|
|
4108
|
+
task = renderTask;
|
|
4109
|
+
await renderTask.promise;
|
|
4110
|
+
if (cancelled) return;
|
|
4111
|
+
const content = await page.getTextContent();
|
|
4112
|
+
if (cancelled) return;
|
|
4113
|
+
layer.textContent = "";
|
|
4114
|
+
await new api.TextLayer({
|
|
4115
|
+
textContentSource: content,
|
|
4116
|
+
container: layer,
|
|
4117
|
+
viewport
|
|
4118
|
+
}).render();
|
|
4119
|
+
if (cancelled) return;
|
|
4120
|
+
register(pageNo, {
|
|
4121
|
+
el: host,
|
|
4122
|
+
marks: marksRef.current,
|
|
4123
|
+
layer
|
|
4124
|
+
});
|
|
4125
|
+
setLayerGen((gen) => gen + 1);
|
|
4126
|
+
onTextLayer(pageNo);
|
|
4127
|
+
page.cleanup();
|
|
4128
|
+
} catch (err) {
|
|
4129
|
+
const name = err instanceof Error ? err.name : "";
|
|
4130
|
+
if (cancelled || name === "RenderingCancelledException") return;
|
|
4131
|
+
console.warn(`[aero-pdf] 第 ${String(pageNo)} 页渲染失败`, err);
|
|
4132
|
+
}
|
|
4133
|
+
})();
|
|
4134
|
+
return () => {
|
|
4135
|
+
cancelled = true;
|
|
4136
|
+
task?.cancel();
|
|
4137
|
+
register(pageNo, null);
|
|
4138
|
+
setLayerGen((gen) => gen + 1);
|
|
4139
|
+
};
|
|
4140
|
+
}, [
|
|
4141
|
+
api,
|
|
4142
|
+
doc,
|
|
4143
|
+
near,
|
|
4144
|
+
pageNo,
|
|
4145
|
+
register,
|
|
4146
|
+
scale,
|
|
4147
|
+
onTextLayer
|
|
4148
|
+
]);
|
|
4149
|
+
(0, react.useEffect)(() => {
|
|
4150
|
+
const marks = marksRef.current;
|
|
4151
|
+
const layer = layerRef.current;
|
|
4152
|
+
if (marks === null || layer === null) return;
|
|
4153
|
+
if (layer.childElementCount === 0) {
|
|
4154
|
+
marks.textContent = "";
|
|
4155
|
+
setPins([]);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
paintPageAnnotations(marks, layer, annos);
|
|
4159
|
+
const next = [];
|
|
4160
|
+
for (const anno of annos) {
|
|
4161
|
+
const note = anno.note.trim();
|
|
4162
|
+
if (note === "") continue;
|
|
4163
|
+
const rect = firstAnnoRect(marks, anno.id);
|
|
4164
|
+
if (rect === null) continue;
|
|
4165
|
+
next.push({
|
|
4166
|
+
id: anno.id,
|
|
4167
|
+
color: anno.color,
|
|
4168
|
+
note,
|
|
4169
|
+
left: rect.left + rect.width + 3,
|
|
4170
|
+
top: rect.top
|
|
4171
|
+
});
|
|
4172
|
+
}
|
|
4173
|
+
setPins(next);
|
|
4174
|
+
}, [annos, layerGen]);
|
|
4175
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4176
|
+
ref: hostRef,
|
|
4177
|
+
className: classes$1.pdfPage,
|
|
4178
|
+
"data-page": pageNo,
|
|
4179
|
+
"data-rendered": near ? "1" : "0",
|
|
4180
|
+
style: {
|
|
4181
|
+
width: `${width.toFixed(2)}px`,
|
|
4182
|
+
height: `${height.toFixed(2)}px`,
|
|
4183
|
+
"--scale-factor": String(scale)
|
|
4184
|
+
},
|
|
4185
|
+
children: [
|
|
4186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
4187
|
+
ref: canvasRef,
|
|
4188
|
+
className: classes$1.pdfCanvas
|
|
4189
|
+
}),
|
|
4190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4191
|
+
ref: marksRef,
|
|
4192
|
+
className: classes$1.pdfMarks,
|
|
4193
|
+
"data-aero-pdf-marks": ""
|
|
4194
|
+
}),
|
|
4195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4196
|
+
ref: layerRef,
|
|
4197
|
+
className: classes$1.pdfTextLayer,
|
|
4198
|
+
"data-aero-pdf-text": ""
|
|
4199
|
+
}),
|
|
4200
|
+
pins.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4201
|
+
className: classes$1.pdfPins,
|
|
4202
|
+
children: pins.map((pin) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4203
|
+
type: "button",
|
|
4204
|
+
className: classes$1.annoPin,
|
|
4205
|
+
"data-aero-pdf-pin": "",
|
|
4206
|
+
"data-color": pin.color,
|
|
4207
|
+
style: {
|
|
4208
|
+
left: `${pin.left.toFixed(1)}px`,
|
|
4209
|
+
top: `${pin.top.toFixed(1)}px`
|
|
4210
|
+
},
|
|
4211
|
+
title: pin.note,
|
|
4212
|
+
"aria-label": `批注:${pin.note}`,
|
|
4213
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
4214
|
+
onClick: () => onOpenAnno(pin.id),
|
|
4215
|
+
children: noteOrder.get(pin.id) ?? ""
|
|
4216
|
+
}, pin.id))
|
|
4217
|
+
}),
|
|
4218
|
+
!near && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4219
|
+
className: classes$1.pdfPageIdle,
|
|
4220
|
+
children: [
|
|
4221
|
+
"第 ",
|
|
4222
|
+
pageNo,
|
|
4223
|
+
" 页"
|
|
4224
|
+
]
|
|
4225
|
+
})
|
|
4226
|
+
]
|
|
4227
|
+
});
|
|
4228
|
+
});
|
|
4229
|
+
function PdfViewer({ relPath, title, cwdParam, onReadingChange, onAsk }) {
|
|
4230
|
+
const scrollRef = (0, react.useRef)(null);
|
|
4231
|
+
const pagesRef = (0, react.useRef)(null);
|
|
4232
|
+
const [scrollEl, setScrollEl] = (0, react.useState)(null);
|
|
4233
|
+
const onReadingRef = (0, react.useRef)(onReadingChange);
|
|
4234
|
+
onReadingRef.current = onReadingChange;
|
|
4235
|
+
const [api, setApi] = (0, react.useState)(null);
|
|
4236
|
+
const [doc, setDoc] = (0, react.useState)(null);
|
|
4237
|
+
const [sizes, setSizes] = (0, react.useState)([]);
|
|
4238
|
+
/**
|
|
4239
|
+
* 缩放(1 = 100%)。**初始为 0 = 还没定**:页尺寸与容器宽度都拿到之后,由适宽逻辑
|
|
4240
|
+
* 一次算定再渲染,避免"先按 100% 画一帧、再缩回去"的跳动(实测首帧会差出 30%)。
|
|
4241
|
+
*/
|
|
4242
|
+
const [scale, setScale] = (0, react.useState)(0);
|
|
4243
|
+
/** true = 跟随容器宽度(改窗口大小时自动重算);用户按过缩放后变 false。 */
|
|
4244
|
+
const [fitWidth, setFitWidth] = (0, react.useState)(true);
|
|
4245
|
+
const [phase, setPhase] = (0, react.useState)("runtime");
|
|
4246
|
+
const [error, setError] = (0, react.useState)(null);
|
|
4247
|
+
const [measured, setMeasured] = (0, react.useState)(0);
|
|
4248
|
+
const [currentPage, setCurrentPage] = (0, react.useState)(1);
|
|
4249
|
+
const [pageInput, setPageInput] = (0, react.useState)("1");
|
|
4250
|
+
const [containerWidth, setContainerWidth] = (0, react.useState)(0);
|
|
4251
|
+
const [resume, setResume] = (0, react.useState)(null);
|
|
4252
|
+
const docKey = pdfKeyOf(relPath);
|
|
4253
|
+
const annos = useAnnotations(docKey);
|
|
4254
|
+
const annosRef = (0, react.useRef)(annos);
|
|
4255
|
+
annosRef.current = annos;
|
|
4256
|
+
const [sel, setSel] = (0, react.useState)(null);
|
|
4257
|
+
const [editor, setEditor] = (0, react.useState)(null);
|
|
4258
|
+
const [panelOpen, setPanelOpen] = (0, react.useState)(false);
|
|
4259
|
+
/** 页面注册表(渲染好的页才有)。 */
|
|
4260
|
+
const pages = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
4261
|
+
const register = (0, react.useCallback)((pageNo, handle) => {
|
|
4262
|
+
if (handle === null) pages.current.delete(pageNo);
|
|
4263
|
+
else pages.current.set(pageNo, handle);
|
|
4264
|
+
}, []);
|
|
4265
|
+
/** 每页顶端的滚动坐标(页尺寸累计;缩放后重算)。 */
|
|
4266
|
+
const tops = (0, react.useMemo)(() => {
|
|
4267
|
+
const out = [];
|
|
4268
|
+
let top = PAGE_TOP_PAD;
|
|
4269
|
+
for (const size of sizes) {
|
|
4270
|
+
out.push(top);
|
|
4271
|
+
top += size.height * scale + PAGE_GAP;
|
|
4272
|
+
}
|
|
4273
|
+
return out;
|
|
4274
|
+
}, [sizes, scale]);
|
|
4275
|
+
const maxPageWidth = (0, react.useMemo)(() => sizes.reduce((m, s) => Math.max(m, s.width), 0), [sizes]);
|
|
4276
|
+
const numPages = sizes.length;
|
|
4277
|
+
const attachScroller = (0, react.useCallback)((el) => {
|
|
4278
|
+
scrollRef.current = el;
|
|
4279
|
+
setScrollEl(el);
|
|
4280
|
+
}, []);
|
|
4281
|
+
const lastWidthRef = (0, react.useRef)(0);
|
|
4282
|
+
(0, react.useEffect)(() => {
|
|
4283
|
+
if (scrollEl === null || typeof ResizeObserver === "undefined") return;
|
|
4284
|
+
const measure = () => {
|
|
4285
|
+
const width = scrollEl.clientWidth;
|
|
4286
|
+
if (width <= 0) return;
|
|
4287
|
+
if (lastWidthRef.current !== 0 && Math.abs(width - lastWidthRef.current) < 8) return;
|
|
4288
|
+
lastWidthRef.current = width;
|
|
4289
|
+
setContainerWidth(width);
|
|
4290
|
+
};
|
|
4291
|
+
const observer = new ResizeObserver(measure);
|
|
4292
|
+
observer.observe(scrollEl);
|
|
4293
|
+
measure();
|
|
4294
|
+
return () => observer.disconnect();
|
|
4295
|
+
}, [scrollEl]);
|
|
4296
|
+
/** 按容器宽度算缩放(适宽)。 */
|
|
4297
|
+
const fitScaleOf = (0, react.useCallback)((container, maxWidth) => {
|
|
4298
|
+
if (maxWidth <= 0 || container <= 0) return 1;
|
|
4299
|
+
return Math.min(3, Math.max(.2, Math.max(120, container - 28) / maxWidth));
|
|
4300
|
+
}, []);
|
|
4301
|
+
(0, react.useEffect)(() => {
|
|
4302
|
+
let cancelled = false;
|
|
4303
|
+
let opened = null;
|
|
4304
|
+
setApi(null);
|
|
4305
|
+
setDoc(null);
|
|
4306
|
+
setSizes([]);
|
|
4307
|
+
setPhase("runtime");
|
|
4308
|
+
setError(null);
|
|
4309
|
+
setMeasured(0);
|
|
4310
|
+
(async () => {
|
|
4311
|
+
try {
|
|
4312
|
+
const runtime = await loadPdfjs();
|
|
4313
|
+
if (cancelled) return;
|
|
4314
|
+
setApi(runtime);
|
|
4315
|
+
setPhase("document");
|
|
4316
|
+
const proxy = await runtime.getDocument({
|
|
4317
|
+
url: `/aero-kb/pdf?p=${encodeURIComponent(relPath)}${cwdParam}`,
|
|
4318
|
+
cMapUrl: `${PDFJS_BASE}cmaps/`,
|
|
4319
|
+
cMapPacked: true,
|
|
4320
|
+
standardFontDataUrl: `${PDFJS_BASE}standard_fonts/`,
|
|
4321
|
+
isEvalSupported: false
|
|
4322
|
+
}).promise;
|
|
4323
|
+
if (cancelled) {
|
|
4324
|
+
proxy.destroy();
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
opened = proxy;
|
|
4328
|
+
setDoc(proxy);
|
|
4329
|
+
setPhase("measuring");
|
|
4330
|
+
const next = [];
|
|
4331
|
+
for (let i = 1; i <= proxy.numPages; i++) {
|
|
4332
|
+
const page = await proxy.getPage(i);
|
|
4333
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
4334
|
+
next.push({
|
|
4335
|
+
width: viewport.width,
|
|
4336
|
+
height: viewport.height
|
|
4337
|
+
});
|
|
4338
|
+
page.cleanup();
|
|
4339
|
+
if (cancelled) return;
|
|
4340
|
+
if (i % 8 === 0 || i === proxy.numPages) setMeasured(i);
|
|
4341
|
+
}
|
|
4342
|
+
setSizes(next);
|
|
4343
|
+
setPhase("ready");
|
|
4344
|
+
} catch (err) {
|
|
4345
|
+
if (cancelled) return;
|
|
4346
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
4347
|
+
setPhase("error");
|
|
4348
|
+
}
|
|
4349
|
+
})();
|
|
4350
|
+
return () => {
|
|
4351
|
+
cancelled = true;
|
|
4352
|
+
pages.current.clear();
|
|
4353
|
+
if (opened !== null) opened.destroy().catch(() => {});
|
|
4354
|
+
};
|
|
4355
|
+
}, [
|
|
4356
|
+
relPath,
|
|
4357
|
+
cwdParam,
|
|
4358
|
+
fitScaleOf
|
|
4359
|
+
]);
|
|
4360
|
+
const gotoPage = (0, react.useCallback)((page) => {
|
|
4361
|
+
const scroller = scrollRef.current;
|
|
4362
|
+
if (scroller === null || tops.length === 0) return;
|
|
4363
|
+
const target = clampPage(page, numPages);
|
|
4364
|
+
scroller.scrollTo({
|
|
4365
|
+
top: Math.max(0, tops[target - 1] ?? 0),
|
|
4366
|
+
behavior: "smooth"
|
|
4367
|
+
});
|
|
4368
|
+
}, [numPages, tops]);
|
|
4369
|
+
const scrollRaf = (0, react.useRef)(0);
|
|
4370
|
+
const lastSavedAt = (0, react.useRef)(0);
|
|
4371
|
+
const lastContextPage = (0, react.useRef)(0);
|
|
4372
|
+
const restoringRef = (0, react.useRef)(false);
|
|
4373
|
+
const restoredRef = (0, react.useRef)(false);
|
|
4374
|
+
const pendingAnchor = (0, react.useRef)(null);
|
|
4375
|
+
const pageAtScroll = (0, react.useCallback)((scrollTop) => {
|
|
4376
|
+
if (tops.length === 0 || scale <= 0) return {
|
|
4377
|
+
page: 1,
|
|
4378
|
+
frac: 0
|
|
4379
|
+
};
|
|
4380
|
+
const probe = scrollTop + 60;
|
|
4381
|
+
let idx = 0;
|
|
4382
|
+
for (let i = 0; i < tops.length; i++) if ((tops[i] ?? 0) <= probe) idx = i;
|
|
4383
|
+
else break;
|
|
4384
|
+
const size = sizes[idx];
|
|
4385
|
+
const height = size === void 0 ? 0 : size.height * scale;
|
|
4386
|
+
const frac = height <= 0 ? 0 : Math.min(1, Math.max(0, (probe - (tops[idx] ?? 0)) / height));
|
|
4387
|
+
return {
|
|
4388
|
+
page: idx + 1,
|
|
4389
|
+
frac
|
|
4390
|
+
};
|
|
4391
|
+
}, [
|
|
4392
|
+
sizes,
|
|
4393
|
+
scale,
|
|
4394
|
+
tops
|
|
4395
|
+
]);
|
|
4396
|
+
const savePosNow = (0, react.useCallback)(() => {
|
|
4397
|
+
const scroller = scrollRef.current;
|
|
4398
|
+
if (scroller === null || sizes.length === 0 || restoringRef.current) return;
|
|
4399
|
+
const { page, frac } = pageAtScroll(scroller.scrollTop);
|
|
4400
|
+
savePdfPos(docKey, {
|
|
4401
|
+
page,
|
|
4402
|
+
frac,
|
|
4403
|
+
at: Date.now()
|
|
4404
|
+
});
|
|
4405
|
+
lastSavedAt.current = Date.now();
|
|
4406
|
+
}, [
|
|
4407
|
+
docKey,
|
|
4408
|
+
pageAtScroll,
|
|
4409
|
+
sizes.length
|
|
4410
|
+
]);
|
|
4411
|
+
/** 读当前页的文字摘要(阅读上下文用)。 */
|
|
4412
|
+
const excerptOf = (0, react.useCallback)((page) => {
|
|
4413
|
+
const handle = pages.current.get(page);
|
|
4414
|
+
if (handle?.layer == null) return "";
|
|
4415
|
+
return pageIndex(handle.layer).text.replace(/\s+/g, " ").trim().slice(0, 900);
|
|
4416
|
+
}, []);
|
|
4417
|
+
(0, react.useEffect)(() => {
|
|
4418
|
+
if (scrollEl === null || phase !== "ready") return;
|
|
4419
|
+
const onScroll = () => {
|
|
4420
|
+
if (scrollRaf.current !== 0) return;
|
|
4421
|
+
scrollRaf.current = window.requestAnimationFrame(() => {
|
|
4422
|
+
scrollRaf.current = 0;
|
|
4423
|
+
const { page } = pageAtScroll(scrollEl.scrollTop);
|
|
4424
|
+
setCurrentPage((prev) => prev === page ? prev : page);
|
|
4425
|
+
if (page !== lastContextPage.current) {
|
|
4426
|
+
lastContextPage.current = page;
|
|
4427
|
+
onReadingRef.current({
|
|
4428
|
+
relPath,
|
|
4429
|
+
title,
|
|
4430
|
+
section: `第 ${String(page)} 页 / 共 ${String(numPages)} 页`,
|
|
4431
|
+
excerpt: excerptOf(page)
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
if (!restoringRef.current && Date.now() - lastSavedAt.current > POS_SAVE_THROTTLE_MS) savePosNow();
|
|
4435
|
+
});
|
|
4436
|
+
};
|
|
4437
|
+
scrollEl.addEventListener("scroll", onScroll, { passive: true });
|
|
4438
|
+
onScroll();
|
|
4439
|
+
return () => {
|
|
4440
|
+
scrollEl.removeEventListener("scroll", onScroll);
|
|
4441
|
+
if (scrollRaf.current !== 0) {
|
|
4442
|
+
window.cancelAnimationFrame(scrollRaf.current);
|
|
4443
|
+
scrollRaf.current = 0;
|
|
4444
|
+
}
|
|
4445
|
+
savePosNow();
|
|
4446
|
+
};
|
|
4447
|
+
}, [
|
|
4448
|
+
excerptOf,
|
|
4449
|
+
numPages,
|
|
4450
|
+
pageAtScroll,
|
|
4451
|
+
phase,
|
|
4452
|
+
relPath,
|
|
4453
|
+
savePosNow,
|
|
4454
|
+
scrollEl,
|
|
4455
|
+
title
|
|
4456
|
+
]);
|
|
4457
|
+
(0, react.useLayoutEffect)(() => {
|
|
4458
|
+
if (phase !== "ready" || scale <= 0 || restoredRef.current || sizes.length === 0) return;
|
|
4459
|
+
restoredRef.current = true;
|
|
4460
|
+
const scroller = scrollRef.current;
|
|
4461
|
+
if (scroller === null) return;
|
|
4462
|
+
const saved = loadPdfPos(docKey);
|
|
4463
|
+
if (saved === null) return;
|
|
4464
|
+
const idx = clampPage(saved.page, numPages) - 1;
|
|
4465
|
+
const size = sizes[idx];
|
|
4466
|
+
if (size === void 0) return;
|
|
4467
|
+
const top = (tops[idx] ?? 0) + saved.frac * size.height * scale;
|
|
4468
|
+
if (top < 40) return;
|
|
4469
|
+
restoringRef.current = true;
|
|
4470
|
+
const previous = scroller.style.scrollBehavior;
|
|
4471
|
+
scroller.style.scrollBehavior = "auto";
|
|
4472
|
+
scroller.scrollTop = top;
|
|
4473
|
+
scroller.style.scrollBehavior = previous;
|
|
4474
|
+
window.requestAnimationFrame(() => {
|
|
4475
|
+
restoringRef.current = false;
|
|
4476
|
+
});
|
|
4477
|
+
setResume({
|
|
4478
|
+
page: idx + 1,
|
|
4479
|
+
at: saved.at
|
|
4480
|
+
});
|
|
4481
|
+
setCurrentPage(idx + 1);
|
|
4482
|
+
}, [
|
|
4483
|
+
docKey,
|
|
4484
|
+
numPages,
|
|
4485
|
+
phase,
|
|
4486
|
+
scale,
|
|
4487
|
+
sizes,
|
|
4488
|
+
tops
|
|
4489
|
+
]);
|
|
4490
|
+
(0, react.useLayoutEffect)(() => {
|
|
4491
|
+
const anchor = pendingAnchor.current;
|
|
4492
|
+
pendingAnchor.current = null;
|
|
4493
|
+
const scroller = scrollRef.current;
|
|
4494
|
+
if (anchor === null || scroller === null) return;
|
|
4495
|
+
const size = sizes[anchor.page - 1];
|
|
4496
|
+
if (size === void 0) return;
|
|
4497
|
+
restoringRef.current = true;
|
|
4498
|
+
const previous = scroller.style.scrollBehavior;
|
|
4499
|
+
scroller.style.scrollBehavior = "auto";
|
|
4500
|
+
scroller.scrollTop = (tops[anchor.page - 1] ?? 0) + anchor.frac * size.height * scale;
|
|
4501
|
+
scroller.style.scrollBehavior = previous;
|
|
4502
|
+
window.requestAnimationFrame(() => {
|
|
4503
|
+
restoringRef.current = false;
|
|
4504
|
+
});
|
|
4505
|
+
}, [
|
|
4506
|
+
scale,
|
|
4507
|
+
sizes,
|
|
4508
|
+
tops
|
|
4509
|
+
]);
|
|
4510
|
+
const anchorThenScale = (0, react.useCallback)((next) => {
|
|
4511
|
+
const scroller = scrollRef.current;
|
|
4512
|
+
if (scroller !== null && scale > 0) pendingAnchor.current = pageAtScroll(scroller.scrollTop);
|
|
4513
|
+
setScale(next);
|
|
4514
|
+
}, [pageAtScroll, scale]);
|
|
4515
|
+
(0, react.useEffect)(() => {
|
|
4516
|
+
if (!fitWidth || maxPageWidth <= 0 || containerWidth <= 0) return;
|
|
4517
|
+
const next = fitScaleOf(containerWidth, maxPageWidth);
|
|
4518
|
+
if (Math.abs(next - scale) < .005) return;
|
|
4519
|
+
anchorThenScale(() => next);
|
|
4520
|
+
}, [
|
|
4521
|
+
anchorThenScale,
|
|
4522
|
+
containerWidth,
|
|
4523
|
+
fitScaleOf,
|
|
4524
|
+
fitWidth,
|
|
4525
|
+
maxPageWidth,
|
|
4526
|
+
scale
|
|
4527
|
+
]);
|
|
4528
|
+
(0, react.useEffect)(() => {
|
|
4529
|
+
if (resume === null) return;
|
|
4530
|
+
const timer = window.setTimeout(() => setResume(null), RESUME_HINT_MS);
|
|
4531
|
+
return () => window.clearTimeout(timer);
|
|
4532
|
+
}, [resume]);
|
|
4533
|
+
(0, react.useEffect)(() => {
|
|
4534
|
+
if (phase !== "ready") return;
|
|
4535
|
+
const onKey = (e) => {
|
|
4536
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
4537
|
+
const target = e.target;
|
|
4538
|
+
if (target instanceof HTMLElement && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return;
|
|
4539
|
+
switch (e.key) {
|
|
4540
|
+
case "PageDown":
|
|
4541
|
+
gotoPage(currentPage + 1);
|
|
4542
|
+
break;
|
|
4543
|
+
case "PageUp":
|
|
4544
|
+
gotoPage(currentPage - 1);
|
|
4545
|
+
break;
|
|
4546
|
+
case "Home":
|
|
4547
|
+
gotoPage(1);
|
|
4548
|
+
break;
|
|
4549
|
+
case "End":
|
|
4550
|
+
gotoPage(numPages);
|
|
4551
|
+
break;
|
|
4552
|
+
default: return;
|
|
4553
|
+
}
|
|
4554
|
+
e.preventDefault();
|
|
4555
|
+
};
|
|
4556
|
+
window.addEventListener("keydown", onKey);
|
|
4557
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
4558
|
+
}, [
|
|
4559
|
+
currentPage,
|
|
4560
|
+
gotoPage,
|
|
4561
|
+
numPages,
|
|
4562
|
+
phase
|
|
4563
|
+
]);
|
|
4564
|
+
const pageInputFocused = (0, react.useRef)(false);
|
|
4565
|
+
(0, react.useEffect)(() => {
|
|
4566
|
+
if (!pageInputFocused.current) setPageInput(String(currentPage));
|
|
4567
|
+
}, [currentPage]);
|
|
4568
|
+
(0, react.useEffect)(() => {
|
|
4569
|
+
if (scrollEl === null) return;
|
|
4570
|
+
const onWheel = (e) => {
|
|
4571
|
+
if (!e.ctrlKey) return;
|
|
4572
|
+
e.preventDefault();
|
|
4573
|
+
setFitWidth(false);
|
|
4574
|
+
anchorThenScale((prev) => {
|
|
4575
|
+
if (e.deltaY >= 0) return [...ZOOM_STEPS].reverse().find((s) => s < prev - .001) ?? ZOOM_STEPS[0];
|
|
4576
|
+
return ZOOM_STEPS.find((s) => s > prev + .001) ?? ZOOM_STEPS[ZOOM_STEPS.length - 1];
|
|
4577
|
+
});
|
|
4578
|
+
};
|
|
4579
|
+
scrollEl.addEventListener("wheel", onWheel, { passive: false });
|
|
4580
|
+
return () => scrollEl.removeEventListener("wheel", onWheel);
|
|
4581
|
+
}, [anchorThenScale, scrollEl]);
|
|
4582
|
+
(0, react.useEffect)(() => {
|
|
4583
|
+
if (phase !== "ready") return;
|
|
4584
|
+
let raf = 0;
|
|
4585
|
+
const read = () => {
|
|
4586
|
+
raf = 0;
|
|
4587
|
+
const selection = window.getSelection();
|
|
4588
|
+
if (selection === null || selection.rangeCount === 0 || selection.isCollapsed) {
|
|
4589
|
+
setSel(null);
|
|
4590
|
+
return;
|
|
4591
|
+
}
|
|
4592
|
+
const range = selection.getRangeAt(0);
|
|
4593
|
+
const container = pagesRef.current;
|
|
4594
|
+
if (container === null || !container.contains(range.commonAncestorContainer)) {
|
|
4595
|
+
setSel(null);
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
const found = [];
|
|
4599
|
+
for (const [pageNo, handle] of pages.current) {
|
|
4600
|
+
const layer = handle.layer;
|
|
4601
|
+
if (layer === null) continue;
|
|
4602
|
+
const clipped = clipRangeToLayer(range, layer);
|
|
4603
|
+
if (clipped === null) continue;
|
|
4604
|
+
const anchor = anchorInPage(layer, clipped, pageNo);
|
|
4605
|
+
if (anchor !== null) found.push(anchor);
|
|
4606
|
+
}
|
|
4607
|
+
const first = found.sort((a, b) => a.page - b.page)[0];
|
|
4608
|
+
if (first === void 0) {
|
|
4609
|
+
setSel(null);
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
const rect = range.getBoundingClientRect();
|
|
4613
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
4614
|
+
setSel(null);
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
const anchor = {
|
|
4618
|
+
start: first.start,
|
|
4619
|
+
end: first.end,
|
|
4620
|
+
quote: first.quote,
|
|
4621
|
+
prefix: first.prefix,
|
|
4622
|
+
suffix: first.suffix
|
|
4623
|
+
};
|
|
4624
|
+
setSel({
|
|
4625
|
+
x: rect.left + rect.width / 2,
|
|
4626
|
+
y: rect.top,
|
|
4627
|
+
anchor,
|
|
4628
|
+
page: first.page,
|
|
4629
|
+
more: found.slice(1)
|
|
4630
|
+
});
|
|
4631
|
+
};
|
|
4632
|
+
const schedule = (e) => {
|
|
4633
|
+
const target = e.target;
|
|
4634
|
+
if (target instanceof Element && target.closest("[data-aero-mark-ui]") !== null) return;
|
|
4635
|
+
if (raf !== 0) return;
|
|
4636
|
+
raf = window.requestAnimationFrame(read);
|
|
4637
|
+
};
|
|
4638
|
+
document.addEventListener("mouseup", schedule);
|
|
4639
|
+
document.addEventListener("keyup", schedule);
|
|
4640
|
+
document.addEventListener("selectionchange", schedule);
|
|
4641
|
+
const onScroll = () => {
|
|
4642
|
+
setSel(null);
|
|
4643
|
+
setEditor(null);
|
|
4644
|
+
};
|
|
4645
|
+
scrollEl?.addEventListener("scroll", onScroll, { passive: true });
|
|
4646
|
+
return () => {
|
|
4647
|
+
document.removeEventListener("mouseup", schedule);
|
|
4648
|
+
document.removeEventListener("keyup", schedule);
|
|
4649
|
+
document.removeEventListener("selectionchange", schedule);
|
|
4650
|
+
scrollEl?.removeEventListener("scroll", onScroll);
|
|
4651
|
+
if (raf !== 0) window.cancelAnimationFrame(raf);
|
|
4652
|
+
};
|
|
4653
|
+
}, [phase, scrollEl]);
|
|
4654
|
+
const clearSelection = (0, react.useCallback)(() => {
|
|
4655
|
+
window.getSelection()?.removeAllRanges();
|
|
4656
|
+
setSel(null);
|
|
4657
|
+
}, []);
|
|
4658
|
+
/** 当前选区覆盖的所有页锚点(跨页时不止一条)。 */
|
|
4659
|
+
const selAnchors = (0, react.useCallback)((state) => {
|
|
4660
|
+
return [{
|
|
4661
|
+
page: state.page,
|
|
4662
|
+
...state.anchor
|
|
4663
|
+
}, ...state.more];
|
|
4664
|
+
}, []);
|
|
4665
|
+
/** 「刚建好的标注,绘制完成后自动打开浮层」。 */
|
|
4666
|
+
const pendingEditorRef = (0, react.useRef)(null);
|
|
4667
|
+
/** 取色 / 划线:每一页的选中部分各落一条(同页同段落笔会合并成一条)。 */
|
|
4668
|
+
const applyMark = (0, react.useCallback)((color, style) => {
|
|
4669
|
+
if (sel !== null) for (const anchor of selAnchors(sel)) paintPageTextAnnotation(docKey, anchor, color, style, "");
|
|
4670
|
+
clearSelection();
|
|
4671
|
+
}, [
|
|
4672
|
+
sel,
|
|
4673
|
+
selAnchors,
|
|
4674
|
+
docKey,
|
|
4675
|
+
clearSelection
|
|
4676
|
+
]);
|
|
4677
|
+
/** 写想法:先落一条荧光黄,绘制完成后自动打开它的编辑浮层。 */
|
|
4678
|
+
const applyNote = (0, react.useCallback)(() => {
|
|
4679
|
+
if (sel === null) return;
|
|
4680
|
+
const anchors = selAnchors(sel);
|
|
4681
|
+
const first = anchors[0];
|
|
4682
|
+
for (const anchor of anchors) {
|
|
4683
|
+
const created = paintPageTextAnnotation(docKey, anchor, "yellow", "highlight", "");
|
|
4684
|
+
if (anchor === first) pendingEditorRef.current = created.id;
|
|
4685
|
+
}
|
|
4686
|
+
clearSelection();
|
|
4687
|
+
}, [
|
|
4688
|
+
sel,
|
|
4689
|
+
selAnchors,
|
|
4690
|
+
docKey,
|
|
4691
|
+
clearSelection
|
|
4692
|
+
]);
|
|
4693
|
+
/** 量某条标注的视口落点(没画出来就等几帧——页面可能刚滚进视口)。 */
|
|
4694
|
+
const openEditorFor = (0, react.useCallback)((id) => {
|
|
4695
|
+
const anno = annosRef.current.find((a) => a.id === id);
|
|
4696
|
+
if (anno === void 0) return;
|
|
4697
|
+
if (!pages.current.has(anno.page)) gotoPage(anno.page);
|
|
4698
|
+
let tries = 0;
|
|
4699
|
+
const tick = () => {
|
|
4700
|
+
tries++;
|
|
4701
|
+
const handle = pages.current.get(anno.page);
|
|
4702
|
+
const marks = handle?.marks ?? null;
|
|
4703
|
+
const host = handle?.el ?? null;
|
|
4704
|
+
if (marks !== null && host !== null) {
|
|
4705
|
+
const rect = firstAnnoRect(marks, id);
|
|
4706
|
+
if (rect !== null) {
|
|
4707
|
+
const box = host.getBoundingClientRect();
|
|
4708
|
+
setEditor({
|
|
4709
|
+
id,
|
|
4710
|
+
...popoverAt({
|
|
4711
|
+
left: box.left + rect.left,
|
|
4712
|
+
top: box.top + rect.top,
|
|
4713
|
+
right: box.left + rect.left + rect.width,
|
|
4714
|
+
bottom: box.top + rect.top + rect.height
|
|
4715
|
+
})
|
|
4716
|
+
});
|
|
4717
|
+
return;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
if (tries < 40) window.requestAnimationFrame(tick);
|
|
4721
|
+
else setEditor({
|
|
4722
|
+
id,
|
|
4723
|
+
left: 80,
|
|
4724
|
+
top: 96
|
|
4725
|
+
});
|
|
4726
|
+
};
|
|
4727
|
+
window.requestAnimationFrame(tick);
|
|
4728
|
+
}, [gotoPage]);
|
|
4729
|
+
(0, react.useEffect)(() => {
|
|
4730
|
+
const pending = pendingEditorRef.current;
|
|
4731
|
+
if (pending === null) return;
|
|
4732
|
+
pendingEditorRef.current = null;
|
|
4733
|
+
openEditorFor(pending);
|
|
4734
|
+
}, [annos, openEditorFor]);
|
|
4735
|
+
const locateAnno = (0, react.useCallback)((anno) => {
|
|
4736
|
+
gotoPage(anno.page);
|
|
4737
|
+
openEditorFor(anno.id);
|
|
4738
|
+
}, [gotoPage, openEditorFor]);
|
|
4739
|
+
const currentAnno = (0, react.useMemo)(() => editor === null ? null : annos.find((a) => a.id === editor.id) ?? null, [editor, annos]);
|
|
4740
|
+
const patchAnno = (0, react.useCallback)((patch) => {
|
|
4741
|
+
if (editor === null) return;
|
|
4742
|
+
updateAnnotation(docKey, editor.id, patch);
|
|
4743
|
+
}, [docKey, editor]);
|
|
4744
|
+
const deleteAnno = (0, react.useCallback)((id) => {
|
|
4745
|
+
removeAnnotation(docKey, id);
|
|
4746
|
+
setEditor(null);
|
|
4747
|
+
}, [docKey]);
|
|
4748
|
+
const askAnno = (0, react.useCallback)((quote, note) => {
|
|
4749
|
+
onAsk?.(annotationQuestion(quote, note));
|
|
4750
|
+
}, [onAsk]);
|
|
4751
|
+
(0, react.useEffect)(() => {
|
|
4752
|
+
if (sel === null && editor === null && !panelOpen) return;
|
|
4753
|
+
const onKey = (e) => {
|
|
4754
|
+
if (e.key !== "Escape") return;
|
|
4755
|
+
e.preventDefault();
|
|
4756
|
+
e.stopPropagation();
|
|
4757
|
+
clearSelection();
|
|
4758
|
+
setEditor(null);
|
|
4759
|
+
setPanelOpen(false);
|
|
4760
|
+
};
|
|
4761
|
+
window.addEventListener("keydown", onKey, true);
|
|
4762
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
4763
|
+
}, [
|
|
4764
|
+
sel,
|
|
4765
|
+
editor,
|
|
4766
|
+
panelOpen,
|
|
4767
|
+
clearSelection
|
|
4768
|
+
]);
|
|
4769
|
+
/**
|
|
4770
|
+
* 点已上笔的色块 → 打开编辑浮层。
|
|
4771
|
+
* 色块自身 `pointer-events: none`(否则它盖在文字层上就拖不出选区了),
|
|
4772
|
+
* 因此点击靠页内坐标反查:底下的标记矩形命中才算点中。
|
|
4773
|
+
*/
|
|
4774
|
+
const onPagesClick = (0, react.useCallback)((e) => {
|
|
4775
|
+
const selection = window.getSelection();
|
|
4776
|
+
if (selection !== null && !selection.isCollapsed) return;
|
|
4777
|
+
const host = e.target?.closest("[data-page]") ?? null;
|
|
4778
|
+
if (host === null) return;
|
|
4779
|
+
const pageNo = Number(host.getAttribute("data-page") ?? "0");
|
|
4780
|
+
const handle = pages.current.get(pageNo);
|
|
4781
|
+
if (handle?.marks == null) return;
|
|
4782
|
+
const box = host.getBoundingClientRect();
|
|
4783
|
+
const id = hitTestAnnotations(handle.marks, e.clientX - box.left, e.clientY - box.top);
|
|
4784
|
+
if (id !== null) openEditorFor(id);
|
|
4785
|
+
}, [openEditorFor]);
|
|
4786
|
+
const zoomBy = (0, react.useCallback)((dir) => {
|
|
4787
|
+
setFitWidth(false);
|
|
4788
|
+
anchorThenScale((prev) => {
|
|
4789
|
+
if (dir === 1) return ZOOM_STEPS.find((s) => s > prev + .001) ?? ZOOM_STEPS[ZOOM_STEPS.length - 1];
|
|
4790
|
+
return [...ZOOM_STEPS].reverse().find((s) => s < prev - .001) ?? ZOOM_STEPS[0];
|
|
4791
|
+
});
|
|
4792
|
+
}, [anchorThenScale]);
|
|
4793
|
+
/** 文字层就绪:当前页的摘要可能刚刚才有(首次渲染时上下文只有页码没有摘要)。 */
|
|
4794
|
+
const onTextLayer = (0, react.useCallback)((pageNo) => {
|
|
4795
|
+
if (pageNo !== lastContextPage.current) return;
|
|
4796
|
+
onReadingRef.current({
|
|
4797
|
+
relPath,
|
|
4798
|
+
title,
|
|
4799
|
+
section: `第 ${String(pageNo)} 页 / 共 ${String(numPages)} 页`,
|
|
4800
|
+
excerpt: excerptOf(pageNo)
|
|
4801
|
+
});
|
|
4802
|
+
}, [
|
|
4803
|
+
excerptOf,
|
|
4804
|
+
numPages,
|
|
4805
|
+
relPath,
|
|
4806
|
+
title
|
|
4807
|
+
]);
|
|
4808
|
+
const noteCount = (0, react.useMemo)(() => annos.filter((a) => a.note.trim() !== "").length, [annos]);
|
|
4809
|
+
const noteOrder = (0, react.useMemo)(() => {
|
|
4810
|
+
const map = /* @__PURE__ */ new Map();
|
|
4811
|
+
let n = 0;
|
|
4812
|
+
for (const anno of annos) {
|
|
4813
|
+
if (anno.note.trim() === "") continue;
|
|
4814
|
+
n++;
|
|
4815
|
+
map.set(anno.id, n);
|
|
4816
|
+
}
|
|
4817
|
+
return map;
|
|
4818
|
+
}, [annos]);
|
|
4819
|
+
const pageAnnosRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
4820
|
+
const annosByPage = (0, react.useMemo)(() => {
|
|
4821
|
+
const next = /* @__PURE__ */ new Map();
|
|
4822
|
+
for (const anno of annos) {
|
|
4823
|
+
const list = next.get(anno.page);
|
|
4824
|
+
if (list === void 0) next.set(anno.page, [anno]);
|
|
4825
|
+
else list.push(anno);
|
|
4826
|
+
}
|
|
4827
|
+
for (const [page, list] of next) {
|
|
4828
|
+
const prev = pageAnnosRef.current.get(page);
|
|
4829
|
+
if (prev !== void 0 && prev.length === list.length && prev.every((a, i) => a === list[i])) next.set(page, prev);
|
|
4830
|
+
}
|
|
4831
|
+
pageAnnosRef.current = next;
|
|
4832
|
+
return next;
|
|
4833
|
+
}, [annos]);
|
|
4834
|
+
const showPanel = panelOpen;
|
|
4835
|
+
const fabRight = showPanel ? 314 : 14;
|
|
4836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4837
|
+
className: classes$1.pdfWrap,
|
|
4838
|
+
"data-aero-pdf-wrap": "",
|
|
4839
|
+
children: [
|
|
4840
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4841
|
+
className: classes$1.pdfToolbar,
|
|
4842
|
+
children: [
|
|
4843
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4844
|
+
type: "button",
|
|
4845
|
+
className: classes$1.pdfBtn,
|
|
4846
|
+
onClick: () => gotoPage(currentPage - 1),
|
|
4847
|
+
disabled: currentPage <= 1,
|
|
4848
|
+
title: "上一页",
|
|
4849
|
+
children: "上一页"
|
|
4850
|
+
}),
|
|
4851
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4852
|
+
className: classes$1.pdfPageInput,
|
|
4853
|
+
value: pageInput,
|
|
4854
|
+
inputMode: "numeric",
|
|
4855
|
+
"aria-label": "页码",
|
|
4856
|
+
onChange: (e) => setPageInput(e.target.value.replace(/[^\d]/g, "")),
|
|
4857
|
+
onFocus: () => {
|
|
4858
|
+
pageInputFocused.current = true;
|
|
4859
|
+
},
|
|
4860
|
+
onBlur: () => {
|
|
4861
|
+
pageInputFocused.current = false;
|
|
4862
|
+
setPageInput(String(currentPage));
|
|
4863
|
+
},
|
|
4864
|
+
onKeyDown: (e) => {
|
|
4865
|
+
if (e.key !== "Enter") return;
|
|
4866
|
+
gotoPage(Number(pageInput) || currentPage);
|
|
4867
|
+
pageInputFocused.current = false;
|
|
4868
|
+
}
|
|
4869
|
+
}),
|
|
4870
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4871
|
+
className: classes$1.pdfPageTotal,
|
|
4872
|
+
children: [
|
|
4873
|
+
"/ ",
|
|
4874
|
+
numPages > 0 ? String(numPages) : "—",
|
|
4875
|
+
" 页"
|
|
4876
|
+
]
|
|
4877
|
+
}),
|
|
4878
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4879
|
+
type: "button",
|
|
4880
|
+
className: classes$1.pdfBtn,
|
|
4881
|
+
onClick: () => gotoPage(currentPage + 1),
|
|
4882
|
+
disabled: numPages > 0 && currentPage >= numPages,
|
|
4883
|
+
title: "下一页",
|
|
4884
|
+
children: "下一页"
|
|
4885
|
+
}),
|
|
4886
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: classes$1.pdfSpacer }),
|
|
4887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4888
|
+
type: "button",
|
|
4889
|
+
className: classes$1.pdfBtn,
|
|
4890
|
+
onClick: () => zoomBy(-1),
|
|
4891
|
+
disabled: scale <= 0,
|
|
4892
|
+
title: "缩小(Ctrl + 滚轮也可以)",
|
|
4893
|
+
children: "缩小"
|
|
4894
|
+
}),
|
|
4895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4896
|
+
type: "button",
|
|
4897
|
+
className: fitWidth ? `${classes$1.pdfBtn} ${classes$1.pdfBtnOn}` : classes$1.pdfBtn,
|
|
4898
|
+
onClick: () => setFitWidth(true),
|
|
4899
|
+
title: "按窗口宽度自适应",
|
|
4900
|
+
"aria-pressed": fitWidth,
|
|
4901
|
+
children: "适宽"
|
|
4902
|
+
}),
|
|
4903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4904
|
+
type: "button",
|
|
4905
|
+
className: classes$1.pdfBtn,
|
|
4906
|
+
onClick: () => zoomBy(1),
|
|
4907
|
+
disabled: scale <= 0,
|
|
4908
|
+
title: "放大(Ctrl + 滚轮也可以)",
|
|
4909
|
+
children: "放大"
|
|
4910
|
+
}),
|
|
4911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4912
|
+
className: classes$1.pdfZoom,
|
|
4913
|
+
children: scale > 0 ? `${String(Math.round(scale * 100))}%` : "—"
|
|
4914
|
+
}),
|
|
4915
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4916
|
+
className: classes$1.pdfHint,
|
|
4917
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "选中文字" }), "即可取色标记 / 写批注"]
|
|
4918
|
+
})
|
|
4919
|
+
]
|
|
4920
|
+
}),
|
|
4921
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4922
|
+
className: classes$1.pdfBody,
|
|
4923
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4924
|
+
ref: attachScroller,
|
|
4925
|
+
className: classes$1.pdfScroll,
|
|
4926
|
+
children: [phase !== "ready" || scale <= 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4927
|
+
className: classes$1.pdfNotice,
|
|
4928
|
+
children: phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: ["PDF 打不开:", error] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4929
|
+
className: classes$1.pdfNoticeHint,
|
|
4930
|
+
children: "排障:① 宿主是否已重启(新路由 `/aero-kb/pdf`)② 文件是否在库里 ③ 控制台里搜 `aero-pdf`"
|
|
4931
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: classes$1.spinner }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
4932
|
+
phase === "runtime" && "正在加载 PDF 渲染引擎…",
|
|
4933
|
+
phase === "document" && "正在打开 PDF…",
|
|
4934
|
+
phase === "measuring" && `正在读取版面:${String(measured)} 页…`,
|
|
4935
|
+
phase === "ready" && scale <= 0 && "正在按窗口宽度排版…"
|
|
4936
|
+
] })] })
|
|
4937
|
+
}) : null, phase === "ready" && scale > 0 && doc !== null && api !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4938
|
+
ref: pagesRef,
|
|
4939
|
+
className: classes$1.pdfPages,
|
|
4940
|
+
onClick: onPagesClick,
|
|
4941
|
+
children: sizes.map((size, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PdfPage, {
|
|
4942
|
+
api,
|
|
4943
|
+
doc,
|
|
4944
|
+
pageNo: i + 1,
|
|
4945
|
+
size,
|
|
4946
|
+
scale,
|
|
4947
|
+
root: scrollEl,
|
|
4948
|
+
annos: annosByPage.get(i + 1) ?? NO_ANNOS,
|
|
4949
|
+
noteOrder,
|
|
4950
|
+
register,
|
|
4951
|
+
onOpenAnno: openEditorFor,
|
|
4952
|
+
onTextLayer
|
|
4953
|
+
}, i))
|
|
4954
|
+
})]
|
|
4955
|
+
}), showPanel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarksPanel, {
|
|
4956
|
+
title,
|
|
4957
|
+
annos,
|
|
4958
|
+
exportKey: docKey,
|
|
4959
|
+
showPage: true,
|
|
4960
|
+
onLocate: locateAnno,
|
|
4961
|
+
onAsk: onAsk === void 0 ? null : (anno) => askAnno(anno.quote, anno.note),
|
|
4962
|
+
onDelete: (anno) => deleteAnno(anno.id),
|
|
4963
|
+
onClear: () => {
|
|
4964
|
+
for (const anno of annos) removeAnnotation(docKey, anno.id);
|
|
4965
|
+
setEditor(null);
|
|
4966
|
+
},
|
|
4967
|
+
onClose: () => setPanelOpen(false)
|
|
4968
|
+
})]
|
|
4969
|
+
}),
|
|
4970
|
+
resume !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4971
|
+
className: classes$1.docResume,
|
|
4972
|
+
"data-aero-resume": "",
|
|
4973
|
+
children: [
|
|
4974
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4975
|
+
className: classes$1.docResumeText,
|
|
4976
|
+
role: "status",
|
|
4977
|
+
children: [
|
|
4978
|
+
"已回到上次读到处",
|
|
4979
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4980
|
+
className: classes$1.docResumeName,
|
|
4981
|
+
children: [
|
|
4982
|
+
"第 ",
|
|
4983
|
+
resume.page,
|
|
4984
|
+
" 页"
|
|
4985
|
+
]
|
|
4986
|
+
}),
|
|
4987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4988
|
+
className: classes$1.docResumeAge,
|
|
4989
|
+
children: describeAge(resume.at)
|
|
4990
|
+
})
|
|
4991
|
+
]
|
|
4992
|
+
}),
|
|
4993
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4994
|
+
type: "button",
|
|
4995
|
+
className: classes$1.docResumeBtn,
|
|
4996
|
+
onClick: () => {
|
|
4997
|
+
clearPdfPos(docKey);
|
|
4998
|
+
setResume(null);
|
|
4999
|
+
scrollRef.current?.scrollTo({
|
|
5000
|
+
top: 0,
|
|
5001
|
+
behavior: "smooth"
|
|
5002
|
+
});
|
|
5003
|
+
},
|
|
5004
|
+
title: "滚到第一页,并忘掉这份 PDF 的阅读位置",
|
|
5005
|
+
children: "从头读"
|
|
5006
|
+
}),
|
|
5007
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5008
|
+
type: "button",
|
|
5009
|
+
className: classes$1.docResumeClose,
|
|
5010
|
+
onClick: () => setResume(null),
|
|
5011
|
+
title: "知道了(阅读位置继续保留)",
|
|
5012
|
+
"aria-label": "关闭提示",
|
|
5013
|
+
children: "×"
|
|
5014
|
+
})
|
|
5015
|
+
]
|
|
5016
|
+
}),
|
|
5017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarksFab, {
|
|
5018
|
+
count: annos.length,
|
|
5019
|
+
noted: noteCount,
|
|
5020
|
+
open: showPanel,
|
|
5021
|
+
right: fabRight,
|
|
5022
|
+
onClick: () => {
|
|
5023
|
+
setPanelOpen((open) => !open);
|
|
5024
|
+
setEditor(null);
|
|
5025
|
+
clearSelection();
|
|
5026
|
+
}
|
|
5027
|
+
}),
|
|
5028
|
+
sel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionBar, {
|
|
5029
|
+
sel,
|
|
5030
|
+
onPick: applyMark,
|
|
5031
|
+
onNote: applyNote,
|
|
5032
|
+
onAsk: onAsk === void 0 ? null : () => askAnno(sel.anchor.quote, ""),
|
|
5033
|
+
onDismiss: clearSelection
|
|
5034
|
+
}),
|
|
5035
|
+
editor !== null && currentAnno !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoteEditor, {
|
|
5036
|
+
anno: currentAnno,
|
|
5037
|
+
at: editor,
|
|
5038
|
+
onPatch: patchAnno,
|
|
5039
|
+
onDelete: () => deleteAnno(editor.id),
|
|
5040
|
+
onAsk: onAsk === void 0 ? null : (note) => askAnno(currentAnno.quote, note),
|
|
5041
|
+
onClose: () => setEditor(null)
|
|
5042
|
+
}, editor.id)
|
|
5043
|
+
]
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
// src/client/SlidevEnvPanel.tsx
|
|
5047
|
+
/**
|
|
5048
|
+
* Slidev 环境体检面板。
|
|
5049
|
+
*
|
|
5050
|
+
* 课件「一直显示正在准备」的根因几乎都在宿主机环境(缺 @slidev/cli、版本过旧、
|
|
5051
|
+
* 主题依赖没装)。本面板把后端体检结果摊开给用户看:缺什么、为什么、怎么修;
|
|
5052
|
+
* 并且**只有在用户于确认框中明确同意后**才调用一键安装接口
|
|
5053
|
+
* (POST /aero-kb/slidev/env,带体检返回的同意票据)。
|
|
5054
|
+
*/
|
|
5055
|
+
const STATUS_TEXT = {
|
|
5056
|
+
ok: "正常",
|
|
5057
|
+
warn: "注意",
|
|
5058
|
+
fail: "缺失"
|
|
5059
|
+
};
|
|
5060
|
+
async function fetchEnv(relPath, cwdParam, force) {
|
|
5061
|
+
const qs = `?p=${encodeURIComponent(relPath)}${cwdParam}${force ? "&force=1" : ""}`;
|
|
5062
|
+
const r = await fetch(`/aero-kb/slidev/env${qs}`);
|
|
5063
|
+
const body = await r.json();
|
|
5064
|
+
if (!r.ok || !body.ok || body.report === void 0) throw new Error(body.error !== void 0 && body.error !== "" ? body.error : `HTTP ${r.status}`);
|
|
5065
|
+
return body.report;
|
|
5066
|
+
}
|
|
5067
|
+
/** 确认框里展示的「将要执行的命令与写入位置」——用户点确认前必须看到这些。 */
|
|
5068
|
+
function describeAction(a) {
|
|
5069
|
+
return [
|
|
5070
|
+
`即将执行:${a.command}`,
|
|
5071
|
+
a.cwd !== "" ? `执行目录:${a.cwd}` : "执行目录:宿主机当前目录",
|
|
5072
|
+
`写入范围:${a.writes}`,
|
|
5073
|
+
a.scope === "global" ? "⚠️ 这是全局安装,会修改本机 npm 全局环境(影响所有项目)。" : "仅影响本仓库内文件。",
|
|
5074
|
+
"",
|
|
5075
|
+
"确认执行吗?"
|
|
5076
|
+
].join("\n");
|
|
5077
|
+
}
|
|
5078
|
+
function SlidevEnvPanel({ relPath, cwdParam, onReady }) {
|
|
5079
|
+
const [report, setReport] = (0, react.useState)(null);
|
|
5080
|
+
const [error, setError] = (0, react.useState)(null);
|
|
5081
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
5082
|
+
const [running, setRunning] = (0, react.useState)("");
|
|
5083
|
+
const [output, setOutput] = (0, react.useState)("");
|
|
5084
|
+
const [copied, setCopied] = (0, react.useState)("");
|
|
5085
|
+
const load = (0, react.useCallback)(async (force) => {
|
|
5086
|
+
setBusy(true);
|
|
5087
|
+
setError(null);
|
|
5088
|
+
try {
|
|
5089
|
+
const r = await fetchEnv(relPath, cwdParam, force);
|
|
5090
|
+
setReport(r);
|
|
5091
|
+
} catch (e) {
|
|
5092
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
5093
|
+
} finally {
|
|
5094
|
+
setBusy(false);
|
|
5095
|
+
}
|
|
5096
|
+
}, [relPath, cwdParam]);
|
|
5097
|
+
(0, react.useEffect)(() => {
|
|
5098
|
+
load(false);
|
|
5099
|
+
}, [load]);
|
|
5100
|
+
const copy = (0, react.useCallback)((text) => {
|
|
5101
|
+
const done = () => {
|
|
5102
|
+
setCopied(text);
|
|
5103
|
+
window.setTimeout(() => setCopied(""), 1500);
|
|
5104
|
+
};
|
|
5105
|
+
try {
|
|
5106
|
+
navigator.clipboard.writeText(text).then(done, () => {});
|
|
5107
|
+
} catch {}
|
|
5108
|
+
}, []);
|
|
5109
|
+
const install = (0, react.useCallback)(async (action) => {
|
|
5110
|
+
if (!window.confirm(describeAction(action))) return;
|
|
5111
|
+
setRunning(action.id);
|
|
5112
|
+
setOutput("");
|
|
5113
|
+
setError(null);
|
|
5114
|
+
try {
|
|
5115
|
+
const r = await fetch(`/aero-kb/slidev/env?p=${encodeURIComponent(relPath)}${cwdParam}`, {
|
|
5116
|
+
method: "POST",
|
|
5117
|
+
headers: { "Content-Type": "application/json" },
|
|
5118
|
+
body: JSON.stringify({
|
|
5119
|
+
actionId: action.id,
|
|
5120
|
+
token: action.token,
|
|
5121
|
+
confirm: true,
|
|
5122
|
+
p: relPath
|
|
5123
|
+
})
|
|
5124
|
+
});
|
|
5125
|
+
const body = await r.json();
|
|
5126
|
+
if (!r.ok || !body.ok || body.result === void 0) throw new Error(body.error !== void 0 && body.error !== "" ? body.error : `HTTP ${r.status}`);
|
|
5127
|
+
setOutput(body.result.output);
|
|
5128
|
+
setReport(body.result.report);
|
|
5129
|
+
if (!body.result.ok) setError(`安装未成功(退出码 ${String(body.result.code)}):${body.result.error}`);
|
|
5130
|
+
else if (body.result.report.ready) onReady?.();
|
|
5131
|
+
} catch (e) {
|
|
5132
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
5133
|
+
} finally {
|
|
5134
|
+
setRunning("");
|
|
3452
5135
|
}
|
|
3453
5136
|
}, [
|
|
3454
5137
|
relPath,
|
|
@@ -5432,6 +7115,14 @@ window.__ModuleLoader__.load({
|
|
|
5432
7115
|
});
|
|
5433
7116
|
return;
|
|
5434
7117
|
}
|
|
7118
|
+
if ((node.ext ?? "").toLowerCase() === "pdf") {
|
|
7119
|
+
setContent({
|
|
7120
|
+
kind: "pdf",
|
|
7121
|
+
relPath: node.path,
|
|
7122
|
+
name: node.name
|
|
7123
|
+
});
|
|
7124
|
+
return;
|
|
7125
|
+
}
|
|
5435
7126
|
loadDoc(node.path, node.name, 0);
|
|
5436
7127
|
}, [loadDoc]);
|
|
5437
7128
|
/** 同一文档翻段(宿主按渲染体积切段,见 src/docsplit.ts)。 */
|
|
@@ -5640,6 +7331,13 @@ window.__ModuleLoader__.load({
|
|
|
5640
7331
|
cwdParam,
|
|
5641
7332
|
onReadingChange,
|
|
5642
7333
|
onAsk: askAnnotated
|
|
7334
|
+
}),
|
|
7335
|
+
content !== null && content.kind === "pdf" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PdfViewer, {
|
|
7336
|
+
relPath: content.relPath,
|
|
7337
|
+
title: content.name,
|
|
7338
|
+
cwdParam,
|
|
7339
|
+
onReadingChange,
|
|
7340
|
+
onAsk: askAnnotated
|
|
5643
7341
|
})
|
|
5644
7342
|
]
|
|
5645
7343
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|