minecodex 2.0.53 → 2.0.55
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 +1 -1
- package/apps/dashboard/assets/preview-content.js +2 -2
- package/features/mermaid-diagrams/assets/qv-dependencies.js +850 -815
- package/features/mermaid-diagrams/assets/qv-template.json +1 -1
- package/features/mermaid-diagrams/codex-feature.json +4 -4
- package/features/mermaid-diagrams/src/adapters/codex/native.mjs +12 -3
- package/features/mermaid-diagrams/src/mermaid/detail-position.mjs +7 -0
- package/features/mermaid-diagrams/src/mermaid/frame.mjs +1 -1
- package/features/mermaid-diagrams/src/mermaid/qv-git.js +95 -0
- package/features/mermaid-diagrams/src/mermaid/qv-motion.js +25 -0
- package/features/mermaid-diagrams/src/mermaid/qv-polish.css +24 -0
- package/features/mermaid-diagrams/src/mermaid/qv-title.mjs +11 -0
- package/features/mermaid-diagrams/vendor/qv/THIRD_PARTY_NOTICES.txt +27 -0
- package/package.json +2 -1
- package/packages/cli/src/config.mjs +1 -1
- package/packages/plugin/assets/app.js +2 -2
- package/packages/plugin/assets/prompts.js +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"markup":"<div id=\"mcx-qv-root\" class=\"qv-mermaid\" data-state=\"loading\" role=\"figure\" aria-label=\"\">\n<style>\n#mcx-qv-root{position:relative;padding-top:32px;width:100%;color:var(--foreground)}\n#mcx-qv-root h3{margin:0 90px 32px 0;font-weight:500}\n#mcx-qv-root .scope-nav{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 24px;font-size:12px;color:var(--muted-foreground)}\n#mcx-qv-root .scope-nav button{font:inherit;color:inherit;border:0;background:none;padding:5px 4px;cursor:var(--cursor-interaction,pointer)}\n#mcx-qv-root .scope-nav button:hover{color:var(--foreground)}\n#mcx-qv-root .scope-nav [aria-current]{color:var(--foreground);font-weight:500}\n#mcx-qv-root .scope-nav .scope-count{margin-left:auto;font-variant-numeric:tabular-nums}\n#mcx-qv-root[data-navigation=\"true\"] button.node{justify-content:flex-start;text-align:left;background:var(--background);color:var(--foreground)}\n#mcx-qv-root[data-navigation=\"true\"] button.node.group-card{flex-direction:column;align-items:flex-start;justify-content:center;gap:9px;border-color:color-mix(in srgb,var(--viz-series-1) 20%,var(--border));background:color-mix(in srgb,var(--viz-series-1) 3%,var(--background));padding:14px 16px}\n#mcx-qv-root button.node.group-card .group-detail{display:flex;align-items:center;justify-content:space-between;width:100%;color:var(--muted-foreground);font-size:12px}\n#mcx-qv-root button.node.group-card:hover{border-color:var(--viz-series-1)}\n#mcx-qv-root button.node.external{flex-direction:column;align-items:flex-start;justify-content:center;gap:6px;color:var(--muted-foreground)}\n#mcx-qv-root button.node .external-label{font-size:11px;color:var(--muted-foreground)}\n#mcx-qv-root[data-navigation=\"true\"] .edge-label{font-size:11px}\n#mcx-qv-root .scope-nav svg{width:14px;height:14px;flex:none}\n#mcx-qv-root .scene{position:relative;width:100%}\n#mcx-qv-root .diagram-scroll{width:100%;overflow-x:auto;overscroll-behavior-x:contain;padding-bottom:4px}\n#mcx-qv-root .qv-group{position:absolute;border:1px solid var(--border);border-radius:12px;box-sizing:border-box;pointer-events:none;background:color-mix(in srgb,var(--muted) 16%,transparent)}\n#mcx-qv-root .qv-group>span{position:absolute;left:16px;top:10px;color:var(--muted-foreground);font-size:12px}\n#mcx-qv-root button.node .name{position:relative;z-index:1}\n#mcx-qv-root button.node .node-outline{position:absolute;inset:0;width:100%;height:100%;overflow:visible;fill:var(--background);stroke:var(--node-highlight,var(--border));stroke-width:1}\n#mcx-qv-root button.node.shaped{border:0}\n#mcx-qv-root button.node.junction{border:0;background:var(--muted-foreground);border-radius:50%;padding:0}\n#mcx-qv-root .wires{display:block;width:100%;position:absolute;inset:0;overflow:visible;pointer-events:none}\n#mcx-qv-root .arrow-head{fill:color-mix(in srgb,var(--muted-foreground) 26%,var(--background));pointer-events:none}\n#mcx-qv-root .wire{fill:none;stroke:color-mix(in srgb,var(--muted-foreground) 26%,var(--background));stroke-width:1;opacity:1;transition:opacity 140ms ease}\n#mcx-qv-root button.node{position:absolute;display:flex;flex-direction:row;align-items:center;justify-content:center;padding:12px 14px;text-align:left;font:inherit;color:var(--foreground);background:var(--background);border:1px solid var(--border);border-radius:10px;box-sizing:border-box;cursor:var(--cursor-interaction,pointer);transition:opacity 140ms ease,border-color 140ms ease}\n#mcx-qv-root .edge-label{position:absolute;transform:translate(-50%,-50%);padding:2px 5px;background:var(--background);color:var(--muted-foreground);font-size:12px;pointer-events:none}\n#mcx-qv-root .qv-notice{font-size:12px;color:var(--muted-foreground)}\n#mcx-qv-root button.node .name{display:flex;align-items:center;gap:8px;font-weight:500;white-space:nowrap}\n#mcx-qv-root button.node[data-layer=\"1\"]{background:color-mix(in srgb,var(--viz-series-1) 6%,var(--background))}\n#mcx-qv-root button.node[data-layer=\"3\"]{background:var(--secondary);color:var(--secondary-foreground)}\n#mcx-qv-root .layer{position:absolute;left:0;color:var(--muted-foreground);font-size:12px;pointer-events:none}\n#mcx-qv-root .layer:after{content:'';position:absolute;left:64px;top:50%;width:var(--rule-width);height:1px;background:var(--border);opacity:.55}\n#mcx-qv-root .edge-key{display:flex;justify-content:center;gap:22px;margin-top:20px;padding:16px 0;color:var(--muted-foreground);font-size:12px}\n#mcx-qv-root [hidden]{display:none!important}\n#mcx-qv-root .edge-key span{display:inline-flex;align-items:center;gap:8px}\n#mcx-qv-root .edge-key i{width:20px;border-top:1px solid var(--muted-foreground)}\n#mcx-qv-root .edge-key .async{border-top-style:dashed}\n@media(prefers-reduced-motion:reduce){#mcx-qv-root button.node,#mcx-qv-root .wire{transition:none}}\n#mcx-qv-root .route-controls{display:flex;justify-content:flex-end;margin:-16px 0 24px;gap:8px;align-items:center}\n#mcx-qv-root .route-controls label{white-space:nowrap;flex:none}\n#mcx-qv-root .node-tip{position:absolute;z-index:10;max-width:260px;width:max-content;box-sizing:border-box;padding:10px 12px;border-radius:7px;background:#202020;color:#fff;font-size:12px;line-height:1.6;pointer-events:none;box-shadow:0 3px 12px #0002}\n\n#mcx-qv-root[data-navigation=\"true\"] button.node.preview-group{padding:18px 16px;justify-content:flex-start;gap:0;background:color-mix(in srgb,var(--muted) 18%,var(--background));border-color:var(--border);border-radius:14px}\n#mcx-qv-root button.preview-group>.name{font-size:14px;gap:8px}\n#mcx-qv-root button.preview-group>.name svg{color:var(--muted-foreground)}\n#mcx-qv-root button.node.preview-group .group-detail{position:absolute;bottom:12px;left:16px;width:calc(100% - 32px);font-size:11px}\n#mcx-qv-root .preview-item{position:absolute;left:26px;right:12px;display:flex;gap:8px;align-items:center;font-size:12px;font-weight:400;white-space:nowrap;height:20px;overflow:hidden;text-overflow:ellipsis}\n#mcx-qv-root .preview-item svg{width:14px;height:14px;flex:none;color:var(--muted-foreground)}\n#mcx-qv-root .preview-section{position:absolute;left:24px;right:12px;font-size:11px;font-weight:500;color:var(--muted-foreground);text-align:left}\n#mcx-qv-root .preview-wires{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}\n#mcx-qv-root .preview-link{stroke:color-mix(in srgb,var(--muted-foreground) 22%,transparent);stroke-width:.8}\n#mcx-qv-root .preview-arrow{fill:color-mix(in srgb,var(--muted-foreground) 30%,transparent)}\n#mcx-qv-root .preview-group:hover .preview-link{stroke:color-mix(in srgb,var(--viz-series-1) 45%,transparent)}\n#mcx-qv-root .preview-group:hover .preview-arrow{fill:var(--viz-series-1)}\n\n#mcx-qv-root .scope-toggle{margin-left:auto}\n#mcx-qv-root button.node.group-card>.name{width:100%}\n#mcx-qv-root button.node .group-enter{display:flex;margin-left:auto;width:14px;height:14px;color:var(--muted-foreground)}\n#mcx-qv-root .edge-count{position:absolute;transform:translate(-50%,-50%);width:22px;height:22px;display:grid;place-items:center;border-radius:50%;border:1px solid var(--border);background:var(--background);color:var(--muted-foreground);font-size:11px;font-variant-numeric:tabular-nums;pointer-events:none}\n#mcx-qv-root[data-has-groups=\"true\"] button.node:not(.group-card){background:color-mix(in srgb,var(--muted) 18%,var(--background));color:var(--foreground);border-radius:12px;font-size:12px;padding:12px 16px}\n#mcx-qv-root[data-has-groups=\"true\"] button.node .name svg{width:14px;height:14px;color:var(--muted-foreground)}\n#mcx-qv-root[data-has-groups=\"true\"] .qv-group{background:color-mix(in srgb,var(--muted) 12%,transparent);border-radius:14px}\n\n#mcx-qv-root .scope-envelope{position:absolute;box-sizing:border-box;border:1px solid var(--border);border-radius:14px;transform-origin:0 0;pointer-events:none}\n#mcx-qv-root button.node:active{filter:brightness(.98)}\n#mcx-qv-root button.node.shaped[data-shape]{background:transparent}\n\n#mcx-qv-root[data-architecture=true] button.node>.name{width:100%;align-items:flex-start;white-space:normal;line-height:20px}\n#mcx-qv-root[data-architecture=true] .architecture-name{flex:1;min-width:0;font-size:14px;line-height:20px;overflow-wrap:anywhere;white-space:normal}\n#mcx-qv-root[data-architecture=true] button.node>.name>svg{flex:none;width:16px;height:16px;position:relative;top:2px}\n#mcx-qv-root[data-architecture=true] button.preview-group>.name{position:absolute;left:16px;top:16px;width:calc(100% - 32px)}\n#mcx-qv-root[data-architecture=true] button.preview-group>.name .group-enter{position:static;flex:none;align-self:flex-start}\n#mcx-qv-root[data-architecture=true] .preview-item{height:auto;white-space:normal;overflow:visible;line-height:18px;align-items:flex-start}\n#mcx-qv-root[data-architecture=true] .preview-item>svg{flex:none}\n#mcx-qv-root[data-architecture=true] .preview-section{white-space:normal;line-height:18px}\n#mcx-qv-root[data-architecture=true] .qv-group>span{max-width:calc(100% - 32px);line-height:18px;overflow-wrap:anywhere}\n\n#mcx-qv-root[data-architecture=true] button.node.junction{padding:0;min-width:0;min-height:0;border:0;border-radius:50%;background:var(--muted-foreground)}\n#mcx-qv-root .architecture-detail{position:absolute;z-index:20;box-sizing:border-box;background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);border-radius:12px;padding:14px 16px;font-size:12px;line-height:18px;white-space:normal;overflow-wrap:anywhere;max-height:min(380px,calc(100vh - 32px));overflow-y:auto;pointer-events:auto;box-shadow:0 8px 24px #00000024}\n#mcx-qv-root .architecture-detail-name{font-size:14px;line-height:20px;font-weight:500;margin-bottom:4px}\n#mcx-qv-root .architecture-detail-label{color:var(--muted-foreground);font-size:11px;line-height:16px}\n#mcx-qv-root .architecture-detail-section{border-top:1px solid var(--border);padding-top:10px;margin-top:10px}\n#mcx-qv-root .architecture-detail-body{margin-top:4px}\n#mcx-qv-root .architecture-detail-link{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);gap:10px;margin-top:8px;align-items:start}\n#mcx-qv-root .architecture-detail-arrow{color:var(--muted-foreground);padding-top:1px}\n#mcx-qv-root .architecture-group-title{position:absolute;left:16px;top:10px;max-width:calc(100% - 32px);padding:0;border:0;background:none;color:var(--muted-foreground);font:inherit;font-size:12px;line-height:18px;text-align:left;overflow-wrap:anywhere;pointer-events:auto;cursor:var(--cursor-interaction,pointer)}\n\n#mcx-qv-root[data-kind=flowchart] button.node:not(.group-card)>.name{margin:auto;justify-content:center;max-width:100%;white-space:normal;flex:none}\n#mcx-qv-root[data-kind=flowchart] .flowchart-name{flex:1;min-width:0;text-align:center;white-space:normal;overflow-wrap:anywhere}\n#mcx-qv-root[data-kind=flowchart] button.node>.name>svg{flex:none}\n#mcx-qv-root[data-kind=flowchart] .preview-item{height:auto;white-space:normal;overflow:visible;line-height:18px;align-items:flex-start}\n#mcx-qv-root[data-kind=flowchart] .preview-section{white-space:normal;line-height:18px}\n#mcx-qv-root[data-kind=flowchart] .preview-item>svg{flex:none}\n#mcx-qv-root[data-kind=flowchart] .preview-item .flowchart-name{text-align:left}\n#mcx-qv-root[data-kind=flowchart]{--flow-wire:light-dark(color-mix(in srgb,var(--muted-foreground) 26%,var(--background)),color-mix(in srgb,var(--muted-foreground) 36%,var(--background)))}\n#mcx-qv-root[data-kind=flowchart] .wire{stroke:var(--flow-wire)}\n#mcx-qv-root[data-kind=flowchart] .arrow-head{fill:var(--flow-wire)}\n#mcx-qv-root .scope-peers{position:fixed;inset:auto;margin:0;padding:6px;width:240px;max-width:calc(100vw - 16px);max-height:min(320px,calc(100vh - 16px));overflow:auto;box-sizing:border-box;border:1px solid var(--border);border-radius:10px;background:var(--background);color:var(--foreground);box-shadow:0 8px 24px #0002;font:inherit}\n#mcx-qv-root .scope-peers::backdrop{background:transparent;pointer-events:none}\n#mcx-qv-root .scope-peers button{display:block;width:100%;padding:9px 10px;text-align:left;border:0;border-radius:6px;background:none;color:inherit;font:inherit;font-size:12px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n#mcx-qv-root .scope-peers button:hover,#mcx-qv-root .scope-peers button:focus-visible{background:var(--secondary)}\n#mcx-qv-root .scope-peers button[aria-current]{color:var(--viz-series-1);font-weight:500}\n#mcx-qv-root button.node.external-group{flex-direction:row;align-items:center}\n#mcx-qv-root button.node.external-group>.name{margin:0;width:100%;justify-content:flex-start}\n#mcx-qv-root button.node.external-group>.name>svg:last-child{width:14px;height:14px;margin-left:8px;color:var(--muted-foreground);flex:none}\n#mcx-qv-root button.node.external-group .flowchart-name{text-align:left}\n#mcx-qv-root[data-kind=flowchart] .qv-group{background:linear-gradient(var(--node-surface,var(--background)),var(--node-surface,var(--background))),var(--background)}\n#mcx-qv-root[data-kind=flowchart] button.node:not(.junction){background:linear-gradient(var(--node-surface,var(--background)),var(--node-surface,var(--background))),var(--background);color:var(--foreground)}\n#mcx-qv-root[data-kind=flowchart] button.node.shaped[data-shape]{background:transparent}\n#mcx-qv-root[data-kind=flowchart] .node-outline{fill:var(--node-surface,var(--background))}\n#mcx-qv-root[data-kind=flowchart] .wire{transition:opacity 140ms ease,stroke 140ms ease,stroke-width 140ms ease}\n#mcx-qv-root[data-kind=flowchart] .arrow-head{transition:opacity 140ms ease,fill 140ms ease}\n#mcx-qv-root[data-kind=flowchart] .edge-label,#mcx-qv-root[data-kind=flowchart] .edge-count{transition:opacity 140ms ease}\n#mcx-qv-root[data-kind=flowchart] .node-outline{transition:stroke 140ms ease}\n@media(prefers-reduced-motion:reduce){#mcx-qv-root[data-kind=flowchart] .wire,#mcx-qv-root[data-kind=flowchart] .arrow-head,#mcx-qv-root[data-kind=flowchart] .edge-label,#mcx-qv-root[data-kind=flowchart] .edge-count,#mcx-qv-root[data-kind=flowchart] .node-outline{transition:none}}\n#mcx-qv-root[data-kind=flowchart]{--flow-container-surface:color-mix(in srgb,var(--muted) 18%,var(--background))}\n#mcx-qv-root[data-kind=flowchart] .qv-group{z-index:0}\n#mcx-qv-root[data-kind=flowchart] .wires{z-index:1}\n#mcx-qv-root[data-kind=flowchart] button.node{z-index:2}\n#mcx-qv-root[data-kind=flowchart] .edge-label,#mcx-qv-root[data-kind=flowchart] .edge-count{z-index:3}\n\n#mcx-qv-root[data-architecture=true] .qv-group{z-index:0;background:var(--node-surface,var(--background))}\n#mcx-qv-root[data-architecture=true] .wires{z-index:1}\n#mcx-qv-root[data-architecture=true] button.node{z-index:2}\n#mcx-qv-root[data-architecture=true] button.node:not(.junction){background:var(--node-surface,var(--background))}\n#mcx-qv-root[data-architecture=true] .edge-label{z-index:3}\n#mcx-qv-root[data-architecture=true] .wire{stroke:light-dark(color-mix(in srgb,var(--muted-foreground) 36%,var(--background)),color-mix(in srgb,var(--muted-foreground) 48%,var(--background)))}\n#mcx-qv-root[data-architecture=true] .arrow-head{fill:light-dark(color-mix(in srgb,var(--muted-foreground) 36%,var(--background)),color-mix(in srgb,var(--muted-foreground) 48%,var(--background)))}\n</style>\n<h3 hidden></h3><nav class=\"scope-nav\" aria-label=\"架构层级\" hidden></nav><span class=\"scope-status sr-only\" aria-live=\"polite\"></span><div class=\"qv-notice\" role=\"status\" hidden></div><div class=\"node-tip\" id=\"mcx-qv-root-tip\" role=\"tooltip\" hidden></div><div class=\"diagram-scroll\" role=\"region\" aria-label=\"架构图,可横向滚动\" aria-busy=\"true\"><div class=\"scene\"><svg class=\"wires\" aria-hidden=\"true\"></svg></div></div>\n<div class=\"edge-key\"><span class=\"solid-key\"><i></i><span class=\"key-label\"></span></span><span class=\"async-key\"><i class=\"async\"></i><span class=\"key-label\"></span></span></div>\n\n</div>\n","script":"\nwindow.__mcxQVReady=(async()=>{document.addEventListener(\"qv:ready\",event=>queueMicrotask(()=>{const root=event.target.closest?.(\".qv-mermaid\");if(root&&[\"enhanced\",\"native\"].includes(root.dataset.state))createMermaidViewport(root);}));\nconst root=document.getElementById('mcx-qv-root'),scene=root.querySelector('.scene'),svg=root.querySelector('.wires'),cfg=JSON.parse(JSON.stringify(window.__mcxDiagramConfig));\nconst reduced=matchMedia('(prefers-reduced-motion: reduce)');\n\nfunction declaredMermaidIcon(value){\n if(!value)return null;\n const name=value.startsWith('lucide:')?value.slice(7):'',key=name.replace(/(^|-)([a-z0-9])/g,(_,dash,c)=>c.toUpperCase());\n if(!name||!window.lucide?.icons?.[key])throw new Error('图标不可用:'+value+';请使用宿主已有的 lucide 图标');\n return name;\n}\nconst hasRichMermaidLabel=value=>/<[^>]+>/.test(String(value||''));\n// 只在这一层访问固定版本的 Mermaid 数据接口,不解析源码字符串。\nasync function readSimpleMermaidModel(mermaid, source, descriptions) {\n const parsed = await mermaid.parse(source);\n if (!['flowchart-v2', 'flowchart', 'flowchart-elk'].includes(parsed.diagramType)) {\n return {reason: `${parsed.diagramType} 尚未适配 qv 绘制`,diagramType:parsed.diagramType};\n }\n // Mermaid 11.12.0 没有公开通用 AST;将版本相关接口隔离在此处。\n const diagram = await mermaid.mermaidAPI.getDiagramFromText(source);\n const db = diagram.db;\n if (!db.getVertices || !db.getEdges || !db.getSubGraphs || !db.getDirection) {\n return {reason: 'Mermaid 数据接口不兼容'};\n }\n const vertices = [...db.getVertices().values()];\n const groups = db.getSubGraphs();\n const links = db.getEdges();\n const ids = new Set(vertices.map(v => v.id));\n const supportedShapes = new Set(['square', 'round']);\n if (!vertices.length || vertices.some(v => (!supportedShapes.has(v.type || 'square') || v.icon&&v.form==='circle') || v.labelType === 'markdown' || hasRichMermaidLabel(v.text))) {\n return {reason: '此图包含尚未适配的节点形状或富文本'};\n }\n if (groups.some(g=>hasRichMermaidLabel(g.title)) || links.some(e=>hasRichMermaidLabel(e.text))) return {reason:'富文本标签尚未适配 qv 绘制'};\n if (vertices.some(v => v.styles?.length || v.classes?.length) || links.some(e => e.style?.length || e.classes?.length)) {\n return {reason: '此图包含自定义样式,保留原始语义'};\n }\n if (links.some(e => !ids.has(e.start) || !ids.has(e.end) || !['arrow_point', 'arrow_open'].includes(e.type))) {\n return {reason: '此图包含分组端点或尚未适配的箭头类型'};\n }\n if (groups.some(g => g.nodes.some(id => !ids.has(id)))) {\n return {reason: '嵌套分组尚未适配 qv 绘制'};\n }\n const direction = db.getDirection() || 'TB';\n if (groups.length && (!['TB','BT'].includes(direction) || groups.some(g => g.dir))) return {reason: '此分组方向尚未适配 qv 布局'};\n if (links.some(e => e.start === e.end)) return {reason: '自环尚未适配 qv 连线'};\n const iconFor = label => {\n const rules = [\n [/网关|gateway/i, 'network'], [/web|网页|浏览器/i, 'monitor'],\n [/移动|手机|app\\b/i, 'smartphone'], [/认证|登录|auth/i, 'key-round'],\n [/内容|文档|content/i, 'file-text'], [/搜索索引|index/i, 'scan-search'],\n [/搜索|search/i, 'search'], [/后台|任务|worker/i, 'workflow'],\n [/postgres|mysql|数据库|database|\\bdb\\b/i, 'database'], [/redis|缓存|cache/i, 'zap'],\n [/对象|文件存储|storage|object/i, 'archive'], [/队列|queue/i, 'list-tree'],\n [/api|服务|server/i, 'server']\n ];\n return rules.find(([pattern]) => pattern.test(label))?.[1] || null;\n };\n const nodes = vertices.map(v => ({id: v.id, name: v.text || v.id, icon: declaredMermaidIcon(v.icon)||iconFor(v.text || v.id), explicitIcon:!!v.icon,\n description: descriptions[v.id] || '', layer: 0}));\n const byId = new Map(nodes.map(n => [n.id, n]));\n let layers;\n if (groups.length) {\n const memberships = new Map();\n for (const g of groups) for (const id of g.nodes) {\n if (memberships.has(id)) return {reason: '重叠分组尚未适配 qv 绘制'};\n memberships.set(id, g.id);\n }\n if (nodes.some(n => !memberships.has(n.id))) return {reason: '部分分组尚未适配 qv 绘制'};\n layers = groups.map(g => g.title || g.id);\n groups.forEach((g, i) => g.nodes.forEach(id => {byId.get(id).layer = i;}));\n } else {\n const degree = new Map(nodes.map(n => [n.id, 0]));\n links.forEach(e => degree.set(e.end, degree.get(e.end) + 1));\n const queue = nodes.filter(n => degree.get(n.id) === 0);\n for (let i = 0; i < queue.length; i++) {\n const n = queue[i];\n for (const e of links.filter(e => e.start === n.id)) {\n const next = byId.get(e.end);\n next.layer = Math.max(next.layer, n.layer + 1);\n degree.set(e.end, degree.get(e.end) - 1);\n if (degree.get(e.end) === 0) queue.push(next);\n }\n }\n if (queue.length !== nodes.length) return {reason: '无分组回路尚未适配 qv 布局'};\n layers = Array.from({length: Math.max(...nodes.map(n => n.layer)) + 1}, () => '');\n }\n if (links.some(e => Math.abs(byId.get(e.start).layer - byId.get(e.end).layer) !== 1)) return {reason: '同层或跨多层连线尚未适配 qv 路由'};\n for(const g of groups)for(const id of g.nodes)byId.get(id).parent=g.id;\n return {data: {nodes, groups:groups.map(g=>({id:g.id,name:g.title||g.id,description:descriptions[g.id]||''})), layers, direction, grouped: !!groups.length,\n edges: links.map(e => ({from: e.start, to: e.end, async: e.stroke === 'dotted',\n thick: e.stroke === 'thick', arrow: e.type !== 'arrow_open', label: e.text || ''}))}};\n}\n\nasync function readMermaidModel(mermaid,source,descriptions) {\n const simple=await readSimpleMermaidModel(mermaid,source,descriptions);\n if(simple.diagramType==='block')return readMermaidBlockModel(mermaid,source,descriptions);\n if(simple.diagramType==='c4')return readMermaidC4Model(mermaid,source,descriptions);\n if(simple.diagramType==='quadrantChart')return readMermaidQuadrantModel(mermaid,source,descriptions);\n if(simple.data){simple.data.kind='flowchart';simple.data.generic=true;return simple;}\n const parsed=await mermaid.parse(source),diagram=await mermaid.mermaidAPI.getDiagramFromText(source),db=diagram.db;\n if(parsed.diagramType==='sequence')return readMermaidSequenceModel(mermaid,source,descriptions);\n if(parsed.diagramType==='sankey')return readMermaidSankeyModel(db,descriptions);\n if(parsed.diagramType==='radar')return readMermaidRadarModel(db,descriptions);\n if(parsed.diagramType==='xychart')return readMermaidXYModel(db,descriptions);\n if(parsed.diagramType==='pie')return readMermaidPieModel(db,descriptions);\n if(parsed.diagramType==='kanban')return readMermaidKanbanModel(db,descriptions);\n if(parsed.diagramType==='gantt')return readMermaidGanttModel(db,descriptions);\n if(parsed.diagramType==='mindmap')return readMermaidMindmapModel(db,descriptions);\n if(parsed.diagramType==='journey')return readMermaidJourneyModel(db,descriptions);\n if(parsed.diagramType==='timeline')return readMermaidTimelineModel(db,descriptions);\n if(parsed.diagramType==='class')return readMermaidClassModel(db,descriptions);\n if(parsed.diagramType==='er')return readMermaidErModel(db,descriptions);\n if(parsed.diagramType==='stateDiagram')return readMermaidStateModel(db,descriptions);\n const iconFor=name=>{const rules=[[/web|客户端|client/i,'monitor'],[/app|移动/i,'smartphone'],[/网关|gateway/i,'network'],[/数据库|存储|database|disk|postgres|mysql/i,'database'],[/缓存|redis/i,'zap'],[/队列|queue/i,'list-tree'],[/cloud|云/i,'cloud'],[/server|服务|api/i,'server'],[/internet|网络/i,'globe'],[/auth|认证/i,'key-round']];return rules.find(([re])=>re.test(name))?.[1]||null;};\n if(parsed.diagramType==='architecture'){\n const groups=db.getGroups().map(g=>({id:g.id,name:g.title||g.id,description:descriptions[g.id]||'',parent:g.in,icon:({cloud:'cloud',database:'database',disk:'hard-drive',internet:'globe',server:'server'}[g.icon]||'layers')})),all=db.getNodes();\n if(all.some(n=>n.iconText||(n.icon&&!['cloud','database','disk','internet','server'].includes(n.icon))))return {reason:'自定义图标或图标内文字尚未适配 qv 绘制'};\n const nodes=all.map(n=>({id:n.id,name:n.title||n.id,resource:n.icon||'',parent:n.in,icon:({cloud:'cloud',database:'database',disk:'hard-drive',internet:'globe',server:'server'}[n.icon]||iconFor(n.title||n.id)),explicitIcon:!!n.icon,junction:n.type==='junction',description:descriptions[n.id]||'',layer:0}));\n const dirs={T:'NORTH',B:'SOUTH',L:'WEST',R:'EAST'};\n const edges=db.getEdges().map(e=>({from:e.lhsGroup?(all.find(n=>n.id===e.lhsId).in||e.lhsId):e.lhsId,to:e.rhsGroup?(all.find(n=>n.id===e.rhsId).in||e.rhsId):e.rhsId,sourceFrom:e.lhsId,sourceTo:e.rhsId,fromGroup:!!e.lhsGroup,toGroup:!!e.rhsGroup,fromSide:dirs[e.lhsDir],toSide:dirs[e.rhsDir],arrow:!!e.rhsInto,startArrow:!!e.lhsInto,label:e.title||''}));\n return {data:{nodes,groups,edges,generic:true,nativeGeometry:true,architecture:true,db,layers:[],direction:'LR',grouped:true}};\n }\n if(!['flowchart','flowchart-v2','flowchart-elk'].includes(parsed.diagramType))return simple;\n const vertices=[...db.getVertices().values()],rawGroups=db.getSubGraphs(),links=db.getEdges();\n if(vertices.some(n=>n.styles?.length||n.classes?.length)||links.some(e=>e.style?.length||e.classes?.length))return {reason:'此图包含自定义样式,保留 Mermaid 原始外观'};\n const shapes={square:'rect',rect:'rect',round:'round',rounded:'round',diamond:'diamond',question:'diamond',hexagon:'hexagon',hex:'hexagon',circle:'circle',doublecircle:'doublecircle',stadium:'stadium',cylinder:'cylinder',cyl:'cylinder',subroutine:'subroutine','fr-rect':'subroutine',lean_left:'lean-left',lean_right:'lean-right',trapezoid:'trapezoid',inv_trapezoid:'inv-trapezoid',odd:'odd'};\n if(vertices.some(n=>!shapes[n.type||'square']||n.labelType==='markdown'||hasRichMermaidLabel(n.text)))return simple;\n if(rawGroups.some(g=>hasRichMermaidLabel(g.title))||links.some(e=>hasRichMermaidLabel(e.text)))return {reason:'富文本标签尚未适配 qv 绘制'};\n const parents=new Map();for(const g of rawGroups)for(const id of g.nodes)parents.set(id,g.id);\n const groupIds=new Set(rawGroups.map(g=>g.id));\n const groups=rawGroups.map(g=>({id:g.id,name:g.title||g.id,description:descriptions[g.id]||'',parent:parents.get(g.id),direction:g.dir}));\n const nodes=vertices.filter(n=>!groupIds.has(n.id)).map(n=>({id:n.id,name:n.text||n.id,shape:n.icon?(n.form==='circle'?'circle':'round'):shapes[n.type||'square'],parent:parents.get(n.id),icon:declaredMermaidIcon(n.icon)||iconFor(n.text||n.id),explicitIcon:!!n.icon,description:descriptions[n.id]||'',layer:0}));\n const edges=links.map(e=>({from:e.start,to:e.end,label:e.text||'',arrow:e.type!=='arrow_open',startArrow:e.type?.startsWith('double_'),marker:e.type?.includes('circle')?'circle':e.type?.includes('cross')?'cross':'point',async:e.stroke==='dotted',invisible:e.stroke==='invisible',thick:e.stroke==='thick'}));\n return {data:{nodes,groups,edges,kind:'flowchart',generic:true,nativeGeometry:groups.some(g=>g.direction),db,layers:[],direction:db.getDirection()||'TB',grouped:!!groups.length}};\n}\n\n// 只访问固定版本的时序图 DB;不从源码或 SVG 猜测消息语义。\nasync function readMermaidSequenceModel(mermaid,source,descriptions={}) {\n const db=(await mermaid.mermaidAPI.getDiagramFromText(source)).db;\n const rawActors=[...db.getActors()],boxes=db.getBoxes?.();\n const actors=rawActors.map(([id,a])=>({id,name:a.description||a.name,type:a.type,description:descriptions[id]||''}));\n const messages=db.getMessages().map(m=>({...m}));\n const supported=new Set([0,1,2,3,4,5,6,10,11,12,13,14,15,16,17,18,19,20,21,24,25,26,27,28,29,30,31,33,34]);\n if(!actors.length||actors.some(a=>!['participant','actor'].includes(a.type)||hasRichMermaidLabel(a.name))||messages.some(m=>!supported.has(m.type)||typeof m.message==='string'&&hasRichMermaidLabel(m.message))||(boxes?.size||boxes?.length)||rawActors.some(([,a])=>a.rectData||a.wrap||Object.keys(a.links||{}).length||Object.keys(a.properties||{}).length))return {reason:'此时序图包含未适配的参与方、富文本或自定义区域,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!actors.some(a=>a.id===id));if(unknown.length)throw new Error('说明引用了未知参与方:'+unknown.join('、'));\n let number=1,step=1,numbered=false;\n for(const m of messages){if(m.type===26){numbered=m.message.visible!==false;if(Number.isFinite(m.message.start))number=m.message.start;if(Number.isFinite(m.message.step))step=m.message.step;}else if([0,1,3,4,5,6,24,25,33,34].includes(m.type)&&numbered){m.number=number;number+=step;}}\n return {data:{kind:'sequence',actors,messages}};\n}\n\n// ELK 只负责复杂拓扑的布局;节点、边、提示和动效仍由 qv 绘制。\nlet elkLoad;\nasync function loadMermaidLayoutEngine() {if(!window.ELK)throw new Error(\"QV ELK unavailable\");}\nasync function prepareMermaidLayout(data) {\n await loadMermaidLayoutEngine();\n const direction={TB:'DOWN',BT:'UP',LR:'RIGHT',RL:'LEFT'};\n const graph={id:'qv-layout',layoutOptions:{'elk.algorithm':'layered','elk.direction':direction[data.direction]||'DOWN','elk.hierarchyHandling':'INCLUDE_CHILDREN','elk.edgeRouting':'ORTHOGONAL','elk.json.edgeCoords':'ROOT','elk.spacing.nodeNode':'38','elk.layered.spacing.nodeNodeBetweenLayers':data.navigation?'56':'84','elk.padding':'[top=28,left=28,bottom=28,right=28]',...(['er','class'].includes(data.kind)?{'elk.spacing.edgeNode':'28','elk.layered.spacing.edgeNodeBetweenLayers':'28'}:{}),...(data.kind==='state'?{'elk.layered.cycleBreaking.strategy':'MODEL_ORDER','elk.layered.considerModelOrder.strategy':'NODES_AND_EDGES','elk.layered.spacing.nodeNodeBetweenLayers':'48'}:{})},children:[],edges:[]};\n const byId=new Map([[null,graph]]);\n for(const g of data.groups||[])byId.set(g.id,{id:g.id,children:[],layoutOptions:{'elk.padding':'[top=44,left=24,bottom=24,right=24]',...(g.direction?{'elk.direction':direction[g.direction]}:{}),...(data.kind==='state'?{'elk.layered.cycleBreaking.strategy':'MODEL_ORDER'}:{})}});\n for(const g of data.groups||[])(byId.get(g.parent)||graph).children.push(byId.get(g.id));\n for(const n of data.nodes){const shaped=['diamond','hexagon','circle','doublecircle'].includes(n.shape),w=n.junction?10:Math.max(shaped?128:154,Math.min(260,[...n.name].reduce((s,c)=>s+(c.charCodeAt(0)>255?14:8),0)+52)),h=n.junction?10:['circle','doublecircle'].includes(n.shape)?w:shaped?94:48;\n const node={id:n.id,width:n.layoutWidth??(n.groupCard?Math.max(184,w):w),height:n.layoutHeight??(n.groupCard?(n.preview?.height||76):n.external?64:h),ports:[],layoutOptions:{'elk.portConstraints':'FIXED_SIDE',...(data.kind==='state'&&['stateStart','stateEnd'].includes(n.shape)?{'elk.layered.layering.layerConstraint':n.shape==='stateStart'?'FIRST':'LAST'}:{})}};\n if(data.kind==='flowchart'&&!n.groupCard){const measured=measureFlowchartNode(n,root.dataset.hasGroups==='true');node.width=measured.w;node.height=measured.h;}\n n.w=node.width;n.h=node.height;byId.set(n.id,node);(byId.get(n.parent)||graph).children.push(node);\n }\n const port=(id,face,incoming,edgeIndex)=>{const n=byId.get(id),pid=id+'::'+face+'::'+(incoming?'in':'out')+(['er','class'].includes(data.kind)?'::'+edgeIndex:'');n.ports??=[];if(!n.ports.some(p=>p.id===pid))n.ports.push({id:pid,width:0,height:0,layoutOptions:{'elk.port.side':face}});return pid;};\n const defaultFaces={TB:['SOUTH','NORTH'],BT:['NORTH','SOUTH'],LR:['EAST','WEST'],RL:['WEST','EAST']}[data.direction]||['SOUTH','NORTH'];\n data.edges.forEach((e,i)=>{const label=e.label?{id:'label'+i,text:e.label,width:e.labelWidth??Math.max(30,[...e.label].length*12+12),height:e.labelHeight??22}:null;graph.edges.push({id:'edge'+i,sources:[port(e.from,e.fromSide||defaultFaces[0],false,i)],targets:[port(e.to,e.toSide||defaultFaces[1],true,i)],...(label?{labels:[label]}:{})});});\n if(data.order)for(const node of byId.values())if(node.children)node.children.sort((a,b)=>(data.order.get(a.id)||0)-(data.order.get(b.id)||0));\n const result=await new window.ELK().layout(graph),positions=new Map(),groups=[];\n function visit(g,x=0,y=0){const gx=x+(g.x||0),gy=y+(g.y||0);positions.set(g.id,{x:gx,y:gy,w:g.width,h:g.height});if(g!==result&&g.children)groups.push({...data.groups.find(n=>n.id===g.id),x:gx,y:gy,w:g.width,h:g.height});for(const c of g.children||[])visit(c,gx,gy);}\n visit(result);\n const routes=new Map();function collect(g,ox=0,oy=0){const x=ox+(g.x||0),y=oy+(g.y||0);for(const e of g.edges||[]){const points=(e.sections||[]).flatMap(s=>[s.startPoint,...s.bendPoints||[],s.endPoint].map(p=>[p.x,p.y]));routes.set(e.id,{points,labelPosition:e.labels?.[0]?{x:e.labels[0].x+e.labels[0].width/2,y:e.labels[0].y+e.labels[0].height/2}:null});}for(const c of g.children||[])collect(c,x,y);}collect(result);\n data.nodes.forEach(n=>Object.assign(n,positions.get(n.id)));\n data.edges.forEach((e,i)=>{Object.assign(e,routes.get('edge'+i));if(!e.points?.length)throw new Error('布局缺少连线:'+e.from+' → '+e.to);});\n data.layout={width:result.width,height:result.height,groups};\n // 动效按空间层级推进,回路不会导致无限拓扑遍历。\n const ys=[...new Set(data.nodes.map(n=>Math.round(data.direction==='LR'||data.direction==='RL'?n.x:n.y)))].sort((a,b)=>['RL','BT'].includes(data.direction)?b-a:a-b);\n data.nodes.forEach(n=>n.layer=Math.min(6,ys.indexOf(Math.round(data.direction==='LR'||data.direction==='RL'?n.x:n.y))));\n}\n\n// 使用 Mermaid 自己的方向约束布局;只读取显式节点 ID 对应的几何,不从图形猜测关系。\nasync function prepareNativeGeometry(data,mermaid,source,db,architecture) {\n if(window.lucide){const icons={};for(const name of new Set(data.nodes.map(n=>n.icon).filter(Boolean))){const key=name.replace(/(^|-)([a-z0-9])/g,(_,dash,c)=>c.toUpperCase()),definition=window.lucide.icons[key];if(!definition)continue;const svg=window.lucide.createElement(definition),group=document.createElementNS(svg.namespaceURI,'g');for(const a of svg.attributes)if(!['width','height','viewBox','xmlns','class'].includes(a.name))group.setAttribute(a.name,a.value);for(const child of [...svg.children])group.append(child);icons[name]={body:group.outerHTML};}mermaid.registerIconPacks([{name:'lucide',icons:{prefix:'lucide',width:24,height:24,icons}}]);}\n const id='qv-measure-'+Math.random().toString(36).slice(2),result=await mermaid.render(id,source),host=document.createElement('div');host.style.cssText='position:absolute;left:-100000px;visibility:hidden;pointer-events:none';host.innerHTML=result.svg;document.body.append(host);\n try {\n const svg=host.querySelector('svg'),view=svg.viewBox.baseVal;svg.style.cssText=`width:${view.width}px;height:${view.height}px;max-width:none`;const sr=svg.getBoundingClientRect();\n const scaleX=architecture?1.8:1.65,scaleY=architecture?1.2:1.45;\n const rect=(id,service=false)=>{const el=host.querySelector('#'+CSS.escape(id));if(!el)throw new Error('缺少布局节点:'+id);let b=el.getBoundingClientRect();if(service){const matrix=el.getScreenCTM(),size=db.getConfigField('iconSize'),p=new DOMPoint(0,0).matrixTransform(matrix);b={x:p.x,y:p.y,width:size*matrix.a,height:size*matrix.d};}return {x:(b.x-sr.x)*scaleX+24,y:(b.y-sr.y)*scaleY+24,w:b.width*scaleX,h:b.height*scaleY};};\n const measureText=(name,width,size=14)=>{const text=document.createElement('span');text.style.cssText='position:absolute;left:-100000px;width:'+width+'px;font:'+'500 '+size+'px/'+(size===14?20:18)+'px '+getComputedStyle(root).fontFamily+';white-space:normal;overflow-wrap:anywhere';text.textContent=name;root.append(text);const height=text.offsetHeight;text.remove();return height;};\n for(const n of data.nodes){const b=rect(architecture?(n.junction?'node-':'service-')+n.id:db.lookUpDomId(n.id),architecture&&!n.junction);n.w=n.junction?10:Math.max(154,Math.min(260,b.w));n.h=n.junction?10:['circle','doublecircle'].includes(n.shape)?n.w:['diamond','hexagon'].includes(n.shape)?94:48;if(architecture&&!n.junction){n.w=240;n.h=Math.max(48,26+measureText(n.name,n.w-56));}if(data.kind==='flowchart')Object.assign(n,measureFlowchartNode(n,root.dataset.hasGroups==='true'));n.x=b.x+b.w/2-n.w/2;n.y=b.y+b.h/2-n.h/2;}\n const groups=data.groups.map(g=>({...g,...rect(architecture?'group-'+g.id:g.id)}));\n // 将原生图标布局转换成卡片后重新包住后代,并为标题留空间。\n const children=id=>[...data.nodes.filter(n=>n.parent===id),...groups.filter(g=>g.parent===id)];\n function bounds(g){for(const child of groups.filter(n=>n.parent===g.id))bounds(child);const list=children(g.id);if(list.length){const x=Math.min(...list.map(n=>n.x))-24,y=Math.min(...list.map(n=>n.y))-44;g.w=Math.max(Math.max(...list.map(n=>n.x+n.w))+24-x,architecture?240:0);g.h=Math.max(...list.map(n=>n.y+n.h))+24-y;g.x=x;g.y=y;}}\n for(const g of groups.filter(g=>!g.parent))bounds(g);\n const all=[...groups,...data.nodes],dx=24-Math.min(...all.map(n=>n.x)),dy=24-Math.min(...all.map(n=>n.y));for(const n of all){n.x+=dx;n.y+=dy;}\n finishMermaidGeometry(data,groups,architecture);\n } finally{host.remove();}\n}\nfunction routeAroundNodes(start,end,nodes){\n const blocks=nodes.filter(n=>!n.junction).map(n=>({l:n.x-8,r:n.x+n.w+8,t:n.y-8,b:n.y+n.h+8}));\n const xs=[...new Set([start[0],end[0],...blocks.flatMap(b=>[b.l-8,b.r+8])])].sort((a,b)=>a-b),ys=[...new Set([start[1],end[1],...blocks.flatMap(b=>[b.t-8,b.b+8])])].sort((a,b)=>a-b);\n const cross=(a,b)=>blocks.some(r=>a[0]===b[0]?a[0]>r.l&&a[0]<r.r&&Math.max(a[1],b[1])>r.t&&Math.min(a[1],b[1])<r.b:a[1]>r.t&&a[1]<r.b&&Math.max(a[0],b[0])>r.l&&Math.min(a[0],b[0])<r.r);\n const id=(x,y)=>y*xs.length+x,first=id(xs.indexOf(start[0]),ys.indexOf(start[1])),last=id(xs.indexOf(end[0]),ys.indexOf(end[1])),dist=new Map([[first,0]]),prev=new Map(),open=new Set([first]);\n while(open.size){let current,score=Infinity;for(const k of open){const d=dist.get(k)+Math.abs(xs[k%xs.length]-end[0])+Math.abs(ys[Math.floor(k/xs.length)]-end[1]);if(d<score){score=d;current=k;}}if(current===last){const path=[];for(let k=last;k!==undefined;k=prev.get(k))path.push([xs[k%xs.length],ys[Math.floor(k/xs.length)]]);return path.reverse();}open.delete(current);const x=current%xs.length,y=Math.floor(current/xs.length);for(const [nx,ny] of [[x-1,y],[x+1,y],[x,y-1],[x,y+1]]){if(nx<0||ny<0||nx>=xs.length||ny>=ys.length)continue;const a=[xs[x],ys[y]],b=[xs[nx],ys[ny]],next=id(nx,ny);if(cross(a,b))continue;const cost=dist.get(current)+Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1])+1;if(cost<(dist.get(next)??Infinity)){dist.set(next,cost);prev.set(next,current);open.add(next);}}}\n throw new Error('没有找到避开节点的连线路径');\n}\n\nfunction finishMermaidGeometry(data,groups,architecture=false){\n const all=[...groups,...data.nodes],map=new Map(all.map(n=>[n.id,n])),face=(a,b,out)=>{if(architecture)return out.fromSide;if(data.balanced){const dx=b.x+b.w/2-a.x-a.w/2,dy=b.y+b.h/2-a.y-a.h/2;return Math.abs(dx)>Math.abs(dy)?(dx>=0?'EAST':'WEST'):(dy>=0?'SOUTH':'NORTH');}const vertical=['TB','BT'].includes(data.direction);return vertical?(b.y>a.y?'SOUTH':'NORTH'):(b.x>a.x?'EAST':'WEST');};\n const dir={NORTH:[0,-1],SOUTH:[0,1],EAST:[1,0],WEST:[-1,0]},usages=new Map();\n for(const e of data.edges){const a=map.get(e.from),b=map.get(e.to);e.fs=e.fromSide||(data.balanced&&a===b?'EAST':face(a,b,e));e.ts=e.toSide||(data.balanced&&a===b?'SOUTH':({NORTH:'SOUTH',SOUTH:'NORTH',EAST:'WEST',WEST:'EAST'}[e.fs]));for(const [id,s,role] of [[e.from,e.fs,'out'],[e.to,e.ts,'in']]){const key=id+':'+s;if(!usages.has(key))usages.set(key,new Set());usages.get(key).add(role);}}\n function point(n,s,incoming){const split=usages.get(n.id+':'+s).size>1,offset=split?(incoming?-7:7):0;return s==='NORTH'?[n.x+n.w/2+offset,n.y]:s==='SOUTH'?[n.x+n.w/2+offset,n.y+n.h]:s==='WEST'?[n.x,n.y+n.h/2+offset]:[n.x+n.w,n.y+n.h/2+offset];}\n for(const e of data.edges){const a=map.get(e.from),b=map.get(e.to),start=point(a,e.fs,false),end=point(b,e.ts,true),u=dir[e.fs],v=dir[e.ts],s=[start[0]+u[0]*20,start[1]+u[1]*20],t=[end[0]+v[0]*20,end[1]+v[1]*20];delete e.labelPosition;e.points=[start,...routeAroundNodes(s,t,data.nodes),end];}\n const width=Math.max(...all.map(n=>n.x+n.w),...data.edges.flatMap(e=>e.points.map(p=>p[0])))+32,height=Math.max(...all.map(n=>n.y+n.h),...data.edges.flatMap(e=>e.points.map(p=>p[1])))+32;\n data.layout={width,height,groups};const coordinate=n=>Math.round(!architecture&&['LR','RL'].includes(data.direction)?n.x:n.y),levels=[...new Set(data.nodes.map(coordinate))].sort((a,b)=>['RL','BT'].includes(data.direction)?b-a:a-b);data.nodes.forEach(n=>n.layer=Math.min(6,levels.indexOf(coordinate(n))));\n}\n\n// 以真实字体测量换行高度;菱形与圆形只使用轮廓中央的安全区域。\nfunction measureFlowchartNode(n,grouped){\n const size=grouped?12:parseFloat(getComputedStyle(root).fontSize),line=grouped?18:Math.ceil(size*1.4),icon=(!!window.lucide&&(n.explicitIcon||(cfg.icons!=='none'&&n.icon)))?((grouped?14:16)+8):0,portal=n.external&&n.targetGroup!==undefined?22:0;\n const text=document.createElement('span');text.style.cssText=`position:absolute;left:-100000px;font:500 ${size}px/${line}px ${getComputedStyle(root).fontFamily};white-space:pre`;text.textContent=n.name;root.append(text);\n const natural=Math.ceil(text.getBoundingClientRect().width),limit=228-icon-portal,textWidth=Math.min(natural,limit)+icon+portal;\n text.style.width=(textWidth-icon-portal)+'px';text.style.whiteSpace='normal';text.style.overflowWrap='anywhere';const textHeight=text.offsetHeight;text.remove();\n const shaped=['diamond','circle','doublecircle'].includes(n.shape),w=Math.max(shaped?128:154,shaped?Math.ceil((textWidth+16)/.7):textWidth+34);\n let h=Math.max(48,textHeight+26);if(n.shape==='diamond')h=Math.max(94,Math.ceil((textHeight+12)/.3));else if(n.shape==='hexagon')h=Math.max(94,h);else if(['circle','doublecircle'].includes(n.shape))h=Math.max(w,Math.ceil((textHeight+20)/.7));\n if(n.external&&n.targetGroup===undefined)h+=22;\n n.textWidth=textWidth;n.labelLine=line;n.labelWrapped=natural>limit;return {w,h};\n}\nasync function prepareFlowchartLayout(data,mermaid,source){\n data.balanced=false;\n if(data.nativeGeometry)await prepareNativeGeometry(data,mermaid,source,data.db,false);else await prepareMermaidLayout(data);\n if(data.navigation||data.layout.width<=(cfg.readingWidth||root.clientWidth))return;\n // 只折叠过长的流程,按源节点顺序蛇形排列;每层分组独立保留边界。\n const groups=(data.groups||[]).map(g=>({...g}));\n const place=parent=>{\n const items=[...data.nodes.filter(n=>(n.parent||null)===parent),...groups.filter(g=>(g.parent||null)===parent)];\n const order=n=>groups.includes(n)?Math.min(...data.nodes.filter(node=>{for(let parent=node.parent;parent;parent=groups.find(g=>g.id===parent)?.parent)if(parent===n.id)return true;return false;}).map(node=>data.nodes.indexOf(node))):data.nodes.indexOf(n);items.sort((a,b)=>order(a)-order(b));\n for(const g of items.filter(n=>groups.includes(n)))Object.assign(g,place(g.id));\n if(!items.length)return {w:160,h:80};\n const direction=groups.find(g=>g.id===parent)?.direction||data.direction,vertical=['TB','BT'].includes(direction);\n let best;\n for(let lanes=1;lanes<=items.length;lanes++){\n const cells=items.map((n,i)=>{const run=Math.floor(i/lanes),offset=i%lanes,across=run%2?lanes-1-offset:offset;return {n,c:vertical?run:across,r:vertical?across:run};});\n const cols=Array.from({length:Math.max(...cells.map(c=>c.c))+1},(_,i)=>Math.max(...cells.filter(c=>c.c===i).map(c=>c.n.w))),rows=Array.from({length:Math.max(...cells.map(c=>c.r))+1},(_,i)=>Math.max(...cells.filter(c=>c.r===i).map(c=>c.n.h)));\n const w=cols.reduce((a,b)=>a+b,0)+(cols.length-1)*84+48,h=rows.reduce((a,b)=>a+b,0)+(rows.length-1)*72+68;\n const score=1000*Math.max(0,w/(cfg.readingWidth||root.clientWidth)-1)+Math.abs(Math.log(w/h))+.06*(w*h)/(items.reduce((sum,n)=>sum+n.w*n.h,0));\n if(!best||score<best.score)best={cells,cols,rows,w,h,score};\n }\n for(const {n,c,r} of best.cells){n.x=24+best.cols.slice(0,c).reduce((a,b)=>a+b+84,0)+(best.cols[c]-n.w)/2;n.y=44+best.rows.slice(0,r).reduce((a,b)=>a+b+72,0)+(best.rows[r]-n.h)/2;if(direction==='RL')n.x=best.w-n.x-n.w;if(direction==='BT')n.y=best.h-n.y-n.h;}\n return {w:best.w,h:best.h};\n };\n place(null);\n const translate=(parent,dx=0,dy=0)=>{for(const n of [...data.nodes,...groups].filter(n=>(n.parent||null)===parent)){n.x+=dx;n.y+=dy;if(groups.includes(n))translate(n.id,n.x,n.y);}};translate(null);\n data.balanced=true;finishMermaidGeometry(data,groups);\n}\n\n// 下钻只改变可见投影;完整模型与关系始终保留。\nfunction mermaidScopeView(model, scope = null) {\n const groups = new Map((model.groups || []).map(g => [g.id, g]));\n const all = new Map([...groups.values(), ...model.nodes].map(n => [n.id, n]));\n if (scope !== null && !groups.has(scope)) throw new Error('分组不存在:' + scope);\n const inside = (id, parent) => {\n if (parent === null) return true;\n for (let n = all.get(id); n; n = all.get(n.parent)) if (n.id === parent) return true;\n return false;\n };\n // 每个端点映射到当前层的直接孩子,外部端点映射到最近的外部边界。\n function representative(id) {\n if (id === scope) return {id, external:true};\n if (inside(id, scope)) {\n let n = all.get(id);\n while (n.parent && n.parent !== scope) n = all.get(n.parent);\n return {id:n.id, external:false};\n }\n let n = all.get(id);\n const ancestors = new Set();\n for (let p = all.get(scope); p; p = all.get(p.parent)) ancestors.add(p.id);\n while (n.parent && !ancestors.has(n.parent)) n = all.get(n.parent);\n return {id:n.id, external:true};\n }\n const visible = new Map();\n function add(id, external = false) {\n const key = external ? 'qv-external:' + id : id;\n if (visible.has(key)) return key;\n const raw = all.get(id), group = groups.has(id);\n const count = group ? model.nodes.filter(n => !n.junction && inside(n.id,id)).length : 0;\n let preview;\n if(group&&!external){\n const rows=[];let shown=0;\n function visit(parent,depth=0){for(const child of all.values())if(child.parent===parent){\n if(shown>=8)break;\n if(groups.has(child.id)){rows.push({id:child.id,name:child.name,heading:true,depth});visit(child.id,depth+1);}\n else if(!child.junction){rows.push({id:child.id,name:child.name,icon:child.icon,depth});shown++;}\n }}\n visit(id);let y=model.architecture?96:62;rows.forEach(row=>{row.y=y;if(model.architecture||model.kind==='flowchart'){const text=document.createElement('span');text.style.cssText='position:absolute;left:-100000px;width:248px;font:12px/18px '+getComputedStyle(root).fontFamily+';white-space:normal;overflow-wrap:anywhere';text.textContent=row.name;root.append(text);row.h=Math.max(18,text.offsetHeight);text.remove();y+=row.h+14;}else y+=row.heading?26:32;});\n const ids=new Set(rows.filter(r=>!r.heading).map(r=>r.id));\n preview={rows,edges:model.edges.filter(e=>ids.has(e.from)&&ids.has(e.to)&&e.from!==e.to),remaining:count-shown,height:y+(count>shown?28:10)};\n }\n visible.set(key, {...raw, id:key, parent:undefined, shape:group?'round':raw.shape,\n groupCard:group&&!external, external, count, preview, icon:group?(raw.icon||'layers'):raw.icon,\n targetGroup:group ? (id===scope?raw.parent||null:id) : undefined,\n description:raw.description||'',\n layoutWidth:model.architecture||group&&!external&&model.kind==='flowchart'?320:undefined,layoutHeight:model.architecture?(preview?.height||64):undefined,\n layer:0});\n return key;\n }\n for (const n of all.values()) if ((n.parent||null)===scope) add(n.id);\n const edges = [], bundles = new Map();\n model.edges.forEach((edge,index) => {\n if (!inside(edge.from,scope)&&!inside(edge.to,scope)) return;\n const a=representative(edge.from), b=representative(edge.to);\n // 折叠组内部的边留在原模型;真实叶节点自环仍显示。\n if (a.id===b.id && !(edge.from===edge.to&&!groups.has(a.id))) return;\n const from=add(a.id,a.external),to=add(b.id,b.external);\n const key=JSON.stringify([from,to,edge.async,edge.thick,edge.arrow,edge.startArrow,edge.marker,edge.invisible,edge.label||'']);\n if(bundles.has(key)){bundles.get(key).members.push(index);return;}\n const projected={...edge,from,to,points:undefined,labelPosition:undefined,\n fromSide:from===edge.from?edge.fromSide:undefined,toSide:to===edge.to?edge.toSide:undefined,members:[index]};\n edges.push(projected);bundles.set(key,projected);\n });\n for(const edge of edges)if(edge.members.length>1)edge.bundleCount=edge.members.length;\n return {nodes:[...visible.values()],edges,groups:[],layers:[],generic:true,navigation:true,\n direction:model.direction||'LR',grouped:false,scope,kind:model.kind,architectureCards:!!model.architecture||model.kind==='flowchart'};\n}\n\n\n// 总览保留源码的分组顺序,反馈连接绕到外围,不让循环反转主阅读方向。\nasync function prepareNavigationView(data){\n if(!['LR','RL'].includes(data.direction)||data.scope!==null||!data.nodes.length||data.nodes.length>5||!data.nodes.every(n=>n.groupCard))return prepareMermaidLayout(data);\n const reversed=['RL','BT'].includes(data.direction),ordered=reversed?[...data.nodes].reverse():data.nodes;\n const width=data.architectureCards?320:184,gap=64,top=24,maxHeight=Math.max(...ordered.map(n=>n.preview.height));\n ordered.forEach((n,i)=>Object.assign(n,{x:16+i*(width+gap),y:top+(maxHeight-n.preview.height)/2,w:width,h:n.preview.height,layer:i}));\n const map=new Map(data.nodes.map(n=>[n.id,n]));let returns=0;\n data.edges.forEach(e=>{const a=map.get(e.from),b=map.get(e.to),forward=reversed?a.x>b.x:a.x<b.x;\n if(forward&&Math.abs(a.x-b.x)<(width+gap)*1.1){\n const right=b.x>a.x,sx=a.x+(right?a.w:0),tx=b.x+(right?0:b.w),sy=a.y+a.h/2+(e.async?18:-10),ty=b.y+b.h/2+(e.async?18:-10);\n e.curve=[sx,sy,(sx+tx)/2,sy,(sx+tx)/2,ty,tx,ty];e.points=undefined;e.labelPosition={x:(sx+tx)/2,y:(sy+ty)/2-10};\n }else{\n const y=top+maxHeight+90+returns++*36,side=b.x>a.x?1:-1,ax=a.x+a.w/2+side*12,bx=b.x+b.w/2-side*12;\n e.curve=[ax,a.y+a.h,ax,y,bx,y,bx,b.y+b.h+1];e.points=undefined;e.labelPosition={x:(ax+bx)/2,y:((a.y+a.h)+6*y+(b.y+b.h))/8-10};\n }\n });\n data.layout={width:ordered.length*width+(ordered.length-1)*gap+32,height:top+maxHeight+returns*36+70,groups:[]};\n}\n\n// 局部视图完整展示当前层;过宽时尝试纵向排布,不按邻域过滤节点。\nasync function prepareCompleteScope(data,width){\n await prepareNavigationView(data);\n if(data.scope===null||data.layout.width<=(cfg.readingWidth||root.clientWidth)||!['LR','RL'].includes(data.direction)||data.edges.some(e=>e.fromSide||e.toSide))return;\n const alternative={...data,direction:'TB',nodes:data.nodes.map(n=>({...n})),edges:data.edges.map(e=>({...e,curve:undefined,points:undefined})),groups:[]};\n await prepareMermaidLayout(alternative);\n if(alternative.layout.width<data.layout.width)Object.assign(data,alternative);\n}\n\n// 详情浮层独立于缩放场景,保留完整源身份和真实端口关系。\nfunction createArchitectureReader(root, model, viewport, controller, highlight, keepAlive, leave) {\n const entries=[...(model.groups||[]).map(g=>({...g,isGroup:true})),...model.nodes],byId=new Map(entries.map(n=>[n.id,n]));\n const detail=document.createElement('div');detail.className='architecture-detail';detail.id=root.id+'-architecture-detail';detail.setAttribute('role','tooltip');detail.hidden=true;root.append(detail);\n let selected=null,epoch=0,scrollTimer;\n const sides={NORTH:'上',SOUTH:'下',WEST:'左',EAST:'右'};\n function path(n){const chain=[];for(let parent=byId.get(n.parent);parent;parent=byId.get(parent.parent))chain.unshift(parent.name);return chain.join(' / ');}\n const row=(cls,text)=>{const n=document.createElement('div');n.className=cls;n.textContent=text;return n;};\n function section(title){const el=document.createElement('section');el.className='architecture-detail-section';el.append(row('architecture-detail-label',title));detail.append(el);return el;}\n function endpoint(id,side){const el=document.createElement('div');el.append(row('architecture-detail-endpoint',byId.get(id)?.name||id));el.append(row('architecture-detail-label',sides[side]?'端口 · '+sides[side]:''));return el;}\n function fill(id){\n const n=byId.get(id);detail.replaceChildren();detail.append(row('architecture-detail-name',n.name),row('architecture-detail-label',(n.isGroup?'分组':n.junction?'汇接点':'资源'+(n.resource?' · '+n.resource:''))+' · '+n.id));\n if(n.description)section('说明').append(row('architecture-detail-body',n.description));\n if(n.parent)section('所属分组').append(row('architecture-detail-body',path(n)));\n if(n.isGroup){const children=entries.filter(c=>c.parent===id);if(children.length){const list=section('直接成员');for(const child of children)list.append(row('architecture-detail-body',child.name));}}\n const links=model.edges.filter(e=>e.from===id||e.to===id);\n if(links.length){const list=section('直接连接');for(const e of links){const link=document.createElement('div');link.className='architecture-detail-link';const arrow=e.startArrow&&e.arrow?'↔':e.startArrow?'←':e.arrow?'→':'—';link.append(endpoint(e.from,e.fromSide),row('architecture-detail-arrow',arrow),endpoint(e.to,e.toSide));list.append(link);if(e.fromGroup||e.toGroup)list.append(row('architecture-detail-label','源端点:'+e.sourceFrom+' '+arrow+' '+e.sourceTo));}}\n }\n function clear(){epoch++;clearTimeout(scrollTimer);selected=null;detail.hidden=true;root.querySelectorAll('[aria-describedby=\"'+detail.id+'\"]').forEach(n=>n.removeAttribute('aria-describedby'));}\n function reveal(pending){if(pending!==epoch||!selected||!selected.anchor?.isConnected)return;highlight(selected.id);}\n function choose(id,anchor,ensure=false){\n if(!byId.has(id))return;\n clear();const pending=epoch;selected={id,anchor};\n if(ensure&&anchor&&!controller.expanded){const a=anchor.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(a.width<=v.width){if(a.left<v.left+8)viewport.scrollLeft-=v.left+8-a.left;else if(a.right>v.right-8)viewport.scrollLeft+=a.right-v.right+8;}}\n requestAnimationFrame(()=>requestAnimationFrame(()=>reveal(pending)));\n }\n function onScroll(){if(!selected)return;detail.hidden=true;const pending=epoch;clearTimeout(scrollTimer);scrollTimer=setTimeout(()=>reveal(pending),100);}\n function refresh(){if(selected)reveal(epoch);}\n detail.addEventListener('pointerenter',keepAlive);detail.addEventListener('pointerleave',leave);\n return {choose,clear,onScroll,refresh,get focused(){return !!selected&&selected.anchor===document.activeElement;}};\n}\n\nfunction createMermaidViewport(root) {\n if (root.__mcxViewport) return root.__mcxViewport;\n let viewport = root.querySelector('.diagram-scroll'), scene = root.querySelector('.scene');\n if (!viewport || !scene) {\n viewport = document.createElement('div'); scene = document.createElement('div');\n viewport.className = 'diagram-scroll'; scene.className = 'scene'; scene.style.width = root.clientWidth + 'px';\n for (const child of [...root.children]) if (!['STYLE','H3'].includes(child.tagName)) scene.append(child);\n viewport.append(scene); root.append(viewport); scene.style.position = 'relative'; viewport.style.width = '100%';\n }\n const positions = new Map(), inlinePositions = new Map();\n let mode = window.openai?.displayMode || 'inline', restorePending = null, frame = 0, drag = null, suppressClick = false;\n const scope = () => root.dataset.scope || 'diagram';\n const expanded = () => mode === 'viewer';\n const send = detail => root.dispatchEvent(new CustomEvent('mcx:qv-viewer-input',{bubbles:true,detail}));\n function refresh() {\n root.dataset.displayMode = mode;\n if (expanded()) {\n viewport.style.paddingBottom = '0'; viewport.style.height = scene.offsetHeight + 'px';\n viewport.style.overflow = 'hidden'; viewport.style.cursor = 'grab'; viewport.style.touchAction = 'none'; viewport.scrollLeft = 0;\n } else {\n for (const key of ['height','overflow','touchAction','cursor','paddingBottom']) viewport.style[key] = '';\n if (restorePending) {\n viewport.scrollLeft = restorePending.left;\n if (viewport.scrollLeft === restorePending.left || viewport.clientWidth === restorePending.width) restorePending = null;\n }\n }\n }\n const schedule = () => { if (!frame) frame = requestAnimationFrame(() => { frame=0;refresh(); }); };\n const captureInline = () => { if (!expanded()) inlinePositions.set(scope(),{left:viewport.scrollLeft,width:viewport.clientWidth}); };\n captureInline();\n viewport.addEventListener('scroll',() => {const saved=inlinePositions.get(scope());if (!expanded()&&!restorePending&&(!saved||saved.width===viewport.clientWidth)) captureInline();},{passive:true});\n window.addEventListener('openai:set_globals',event => {\n const next = event.detail?.globals?.displayMode || window.openai?.displayMode || mode;\n if (next === mode) { schedule();return; }\n if (next === 'viewer') {const saved=inlinePositions.get(scope());positions.set(scope(),saved&&saved.width!==viewport.clientWidth?saved:{left:viewport.scrollLeft,width:viewport.clientWidth});}\n else restorePending = positions.get(scope()) || {left:0,width:viewport.clientWidth};\n mode = next; refresh();\n if (expanded()) root.getAnimations?.({subtree:true}).forEach(animation=>animation.cancel());\n // 只让关闭后的正文恢复原版几何,缩放与拖拽不触发图型重绘。\n if (!expanded()) root.dispatchEvent(new Event('qv:display-mode'));\n });\n viewport.addEventListener('wheel',event => {\n if (!expanded()) {captureInline();return;}\n event.preventDefault();event.stopPropagation();send({kind:'wheel',deltaY:event.deltaY,deltaMode:event.deltaMode,x:event.clientX,y:event.clientY});\n },{passive:false});\n viewport.addEventListener('pointerdown',event => {\n if (!expanded()) {captureInline();return;}\n if (event.button!==0 || event.target.closest('input,a')) return;\n suppressClick=false;drag={id:event.pointerId,x:event.clientX,y:event.clientY,moved:false};send({kind:'start'});\n },true);\n viewport.addEventListener('pointermove',event => {\n if (!drag || drag.id!==event.pointerId) return;\n const dx=event.clientX-drag.x,dy=event.clientY-drag.y;\n if (!drag.moved && Math.hypot(dx,dy)<5) return;\n if (!drag.moved) {drag.moved=true;viewport.setPointerCapture(event.pointerId);}\n event.preventDefault();event.stopImmediatePropagation();viewport.style.cursor='grabbing';send({kind:'drag',dx,dy});\n },true);\n const end=event => {\n if (!drag || drag.id!==event.pointerId) return;\n suppressClick=drag.moved;\n if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);\n if (drag.moved) {event.preventDefault();event.stopImmediatePropagation();}\n drag=null;viewport.style.cursor=expanded()?'grab':'';send({kind:'end'});\n };\n viewport.addEventListener('pointerup',end,true);viewport.addEventListener('pointercancel',end,true);\n viewport.addEventListener('click',event => {if(suppressClick){suppressClick=false;event.preventDefault();event.stopImmediatePropagation();}},true);\n document.addEventListener('keydown',event => {\n if (!expanded() || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.target.closest('input,textarea,[contenteditable=true]')) return;\n if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key)) {event.preventDefault();send({kind:'pan',dx:event.key==='ArrowLeft'?-40:event.key==='ArrowRight'?40:0,dy:event.key==='ArrowUp'?-40:event.key==='ArrowDown'?40:0});}\n });\n const observer=new ResizeObserver(schedule);[root,viewport,scene].forEach(e=>observer.observe(e));\n root.__mcxViewport={refresh,get expanded(){return expanded();},get scale(){return 1;},get savedInlinePosition(){const saved=positions.get(scope());return saved?{...saved}:null;}};\n refresh();return root.__mcxViewport;\n}\n// 浮层避让只在触发对象附近进行,局部遮挡优先于失去关联。\nfunction placeMermaidDetail(root,tip,anchor,event,preferredWidth=280){\n const rr=root.getBoundingClientRect(),r=anchor?.getBoundingClientRect();\n if(!r)return;\n tip.style.position='absolute';tip.style.marginTop='0';tip.style.width=Math.min(preferredWidth,root.clientWidth-16)+'px';tip.style.maxWidth='calc(100% - 8px)';tip.style.boxSizing='border-box';tip.style.overflowWrap='anywhere';\n const w=tip.offsetWidth,h=tip.offsetHeight,pointer=event?.type?.startsWith('pointer'),x=pointer?event.clientX-rr.left:r.left-rr.left,y=pointer?event.clientY-rr.top:r.top-rr.top,aw=pointer?0:r.width,ah=pointer?0:r.height;\n const candidates=[];\n for(const gap of [12,20,28])for(const [left,top]of [[x+aw+gap,y+(ah-h)/2],[x-w-gap,y+(ah-h)/2],[x+(aw-w)/2,y-h-gap],[x+(aw-w)/2,y+ah+gap]]){\n const c={x:Math.max(4,Math.min(root.clientWidth-w-4,left)),y:Math.max(4,Math.min(root.clientHeight-h-4,top))};\n c.distance=Math.hypot(Math.max(x-c.x-w,c.x-x-aw,0),Math.max(y-c.y-h,c.y-y-ah,0));if(c.distance<=32)candidates.push(c);\n }\n const overlap=(c,o)=>Math.max(0,Math.min(c.x+w,o.x+o.w)-Math.max(c.x,o.x))*Math.max(0,Math.min(c.y+h,o.y+o.h)-Math.max(c.y,o.y));\n const obstacles=[...root.querySelectorAll('button,h3')].filter(n=>n!==anchor&&!tip.contains(n)).map(n=>{const b=n.getBoundingClientRect();return{x:b.left-rr.left,y:b.top-rr.top,w:b.width,h:b.height};});\n for(const c of candidates){c.anchor=overlap(c,{x,y,w:aw,h:ah});c.score=obstacles.reduce((sum,o)=>sum+overlap(c,o),0)+c.distance*8;}\n candidates.sort((a,b)=>a.anchor-b.anchor||a.score-b.score);const c=candidates[0]||{x:Math.max(4,Math.min(root.clientWidth-w-4,x)),y:y+ah+12};\n tip.style.left=c.x+'px';tip.style.top=c.y+'px';tip.dataset.placement='near';\n}\nfunction createMermaidHover(root,clear){\n let enter=0,leave=0,current=null;\n const cancel=()=>{clearTimeout(enter);clearTimeout(leave);};const reset=()=>{current=null;clear();};\n root.addEventListener('keydown',e=>{if(e.key==='Escape'){cancel();current=null;}});root.addEventListener('qv:display-mode',cancel);\n return (element,activate)=>{\n const queue=()=>{cancel();if(current!==element)enter=setTimeout(()=>{current=element;activate();},100);};element.onpointerenter=queue;element.onpointermove=queue;\n element.onpointerleave=()=>{clearTimeout(enter);leave=setTimeout(reset,160);};\n element.onfocus=()=>{cancel();current=element;activate(true);};element.onblur=()=>{cancel();reset();};element.onclick=()=>{cancel();current=element;activate(true);};\n };\n}\nfunction mermaidContainmentSurface(data,id){\n const groups=new Map(data.groups.map(n=>[n.id,n]));const height=id=>{const children=data.nodes.filter(n=>n.parent===id).concat(data.groups.filter(n=>n.parent===id));return children.length?1+Math.max(...children.map(n=>groups.has(n.id)?height(n.id):0)):1;};\n return height(id)%2?'color-mix(in srgb,var(--muted) 18%,var(--background))':'var(--background)';\n}\n\nfunction renderMermaidSequence(root,data,cfg) {\n root.classList.add('qv-sequence');root.dataset.renderer='qv-sequence';root.dataset.state='enhanced';\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n viewport.setAttribute('aria-label','时序图,消息顺序从上到下,可横向滚动');root.querySelector('.edge-key').hidden=true;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .seq-header{position:absolute;display:flex;align-items:center;justify-content:center;gap:8px;min-height:48px;border:1px solid var(--border);border-radius:10px;background:var(--background);color:var(--foreground);font:inherit;font-weight:500;padding:12px;line-height:20px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .seq-life{stroke:var(--border);stroke-width:1;stroke-dasharray:3 5}\n #${root.id} .seq-message{fill:none;stroke:light-dark(#727a82,#a3aab3);stroke-width:1}\n #${root.id} .seq-arrow{fill:light-dark(#727a82,#a3aab3);stroke:none}\n #${root.id} .seq-label{position:absolute;border:0;background:var(--background);color:var(--foreground);font:inherit;font-size:12px;line-height:18px;min-height:0;padding:2px 7px;border-radius:4px;white-space:normal;overflow-wrap:anywhere;text-align:center;cursor:var(--cursor-interaction,pointer);transform:translateX(-50%)}\n #${root.id} .seq-number{font-variant-numeric:tabular-nums;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));margin-right:8px}\n #${root.id} .seq-frame{fill:transparent;stroke:color-mix(in srgb,var(--muted-foreground) 48%,var(--background));stroke-width:1}\n #${root.id} .seq-branch{position:absolute;font-size:12px;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));padding:0 8px;background:var(--background)}\n #${root.id} .seq-note{position:absolute;padding:10px 12px;border-left:2px solid var(--border);color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font-size:12px;background:var(--background)}\n #${root.id} .seq-activation{fill:light-dark(color-mix(in srgb,var(--viz-series-1) 16%,var(--background)),color-mix(in srgb,var(--viz-series-1) 26%,var(--background)));stroke:color-mix(in srgb,var(--viz-series-1) 65%,var(--background));stroke-width:1}\n #${root.id} .seq-tip{position:absolute;z-index:10;max-width:280px;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);box-shadow:0 3px 12px #0002;font-size:12px;pointer-events:none}\n #${root.id} .seq-hit{transition:opacity 140ms ease} #${root.id} .seq-hit.is-related .seq-message{stroke:var(--viz-series-1);stroke-width:1.4} #${root.id} .seq-hit.is-related .seq-arrow{fill:var(--viz-series-1)}\n #${root.id} .seq-open{fill:none;stroke:var(--muted-foreground)} #${root.id} .seq-hit.is-related .seq-open{fill:none;stroke:var(--viz-series-1)}\n @media(prefers-reduced-motion:reduce){#${root.id} .seq-hit{transition:none}}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='seq-tip';tip.hidden=true;tip.setAttribute('role','tooltip');root.append(tip);\n const el=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const text=(cls,value,x,y)=>{const e=document.createElement('div');e.className=cls;e.textContent=value;e.style.left=x+'px';e.style.top=y+'px';scene.append(e);return e;};\n const metrics=document.createElement('canvas').getContext('2d');metrics.font='12px '+getComputedStyle(root).fontFamily;\n const measure=value=>Math.ceil(metrics.measureText(String(value)).width);\n const spacing=Math.max(220,...data.messages.filter(m=>[0,1,3,4,5,6,24,25,33,34].includes(m.type)&&m.from!==m.to).map(m=>Math.min(288,measure(m.message))+48));\n const pad=100,notes=data.messages.filter(m=>m.type===2),selfs=data.messages.filter(m=>m.from===m.to&&[0,1,3,4,5,6,24,25,33,34].includes(m.type));\n const leftExtra=Math.max(0,...notes.filter(m=>m.placement===0&&m.from===data.actors[0].id).map(m=>Math.min(280,measure(m.message)+28)-48));\n const rightExtra=Math.max(0,...notes.filter(m=>m.placement===1&&m.from===data.actors.at(-1).id).map(m=>Math.min(280,measure(m.message)+28)-48),...selfs.filter(m=>m.from===data.actors.at(-1).id).map(m=>Math.min(288,measure(m.message))+32));\n const core=Math.max(root.clientWidth-leftExtra-rightExtra,pad*2+(data.actors.length-1)*spacing+120),width=core+leftExtra+rightExtra,step=(core-pad*2-120)/Math.max(1,data.actors.length-1),xs=new Map(data.actors.map((a,i)=>[a.id,leftExtra+pad+60+i*step]));\n // 固定几何宽度,展开时由共享视口整体缩放,HTML 和 SVG 不单独伸展。\n scene.style.width=width+'px';scene.style.minWidth=width+'px';const headers=[],marks=[],labels=[],frames=[],active=new Map();const probe=document.createElement('span');probe.style.cssText='position:absolute;left:-100000px;font:12px/18px '+getComputedStyle(root).fontFamily+';white-space:pre-wrap;overflow-wrap:anywhere';root.append(probe);const textHeight=(text,width)=>{probe.style.width=width+'px';probe.textContent=text;return probe.offsetHeight;};let y=Math.max(92,...data.actors.map(a=>textHeight(a.name,156)+60));\n const activationLayer=el('g',{class:'seq-activations'});svg.prepend(activationLayer);let lastMessageY=y;\n const activation=(actor,start,end,depth)=>{const bar=el('rect',{x:xs.get(actor)-5+depth*4,y:start,width:10,height:Math.max(12,end-start),class:'seq-activation','data-actor':actor,'data-depth':depth},activationLayer);activationLayer.prepend(bar);};\n const starts={10:'loop',12:'alt',15:'opt',19:'par',27:'critical',30:'break'},ends=new Set([11,14,16,21,29,31]),branches=new Set([13,20,28]);\n for(const [messageIndex,m] of data.messages.entries()){\n if(m.type===26)continue;\n if(starts[m.type]){frames.push({kind:starts[m.type],label:m.message,y:y-10,depth:frames.length});text('seq-branch',starts[m.type]+' · '+m.message,24+frames.length*8,y-17);y+=34;continue;}\n if(branches.has(m.type)){const f=frames.at(-1);el('line',{x1:24+f.depth*8,y1:y-10,x2:width-24-f.depth*8,y2:y-10,class:'seq-life'});text('seq-branch',(m.type===13?'else':'and')+' · '+m.message,32+f.depth*8,y-17);y+=34;continue;}\n if(ends.has(m.type)){const f=frames.pop();el('rect',{x:24+f.depth*8,y:f.y,width:width-48-f.depth*16,height:y-f.y-6,rx:6,class:'seq-frame'});y+=22;continue;}\n if(m.type===17){const list=active.get(m.from)||[];list.push(lastMessageY);active.set(m.from,list);continue;}\n if(m.type===18){const list=active.get(m.from)||[],start=list.pop();activation(m.from,start,lastMessageY,list.length);continue;}\n if(m.type===2){const from=xs.get(m.from),to=xs.get(m.to||m.from),nw=m.placement===2?Math.max(140,Math.abs(to-from)+96):Math.min(280,measure(m.message)+28),nx=m.placement===0?from-nw-12:m.placement===1?from+12:(from+to-nw)/2,note=text('seq-note',m.message,nx,y-10);note.style.width=nw+'px';note.dataset.placement=m.placement;y+=Math.max(60,note.offsetHeight+24);continue;}\n const labelWidth=Math.min(288,Math.max(48,measure(m.message)+(m.number===undefined?14:40))),labelHeight=textHeight(m.message,labelWidth-40)+4;y+=Math.max(0,labelHeight-22);\n // 激活条跟随真实消息位置;线和箭头位于柱子上层,端点接到柱子边缘。\n const sourceX=xs.get(m.from),targetX=xs.get(m.to),self=sourceX===targetX,direction=targetX>=sourceX?1:-1,next=data.messages[messageIndex+1],sourceDepth=(active.get(m.from)||[]).length,targetDepth=(active.get(m.to)||[]).length+(next?.type===17&&next.from===m.to?1:0),a=sourceX+(sourceDepth?(-5+(sourceDepth-1)*4+(direction>0||self?10:0)):0),b=targetX+(targetDepth?(-5+(targetDepth-1)*4+(direction<0||self?10:0)):0),g=el('g',{class:'seq-hit','data-message':m.id});\n const end=b-(self?0:5*direction),path=el('path',{d:self?`M${a} ${y}h46q8 0 8 8v12q0 8 -8 8H${b}`:`M${a} ${y}H${end}`,class:'seq-message',...( [1,4,6,25,34].includes(m.type)?{'stroke-dasharray':'4 4'}:{})},g);\n const ey=self?y+28:y,dx=self?-1:direction;lastMessageY=ey;\n if([3,4].includes(m.type))el('path',{d:`M${b-3} ${ey-3}l6 6m0 -6l-6 6`,class:'seq-message'},g);\n else if(![5,6].includes(m.type)){const arrow=el('path',{d:[24,25].includes(m.type)?`M${b-5*dx} ${ey-2.5}L${b} ${ey}L${b-5*dx} ${ey+2.5}`:`M${b} ${ey}l${-5*dx} -2.5v5Z`,class:[24,25].includes(m.type)?'seq-arrow seq-open':'seq-arrow'},g);if([33,34].includes(m.type))el('path',{d:`M${a} ${y}l${5*dx} -2.5v5Z`,class:'seq-arrow'},g);}\n const label=document.createElement('button');label.type='button';label.className='seq-label';label.style.width=labelWidth+'px';label.style.left=(self?a+12+labelWidth/2:(a+b)/2)+'px';label.style.top=(y-labelHeight-10)+'px';const num=document.createElement('span');num.className='seq-number';num.textContent=m.number===undefined?'':String(m.number);if(m.number!==undefined)label.append(num);label.append(document.createTextNode(m.message));scene.append(label);label.dataset.message=m.id;label.setAttribute('aria-label',`${m.number??''} ${data.actors.find(n=>n.id===m.from).name} → ${data.actors.find(n=>n.id===m.to).name}:${m.message}`);marks.push({m,g,path,label});labels.push(label);y+=Math.max(self?88:60,labelHeight+36+(self?28:0));\n }\n for(const [actor,list] of active)list.forEach((start,depth)=>activation(actor,start,lastMessageY+24,depth));\n scene.style.height=(y+12)+'px';svg.style.height=(y+12)+'px';svg.setAttribute('viewBox',`0 0 ${width} ${y+12}`);\n for(const a of data.actors){const x=xs.get(a.id),life=el('line',{x1:x,y1:48,x2:x,y2:y,class:'seq-life'});svg.prepend(life);const h=document.createElement('button');h.type='button';h.className='seq-header';if(a.type==='actor'){const icon=document.createElement('i');icon.setAttribute('data-lucide','user-round');icon.setAttribute('aria-hidden','true');h.append(icon);}h.append(document.createTextNode(a.name));const hw=Math.min(240,Math.max(156,Math.ceil(measure(a.name)*7/6)+50));h.style.cssText=`left:${x-hw/2}px;top:0;width:${hw}px`;scene.append(h);headers.push({a,h});}\n probe.remove();window.lucide?.createIcons({attrs:{width:16,height:16}});\n const view=createMermaidViewport(root);\n tip.id=root.id+'-sequence-tip';\n function clear(){tip.hidden=true;for(const mark of marks){mark.g.style.opacity='';mark.label.style.opacity='';mark.g.classList.remove('is-related');mark.label.removeAttribute('aria-describedby');}headers.forEach(({h})=>{h.style.opacity='';h.removeAttribute('aria-describedby');});}\n function placeTip(trigger,value,related=[]){}\n function focus(m,label){for(const mark of marks){const related=mark.m.id===m.id;mark.g.classList.toggle('is-related',related);mark.g.style.opacity=related?'1':'.18';mark.label.style.opacity=related?'1':'.3';}headers.forEach(({a,h})=>h.style.opacity=a.id===m.from||a.id===m.to?'1':'.3');}\n let keyboardTarget=null;const bind=createMermaidHover(root,clear);marks.forEach(({m,label})=>bind(label,ensure=>{clear();keyboardTarget=ensure?label:null;focus(m,label);}));\n function focusActor(a,h){const related=marks.filter(n=>n.m.from===a.id||n.m.to===a.id);for(const mark of marks){const hit=related.includes(mark);mark.g.style.opacity=hit?'1':'.18';mark.label.style.opacity=hit?'1':'.3';mark.g.classList.toggle('is-related',hit);}headers.forEach(n=>n.h.style.opacity=n.a.id===a.id?'1':'.3');if(a.description)placeTip(h,a.description,related);}\n headers.forEach(({a,h})=>bind(h,ensure=>{clear();keyboardTarget=ensure?h:null;focusActor(a,h);}));\n let focusRestore=0;const cancelRestore=()=>{clearTimeout(focusRestore);keyboardTarget=null;clear();};\n viewport.addEventListener('scroll',()=>{clearTimeout(focusRestore);clear();const active=document.activeElement,mark=marks.find(n=>n.label===active),header=headers.find(n=>n.h===active);if(active===keyboardTarget&&(mark||header))focusRestore=setTimeout(()=>{if(document.activeElement!==active)return;if(mark)focus(mark.m,mark.label);else focusActor(header.a,header.h);},120);},{passive:true});root.addEventListener('qv:display-mode',cancelRestore);document.addEventListener('keydown',e=>{if(e.key==='Escape')cancelRestore();});\n const reduced=matchMedia('(prefers-reduced-motion: reduce)');let animations=[],armed=true;\n const defs=el('defs',{});marks.forEach(({m,path})=>{const id=root.id+'-seq-mask-'+m.id,length=path.getTotalLength(),mask=el('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height:y+12},defs),reveal=el('path',{d:path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);path.setAttribute('mask',`url(#${id})`);m.reveal=reveal;});\n function replay(){animations.forEach(a=>a.cancel());animations=[];if(reduced.matches||view.expanded)return;for(const [i,{m,g,path,label}] of marks.entries()){const length=path.getTotalLength(),delay=i*Math.min(140,900/Math.max(1,marks.length-1));animations.push(m.reveal.animate([{strokeDashoffset:length},{strokeDashoffset:0}],{duration:420,delay,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));animations.push(label.animate([{opacity:0},{opacity:1}],{duration:180,delay:delay+70,fill:'backwards'}));g.querySelectorAll('.seq-arrow').forEach(arrow=>animations.push(arrow.animate([{opacity:0},{opacity:1}],{duration:60,delay:delay+400,fill:'backwards'})));}root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);\n document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});root.addEventListener('qv:display-mode',()=>animations.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>animations.forEach(a=>a.cancel()));view.refresh();\n}\n\n// 固定 Mermaid 11.12 的状态 DB 已提供语义形状和 parentId,不按 ID 后缀猜起止点。\nasync function readMermaidStateModel(db,descriptions={}) {\n const raw=db.getData(),shapes=new Set(['rect','rectWithTitle','roundedWithTitle','stateStart','stateEnd','choice','fork','join']);\n if(!raw.nodes.length||Object.keys(db.getClasses?.()||{}).length||raw.nodes.some(n=>!shapes.has(n.shape)||n.cssStyles?.length||n.cssCompiledStyles?.length||hasRichMermaidLabel(n.label)||n.description?.some?.(hasRichMermaidLabel))||raw.edges.some(e=>hasRichMermaidLabel(e.label)))return {reason:'此状态图包含尚未适配的注释、并发区域、富文本或自定义样式,保留原生语义'};\n const all=raw.nodes.map(n=>({id:n.id,name:['stateStart','stateEnd'].includes(n.shape)?(n.shape==='stateStart'?'起点':'终点'):n.shape==='choice'?'选择':n.label||n.id,shape:n.shape,parent:n.parentId,description:descriptions[n.id]||'',lines:n.description||[],direction:n.dir,group:n.isGroup}));\n const unknown=Object.keys(descriptions).filter(id=>!all.some(n=>n.id===id));if(unknown.length)throw new Error('说明引用了未知状态:'+unknown.join('、'));\n return {data:{kind:'state',direction:raw.direction||'TB',nodes:all.filter(n=>!n.group),groups:all.filter(n=>n.group),edges:raw.edges.map((e,i)=>({id:e.id||String(i),from:e.start,to:e.end,label:e.label||'',arrow:true})),layers:[]}};\n}\n\nfunction wrapMermaidStateText(text){return String(text).split('\\n').flatMap(line=>{const chars=Array.from(line),rows=[];for(let i=0;i<chars.length;i+=20)rows.push(chars.slice(i,i+20).join(''));return rows.length?rows:[''];}).join('\\n');}\n\nasync function prepareMermaidStateLayout(data) {\n // 复用 qv 的布局引擎,状态符号保留独立尺寸,不转换成普通服务卡。\n const context=document.createElement('canvas').getContext('2d'),family=getComputedStyle(root).fontFamily;\n const measure=(text,font)=>{context.font=font+' '+family;return Math.max(0,...wrapMermaidStateText(text).split('\\n').map(row=>context.measureText(row).width));};\n for(const n of data.nodes){const terminal=['stateStart','stateEnd'].includes(n.shape),bar=['fork','join'].includes(n.shape),rows=wrapMermaidStateText(n.name).split('\\n').length;\n n.layoutWidth=terminal?20:n.shape==='choice'?32:bar?64:Math.max(n.groupCard?200:128,Math.ceil(Math.max(measure(n.name,'500 16px')+(n.groupCard||n.targetGroup!==undefined?22:0),...n.lines.map(line=>measure(line,'400 12px'))))+32);\n n.layoutHeight=terminal?20:n.shape==='choice'?32:bar?6:Math.max(48,24+24*rows+(n.lines.length?8+18*n.lines.reduce((sum,line)=>sum+wrapMermaidStateText(line).split('\\n').length,0):0)+(n.groupCard?24:0));\n }\n for(const e of data.edges)if(e.label){e.labelWidth=Math.max(30,Math.ceil(measure(e.label,'400 12px'))+12);e.labelHeight=4+18*wrapMermaidStateText(e.label).split('\\n').length;}\n const all=[...data.groups,...data.nodes],rank=new Map();\n // 每个复合状态内部独立从起点遍历;回流只改变布局通道,不改变转移。\n for(const parent of [undefined,...data.groups.map(g=>g.id)]){\n const siblings=all.filter(n=>n.parent===parent),ids=new Set(siblings.map(n=>n.id)),queue=siblings.filter(n=>n.shape==='stateStart').map(n=>n.id);let index=0;\n while(queue.length){const id=queue.shift();if(rank.has(id))continue;rank.set(id,index++);for(const e of data.edges.filter(e=>e.from===id&&ids.has(e.to)))queue.push(e.to);}\n for(const n of siblings)if(!rank.has(n.id))rank.set(n.id,index++);\n }\n data.order=rank;\n for(const e of data.edges){delete e.fromSide;delete e.toSide;if(e.from===e.to){e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';continue;}if(all.find(n=>n.id===e.from)?.parent===all.find(n=>n.id===e.to)?.parent&&rank.get(e.from)>rank.get(e.to)){e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'NORTH':'WEST';}}\n data.balanced=false;await prepareMermaidLayout(data);\n for(const n of data.nodes)n.layer=Math.min(6,rank.get(n.id)||0);\n}\n\nfunction renderMermaidState(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-state');root.dataset.renderer='qv-state';root.dataset.state='enhanced';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','状态转移图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} .state-node{position:absolute;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 14px;border:1px solid var(--border);border-radius:10px;background:var(--background);color:var(--foreground);font-family:inherit;font-size:16px;line-height:24px;font-weight:500;white-space:pre-line;text-align:center;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-node small{font-weight:400;font-size:12px;line-height:18px;color:var(--muted-foreground)}\n #${root.id} .state-symbol{position:absolute;display:flex;align-items:center;justify-content:center;border:0;background:none;padding:0;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-symbol svg{width:100%;height:100%;overflow:visible}\n #${root.id} .state-symbol .state-mark{fill:var(--background);stroke:var(--muted-foreground);stroke-width:1}\n #${root.id} .state-symbol .state-solid{fill:var(--muted-foreground);stroke:none}\n #${root.id} .state-group{position:absolute;border:1px solid var(--border);border-radius:12px;background:var(--background)}\n #${root.id} .state-group>button{position:absolute;left:14px;top:8px;border:0;background:inherit;color:var(--muted-foreground);font:inherit;font-size:12px;display:flex;max-width:calc(100% - 28px);padding:0 4px;white-space:pre-line;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-wire{fill:none;stroke:light-dark(color-mix(in srgb,var(--muted-foreground) 55%,var(--background)),color-mix(in srgb,var(--muted-foreground) 68%,var(--background)));stroke-width:1}\n #${root.id} .state-arrow{fill:light-dark(color-mix(in srgb,var(--muted-foreground) 55%,var(--background)),color-mix(in srgb,var(--muted-foreground) 68%,var(--background)))}\n #${root.id} .state-label{position:absolute;transform:translate(-50%,-50%);padding:2px 6px;border:0;border-radius:4px;background:transparent;color:var(--muted-foreground);font:inherit;font-size:12px;line-height:18px;min-height:0;white-space:pre-line;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-tip,#${root.id} .state-peers{position:absolute;z-index:10;border-radius:12px;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:none;white-space:pre-line}\n #${root.id} .state-node svg,#${root.id} .state-group>button svg{width:14px;height:14px;flex:none}\n #${root.id} .state-tip-relation{display:grid;grid-template-columns:minmax(0,1fr) 14px minmax(0,1fr);align-items:center;gap:12px}\n #${root.id} .state-tip-relation svg{width:14px;height:14px;color:var(--muted-foreground)}\n #${root.id} .state-tip-endpoint{min-width:0}\n #${root.id} .state-tip-caption{display:block;font-size:11px;line-height:16px;color:var(--muted-foreground);font-weight:400;margin-bottom:4px}\n #${root.id} .state-tip-name{display:block;font-size:14px;line-height:20px;font-weight:600;white-space:normal;overflow-wrap:anywhere}\n #${root.id} .state-tip-body{margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:12px;line-height:20px;overflow-wrap:anywhere}\n #${root.id} .state-title{display:inline-flex;align-items:center;justify-content:center;gap:8px;max-width:100%;min-width:0}\n #${root.id} .state-title-name{min-width:0;white-space:pre-line}\n #${root.id} .state-node small:first-of-type{margin-top:8px}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='state-tip';tip.id=root.id+'-state-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);\n const element=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const source=structuredClone(data),sourceGroups=new Map(source.groups.map(g=>[g.id,g])),all=new Map([...source.groups,...source.nodes].map(n=>[n.id,n]));\n const level=id=>sourceGroups.has(id)?1+Math.max(0,...[...all.values()].filter(n=>n.parent===id).map(n=>level(n.id))):0;\n const surface=(el,id)=>{el.style.background=level(id)%2?'color-mix(in srgb,var(--muted) 18%,var(--background))':'var(--background)';};\n const icon=(el,name)=>{const i=document.createElement('i');i.dataset.lucide=name;i.setAttribute('aria-hidden','true');el.append(i);};\n function drawTip(value){\n tip.replaceChildren();const text=(parent,tag,cls,value)=>{const el=document.createElement(tag);el.className=cls;el.textContent=value;parent.append(el);return el;};\n if(value.from!==undefined){const relation=document.createElement('div');relation.className='state-tip-relation';tip.append(relation);for(const [caption,name]of [['起始状态',value.from],['目标状态',value.to]]){if(caption==='目标状态')icon(relation,'arrow-right');const endpoint=document.createElement('div');endpoint.className='state-tip-endpoint';text(endpoint,'span','state-tip-caption',caption);text(endpoint,'span','state-tip-name',name);relation.append(endpoint);}}\n else text(tip,'span','state-tip-name',value.title);\n const body=document.createElement('div');body.className='state-tip-body';tip.append(body);if(value.label)text(body,'span','state-tip-caption','转移说明'+(value.count>1?' · '+value.count+' 条转移':''));text(body,'div','state-tip-content',wrapMermaidStateText(value.label||value.description));if(window.lucide)window.lucide.createIcons({attrs:{width:14,height:14}});\n }\n const title=(el,name,arrow)=>{const row=document.createElement('span');row.className='state-title';const label=document.createElement('span');label.className='state-title-name';label.textContent=wrapMermaidStateText(name);row.append(label);if(arrow)icon(row,arrow);el.append(row);};\n const nav=root.querySelector('.scope-nav'),positions=new Map();let grouped=false,scope=null,busy=false,dispose=()=>{};\n const view=createMermaidViewport(root);\n async function navigate(target){\n const enabled=target!==null;\n if(busy)return;busy=true;positions.set(root.dataset.scope||'complete',viewport.scrollLeft);viewport.setAttribute('aria-busy','true');\n try{const next=enabled?mermaidScopeView(source,target):structuredClone(source);await prepareMermaidStateLayout(next);dispose();grouped=enabled;scope=target;root.dataset.scope=enabled?(target||'overview'):'complete';draw(next);navigation();view.refresh();viewport.scrollLeft=positions.get(root.dataset.scope)||0;}\n finally{busy=false;viewport.setAttribute('aria-busy','false');}\n }\n function navigation(){\n nav.hidden=!sourceGroups.size||scope===null;nav.setAttribute('aria-label','状态层级');nav.replaceChildren();if(nav.hidden)return;\n const path=[];for(let g=sourceGroups.get(scope);g;g=sourceGroups.get(g.parent))path.unshift(g);\n for(const [index,g]of [{id:null,name:'总览'},...(grouped?path:[])].entries()){\n if(index){const sep=document.createElement('span');sep.textContent='/';nav.append(sep);}\n const b=document.createElement('button');b.type='button';b.textContent=g.name;b.dataset.scope=g.id||'';b.onclick=()=>navigate(g.id);if(g.id===scope)b.setAttribute('aria-current','location');nav.append(b);\n if(g.id!==null){const peers=[...sourceGroups.values()].filter(p=>p.parent===g.parent&&p.id!==g.id);if(peers.length){\n const panel=document.createElement('div');panel.className='state-peers';panel.style.pointerEvents='auto';panel.hidden=true;b.setAttribute('aria-haspopup','true');b.setAttribute('aria-expanded','false');let close;\n const hide=()=>{panel.hidden=true;b.setAttribute('aria-expanded','false');};const open=()=>{clearTimeout(close);const r=b.getBoundingClientRect(),rr=root.getBoundingClientRect();panel.style.left=Math.max(0,r.left-rr.left)+'px';panel.style.top=(r.bottom-rr.top)+'px';panel.hidden=false;b.setAttribute('aria-expanded','true');};\n b.onpointerenter=open;b.onpointerleave=()=>close=setTimeout(hide,180);b.onkeydown=e=>{if(e.key==='ArrowDown'){e.preventDefault();open();panel.querySelector('button').focus();}};panel.onpointerenter=()=>clearTimeout(close);panel.onpointerleave=()=>close=setTimeout(hide,180);panel.onkeydown=e=>{if(e.key==='Escape'){hide();b.focus();}};\n for(const peer of peers){const item=document.createElement('button');item.type='button';item.textContent=peer.name;item.style.cssText='display:block;border:0;background:transparent;color:inherit;font:inherit;padding:6px 0';item.onclick=()=>navigate(peer.id);panel.append(item);}nav.append(panel);\n }}\n }\n\n }\n function draw(data){\n const abort=new AbortController(),signal=abort.signal;scene.querySelectorAll(':scope > :not(svg)').forEach(e=>e.remove());svg.replaceChildren();tip.hidden=true;\n const width=Math.max(root.clientWidth,data.layout.width),height=data.layout.height,offset=(width-data.layout.width)/2;\n scene.style.cssText=`width:${width}px;min-width:${width}px;height:${height}px`;svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\n const nodes=data.nodes.map(n=>({...n,x:n.x+offset})),groups=data.layout.groups.map(n=>({...n,x:n.x+offset})),map=new Map([...nodes,...groups].map(n=>[n.id,n])),objects=[],edges=[];\n for(const g of groups.sort((a,b)=>level(b.id)-level(a.id))){const box=document.createElement('div');box.className='state-group';box.style.cssText=`left:${g.x}px;top:${g.y}px;width:${g.w}px;height:${g.h}px`;const b=document.createElement('button');b.type='button';b.setAttribute('aria-label',g.name+',进入此层级');title(b,g.name,'chevron-right');surface(box,g.id);b.dataset.stateNode=g.id;box.append(b);scene.insertBefore(box,svg);objects.push({n:g,b,box});}\n for(const n of nodes){const b=document.createElement('button');b.type='button';b.className=['rect','rectWithTitle','roundedWithTitle','round'].includes(n.shape)?'state-node':'state-symbol';b.dataset.stateNode=n.id;b.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;b.setAttribute('aria-label',n.name);\n if(b.className==='state-node'){surface(b,n.targetGroup!==undefined?n.targetGroup:n.id);title(b,n.name,n.groupCard||n.targetGroup!==undefined?(n.external?'arrow-up-right':'chevron-right'):null);if(n.groupCard||n.targetGroup!==undefined){if(n.groupCard){const count=document.createElement('small');count.textContent=n.count+' 个状态';b.append(count);}}for(const line of n.lines){const small=document.createElement('small');small.textContent=wrapMermaidStateText(line);b.append(small);}}\n else {const s=element('svg',{viewBox:`0 0 ${n.w} ${n.h}`,'aria-hidden':'true'},b);if(n.shape==='stateStart')element('circle',{cx:10,cy:10,r:5,class:'state-solid'},s);else if(n.shape==='stateEnd'){element('circle',{cx:10,cy:10,r:9,class:'state-mark'},s);element('circle',{cx:10,cy:10,r:5,class:'state-solid'},s);}else if(n.shape==='choice')element('path',{d:'M16 1L31 16L16 31L1 16Z',class:'state-mark'},s);else element('rect',{width:n.w,height:n.h,rx:2,class:'state-solid'},s);}\n scene.append(b);objects.push({n,b,box:b});\n }\n const rounded=points=>{let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],l1=Math.hypot(b[0]-a[0],b[1]-a[1]),l2=Math.hypot(c[0]-b[0],c[1]-b[1]),r=Math.min(8,l1/2,l2/2);if(!l1||!l2)continue;d+=`L${b[0]-(b[0]-a[0])*r/l1} ${b[1]-(b[1]-a[1])*r/l1}Q${b[0]} ${b[1]} ${b[0]+(c[0]-b[0])*r/l2} ${b[1]+(c[1]-b[1])*r/l2}`;}return d+`L${points.at(-1)[0]} ${points.at(-1)[1]}`};\n for(const e of data.edges){const pts=e.points.map(([x,y])=>[x+offset,y]),last=pts.at(-1),prev=pts.at(-2),a=Math.atan2(last[1]-prev[1],last[0]-prev[0]),ux=Math.cos(a),uy=Math.sin(a),line=pts.slice(0,-1).concat([[last[0]-5*ux,last[1]-5*uy]]),path=element('path',{d:rounded(line),class:'state-wire','data-state-edge':e.id,'data-from':e.from,'data-to':e.to}),head=element('path',{d:`M${last[0]} ${last[1]}L${last[0]-5*ux+2.2*uy} ${last[1]-5*uy-2.2*ux}L${last[0]-5*ux-2.2*uy} ${last[1]-5*uy+2.2*ux}Z`,class:'state-arrow'});let label;\n if(e.label){const p=e.labelPosition||path.getPointAtLength(path.getTotalLength()/2);label=document.createElement('button');label.type='button';label.className='state-label';label.textContent=wrapMermaidStateText(e.label);label.style.left=(e.labelPosition?p.x+offset:p.x)+'px';label.style.top=p.y+'px';label.setAttribute('aria-label',map.get(e.from).name+' → '+map.get(e.to).name+':'+e.label);scene.append(label);}\n edges.push({...e,labelText:e.label,path,head,label});\n }\n // 在所属路径附近寻找空白位置;折行后仍避开节点、其他条件和场景边界。\n const placed=[];\n for(const e of edges)if(e.label){\n const label=e.label,w=label.offsetWidth,h=label.offsetHeight,length=e.path.getTotalLength(),px=parseFloat(label.style.left),py=parseFloat(label.style.top),candidates=[];\n const obstacles=[...nodes.map(n=>({x:n.x,y:n.y,w:n.w,h:n.h})),...placed];\n for(let d=0;d<=length;d+=12){const p=e.path.getPointAtLength(d),a=e.path.getPointAtLength(Math.max(0,d-1)),b=e.path.getPointAtLength(Math.min(length,d+1)),horizontal=Math.abs(b.x-a.x)>=Math.abs(b.y-a.y);\n for(const clearance of [8,24,48,80])for(const side of [-1,1]){const x=horizontal?p.x:p.x+side*(w/2+clearance),y=horizontal?p.y+side*(h/2+clearance):p.y;\n if(x-w/2<4||x+w/2>width-4||y-h/2<4||y+h/2>height-4)continue;\n const overlap=obstacles.reduce((sum,o)=>sum+Math.max(0,Math.min(x+w/2,o.x+o.w)-Math.max(x-w/2,o.x))*Math.max(0,Math.min(y+h/2,o.y+o.h)-Math.max(y-h/2,o.y)),0);\n candidates.push({x,y,overlap,distance:Math.hypot(x-px,y-py)});\n }\n }\n candidates.sort((a,b)=>a.overlap-b.overlap||a.distance-b.distance);const p=candidates[0]||{x:Math.max(w/2+4,Math.min(width-w/2-4,px)),y:Math.max(h/2+4,Math.min(height-h/2-4,py))};label.style.left=p.x+'px';label.style.top=p.y+'px';placed.push({x:p.x-w/2,y:p.y-h/2,w,h});\n }\n function clear(){tip.hidden=true;objects.forEach(({b,box})=>{b.style.opacity='';box.style.borderColor='';b.removeAttribute('aria-describedby')});edges.forEach(e=>{e.path.style.opacity='';e.path.style.stroke='';e.head.style.opacity='';e.head.style.fill='';if(e.label){e.label.style.opacity='';e.label.removeAttribute('aria-describedby')}});}\n function show(trigger,value,ids,selected){clear();const relation=e=>selected?e.id===selected:e.from===ids||e.to===ids,related=edges.filter(relation),neighbors=new Set([ids,...related.flatMap(e=>[e.from,e.to])]);objects.forEach(({n,b})=>b.style.opacity=neighbors.has(n.id)?'1':'.3');edges.forEach(e=>{const hit=related.includes(e);e.path.style.opacity=e.head.style.opacity=hit?'1':'.18';e.path.style.stroke=e.head.style.fill=hit?'var(--viz-series-1)':'';if(e.label)e.label.style.opacity=hit?'1':'.3'});\n }\n if(window.lucide)window.lucide.createIcons({attrs:{width:14,height:14}});\n const bind=(b,show)=>{b.onpointerenter=show;b.onpointerleave=clear;b.onfocus=show;b.onblur=clear;b.onclick=show};objects.forEach(({n,b})=>bind(b,()=>show(b,n.description?{title:n.name,description:n.description}:null,n.id)));objects.forEach(({n,b})=>{if(sourceGroups.has(n.id)||n.targetGroup!==undefined)b.onclick=()=>navigate(n.targetGroup!==undefined?n.targetGroup:n.id);});edges.forEach(e=>{if(e.label)bind(e.label,()=>show(e.label,{from:map.get(e.from).name,to:map.get(e.to).name,label:e.labelText,count:e.bundleCount||1},null,e.id))});\n viewport.addEventListener('scroll',clear,{passive:true,signal});root.addEventListener('qv:display-mode',clear,{signal});document.addEventListener('keydown',e=>{if(e.key==='Escape')clear()},{signal});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),defs=element('defs',{});let running=[],armed=true;\n edges.forEach((e,i)=>{const length=e.path.getTotalLength(),id=root.id+'-state-mask-'+i,mask=element('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs);e.reveal=element('path',{d:e.path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);e.path.setAttribute('mask',`url(#${id})`)});\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;objects.forEach(({n,b})=>running.push(b.animate([{opacity:0,transform:'scale(.08)'},{opacity:1,transform:'scale(1)'}],{duration:240,delay:(n.layer||0)*180,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'})));edges.forEach(e=>{const delay=(map.get(e.from).layer||0)*180+110;running.push(e.reveal.animate([{strokeDashoffset:e.path.getTotalLength()},{strokeDashoffset:0}],{duration:650,delay,fill:'backwards'}));running.push(e.head.animate([{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'}));if(e.label)running.push(e.label.animate([{opacity:0},{opacity:1}],{duration:180,delay:delay+120,fill:'backwards'}))});root.dataset.replays=String(+(root.dataset.replays||0)+1)}\n const observer=new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]});observer.observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}},{signal});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()),{signal});reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()),{signal});dispose=()=>{abort.abort();observer.disconnect();running.forEach(a=>a.cancel());};view.refresh();\n }\n root.dataset.scope='complete';draw(data);navigation();\n}\n\n// 固定 Mermaid 11.12 DB;getData 的端点标记已处理 relSpec 左右命名反转。\nasync function readMermaidErModel(db,descriptions={}) {\n const entities=[...db.getEntities()],raw=db.getData(),cards=new Set(['only_one','zero_or_one','one_or_more','zero_or_more']);\n if(Object.keys(db.getClasses()).length||entities.some(([,n])=>n.cssStyles?.length||n.cssCompiledStyles?.length||n.cssClasses!=='default'||hasRichMermaidLabel(n.alias||n.label)||n.attributes.some(a=>[a.name,a.type,a.comment].some(hasRichMermaidLabel)))||raw.edges.some(e=>!cards.has(e.arrowTypeStart)||!cards.has(e.arrowTypeEnd)||hasRichMermaidLabel(e.label)))return {reason:'此实体关系图包含尚未适配的样式、富文本或关系标记,保留原生语义'};\n const nodes=entities.map(([id,n])=>({id,name:n.alias||n.label,technicalName:n.label,attributes:n.attributes.map(a=>({...a,keys:[...a.keys]})),description:descriptions[id]||'',shape:'er'})),ids=new Map(entities.map(([id,n])=>[n.id,id]));\n const unknown=Object.keys(descriptions).filter(id=>!nodes.some(n=>n.id===id));if(unknown.length)throw new Error('说明引用了未知实体:'+unknown.join('、'));\n return {data:{kind:'er',direction:db.getDirection()||'TB',nodes,groups:[],layers:[],edges:raw.edges.map(e=>({id:e.id,from:ids.get(e.start),to:ids.get(e.end),label:e.label||'',startCard:e.arrowTypeStart,endCard:e.arrowTypeEnd,async:e.pattern==='dashed'}))}};\n}\nasync function prepareMermaidErLayout(data) {\n const context=document.createElement('canvas').getContext('2d'),font=getComputedStyle(document.body).fontFamily;context.font='12px '+font;const measure=s=>Math.ceil(context.measureText(s||'').width);\n const probe=document.createElement('span');probe.style.cssText='position:absolute;left:-100000px;white-space:pre-wrap;overflow-wrap:anywhere;box-sizing:border-box';document.body.append(probe);\n const height=(text,width,size=12,line=18,padding=0)=>{probe.style.width=width+'px';probe.style.font=size+'px/'+line+'px '+font;probe.style.padding=padding+'px 0';probe.textContent=text;return probe.offsetHeight;};\n for(const n of data.nodes){\n n.layoutWidth=Math.min(360,Math.max(240,measure(n.name)+32,...n.attributes.map(a=>measure(a.name)+measure(a.type)+112)));\n const available=n.layoutWidth-40,key=46,type=Math.min(90,Math.max(52,...n.attributes.map(a=>measure(a.type))));n.columns={key,name:available-key-type-16,type};\n n.headerHeight=Math.max(52,height(n.name,n.layoutWidth-32,14,20)+24)+(n.name!==n.technicalName?height(n.technicalName,n.layoutWidth-32,12,18)+4:0);\n n.attributes.forEach(a=>a.rowHeight=Math.max(30,Math.max(height(a.name,n.columns.name),height(a.type,n.columns.type),height(a.keys.join(', '),key))+12));\n // 字段区上下内边距8px、底部留白10px,以及卡片上下边框2px。\n n.layoutHeight=n.headerHeight+n.attributes.reduce((sum,a)=>sum+a.rowHeight,0)+20;\n }\n for(const e of data.edges){e.labelWidth=Math.min(180,Math.max(30,measure(e.label)+12));e.labelHeight=Math.max(22,height(e.label,e.labelWidth-12)+4);}probe.remove();\n // 自关联独占侧面端口,避免与主轴上的关系基数叠合。\n for(const e of data.edges)if(e.from===e.to)e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';\n await prepareMermaidLayout(data);\n}\n\nfunction renderMermaidEr(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-er');root.dataset.renderer='qv-er';root.dataset.state='enhanced';const key=root.querySelector('.edge-key');key.hidden=false;key.replaceChildren();for(const [line,text] of [['solid','识别关系'],['dotted','非识别关系']])if(data.edges.some(e=>(e.async?'dotted':'solid')===line)){const item=document.createElement('span'),swatch=document.createElement('i');swatch.className='key-line';if(line==='dotted')swatch.style.borderTopStyle='dashed';item.append(swatch,document.createTextNode(text));key.append(item);}viewport.setAttribute('aria-label','实体关系图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} .er-entity{position:absolute;border:1px solid var(--border);border-radius:14px;background:var(--background);overflow:hidden;transition:opacity 140ms ease,border-color 140ms ease}\n #${root.id} .er-header{display:flex;align-items:center;justify-content:center;flex-direction:column;align-items:flex-start;gap:4px;width:100%;border:0;background:none;color:var(--foreground);font:inherit;font-weight:500;padding:12px 16px;overflow-wrap:anywhere;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .er-header small{color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font-size:12px;font-weight:400}\n #${root.id} .er-field{display:grid;gap:8px;align-items:center;width:100%;min-height:30px;padding:6px 10px;line-height:18px;white-space:pre-wrap;overflow-wrap:anywhere;border:0;background:none;color:var(--foreground);font:inherit;font-size:12px;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .er-field .er-key{color:var(--viz-series-1)} #${root.id} .er-field .er-type{color:light-dark(#287d7b,#80c3ba)} #${root.id} .er-body{margin:0 10px 10px;padding:4px 0;border-radius:8px;background:color-mix(in srgb,var(--foreground) 4%,var(--background))}\n #${root.id} .er-field[data-active=true]{background:var(--accent)}\n #${root.id} .er-markers{color:light-dark(#727a82,#a3aab3)}\n #${root.id} .er-cardinality{color:inherit}\n #${root.id} .er-cardinality .er-wire{stroke:currentColor}\n #${root.id} .er-zero{stroke:currentColor;stroke-width:1;fill:var(--background)}\n #${root.id} .er-wire{fill:none;stroke:light-dark(#727a82,#a3aab3);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .er-arrow{fill:light-dark(#727a82,#a3aab3)}\n #${root.id} .er-label{position:absolute;transform:translate(-50%,-50%);padding:2px 6px;border:0;border-radius:4px;background:var(--background);color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font:inherit;font-size:12px;line-height:18px;min-height:0;white-space:normal;overflow-wrap:anywhere;max-width:180px;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .er-tip{position:absolute;z-index:10;border-radius:12px;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:none}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='er-tip';tip.id=root.id+'-er-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);\n const element=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const width=data.layout.width,height=data.layout.height,offset=(width-data.layout.width)/2;\n scene.style.cssText=`width:${width}px;min-width:${width}px;height:${height}px`;svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\n const nodes=data.nodes.map(n=>({...n,x:n.x+offset})),groups=data.layout.groups.map(n=>({...n,x:n.x+offset})),map=new Map([...nodes,...groups].map(n=>[n.id,n])),objects=[],edges=[];\n for(const n of nodes){\n const box=document.createElement('div');box.className='er-entity';box.dataset.erEntity=n.id;box.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;\n const b=document.createElement('button');b.type='button';b.className='er-header';b.dataset.erNode=n.id;b.textContent=n.name;b.style.height=n.headerHeight+'px';if(n.name!==n.technicalName){const code=document.createElement('small');code.textContent=n.technicalName;b.append(code);}box.append(b);const body=document.createElement('div');body.className='er-body';box.append(body);\n for(const attribute of n.attributes){const row=document.createElement('button');row.type='button';row.className='er-field';row.dataset.field=attribute.name;row.style.height=attribute.rowHeight+'px';row.style.gridTemplateColumns=`${n.columns.key}px ${n.columns.name}px ${n.columns.type}px`;for(const [value,cl]of [[attribute.keys.join(', '),'er-key'],[attribute.name,'er-name'],[attribute.type,'er-type']]){const cell=document.createElement('span');cell.className=cl;cell.textContent=value;row.append(cell);}row.setAttribute('aria-label',`${n.name} · ${attribute.name} · ${attribute.type} · ${attribute.keys.join(', ')} · ${attribute.comment||''}`);body.append(row);}\n scene.append(box);objects.push({n,b,box});\n }\n const rounded=points=>{let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],l1=Math.hypot(b[0]-a[0],b[1]-a[1]),l2=Math.hypot(c[0]-b[0],c[1]-b[1]),r=Math.min(8,l1/2,l2/2);if(!l1||!l2)continue;d+=`L${b[0]-(b[0]-a[0])*r/l1} ${b[1]-(b[1]-a[1])*r/l1}Q${b[0]} ${b[1]} ${b[0]+(c[0]-b[0])*r/l2} ${b[1]+(c[1]-b[1])*r/l2}`;}return d+`L${points.at(-1)[0]} ${points.at(-1)[1]}`};\n const cardNames={only_one:'1',zero_or_one:'0..1',one_or_more:'1..*',zero_or_more:'0..*'};\n function cardinality(point,next,kind){const angle=Math.atan2(next[1]-point[1],next[0]-point[0]),g=element('g',{class:'er-cardinality',transform:`translate(${point[0]} ${point[1]}) rotate(${angle*180/Math.PI})`});\n const path=d=>element('path',{d,class:'er-wire'},g);\n if(kind==='only_one')path('M7 -5V5M13 -5V5');\n if(kind==='zero_or_one'){path('M7 -5V5');element('circle',{cx:19,cy:0,r:3.5,class:'er-zero'},g);}\n if(kind==='one_or_more'||kind==='zero_or_more'){path('M0 -5L12 0L0 5M0 0H12');if(kind==='one_or_more')path('M19 -5V5');else element('circle',{cx:20,cy:0,r:3.5,class:'er-zero'},g);}\n return g;\n }\n for(const e of data.edges){const pts=e.points.map(([x,y])=>[x+offset,y]),path=element('path',{d:rounded(pts),class:'er-wire','data-er-edge':e.id,'data-from':e.from,'data-to':e.to,...(e.async?{'stroke-dasharray':'4 4'}:{})}),start=cardinality(pts[0],pts[1],e.startCard),end=cardinality(pts.at(-1),pts.at(-2),e.endCard),head=element('g',{class:'er-markers'});head.append(start,end);let label;\n if(e.label){const p=e.labelPosition||path.getPointAtLength(path.getTotalLength()/2);label=document.createElement('button');label.type='button';label.className='er-label';label.textContent=e.label;label.style.left=(e.labelPosition?p.x+offset:p.x)+'px';label.style.top=p.y+'px';label.setAttribute('aria-label',`${map.get(e.from).name} ${cardNames[e.startCard]} — ${map.get(e.to).name} ${cardNames[e.endCard]} · ${e.label} · ${e.async?'非识别关系':'识别关系'}`);scene.append(label);}\n edges.push({...e,path,head,label});\n }\n // 标签沿所属路径法向留8px间距,不能用背板擦断方向证据。\n for(const e of edges)if(e.label){const b=e.label,px=parseFloat(b.style.left),py=parseFloat(b.style.top),length=e.path.getTotalLength();let nearest,distance=Infinity,at=0;for(let d=0;d<=length;d+=3){const p=e.path.getPointAtLength(d),delta=Math.hypot(px-p.x,py-p.y);if(delta<distance){nearest=p;distance=delta;at=d;}}const a=e.path.getPointAtLength(Math.max(0,at-1)),c=e.path.getPointAtLength(Math.min(length,at+1));if(Math.abs(c.x-a.x)>=Math.abs(c.y-a.y)){const gap=b.offsetHeight/2+8;if(Math.abs(py-nearest.y)<gap)b.style.top=(nearest.y+(py<nearest.y?-gap:gap))+'px';}else{const gap=b.offsetWidth/2+8;if(Math.abs(px-nearest.x)<gap)b.style.left=(nearest.x+(px<nearest.x?-gap:gap))+'px';}}\n const view=createMermaidViewport(root);\n function clear(){tip.hidden=true;root.querySelectorAll('[data-active]').forEach(n=>n.removeAttribute('data-active'));objects.forEach(({b,box})=>{box.style.opacity='';box.style.borderColor='';box.querySelectorAll('[aria-describedby]').forEach(n=>n.removeAttribute('aria-describedby'))});edges.forEach(e=>{e.path.style.opacity='';e.path.style.stroke='';e.head.style.opacity='';e.head.style.color='';if(e.label){e.label.style.opacity='';e.label.removeAttribute('aria-describedby')}});}\n function show(trigger,value,ids,selected,local=false){clear();if(local){trigger.dataset.active='true';objects.forEach(({n,box})=>{box.style.opacity=n.id===ids?'1':'.26';box.style.borderColor=n.id===ids?'var(--viz-series-1)':'';});}const relation=e=>selected?e.id===selected:e.from===ids||e.to===ids,related=edges.filter(relation),neighbors=new Set([ids,...related.flatMap(e=>[e.from,e.to])]);if(!local){objects.forEach(({n,box})=>box.style.opacity=neighbors.has(n.id)?'1':'.3');edges.forEach(e=>{const hit=related.includes(e);e.path.style.opacity=e.head.style.opacity=hit?'1':'.18';e.path.style.stroke=e.head.style.color=hit?'var(--viz-series-1)':'';if(e.label)e.label.style.opacity=hit?'1':'.3'});}\n }\n let focusedDetail=null,focusEpoch=0,escapeClosed=false;\n function restoreFocus(){const epoch=++focusEpoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(epoch===focusEpoch&&!escapeClosed&&focusedDetail&&document.activeElement===focusedDetail.b&&focusedDetail.b.matches(':focus-visible'))focusedDetail.show();}));}\n const stable=createMermaidHover(root,clear);const bind=(b,show)=>{const activate=()=>{escapeClosed=false;show()};stable(b,activate);b.onfocus=()=>{focusedDetail={b,show};escapeClosed=false;show();restoreFocus()};b.onblur=()=>{focusedDetail=null;++focusEpoch;clear()};b.onclick=activate};\n objects.forEach(({n,b,box})=>{bind(b,()=>show(b,n.description||n.name+' · '+n.attributes.length+' 个字段 · '+edges.filter(e=>e.from===n.id||e.to===n.id).length+' 条关系',n.id));box.querySelectorAll('.er-field').forEach(row=>bind(row,()=>show(row,n.attributes.find(a=>a.name===row.dataset.field)?.comment||'',n.id,null,true)));});edges.forEach(e=>{if(e.label)bind(e.label,()=>show(e.label,e.label.getAttribute('aria-label'),null,e.id))});\n viewport.addEventListener('scroll',()=>{clear();restoreFocus()},{passive:true});root.addEventListener('qv:display-mode',()=>{focusedDetail=null;++focusEpoch;clear()});document.addEventListener('keydown',e=>{if(e.key==='Escape'){escapeClosed=true;++focusEpoch;clear()}});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),defs=element('defs',{});let running=[],armed=true;\n edges.forEach((e,i)=>{const length=e.path.getTotalLength(),id=root.id+'-er-mask-'+i,mask=element('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs);e.reveal=element('path',{d:e.path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);e.path.setAttribute('mask',`url(#${id})`)});\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;objects.forEach(({n,box})=>running.push(box.animate([{opacity:0,transform:'scale(.08)'},{opacity:1,transform:'scale(1)'}],{duration:240,delay:(n.layer||0)*180,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'})));edges.forEach(e=>{const delay=(map.get(e.from).layer||0)*180+110;running.push(e.reveal.animate([{strokeDashoffset:e.path.getTotalLength()},{strokeDashoffset:0}],{duration:650,delay,fill:'backwards'}));running.push(e.head.animate([{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'}));if(e.label)running.push(e.label.animate([{opacity:0},{opacity:1}],{duration:100,delay:delay+600,fill:'backwards'}))});root.dataset.replays=String(+(root.dataset.replays||0)+1)}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\n// 11.12 只公开像素坐标;在原解析器回调中保存输入值,避免反推数值。\nasync function readMermaidQuadrantModel(mermaid,source,descriptions={}) {\n const seed=await mermaid.mermaidAPI.getDiagramFromText('quadrantChart'),db=seed.db;\n const names=['addClass','addPoint','setXAxisLeftText','setXAxisRightText','setYAxisBottomText','setYAxisTopText','setQuadrant1Text','setQuadrant2Text','setQuadrant3Text','setQuadrant4Text'];\n if(names.some(k=>typeof db[k]!=='function'))return {reason:'四象限解析接口不兼容'};\n const original=new Map(names.map(k=>[k,db[k]])),events=[];\n try {\n for(const name of names)db[name]=(...args)=>{events.push({name,args:structuredClone(args)});return original.get(name)(...args);};\n await mermaid.mermaidAPI.getDiagramFromText(source);\n } finally {for(const [name,fn]of original)db[name]=fn;}\n const text=name=>events.filter(e=>e.name===name).at(-1)?.args[0]?.text||'';\n if(events.some(e=>e.args[0]?.type==='markdown'||hasRichMermaidLabel(e.args[0]?.text)))return {reason:'四象限富文本保留原生语义'};\n const raw=events.filter(e=>e.name==='addPoint');\n const classes=new Map(events.filter(e=>e.name==='addClass').map(e=>[e.args[0],db.parseStyles(e.args[1])]));\n const styles=raw.map(e=>db.parseStyles(e.args[4]||[]));\n if([...classes.values(),...styles].some(s=>Object.keys(s).some(k=>k!=='color')))return {reason:'四象限自定义尺寸与描边保留原生语义'};\n const points=raw.map((e,i)=>({id:'point-'+(i+1),name:e.args[0].text,group:e.args[1]||'',color:styles[i].color||classes.get(e.args[1])?.color||'',x:Number(e.args[2]),y:Number(e.args[3]),description:descriptions['point-'+(i+1)]||''}));\n if(points.some(p=>!Number.isFinite(p.x)||!Number.isFinite(p.y)||p.x<0||p.x>1||p.y<0||p.y>1))return {reason:'四象限坐标保留原生语义'};\n const unknown=Object.keys(descriptions).filter(k=>!points.some(p=>p.id===k));if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n return {data:{kind:'quadrant',points,xLow:text('setXAxisLeftText'),xHigh:text('setXAxisRightText'),yLow:text('setYAxisBottomText'),yHigh:text('setYAxisTopText'),quadrants:[1,2,3,4].map(i=>text('setQuadrant'+i+'Text')),title:db.getDiagramTitle?.()||''}};\n}\n\nfunction renderMermaidQuadrant(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');root.dataset.state='enhanced';root.dataset.renderer='qv-quadrant';root.querySelector('.edge-key').hidden=true;svg.removeAttribute('aria-hidden');viewport.setAttribute('aria-label','四象限图,可横向滚动');const title=root.querySelector('h3');if(!title.textContent)title.textContent=data.title;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .wires{pointer-events:auto}\n #${root.id} .quadrant-text{position:absolute;font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere}\n #${root.id} .quadrant-axis{font-size:14px;font-weight:500;color:var(--foreground);box-sizing:border-box;padding:8px 12px;border-radius:999px;background:color-mix(in srgb,var(--muted-foreground) 8%,var(--background))}\n #${root.id} .quadrant-label{position:absolute;border:0;background:transparent;padding:4px 6px;max-width:144px;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .quadrant-detail{position:absolute;z-index:10;width:252px;max-width:calc(100% - 8px);box-sizing:border-box;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:auto;overflow-wrap:anywhere;overflow:auto;max-height:320px}\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',add=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},fmt=v=>new Intl.NumberFormat(undefined,{maximumFractionDigits:20}).format(v);\n const tip=document.createElement('div');tip.className='quadrant-detail';tip.id=root.id+'-quadrant-detail';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);const hint=document.createElement('div');hint.style.cssText='font-size:12px;line-height:18px;color:var(--muted-foreground);margin-top:12px';viewport.before(hint);\n let labels=[],texts=[],marks=[],leaders=[],animations=[],pending=null,epoch=0,plot,pointPositions=[],activePoint=null,leaveTimer=0;const view=createMermaidViewport(root),reduced=matchMedia('(prefers-reduced-motion:reduce)');\n const box=n=>({x:n.offsetLeft,y:n.offsetTop,w:n.offsetWidth,h:n.offsetHeight}),area=(a,b)=>Math.max(0,Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x))*Math.max(0,Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y));\n function clear(){clearTimeout(leaveTimer);pending=null;activePoint=null;epoch++;tip.hidden=true;root.style.paddingBottom='';tip.style.position='absolute';tip.style.marginTop='0';[...labels,...marks].forEach(b=>b.removeAttribute('aria-describedby'));marks.forEach(m=>m.style.opacity='1');leaders.forEach(l=>l.style.opacity='1');}\n function leave(){clearTimeout(leaveTimer);leaveTimer=setTimeout(clear,100);}\n tip.onpointerenter=()=>clearTimeout(leaveTimer);tip.onpointerleave=leave;\n function show(i,event,anchor,group=false){\n clear();activePoint=i;const p=data.points[i];\n const same=group?data.points.filter(q=>q.x===p.x&&q.y===p.y):[p];\n marks.forEach((m,j)=>m.style.opacity=same.includes(data.points[j])?'1':'.25');\n leaders.forEach((m,j)=>m.style.opacity=same.includes(data.points[j])?'1':'.25');\n const head=document.createElement('div');head.className='quadrant-detail-title';head.textContent=same.length>1?same.length+' 个方案位于同一坐标':p.name;tip.replaceChildren(head);\n if(same.length>1)same.forEach(q=>{const n=document.createElement('div');n.textContent=q.name;tip.append(n);});\n const row=document.createElement('div');row.style.color='var(--muted-foreground)';row.textContent='X '+fmt(p.x)+' · Y '+fmt(p.y);tip.append(row);\n const location=document.createElement('div');location.textContent=p.x===.5||p.y===.5?'位于象限分界线':data.quadrants[p.y>.5?(p.x>.5?0:1):(p.x>.5?3:2)];tip.append(location);\n const hasCategories=data.points.some(q=>q.group),categories=hasCategories?[...new Set(same.map(q=>q.group||''))]:[];\n categories.forEach(category=>{const n=document.createElement('div'),swatch=document.createElement('span');n.className='quadrant-detail-category';n.style.cssText='display:flex;align-items:center;gap:6px;margin-top:6px';swatch.style.cssText='width:7px;height:7px;flex-shrink:0;border-radius:50%';const member=same.find(q=>(q.group||'')===category);swatch.style.background=marks[data.points.indexOf(member)].getAttribute('fill');swatch.setAttribute('aria-hidden','true');n.append(swatch,document.createTextNode(category||'默认'));tip.append(n);});\n same.filter(q=>q.description).forEach(q=>{const d=document.createElement('div');d.style.marginTop='8px';d.textContent=(same.length>1?q.name+':':'')+q.description;tip.append(d);});\n const trigger=anchor||marks[i];trigger.setAttribute('aria-describedby',tip.id);tip.hidden=false;\n const rr=root.getBoundingClientRect(),br=trigger.getBoundingClientRect(),w=tip.offsetWidth;\n const top=Math.max(4,-rr.top+4),bottom=Math.min(root.clientHeight,innerHeight-rr.top-4);\n tip.style.maxHeight=Math.max(64,Math.min(320,bottom-top))+'px';\n const h=tip.offsetHeight,x=br.left+br.width/2-rr.left,y=br.top+br.height/2-rr.top;\n const candidates=[{x:x+14,y:y-h/2},{x:x-w-14,y:y-h/2},{x:x-w/2,y:y+14},{x:x-w/2,y:y-h-14}];\n candidates.forEach(c=>{c.x=Math.max(4,Math.min(root.clientWidth-w-4,c.x));c.y=Math.max(top,Math.min(bottom-h,c.y));c.cover=area({...c,w,h},{x:br.left-rr.left-6,y:br.top-rr.top-6,w:br.width+12,h:br.height+12});});\n candidates.sort((a,b)=>a.cover-b.cover);tip.style.left=candidates[0].x+'px';tip.style.top=candidates[0].y+'px';tip.dataset.placement='overlay';\n }\n function focus(i,ensureVisible=true){pending=i;const b=labels[i].hidden?marks[i]:labels[i],r=b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(ensureVisible&&!view.expanded){if(r.left<v.left+8)viewport.scrollLeft-=v.left+8-r.left;else if(r.right>v.right-8)viewport.scrollLeft+=r.right-v.right+8;}const ticket=++epoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(epoch===ticket){pending=null;show(i,null,marks[i],true);}}));}\n function cancel(){animations.forEach(a=>a.cancel());animations=[];}\n function label(text,x,y,w,align='left'){const n=document.createElement('div');n.className='quadrant-text';n.textContent=text;n.style.left=x+'px';n.style.top=y+'px';n.style.width=w+'px';n.style.textAlign=align;scene.append(n);texts.push(n);return n;}\n function draw(){\n cancel();clear();svg.replaceChildren();[...labels,...texts].forEach(n=>n.remove());labels=[];texts=[];marks=[];leaders=[];\n const width=Math.max(640,Math.min(736,viewport.clientWidth)),size=width-256;\n plot={x:128,y:104,w:size,h:size};const height=size+208;\n hint.textContent=!view.expanded&&viewport.clientWidth<width?'可横向滚动 · 密集区域悬浮点查看完整方案':'';hint.hidden=!hint.textContent;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';\n svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';svg.setAttribute('role','img');svg.setAttribute('aria-label',data.title+','+data.points.map(p=>p.name+' ['+p.x+', '+p.y+']').join(';'));\n const {x,y,w,h}=plot;\n add('line',{x1:x+w/2,y1:y,x2:x+w/2,y2:y+h,stroke:'var(--border)'});\n add('line',{x1:x,y1:y+h/2,x2:x+w,y2:y+h/2,stroke:'var(--border)'});\n const axis=(text,l,t,width)=>{const n=label(text,l,t,width,'center');n.classList.add('quadrant-axis');n.style.width='max-content';n.style.maxWidth=width+'px';n.style.left=l+(width-n.offsetWidth)/2+'px';return n;};\n const left=axis(data.xLow,0,0,112),right=axis(data.xHigh,x+w+16,0,112);[left,right].forEach(n=>n.style.top=y+h/2-n.offsetHeight/2+'px');\n const top=axis(data.yHigh,x+w/2-160,0,320);top.style.top=Math.max(0,y-20-top.offsetHeight)+'px';axis(data.yLow,x+w/2-160,y+h+20,320);\n // 轴说明按真实换行高度留白,不改变归一化坐标。\n const bottom=texts.at(-1);scene.style.height=Math.max(height,bottom.offsetTop+bottom.offsetHeight+16)+'px';\n const cx=x+w/2,cy=y+h/2,captionWidth=Math.min(176,w/2-24);\n data.quadrants.forEach((t,i)=>{const right=i===0||i===3,upper=i<2,n=label(t,right?cx+12:cx-captionWidth-12,0,captionWidth,right?'left':'right');n.classList.add('quadrant-caption');n.style.top=(upper?cy-12-n.offsetHeight:cy+12)+'px';});\n const points=data.points.map(p=>({x:x+p.x*w,y:y+(1-p.y)*h})),occupied=texts.map(box);pointPositions=points;\n const groups=[...new Set(data.points.map(p=>p.group).filter(Boolean))];\n const color=p=>p.color||('var(--viz-series-'+(p.group?groups.indexOf(p.group)%5+2:1)+')');\n const measures=data.points.map(p=>{const b=document.createElement('button');b.className='quadrant-label';b.type='button';b.textContent=p.name;b.dataset.quadrantPoint=p.id;b.setAttribute('aria-label',p.name+',x '+fmt(p.x)+',y '+fmt(p.y));scene.append(b);return b;});\n data.points.forEach((p,i)=>{\n const pos=points[i],b=measures[i],bw=b.offsetWidth,bh=b.offsetHeight;\n const nearby=points.map((q,j)=>({q,j})).filter(({q})=>Math.hypot(q.x-pos.x,q.y-pos.y)<96);\n const load=nearby.reduce((sum,{j})=>sum+measures[j].offsetWidth*measures[j].offsetHeight,0);\n const dense=nearby.length>=3||nearby.length>=2&&load/(Math.PI*96*96)>.14;\n const candidates=[[pos.x+12,pos.y-bh/2],[pos.x-bw-12,pos.y-bh/2],[pos.x-bw/2,pos.y-bh-14],[pos.x-bw/2,pos.y+14],[pos.x+12,pos.y-bh-14],[pos.x-bw-12,pos.y+14]].map(([a,c])=>({x:Math.max(4,Math.min(width-bw-4,a)),y:Math.max(4,Math.min(scene.offsetHeight-bh-4,c)),w:bw,h:bh}));\n candidates.forEach(c=>{c.overlap=occupied.reduce((sum,r)=>sum+area({x:c.x-4,y:c.y-4,w:c.w+8,h:c.h+8},r),0);c.dotOverlap=points.reduce((sum,q)=>sum+(q.x>=c.x-6&&q.x<=c.x+c.w+6&&q.y>=c.y-6&&q.y<=c.y+c.h+6?1:0),0);});\n candidates.sort((a,b)=>a.dotOverlap-b.dotOverlap||a.overlap-b.overlap);const chosen=candidates[0];\n b.style.left=chosen.x+'px';b.style.top=chosen.y+'px';b.hidden=dense||chosen.overlap>0||chosen.dotOverlap>0;b.dataset.dense=String(dense);labels.push(b);if(!b.hidden)occupied.push(chosen);\n const lx=Math.max(chosen.x,Math.min(chosen.x+bw,pos.x)),ly=Math.max(chosen.y,Math.min(chosen.y+bh,pos.y));const leader=add('line',{x1:pos.x,y1:pos.y,x2:lx,y2:ly,stroke:'var(--border)','stroke-width':1},svg);leader.style.display=b.hidden?'none':'';leaders.push(leader);\n const dot=add('circle',{cx:pos.x,cy:pos.y,r:5,fill:color(p),'data-quadrant-mark':p.id,'data-x':p.x,'data-y':p.y,tabindex:b.hidden?'0':'-1',role:'button','aria-label':b.getAttribute('aria-label')});marks.push(dot);\n dot.onfocus=()=>focus(i);dot.onblur=leave;dot.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();focus(i);}};\n b.onpointerenter=()=>focus(i,false);b.onclick=e=>focus(i,e.detail===0);b.onfocus=()=>focus(i);b.onpointerleave=leave;b.onblur=leave;\n });\n svg.onpointermove=svg.onclick=e=>{const r=svg.getBoundingClientRect(),sx=(e.clientX-r.left)*width/r.width,sy=(e.clientY-r.top)*height/r.height;const near=pointPositions.map((p,i)=>({i,d:Math.hypot(p.x-sx,p.y-sy)})).sort((a,b)=>a.d-b.d);if(!near.length||near[0].d>16){leave();return;}clearTimeout(leaveTimer);const i=near[0].i;if(activePoint===i)return;show(i,e,marks[i],true);};svg.onpointerleave=leave;view.refresh();\n }\n function replay(){cancel();if(reduced.matches||view.expanded)return;marks.forEach((m,i)=>animations.push(m.animate([{transform:'scale(0)'},{transform:'scale(1)'}],{duration:240,delay:i*80,fill:'backwards',easing:'cubic-bezier(.25,1,.5,1)'})));marks.forEach(m=>{m.style.transformBox='fill-box';m.style.transformOrigin='center';});labels.forEach((b,i)=>{if(!b.hidden)animations.push(b.animate([{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:300,delay:i*80+120,fill:'backwards',easing:'cubic-bezier(.25,1,.5,1)'}));});}\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let width=root.clientWidth,resizeFrame=0;new ResizeObserver(()=>{if(width!==root.clientWidth&&!resizeFrame)resizeFrame=requestAnimationFrame(()=>{resizeFrame=0;if(width!==root.clientWidth){width=root.clientWidth;draw();}});}).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting){armed=true;cancel();clear();}else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else if(armed){const r=root.getBoundingClientRect();if(r.bottom>0&&r.top<innerHeight){armed=false;replay();}}});reduced.addEventListener('change',cancel);viewport.addEventListener('scroll',()=>{if(pending!==null)focus(pending,false);else clear();},{passive:true});document.addEventListener('keydown',e=>{if(e.key==='Escape'){pending=null;clear();}});window.addEventListener('blur',clear);\n}\n\n// 11.12 的 DB 保留网格树,但连线词法信息会丢失;从原解析器回调记录已解析的 token。\nasync function readMermaidBlockModel(mermaid,source,descriptions={}) {\n const seed=await mermaid.mermaidAPI.getDiagramFromText('block-beta\\n__qv_seed__'),db=seed.db;\n if(!db.getBlock||!db.getEdges||!db.edgeStrToEdgeData)return {reason:'Block 数据接口不兼容'};\n const original=db.edgeStrToEdgeData,tokens=[];\n try{db.edgeStrToEdgeData=token=>{tokens.push(String(token).trim());return original(token);};await mermaid.mermaidAPI.getDiagramFromText(source);}finally{db.edgeStrToEdgeData=original;}\n const rawRoot=db.getBlock('root'),rawEdges=db.getEdges(),classes=db.getClasses();\n if(classes?.size||[...db.getBlocksFlat()].some(n=>n.classes?.length||n.styles?.length))return {reason:'Block 自定义样式保留原生语义'};\n const shapes=new Set(['square','na','round','stadium','cylinder','diamond','circle','doublecircle','hexagon','subroutine','lean_left','lean_right','trapezoid','inv_trapezoid','rect_left_inv_arrow','block_arrow']);\n let reason='',nodes=[],groups=[];\n function copy(raw,parent=null,path='root'){\n const type=raw.type==='composite'?'group':raw.type==='space'?'space':raw.type||'na',span=type==='space'?1:raw.widthInColumns??1;\n if(!Number.isInteger(span)||span<1)reason='Block 跨列宽度保留原生语义';\n if(hasRichMermaidLabel(raw.label))reason='Block 富文本保留原生语义';\n const n={id:type==='space'?'space-'+path:raw.id,name:type==='space'?'':raw.label||'',type,span,parent,description:descriptions[raw.id]||'',directions:[...raw.directions||[]]};\n if(type==='group'){\n n.columns=raw.columns??-1;n.children=(raw.children||[]).map((child,i)=>copy(child,raw.id,path+'-'+i));\n if(n.columns===0||!Number.isInteger(n.columns))reason='Block 列数保留原生语义';\n let pos=0;for(const child of n.children){if(n.columns>0&&child.span>n.columns-pos%n.columns)reason='Block 跨行溢出保留原生布局';pos+=child.span;}\n if(raw.id!=='root')groups.push(n);\n }else if(type!=='space'){\n if(!shapes.has(type)||n.directions.some(d=>!['up','down','left','right','x','y'].includes(d)))reason='Block 形状保留原生语义';\n nodes.push(n);\n }\n return n;\n }\n const tree=copy(rawRoot);if(reason)return {reason};\n if(tokens.length!==rawEdges.length)return {reason:'Block 连线接口不兼容'};\n const ids=new Set([...nodes,...groups].map(n=>n.id)),marker=c=>c==='>'||c==='<'?'point':c==='o'?'circle':c==='x'?'cross':null;\n const edges=rawEdges.map((e,i)=>({id:e.id,from:e.start,to:e.end,label:e.label||'',startMarker:marker(tokens[i][0]),endMarker:marker(tokens[i].at(-1)),dotted:tokens[i].includes('.'),thick:tokens[i].includes('='),invisible:tokens[i].includes('~')}));\n if(edges.some(e=>!ids.has(e.from)||!ids.has(e.to)||hasRichMermaidLabel(e.label)))return {reason:'Block 特殊端点或富文本保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!ids.has(id));if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n return {data:{kind:'block',tree,nodes,groups,edges}};\n}\n\n// 保留原网格顺序与跨列;字号、行高和连接路由由 qv 决定。\nfunction layoutMermaidBlocks(data,measure,{cell=160,gap=24,padding=24}={}){\n const boxes=new Map(),rows=[];\n function minimum(n){\n if(n.type==='space')return {w:0,h:24};\n if(n.type!=='group')return {w:cell,h:measure(n,cell)};\n const cols=n.columns>0?n.columns:Math.max(1,n.children.reduce((s,c)=>s+c.span,0));let unit=cell;\n for(const c of n.children){const m=minimum(c);unit=Math.max(unit,(m.w-gap*(c.span-1))/c.span);}\n return {w:cols*unit+(cols-1)*gap+2*padding,h:0};\n }\n function place(n,x,y,width){\n if(n.type!=='group'){const h=n.type==='space'?24:measure(n,width),box={...n,x,y,w:width,h};boxes.set(n.id,box);return h;}\n const cols=n.columns>0?n.columns:Math.max(1,n.children.reduce((s,c)=>s+c.span,0)),unit=(width-2*padding-gap*(cols-1))/cols,head=n.id!=='root'&&n.name?Math.max(32,measure(n,width-2*padding)-12):0;\n let position=0;const grouped=[];\n for(const c of n.children){const col=position%cols,row=Math.floor(position/cols);(grouped[row]||=[]).push({n:c,col});position+=c.span;}\n let rowY=y+padding+head;\n for(let row=0;row<grouped.length;row++){\n let rowH=0;for(const c of grouped[row]||[]){const w=c.n.span*unit+(c.n.span-1)*gap;rowH=Math.max(rowH,place(c.n,x+padding+c.col*(unit+gap),rowY,w));}\n rows.push({parent:n.id,row,y:rowY,height:rowH});rowY+=rowH+gap;\n }\n const h=rowY-y-(grouped.length?gap:0)+padding;boxes.set(n.id,{...n,x,y,w:width,h,columns:cols});return h;\n }\n const width=minimum(data.tree).w,height=place(data.tree,24,24,width);return {boxes,rows,width:width+48,height:height+48};\n}\nfunction routeMermaidBlocks(layout,edges,{scoreRoute=()=>0,onRoute=()=>{}}={}){\n const all=[...layout.boxes.values()],leaves=all.filter(b=>!['space','group'].includes(b.type)),groups=all.filter(b=>b.type==='group'&&b.id!=='root'&&b.name);\n const obstacles=[...leaves.map(b=>({x:b.x-6,y:b.y-6,w:b.w+12,h:b.h+12})),...groups.map(b=>({x:b.x+12,y:b.y+10,w:b.w-24,h:b.captionHeight??24}))];\n const port=(b,side)=>side===0?{x:b.x+b.w/2,y:b.y,ox:b.x+b.w/2,oy:b.y-14}:side===1?{x:b.x+b.w,y:b.y+b.h/2,ox:b.x+b.w+14,oy:b.y+b.h/2}:side===2?{x:b.x+b.w/2,y:b.y+b.h,ox:b.x+b.w/2,oy:b.y+b.h+14}:{x:b.x,y:b.y+b.h/2,ox:b.x-14,oy:b.y+b.h/2};\n const ports=new Map(all.filter(b=>b.type!=='space'&&b.id!=='root').map(b=>[b.id,[0,1,2,3].map(s=>port(b,s))]));\n const xs=[8,layout.width-8],ys=[8,layout.height-8];for(const r of obstacles){xs.push(r.x-8,r.x+r.w+8);ys.push(r.y-8,r.y+r.h+8);}for(const ps of ports.values())for(const p of ps){xs.push(p.ox);ys.push(p.oy);}\n const X=[...new Set(xs)].sort((a,b)=>a-b),Y=[...new Set(ys)].sort((a,b)=>a-b),points=[],index=new Map();\n for(let iy=0;iy<Y.length;iy++)for(let ix=0;ix<X.length;ix++){const x=X[ix],y=Y[iy];if(obstacles.some(r=>x>r.x&&x<r.x+r.w&&y>r.y&&y<r.y+r.h))continue;const id=points.length;points.push({x,y,ix,iy,neighbors:[]});index.set(ix+','+iy,id);}\n const clear=(a,b)=>!obstacles.some(r=>a.x===b.x?a.x>r.x&&a.x<r.x+r.w&&Math.max(a.y,b.y)>r.y&&Math.min(a.y,b.y)<r.y+r.h:a.y>r.y&&a.y<r.y+r.h&&Math.max(a.x,b.x)>r.x&&Math.min(a.x,b.x)<r.x+r.w);\n for(const a of points)for(const [dx,dy]of [[1,0],[-1,0],[0,1],[0,-1]]){const j=index.get((a.ix+dx)+','+(a.iy+dy));if(j!==undefined&&clear(a,points[j]))a.neighbors.push({id:j,dir:dx?0:1,length:Math.abs(a.x-points[j].x)+Math.abs(a.y-points[j].y)});}\n const ids=p=>index.get(X.indexOf(p.ox)+','+Y.indexOf(p.oy));\n function shortest(start,end){\n if(start===undefined||end===undefined)return null;const costs=new Map(),prev=new Map(),heap=[];\n function push(v){let i=heap.length;heap.push(v);while(i){const p=(i-1)>>1;if(heap[p].score<=v.score)break;heap[i]=heap[p];i=p;}heap[i]=v;}\n function pop(){const first=heap[0],last=heap.pop();if(heap.length){let i=0;while(true){let c=i*2+1;if(c>=heap.length)break;if(c+1<heap.length&&heap[c+1].score<heap[c].score)c++;if(last.score<=heap[c].score)break;heap[i]=heap[c];i=c;}heap[i]=last;}return first;}\n const heuristic=i=>Math.abs(points[i].x-points[end].x)+Math.abs(points[i].y-points[end].y);costs.set(start*3+2,0);push({id:start,dir:2,cost:0,score:heuristic(start)});\n while(heap.length){const state=pop(),key=state.id*3+state.dir;if(state.cost!==costs.get(key))continue;if(state.id===end){const path=[];let k=key;while(k!==undefined){path.push(points[Math.floor(k/3)]);k=prev.get(k);}return {points:path.reverse().map(p=>[p.x,p.y]),cost:state.cost};}for(const n of points[state.id].neighbors){const nk=n.id*3+n.dir,cost=state.cost+n.length+(state.dir!==2&&state.dir!==n.dir?12:0);if(cost<(costs.get(nk)??Infinity)){costs.set(nk,cost);prev.set(nk,key);push({id:n.id,dir:n.dir,cost,score:cost+heuristic(n.id)});}}}\n return null;\n }\n const compact=raw=>{const simple=raw.filter((p,i)=>!i||p[0]!==raw[i-1][0]||p[1]!==raw[i-1][1]);return simple.filter((p,i)=>!i||i===simple.length-1||(simple[i-1][0]!==p[0]||p[0]!==simple[i+1][0])&&(simple[i-1][1]!==p[1]||p[1]!==simple[i+1][1]));};\n return edges.map(e=>{let best;const a=ports.get(e.from),b=ports.get(e.to);for(let i=0;i<4;i++)for(let j=0;j<4;j++){if(e.from===e.to&&i===j)continue;const route=shortest(ids(a[i]),ids(b[j]));if(route){const points=compact([[a[i].x,a[i].y],...route.points,[b[j].x,b[j].y]]),score=route.cost+scoreRoute({...e,points});if(Number.isFinite(score)&&(!best||score<best.score))best={...route,points,score,start:a[i],end:b[j]};}}if(!best)throw Error('无法在原网格中连接:'+e.from+' → '+e.to);const result={...e,points:best.points};onRoute(result);return result;});\n}\n\nfunction renderMermaidBlock(root,data){\n const scene=root.querySelector('.scene'),scroll=root.querySelector('.diagram-scroll'),tip=root.querySelector('.node-tip'),svg=root.querySelector('.wires'),reduced=matchMedia('(prefers-reduced-motion:reduce)'),ns='http://www.w3.org/2000/svg';\n root.dataset.state='enhanced';root.dataset.renderer='qv-block';root.querySelector('.edge-key').hidden=true;scroll.setAttribute('aria-label','方块图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} button.node.block-node{font-size:12px;padding:12px 14px;line-height:18px;background:transparent;border:0;transition:opacity 140ms ease}\n #${root.id} button.node.block-node .name{font-size:12px;display:block;white-space:normal;text-align:center;overflow-wrap:anywhere;font-weight:500;line-height:18px}\n #${root.id} .block-shape{position:absolute;inset:0;width:100%;height:100%;fill:var(--background);stroke:var(--border);stroke-width:1;overflow:visible}\n #${root.id} .block-group{position:absolute;border:1px solid var(--border);border-radius:12px;box-sizing:border-box;pointer-events:none}\n #${root.id} .block-group-label{max-width:calc(100% - 24px);white-space:normal;overflow-wrap:anywhere;position:absolute;left:12px;top:10px;font:inherit;font-size:12px;color:var(--muted-foreground);border:0;padding:2px 4px;background:transparent;pointer-events:auto;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .block-wire{pointer-events:stroke;stroke:light-dark(#727a82,#a3aab3);stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .wires{z-index:1} #${root.id} .block-group{z-index:0} #${root.id} button.node{z-index:2} #${root.id} .edge-label{z-index:3}\n #${root.id} .block-edge-label[data-empty=true]{width:32px;height:24px;padding:0;background:transparent}\n #${root.id} .block-edge-label{font:inherit;font-size:12px;line-height:18px;border:0;color:var(--muted-foreground);padding:2px 5px;background:var(--background);cursor:var(--cursor-interaction,pointer);pointer-events:auto;white-space:normal;text-align:center;max-width:200px}\n #${root.id} .node-tip{padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);box-shadow:none;border:1px solid var(--border);line-height:18px;max-width:260px;overflow-wrap:anywhere}\n #${root.id} .block-tip-title{font-weight:500;margin-bottom:4px}\n #${root.id} .block-tip-detail{color:var(--muted-foreground)}\n #${root.id} .block-node[data-active=true] .block-shape{stroke:var(--viz-series-1)}\n #${root.id} .block-node[data-active=true] .block-shape .block-body{fill:color-mix(in srgb,var(--viz-series-1) 6%,var(--background))}\n @media(prefers-reduced-motion:reduce){#${root.id} .block-node{transition:none}}\n `;root.append(style);\n const controller=createMermaidViewport(root),canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');ctx.font='500 12px '+getComputedStyle(root).fontFamily;\n let layout,routes,buttons=new Map(),groupButtons=new Map(),paths=[],labels=[],selected=null,epoch=0,animations=[],headTimers=[],motionEpoch=0,seen=false,exited=true;\n function element(name,attrs,parent){const el=document.createElementNS(ns,name);for(const [k,v]of Object.entries(attrs))el.setAttribute(k,String(v));parent.append(el);return el;}\n function textHeight(name,width){let count=1,used=0;for(const c of name){if(c==='\\n'){count++;used=0;continue;}const w=ctx.measureText(c).width;if(used+w>width&&used){count++;used=0;}used+=w;}return count*18;}\n const inset=n=>['diamond','hexagon','circle','doublecircle','block_arrow','rect_left_inv_arrow'].includes(n.type)?.56:.86;\n function measure(n,w){const text=textHeight(n.name,Math.max(24,w*inset(n)-12));return ['circle','doublecircle'].includes(n.type)?Math.max(w,text+40):n.type==='diamond'?Math.max(100,text*2+40):['hexagon','block_arrow','rect_left_inv_arrow'].includes(n.type)?Math.max(72,text+32):Math.max(48,text+24+(n.type==='cylinder'?20:0));}\n function shape(n,parent){\n const {w,h,type}=n,s=element('svg',{viewBox:`0 0 ${w} ${h}`,class:'block-shape','aria-hidden':'true'},parent),path=d=>element('path',{d,class:'block-body'},s),poly=p=>element('polygon',{points:p.map(a=>a.join(',')).join(' '),class:'block-body'},s);\n if(['circle','doublecircle'].includes(type)){element('ellipse',{cx:w/2,cy:h/2,rx:w/2-1,ry:h/2-1,class:'block-body'},s);if(type==='doublecircle')element('ellipse',{cx:w/2,cy:h/2,rx:w/2-6,ry:h/2-6,fill:'none'},s);}\n else if(type==='diamond')poly([[w/2,1],[w-1,h/2],[w/2,h-1],[1,h/2]]);\n else if(type==='hexagon')poly([[16,1],[w-16,1],[w-1,h/2],[w-16,h-1],[16,h-1],[1,h/2]]);\n else if(type==='cylinder'){path(`M1 10C1 -2 ${w-1} -2 ${w-1} 10V${h-10}C${w-1} ${h+2} 1 ${h+2} 1 ${h-10}Z`);element('path',{d:`M1 10C1 22 ${w-1} 22 ${w-1} 10`,fill:'none'},s);}\n else if(type==='lean_right'||type==='lean_left'){const flip=type==='lean_left';poly(flip?[[1,1],[w-17,1],[w-1,h-1],[17,h-1]]:[[17,1],[w-1,1],[w-17,h-1],[1,h-1]]);}\n else if(type==='trapezoid'||type==='inv_trapezoid')poly(type==='trapezoid'?[[17,1],[w-17,1],[w-1,h-1],[1,h-1]]:[[1,1],[w-1,1],[w-17,h-1],[17,h-1]]);\n else if(type==='block_arrow'||type==='rect_left_inv_arrow'){\n const dirs=new Set(n.directions.flatMap(d=>d==='x'?['left','right']:d==='y'?['up','down']:[d]));if(type==='rect_left_inv_arrow')dirs.add('left');\n const a=Math.min(20,w/5,h/4),l=dirs.has('left')?a:1,r=dirs.has('right')?w-a:w-1,t=dirs.has('up')?a:1,b=dirs.has('down')?h-a:h-1;\n const p=[[l,t]];if(dirs.has('up'))p.push([w/2-a,t],[w/2,1],[w/2+a,t]);p.push([r,t]);if(dirs.has('right'))p.push([r,h/2-a],[w-1,h/2],[r,h/2+a]);p.push([r,b]);if(dirs.has('down'))p.push([w/2+a,b],[w/2,h-1],[w/2-a,b]);p.push([l,b]);if(dirs.has('left'))p.push([l,h/2+a],[1,h/2],[l,h/2-a]);poly(p);\n }else{element('rect',{x:1,y:1,width:w-2,height:h-2,rx:type==='stadium'?h/2:type==='square'||type==='na'||type==='subroutine'?10:14,class:'block-body'},s);if(type==='subroutine')for(const x of [9,w-9])element('path',{d:`M${x} 1V${h-1}`,fill:'none'},s);}\n }\n function rounded(points){let d=`M${points[0].join(' ')}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],p=points[i],b=points[i+1],la=Math.hypot(p[0]-a[0],p[1]-a[1]),lb=Math.hypot(b[0]-p[0],b[1]-p[1]),r=Math.min(7,la/2,lb/2);d+=`L${p[0]+(a[0]-p[0])*r/la} ${p[1]+(a[1]-p[1])*r/la}Q${p.join(' ')} ${p[0]+(b[0]-p[0])*r/lb} ${p[1]+(b[1]-p[1])*r/lb}`;}return d+`L${points.at(-1).join(' ')}`;}\n function heading(id){let b=layout.boxes.get(id),names=[];while(b&&b.id!=='root'){if(b.name)names.unshift(b.name);b=layout.boxes.get(b.parent);}return names.join(' › ');}\n function detail(id,edge){const n=layout.boxes.get(id);const members=[];const collect=b=>{for(const c of b.children||[])if(c.type==='group')collect(c);else if(c.type!=='space')members.push(c.name);};if(n?.type==='group')collect(n);return edge?{name:edge.label||'连接关系',detail:heading(edge.from)+' → '+heading(edge.to)}:{name:n.name,detail:[n.description||heading(n.parent),members.length?'成员:'+members.join('、'):'',...data.edges.filter(e=>e.from===id||e.to===id).map(e=>(e.from===id?'流向:'+heading(e.to):'来自:'+heading(e.from))+(e.label?' · '+e.label:''))].filter(Boolean).join('; ')};}\n function clear(){epoch++;selected=null;tip.hidden=true;for(const el of buttons.values()){el.style.opacity='';delete el.dataset.active;el.removeAttribute('aria-describedby');}for(const p of paths){p.el.style.opacity='';p.el.style.stroke='';}for(const l of labels){l.el.style.opacity='';l.el.removeAttribute('aria-describedby');}for(const el of groupButtons.values())el.removeAttribute('aria-describedby');}\n function choose(id,edge,ensure=false){\n const pending=++epoch;selected={id,edge};if(ensure&&!controller.expanded){const trigger=edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id);if(trigger){const a=trigger.getBoundingClientRect(),v=scroll.getBoundingClientRect();if(a.left<v.left+8)scroll.scrollLeft-=v.left+8-a.left;else if(a.right>v.right-8)scroll.scrollLeft+=a.right-v.right+8;}}\n requestAnimationFrame(()=>requestAnimationFrame(()=>{if(pending!==epoch)return;const linked=new Set(edge?[edge.from,edge.to]:[id]);const descendants=(n)=>{linked.add(n.id);for(const c of n.children||[])descendants(c);};if(!edge&&layout.boxes.get(id)?.type==='group')descendants(layout.boxes.get(id));for(const e of data.edges)if(!edge&&(e.from===id||e.to===id)){linked.add(e.from);linked.add(e.to);}for(const [key,el]of buttons){el.style.opacity=linked.has(key)?'1':'.3';el.dataset.active=String(linked.has(key));}for(const p of paths){const on=edge?p.edge.id===edge.id:linked.has(p.edge.from)&&linked.has(p.edge.to);p.el.style.opacity=on?'1':'.2';p.el.style.stroke=on?'var(--viz-series-1)':'';}if(edge||data.groups.some(g=>g.id===id)||layout.boxes.get(id)?.description)showTip(detail(id,edge),edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id));}));\n }\n function showTip(d,anchor){}\n const stable=createMermaidHover(root,clear);function bind(el,id,edge){stable(el,ensure=>choose(id,edge,ensure));}\n\n function labelLocation(edge,w,h,occupied){\n const candidates=[];for(let i=1;i<edge.points.length;i++){const a=edge.points[i-1],b=edge.points[i],length=Math.hypot(b[0]-a[0],b[1]-a[1]);for(const f of [.5,.25,.75])candidates.push({x:a[0]+(b[0]-a[0])*f,y:a[1]+(b[1]-a[1])*f,length});}\n const rects=[...layout.boxes.values()].filter(b=>!['group','space'].includes(b.type)).map(b=>({x:b.x-4,y:b.y-4,w:b.w+8,h:b.h+8})).concat(occupied);\n const overlap=p=>rects.reduce((s,r)=>s+Math.max(0,Math.min(p.x+w/2,r.x+r.w)-Math.max(p.x-w/2,r.x))*Math.max(0,Math.min(p.y+h/2,r.y+r.h)-Math.max(p.y-h/2,r.y)),0);\n candidates.sort((a,b)=>overlap(a)-overlap(b)||b.length-a.length);return candidates[0];\n }\n function finishMotion(){motionEpoch++;for(const t of headTimers)clearTimeout(t);headTimers=[];for(const a of animations)a.cancel();animations=[];for(const p of paths)p.el.style.removeProperty('marker-end');root.dataset.motion='complete';}\n function replay(){finishMotion();if(reduced.matches||controller.expanded)return;root.dataset.motion='playing';const playEpoch=motionEpoch;for(const [i,el]of [...buttons.values()].entries())animations.push(el.animate([{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:240,delay:i*30,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));for(const [i,p]of paths.entries()){const length=p.el.getTotalLength(),delay=120+i*35;if(!p.edge.dotted)animations.push(p.el.animate([{strokeDasharray:length,strokeDashoffset:length},{strokeDasharray:length,strokeDashoffset:0}],{duration:650,delay,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));p.el.style.setProperty('marker-end','none');headTimers.push(setTimeout(()=>{if(playEpoch===motionEpoch)p.el.style.removeProperty('marker-end');},delay+650));}for(const l of labels)animations.push(l.el.animate([{opacity:0},{opacity:1}],{duration:240,delay:180,fill:'backwards'}));Promise.allSettled(animations.map(a=>a.finished)).then(()=>{if(playEpoch===motionEpoch&&root.dataset.motion==='playing')finishMotion();});}\n function draw(){\n clear();finishMotion();buttons=new Map();groupButtons=new Map();labels=[];paths=[];scene.replaceChildren(svg);svg.replaceChildren();\n layout=layoutMermaidBlocks(data,measure,{cell:160,gap:24,padding:24});routes=routeMermaidBlocks(layout,data.edges);scene.style.width=layout.width+'px';scene.style.minWidth=layout.width+'px';scene.style.height=layout.height+'px';svg.setAttribute('viewBox',`0 0 ${layout.width} ${layout.height}`);svg.style.height=layout.height+'px';\n for(const n of [...layout.boxes.values()].filter(n=>n.type==='group'&&n.id!=='root').sort((a,b)=>b.w*b.h-a.w*a.h)){\n const group=document.createElement('div');group.className='block-group';group.dataset.group=n.id;Object.assign(group.style,{left:n.x+'px',top:n.y+'px',width:n.w+'px',height:n.h+'px'});group.style.background=mermaidContainmentSurface(data,n.id);scene.append(group);if(n.name){const label=document.createElement('button');label.type='button';label.className='block-group-label cursor-interaction';label.textContent=n.name;group.append(label);groupButtons.set(n.id,label);bind(label,n.id);}\n }\n const defs=element('defs',{},svg);for(const marker of ['point','circle','cross']){const el=element('marker',{id:root.id+'-block-'+marker,viewBox:'0 0 6 6',refX:5,refY:3,markerWidth:6,markerHeight:6,orient:'auto-start-reverse',markerUnits:'userSpaceOnUse'},defs);if(marker==='point')element('path',{d:'M0 .8L5 3L0 5.2Z',fill:'context-stroke'},el);else if(marker==='circle')element('circle',{cx:3,cy:3,r:2,fill:'var(--background)',stroke:'context-stroke','stroke-width':1},el);else element('path',{d:'M1 1L5 5M5 1L1 5',fill:'none',stroke:'context-stroke','stroke-width':1},el);}\n for(const edge of routes){if(edge.invisible)continue;const el=element('path',{d:rounded(edge.points),class:'wire block-wire','data-edge':edge.id,'stroke-width':edge.thick?2:1},svg);if(edge.dotted)el.setAttribute('stroke-dasharray','4 4');for(const [side,marker]of [['start',edge.startMarker],['end',edge.endMarker]])if(marker)el.setAttribute('marker-'+side,`url(#${root.id}-block-${marker})`);el.style.strokeWidth=edge.thick?'2':'1';paths.push({el,edge});}\n for(const n of data.nodes){const box=layout.boxes.get(n.id),button=document.createElement('button');button.type='button';button.className='node block-node cursor-interaction';button.dataset.node=n.id;button.dataset.shape=n.type;button.setAttribute('aria-label',heading(n.id));Object.assign(button.style,{left:box.x+'px',top:box.y+'px',width:box.w+'px',height:box.h+'px'});shape(box,button);const name=document.createElement('span');name.className='name';name.textContent=n.name;name.style.width=Math.max(24,box.w*inset(n)-12)+'px';button.append(name);scene.append(button);buttons.set(n.id,button);bind(button,n.id);}\n const occupied=[];for(const edge of routes){if(edge.invisible)continue;const el=document.createElement('button');el.type='button';el.className='edge-label block-edge-label cursor-interaction';el.textContent=edge.label;el.dataset.empty=String(!edge.label);el.setAttribute('aria-label',heading(edge.from)+' → '+heading(edge.to)+(edge.label?' · '+edge.label:''));el.dataset.edge=edge.id;scene.append(el);const w=el.offsetWidth,h=el.offsetHeight,p=labelLocation(edge,w,h,occupied);el.style.left=p.x+'px';el.style.top=p.y+'px';occupied.push({x:p.x-w/2-4,y:p.y-h/2-4,w:w+8,h:h+8});labels.push({el,edge});bind(el,edge.from,edge);}\n controller.refresh();if(seen&&!controller.expanded&&!reduced.matches)root.dataset.motion='complete';\n }\n root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});scroll.addEventListener('scroll',()=>{if(selected){const {id,edge}=selected;choose(id,edge);}});root.addEventListener('qv:display-mode',()=>{draw();if(!controller.expanded&&controller.savedInlinePosition)scroll.scrollLeft=controller.savedInlinePosition.left;controller.refresh();});reduced.addEventListener('change',finishMotion);\n let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(root.clientWidth!==width){width=root.clientWidth;draw();}})).observe(root);\n new IntersectionObserver(entries=>{const e=entries[0];if(e.intersectionRatio===0){exited=true;finishMotion();}else if(e.intersectionRatio>=.2&&exited){exited=false;seen=true;replay();}},{threshold:[0,.2]}).observe(root);draw();\n}\n\n// 从原解析器记录声明顺序;不建立另一套 C4 语法或关系图。\nasync function readMermaidC4Model(mermaid,source,descriptions={}) {\n const seed=await mermaid.mermaidAPI.getDiagramFromText('C4Context\\nPerson(__qv_seed__, \"seed\")'),db=seed.db;\n const methods=['addPersonOrSystem','addContainer','addComponent','addPersonOrSystemBoundary','addContainerBoundary','addDeploymentNode','updateElStyle','updateRelStyle'];\n if(methods.some(key=>typeof db[key]!=='function')||!db.getC4ShapeArray||!db.getBoundaries||!db.getRels)return {reason:'C4 数据接口不兼容'};\n const originals=new Map(),order=new Map();let customStyle=false,optionalFeatures=false,sequence=0;\n try {\n for(const key of methods){\n const original=db[key];originals.set(key,original);\n db[key]=function(...args){\n if(key==='updateElStyle'||key==='updateRelStyle')customStyle=true;\n else {const boundary=key==='addPersonOrSystemBoundary'||key==='addContainerBoundary',id=args[boundary?0:1];if(!order.has(id))order.set(id,sequence++);const optionalStart=boundary?3:key==='addPersonOrSystem'?4:5;if(args.slice(optionalStart).some(value=>value&&typeof value!=='object'||value&&typeof value==='object'&&Object.values(value).some(Boolean)))optionalFeatures=true;}\n return original.apply(this,args);\n };\n }\n await mermaid.mermaidAPI.getDiagramFromText(source);\n } finally {for(const [key,original]of originals)db[key]=original;}\n if(customStyle)return {reason:'C4 自定义节点或关系样式保留原生语义'};\n if(optionalFeatures)return {reason:'C4 图标、标签或链接保留原生语义'};\n const text=value=>String(value?.text??value??''),types=new Set(['person','external_person',...['system','container','component'].flatMap(base=>[base,base+'_db',base+'_queue','external_'+base,'external_'+base+'_db','external_'+base+'_queue'])]);\n const family=db.getC4Type(),shapeInRow=db.getC4ShapeInRow(),boundaryInRow=db.getC4BoundaryInRow();\n if(!['C4Context','C4Container','C4Component','C4Dynamic','C4Deployment'].includes(family))return {reason:'C4 图表类型保留原生语义'};\n if(!Number.isInteger(shapeInRow)||shapeInRow<1||!Number.isInteger(boundaryInRow)||boundaryInRow<1)return {reason:'C4 行布局保留原生语义'};\n const rawNodes=db.getC4ShapeArray(),rawGroups=db.getBoundaries().filter(n=>n.alias!=='global'),rawEdges=db.getRels();\n if([...rawNodes,...rawGroups,...rawEdges].some(n=>n.sprite||n.tags||n.link))return {reason:'C4 图标、标签或链接保留原生语义'};\n if([...rawNodes,...rawGroups,...rawEdges].some(n=>['label','techn','descr'].some(key=>hasRichMermaidLabel(text(n[key])))))return {reason:'C4 富文本保留原生语义'};\n const nodes=rawNodes.map(n=>({id:n.alias,name:text(n.label),c4Type:text(n.typeC4Shape),technology:text(n.techn),description:text(n.descr),note:descriptions[n.alias]||'',parent:n.parentBoundary==='global'?null:n.parentBoundary,order:order.get(n.alias)}));\n if(nodes.some(n=>!types.has(n.c4Type)))return {reason:'C4 特殊节点类型保留原生语义'};\n const groups=rawGroups.map(n=>({id:n.alias,name:text(n.label),c4Type:text(n.type),nodeType:n.nodeType||'',description:text(n.descr),note:descriptions[n.alias]||'',parent:n.parentBoundary==='global'?null:n.parentBoundary,order:order.get(n.alias)}));\n const ids=new Set([...nodes,...groups].map(n=>n.id)),groupIds=new Set(groups.map(n=>n.id));\n if(ids.size!==nodes.length+groups.length||[...nodes,...groups].some(n=>n.parent&&!groupIds.has(n.parent)))return {reason:'C4 边界结构保留原生语义'};\n const relationTypes=new Set(['rel','birel','rel_b','rel_u','rel_d','rel_l','rel_r']);\n const nodeIds=new Set(nodes.map(n=>n.id));\n if(rawEdges.some(e=>!relationTypes.has(e.type)||!nodeIds.has(e.from)||!nodeIds.has(e.to)))return {reason:'C4 特殊关系端点保留原生语义'};\n const edges=rawEdges.map((e,i)=>({id:'relation-'+(i+1),from:e.type==='rel_b'?e.to:e.from,to:e.type==='rel_b'?e.from:e.to,sourceFrom:e.from,sourceTo:e.to,label:text(e.label),technology:text(e.techn),description:text(e.descr),relationType:e.type,sequence:family==='C4Dynamic'?i+1:null,startMarker:e.type==='birel'?'point':null,endMarker:'point'}));\n const unknown=Object.keys(descriptions).filter(id=>!ids.has(id));if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n const tree={id:'global',name:'',type:'group',children:[]},byId=new Map([['global',tree],...groups.map(n=>[n.id,{...n,type:'group',children:[]}])]);\n for(const n of [...groups,...nodes].sort((a,b)=>a.order-b.order))byId.get(n.parent||'global').children.push(byId.get(n.id)||{...n,type:'node'});\n const visiting=new Set(),visited=new Set();function check(n){if(visiting.has(n.id))return false;if(visited.has(n.id))return true;visiting.add(n.id);for(const child of n.children||[])if(!check(child))return false;visiting.delete(n.id);visited.add(n.id);return true;}\n if(!check(tree)||visited.size!==ids.size+1)return {reason:'C4 循环边界保留原生语义'};\n return {data:{kind:'c4',family,title:db.getTitle(),nodes,groups,edges,tree,shapeInRow,boundaryInRow,accessibility:{title:db.getAccTitle?.()||'',description:db.getAccDescription?.()||''}}};\n}\n\n// 边界内按原生节点/边界声明顺序换行;外部参与者形成独立阅读列,不新建语义分组。\nfunction layoutMermaidC4(data,measure,{cell=200,gap=40,padding=32}={}) {\n const boxes=new Map();\n function rows(children,limit){const result=[];let row=[],width=0,shapes=0,boundaries=0;for(const child of children){const group=child.type==='group';if(row.length&&(width+gap+child.w>limit||!group&&shapes>=data.shapeInRow||group&&boundaries>=data.boundaryInRow)){result.push(row);row=[];width=0;shapes=0;boundaries=0;}width+=child.w+(row.length?gap:0);row.push(child);if(group)boundaries++;else shapes++;}if(row.length)result.push(row);return result;}\n const rowWidth=row=>row.reduce((s,c)=>s+c.w,0)+gap*Math.max(0,row.length-1),rowsHeight=rows=>rows.reduce((s,row)=>s+Math.max(...row.map(c=>c.h)),0)+gap*Math.max(0,rows.length-1);\n function pack(n){\n if(n.type!=='group')return {...n,w:cell,h:measure(n,cell),captionHeight:0};\n const children=n.children.map(pack),leaves=children.filter(c=>c.type!=='group'),groups=children.filter(c=>c.type==='group');\n const captionHeight=n.id==='global'?0:measure({...n,caption:true},cell*2+gap)+12;\n if(n.id==='global'&&leaves.length&&groups.length){const mainRows=rows(groups,1280),sideHeight=leaves.reduce((s,c)=>s+c.h,0)+gap*Math.max(0,leaves.length-1),mainWidth=Math.max(...mainRows.map(rowWidth));return {...n,children,sideNodes:leaves,rows:mainRows,w:cell+gap+mainWidth+padding*2,h:Math.max(sideHeight,rowsHeight(mainRows))+padding*2,captionHeight};}\n const limit=String(n.c4Type).toLowerCase()==='container'?cell*2+gap:1280;\n const packedRows=rows([...leaves,...groups],limit);\n return {...n,children,rows:packedRows,w:Math.max(cell,...packedRows.map(rowWidth))+padding*2,h:padding*2+captionHeight+rowsHeight(packedRows),captionHeight};\n }\n function place(n,x,y){\n boxes.set(n.id,{...n,x,y});let top=y+padding+n.captionHeight,leftInset=x+padding;\n if(n.sideNodes){let sideY=top;for(const child of n.sideNodes){place(child,leftInset,sideY);sideY+=child.h+gap;}leftInset+=cell+gap;}\n for(const row of n.rows||[]){let left=leftInset;const height=Math.max(...row.map(c=>c.h));for(const child of row){place(child,left,top);left+=child.w+gap;}top+=height+gap;}\n }\n const tree=pack(data.tree);place(tree,24,24);return {boxes,width:tree.w+48,height:tree.h+48};\n}\n\nfunction renderMermaidC4(root,data){\n const scene=root.querySelector('.scene'),scroll=root.querySelector('.diagram-scroll'),tip=root.querySelector('.node-tip'),svg=root.querySelector('.wires'),reduced=matchMedia('(prefers-reduced-motion:reduce)'),ns='http://www.w3.org/2000/svg';\n root.dataset.state='enhanced';root.dataset.renderer='qv-c4';root.querySelector('.edge-key').hidden=true;scroll.setAttribute('aria-label','C4 架构图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} button.node.c4-node{font-size:12px;padding:20px;line-height:18px;background:var(--background);border:1px solid var(--border);align-items:flex-start;justify-content:flex-start;flex-direction:column;transition:opacity 140ms ease}\n #${root.id} button.node.c4-node .name{font-size:14px;display:block;white-space:normal;text-align:left;overflow-wrap:anywhere;font-weight:600;line-height:20px;margin-top:8px}\n #${root.id} .c4-type{display:flex;align-items:center;gap:6px;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font-size:12px;line-height:18px}\n #${root.id} .c4-type svg{width:14px;height:14px;flex:none}\n #${root.id} .c4-info{display:flex;flex-direction:column;gap:12px;width:100%;box-sizing:border-box;padding:12px;margin-top:16px;border-radius:8px;background:light-dark(#f5f5f5,#25272b)}\n #${root.id} .c4-field{display:flex;flex-direction:column;gap:4px;width:100%;text-align:left}\n #${root.id} .c4-field-label{font-size:11px;line-height:16px;color:var(--muted-foreground)}\n #${root.id} .c4-technology{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:18px;overflow-wrap:anywhere;color:light-dark(#187f81,#6ec9c9)}\n #${root.id} .c4-description{font-size:12px;line-height:18px;overflow-wrap:anywhere;color:var(--foreground)}\n #${root.id} .c4-group{position:absolute;border:1px solid var(--border);border-radius:12px;box-sizing:border-box;pointer-events:none}\n #${root.id} .c4-group-label{max-width:calc(100% - 24px);white-space:normal;overflow-wrap:anywhere;position:absolute;left:12px;top:10px;font:inherit;font-size:12px;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));border:0;padding:2px 4px;background:transparent;pointer-events:auto;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .c4-wire{pointer-events:stroke;stroke:light-dark(#727a82,#a3aab3);stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .wires{z-index:1} #${root.id} .c4-group{z-index:0} #${root.id} button.node{z-index:2} #${root.id} .edge-label{z-index:3}\n #${root.id} .c4-edge-label[data-empty=true]{width:32px;height:24px;padding:0;background:transparent}\n #${root.id} .c4-edge-label{font:inherit;font-size:12px;line-height:18px;border:0;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));padding:2px 5px;background:transparent;cursor:var(--cursor-interaction,pointer);pointer-events:auto;white-space:normal;text-align:center;max-width:200px}\n #${root.id} .node-tip{padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);box-shadow:none;border:1px solid var(--border);line-height:18px;max-width:260px;overflow-wrap:anywhere}\n #${root.id} .c4-tip-title{font-size:14px;line-height:20px;font-weight:600;margin-bottom:12px}\n #${root.id} .c4-tip-section{margin-top:12px;display:flex;flex-direction:column;gap:6px}\n #${root.id} .c4-tip-row{display:grid;grid-template-columns:48px minmax(0,1fr);gap:8px}\n #${root.id} .c4-tip-value{color:var(--muted-foreground)}\n #${root.id} button.c4-node[data-active=true]{border-color:var(--viz-series-1);background:var(--background)}\n @media(prefers-reduced-motion:reduce){#${root.id} .c4-node{transition:none}}\n `;root.append(style);\n const controller=createMermaidViewport(root),canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');ctx.font='500 12px '+getComputedStyle(root).fontFamily;\n let layout,routes,labelMetrics=new Map(),buttons=new Map(),groupButtons=new Map(),paths=[],labels=[],selected=null,epoch=0,animations=[],headTimers=[],motionEpoch=0,seen=false,exited=true;\n function element(name,attrs,parent){const el=document.createElementNS(ns,name);for(const [k,v]of Object.entries(attrs))el.setAttribute(k,String(v));parent.append(el);return el;}\n function textHeight(name,width,fontSize=12,lineHeight=18){const previous=ctx.font;ctx.font='500 '+fontSize+'px '+getComputedStyle(root).fontFamily;let count=1,used=0;for(const c of name){if(c==='\\n'){count++;used=0;continue;}const w=ctx.measureText(c).width;if(used+w>width&&used){count++;used=0;}used+=w;}ctx.font=previous;return count*lineHeight;}\n const typeName=n=>{if(n.type==='group')return n.nodeType?'部署节点'+(n.c4Type?' · '+n.c4Type:''):({enterprise:'企业边界',container:'容器边界',system:'系统边界'}[String(n.c4Type).toLowerCase()]||'边界 · '+n.c4Type);const t=n.c4Type||'',base=t.replace('external_','').replace(/_db|_queue/g,'');return (t.startsWith('external_')?'外部 · ':'')+({person:'角色',system:'系统',container:'容器',component:'组件'}[base]||'部署节点')+(t.endsWith('_db')?' · 数据库':t.endsWith('_queue')?' · 队列':'');};\n function measure(n,w){if(n.caption)return textHeight(n.name+' · '+typeName(n),w-24)+32;const probe=document.createElement('button');probe.className='node c4-node';probe.style.cssText=`width:${w}px;height:auto;visibility:hidden`;contents(n,probe);scene.append(probe);const height=probe.offsetHeight;probe.remove();return height;}\n function contents(n,button){shape(n,button);const name=document.createElement('span');name.className='name';name.textContent=n.name;button.append(name);const info=document.createElement('span');info.className='c4-info';for(const [key,caption,cls]of [['technology','技术栈','c4-technology'],['description','职责','c4-description']])if(n[key]){const field=document.createElement('span');field.className='c4-field';const label=document.createElement('span');label.className='c4-field-label';label.textContent=caption;const span=document.createElement('span');span.className=cls;span.textContent=n[key];field.append(label,span);info.append(field);}if(info.childElementCount)button.append(info);}\n function shape(n,parent){const type=document.createElement('span');type.className='c4-type';const icon=document.createElement('i');icon.dataset.lucide=n.c4Type.includes('_db')?'database':n.c4Type.includes('_queue')?'list-tree':n.c4Type.includes('person')?'user-round':n.c4Type.includes('component')?'component':n.c4Type.includes('container')?'box':'server';icon.setAttribute('aria-hidden','true');type.append(icon,document.createTextNode(typeName(n)));parent.append(type);}\n function rounded(points){let d=`M${points[0].join(' ')}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],p=points[i],b=points[i+1],la=Math.hypot(p[0]-a[0],p[1]-a[1]),lb=Math.hypot(b[0]-p[0],b[1]-p[1]),r=Math.min(7,la/2,lb/2);d+=`L${p[0]+(a[0]-p[0])*r/la} ${p[1]+(a[1]-p[1])*r/la}Q${p.join(' ')} ${p[0]+(b[0]-p[0])*r/lb} ${p[1]+(b[1]-p[1])*r/lb}`;}return d+`L${points.at(-1).join(' ')}`;}\n function heading(id){let b=layout.boxes.get(id),names=[];while(b&&b.id!=='global'){if(b.name)names.unshift(b.name);b=layout.boxes.get(b.parent);}return names.join(' › ');}\n function detail(id,edge){\n const n=layout.boxes.get(id),rows=[],connections=[];\n if(edge){rows.push(['来源',heading(edge.from)],['目标',heading(edge.to)]);if(edge.technology)rows.push(['协议',edge.technology]);if(edge.description)rows.push(['说明',edge.description]);return {name:(edge.sequence?edge.sequence+': ':'')+(edge.label||'连接关系'),rows,connections};}\n rows.push(['类型',typeName(n)]);for(const [label,key]of [['技术','technology'],['说明','description'],['备注','note']])if(n[key])rows.push([label,n[key]]);if(heading(n.parent))rows.push(['所属',heading(n.parent)]);\n for(const e of data.edges.filter(e=>e.from===id||e.to===id))connections.push([(e.from===id?'流向':'来自'),heading(e.from===id?e.to:e.from)+(e.label?'\\n'+e.label:'')+(e.technology?' · '+e.technology:'')]);\n if(n.type==='group'){const collect=b=>{for(const c of b.children||[])if(c.type==='group')collect(c);else if(c.type!=='space')connections.push(['成员',c.name]);};collect(n);}\n return {name:n.name,rows,connections};\n }\n function clear(){epoch++;selected=null;tip.hidden=true;for(const el of buttons.values()){el.style.opacity='';delete el.dataset.active;el.removeAttribute('aria-describedby');}for(const p of paths){p.el.style.opacity='';p.el.style.stroke='';}for(const l of labels){l.el.style.opacity='';l.el.removeAttribute('aria-describedby');}for(const el of groupButtons.values())el.removeAttribute('aria-describedby');}\n function choose(id,edge,ensure=false){\n const pending=++epoch;selected={id,edge};if(ensure&&!controller.expanded){const trigger=edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id);if(trigger){const a=trigger.getBoundingClientRect(),v=scroll.getBoundingClientRect();if(a.left<v.left+8)scroll.scrollLeft-=v.left+8-a.left;else if(a.right>v.right-8)scroll.scrollLeft+=a.right-v.right+8;}}\n requestAnimationFrame(()=>requestAnimationFrame(()=>{if(pending!==epoch)return;const linked=new Set(edge?[edge.from,edge.to]:[id]);const descendants=(n)=>{linked.add(n.id);for(const c of n.children||[])descendants(c);};if(!edge&&layout.boxes.get(id)?.type==='group')descendants(layout.boxes.get(id));for(const e of data.edges)if(!edge&&(e.from===id||e.to===id)){linked.add(e.from);linked.add(e.to);}for(const [key,el]of buttons){el.style.opacity=linked.has(key)?'1':'.3';el.dataset.active=String(linked.has(key));}for(const p of paths){const on=edge?p.edge.id===edge.id:linked.has(p.edge.from)&&linked.has(p.edge.to);p.el.style.opacity=on?'1':'.2';p.el.style.stroke=on?'var(--viz-series-1)':'';}if(edge||data.groups.some(g=>g.id===id)||layout.boxes.get(id)?.description)showTip(detail(id,edge),edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id));}));\n }\n function showTip(d,anchor){}\n const stable=createMermaidHover(root,clear);function bind(el,id,edge){stable(el,ensure=>choose(id,edge,ensure));}\n\n const relationText=edge=>(edge.sequence?edge.sequence+': ':'')+edge.label+(edge.technology?' · '+edge.technology:'');\n const relationSize=edge=>labelMetrics.get(edge.id);\n function labelLocation(edge,w,h,occupied=[]){\n const candidates=[];for(let i=1;i<edge.points.length;i++){const a=edge.points[i-1],b=edge.points[i],length=Math.hypot(b[0]-a[0],b[1]-a[1]);for(const f of [.5,.25,.75])for(const offset of [-(a[0]===b[0]?w:h)/2-8,(a[0]===b[0]?w:h)/2+8])candidates.push({x:a[0]+(b[0]-a[0])*f+(a[0]===b[0]?offset:0),y:a[1]+(b[1]-a[1])*f+(a[1]===b[1]?offset:0),length,offset});}\n const rects=[...layout.boxes.values()].filter(b=>!['group','space'].includes(b.type)).map(b=>({x:b.x-4,y:b.y-4,w:b.w+8,h:b.h+8})).concat([...groupButtons].map(([id,el])=>{const b=layout.boxes.get(id);return {x:b.x+el.offsetLeft-2,y:b.y+el.offsetTop-2,w:el.offsetWidth+4,h:el.offsetHeight+4};}),occupied);\n const overlap=p=>p.x-w/2<8||p.x+w/2>layout.width-8||p.y-h/2<8||p.y+h/2>layout.height-8||rects.some(r=>Math.min(p.x+w/2,r.x+r.w)>Math.max(p.x-w/2,r.x)&&Math.min(p.y+h/2,r.y+r.h)>Math.max(p.y-h/2,r.y));\n return candidates.filter(p=>!overlap(p)).sort((a,b)=>Math.abs(a.offset)-Math.abs(b.offset)||b.length-a.length)[0];\n }\n function finishMotion(){motionEpoch++;for(const t of headTimers)clearTimeout(t);headTimers=[];for(const a of animations)a.cancel();animations=[];for(const p of paths)for(const side of ['start','end'])p.el.style.removeProperty('marker-'+side);root.dataset.motion='complete';}\n function replay(){finishMotion();if(reduced.matches||controller.expanded)return;root.dataset.motion='playing';const playEpoch=motionEpoch;for(const [i,el]of [...buttons.values()].entries())animations.push(el.animate([{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:240,delay:i*30,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));for(const [i,p]of paths.entries()){const length=p.el.getTotalLength(),delay=120+i*35;if(!p.edge.dotted)animations.push(p.el.animate([{strokeDasharray:length,strokeDashoffset:length},{strokeDasharray:length,strokeDashoffset:0}],{duration:650,delay,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));for(const side of ['start','end'])p.el.style.setProperty('marker-'+side,'none');headTimers.push(setTimeout(()=>{if(playEpoch===motionEpoch)for(const side of ['start','end'])p.el.style.removeProperty('marker-'+side);},delay+650));}for(const l of labels)animations.push(l.el.animate([{opacity:0},{opacity:1}],{duration:240,delay:180,fill:'backwards'}));Promise.allSettled(animations.map(a=>a.finished)).then(()=>{if(playEpoch===motionEpoch&&root.dataset.motion==='playing')finishMotion();});}\n function draw(){\n clear();finishMotion();buttons=new Map();groupButtons=new Map();labels=[];paths=[];scene.replaceChildren(svg);svg.replaceChildren();\n labelMetrics=new Map();for(const e of data.edges){const el=document.createElement('button');el.className='edge-label c4-edge-label';el.textContent=relationText(e);el.dataset.empty=String(!el.textContent);const w=el.textContent?Math.min(180,Math.ceil(ctx.measureText(el.textContent).width)+12):32;el.style.width=w+'px';el.style.boxSizing='border-box';el.style.visibility='hidden';scene.append(el);labelMetrics.set(e.id,{w:el.offsetWidth,h:el.offsetHeight});el.remove();}\n layout=layoutMermaidC4(data,measure,{cell:280,gap:72,padding:40});scene.style.width=layout.width+'px';scene.style.minWidth=layout.width+'px';scene.style.height=layout.height+'px';svg.setAttribute('viewBox',`0 0 ${layout.width} ${layout.height}`);svg.style.height=layout.height+'px';\n for(const n of [...layout.boxes.values()].filter(n=>n.type==='group'&&n.id!=='global').sort((a,b)=>b.w*b.h-a.w*a.h)){\n const group=document.createElement('div');group.className='c4-group';group.dataset.group=n.id;Object.assign(group.style,{left:n.x+'px',top:n.y+'px',width:n.w+'px',height:n.h+'px'});group.style.background=mermaidContainmentSurface(data,n.id);scene.append(group);if(n.name){const label=document.createElement('button');label.type='button';label.className='c4-group-label cursor-interaction';label.textContent=n.name+' · '+typeName(n);group.append(label);groupButtons.set(n.id,label);bind(label,n.id);}\n }\n for(const n of data.nodes){const box=layout.boxes.get(n.id),button=document.createElement('button');button.type='button';button.className='node c4-node cursor-interaction';button.dataset.node=n.id;button.dataset.shape=n.c4Type;button.setAttribute('aria-label',heading(n.id));Object.assign(button.style,{left:box.x+'px',top:box.y+'px',width:box.w+'px',height:box.h+'px'});contents(box,button);scene.append(button);buttons.set(n.id,button);bind(button,n.id);}\n const reserved=[];routes=routeMermaidBlocks(layout,data.edges,{scoreRoute:e=>{const {w,h}=relationSize(e);return labelLocation(e,w,h,reserved)?0:Infinity;},onRoute:e=>{const {w,h}=relationSize(e),p=labelLocation(e,w,h,reserved);if(!p)throw Error('C4 关系标签没有可用位置:'+e.from+' → '+e.to);e.labelPosition=p;reserved.push({x:p.x-w/2-4,y:p.y-h/2-4,w:w+8,h:h+8});}});\n const defs=element('defs',{},svg);for(const marker of ['point']){const el=element('marker',{id:root.id+'-c4-'+marker,viewBox:'0 0 6 6',refX:5,refY:3,markerWidth:6,markerHeight:6,orient:'auto-start-reverse',markerUnits:'userSpaceOnUse'},defs);if(marker==='point')element('path',{d:'M0 .8L5 3L0 5.2Z',fill:'context-stroke'},el);else if(marker==='circle')element('circle',{cx:3,cy:3,r:2,fill:'var(--background)',stroke:'context-stroke','stroke-width':1},el);else element('path',{d:'M1 1L5 5M5 1L1 5',fill:'none',stroke:'context-stroke','stroke-width':1},el);}\n for(const edge of routes){if(edge.invisible)continue;const el=element('path',{d:rounded(edge.points),class:'wire c4-wire','data-edge':edge.id,'stroke-width':edge.thick?2:1},svg);if(edge.dotted)el.setAttribute('stroke-dasharray','4 4');for(const [side,marker]of [['start',edge.startMarker],['end',edge.endMarker]])if(marker)el.setAttribute('marker-'+side,`url(#${root.id}-c4-${marker})`);el.style.strokeWidth=edge.thick?'2':'1';paths.push({el,edge});}\n const occupied=[];for(const edge of routes){if(edge.invisible)continue;const el=document.createElement('button');el.type='button';el.className='edge-label c4-edge-label cursor-interaction';el.textContent=relationText(edge);el.dataset.empty=String(!el.textContent);el.setAttribute('aria-label',heading(edge.from)+' → '+heading(edge.to)+(edge.label?' · '+edge.label:''));el.dataset.edge=edge.id;scene.append(el);const {w,h}=relationSize(edge);el.style.width=w+'px';el.style.boxSizing='border-box';const p=edge.labelPosition;if(!p)throw Error('C4 关系标签没有可用位置:'+e.from+' → '+e.to);el.style.left=p.x+'px';el.style.top=p.y+'px';occupied.push({x:p.x-w/2-4,y:p.y-h/2-4,w:w+8,h:h+8});labels.push({el,edge});bind(el,edge.from,edge);}\n window.lucide?.createIcons({attrs:{width:14,height:14}});controller.refresh();if(seen&&!controller.expanded&&!reduced.matches)root.dataset.motion='complete';\n }\n root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});scroll.addEventListener('scroll',()=>{if(selected){const {id,edge}=selected;choose(id,edge);}});root.addEventListener('qv:display-mode',()=>{draw();if(!controller.expanded&&controller.savedInlinePosition)scroll.scrollLeft=controller.savedInlinePosition.left;controller.refresh();});reduced.addEventListener('change',finishMotion);\n let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(root.clientWidth!==width){width=root.clientWidth;draw();}})).observe(root);\n new IntersectionObserver(entries=>{const e=entries[0];if(e.intersectionRatio===0){exited=true;finishMotion();}else if(e.intersectionRatio>=.2&&exited){exited=false;seen=true;replay();}},{threshold:[0,.2]}).observe(root);draw();\n}\n\n// 固定版本 DB 是唯一数据来源;不另写 CSV 解析器。\nasync function readMermaidSankeyModel(db,descriptions={}) {\n if(!db.getGraph||!db.getConfig)return {reason:'桑基数据接口不兼容'};\n const graph=structuredClone(db.getGraph()),options=structuredClone(db.getConfig());\n if(!graph.links.length||graph.links.some(l=>!Number.isFinite(l.value)||l.value<=0))return {reason:'桑基非正值保留原生语义'};\n if(graph.nodes.some(n=>hasRichMermaidLabel(n.id)))return {reason:'桑基富文本保留原生语义'};\n if(options.linkColor&&!['gradient','source','target'].includes(options.linkColor))return {reason:'桑基自定义带色保留原生语义'};\n const ids=new Set(graph.nodes.map(n=>n.id)),unknown=Object.keys(descriptions).filter(k=>!ids.has(k));\n if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n const children=new Map(graph.nodes.map(n=>[n.id,[]])),indegree=new Map(graph.nodes.map(n=>[n.id,0]));\n for(const l of graph.links){children.get(l.source).push(l.target);indegree.set(l.target,indegree.get(l.target)+1);}\n const queue=graph.nodes.filter(n=>!indegree.get(n.id)).map(n=>n.id);let count=0;\n while(queue.length){const id=queue.shift();count++;for(const to of children.get(id)){indegree.set(to,indegree.get(to)-1);if(!indegree.get(to))queue.push(to);}}\n if(count!==graph.nodes.length)return {reason:'桑基回路保留原生语义'};\n return {data:{kind:'sankey',nodes:graph.nodes.map(n=>({...n,name:n.id,description:descriptions[n.id]||''})),links:graph.links.map((l,i)=>({...l,id:'flow-'+(i+1)})),alignment:options.nodeAlignment||'justify',linkColor:options.linkColor||'gradient',showValues:options.showValues,prefix:options.prefix||'',suffix:options.suffix||'',title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidSankey(root,data) {\n const d3=window.__mcxQVRuntime.sankey;\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');\n root.dataset.state='enhanced';root.dataset.renderer='qv-sankey';root.classList.add('qv-sankey');root.querySelector('.edge-key').hidden=true;svg.removeAttribute('aria-hidden');viewport.setAttribute('aria-label','桑基图,可横向滚动');\n const title=root.querySelector('h3');if(!title.textContent)title.textContent=data.title;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .wires{pointer-events:auto}\n #${root.id} .sankey-label{position:absolute;display:flex;flex-direction:column;gap:4px;width:176px;padding:8px;border:0;background:transparent;color:var(--foreground);font:inherit;font-size:14px;line-height:20px;text-align:left;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .sankey-label .value{font-size:12px;line-height:18px;color:var(--muted-foreground);font-variant-numeric:tabular-nums}\n #${root.id} .sankey-detail{position:absolute;z-index:10;width:200px;max-width:calc(100% - 8px);box-sizing:border-box;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:18px;border:1px solid var(--border);overflow-wrap:anywhere;box-shadow:0 3px 12px #0002;pointer-events:none}\n #${root.id} .sankey-detail-title{font-size:14px;line-height:20px;font-weight:600;margin-bottom:12px}\n #${root.id} .sankey-detail-rows{display:grid;gap:6px}\n #${root.id} .sankey-detail-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start}\n #${root.id} .sankey-detail-description{margin-top:12px;color:var(--muted-foreground)}\n #${root.id} .sankey-detail-row>span:last-child{font-variant-numeric:tabular-nums;white-space:nowrap;color:light-dark(color-mix(in srgb,var(--popover-foreground) 64%,var(--popover)),color-mix(in srgb,var(--popover-foreground) 68%,var(--popover)))}\n #${root.id} .sankey-flow-list{position:absolute;inset:0;pointer-events:none}\n #${root.id} .sankey-flow-key{position:absolute;min-width:24px;height:24px;border:0;background:transparent;padding:2px 4px;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;opacity:0;pointer-events:none}\n #${root.id} .sankey-flow-key:focus-visible{opacity:1;background:var(--background)}\n #${root.id} .sankey-scroll-cue{margin:8px 0 0;text-align:right;font-size:12px;line-height:18px;color:var(--muted-foreground)}\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',add=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},color=i=>`var(--viz-series-${i%6+1})`,fmt=v=>data.prefix+new Intl.NumberFormat(undefined,{maximumFractionDigits:20}).format(v)+data.suffix;\n const tip=document.createElement('div');tip.className='sankey-detail';tip.setAttribute('role','tooltip');tip.id=root.id+'-sankey-detail';tip.hidden=true;root.append(tip);\n const cue=document.createElement('div');cue.className='sankey-scroll-cue';cue.textContent='横向滚动查看其余阶段 →';root.insertBefore(cue,tip);\n const list=document.createElement('div');list.className='sankey-flow-list';list.setAttribute('aria-label','源数据流量');scene.append(list);\n let graph,labels=[],bands=[],bars=[],keys=[],frame=0,pending=null,epoch=0,clip,mask;const reduced=matchMedia('(prefers-reduced-motion:reduce)'),view=createMermaidViewport(root);\n function clear(){epoch++;tip.hidden=true;root.style.paddingBottom='';tip.style.position='absolute';tip.style.marginTop='0';[...labels,...keys].forEach(b=>b.removeAttribute('aria-describedby'));bands.forEach(p=>p.style.opacity='1');bars.forEach(p=>p.style.opacity='1');}\n function emphasize(kind,index){const node=graph.nodes[index],link=graph.links[index];bands.forEach((p,i)=>p.style.opacity=kind==='link'?(i===index?'1':'.12'):(graph.links[i].source===node||graph.links[i].target===node?'1':'.12'));bars.forEach((p,i)=>p.style.opacity=kind==='node'?(i===index||graph.links.some(l=>(l.source===node&&l.target.index===i)||(l.target===node&&l.source.index===i))?'1':'.24'):(i===link.source.index||i===link.target.index?'1':'.24'));}\n function show(kind,index,event,anchor){clear();emphasize(kind,index);const n=graph.nodes[index],l=graph.links[index],rows=kind==='link'?[['流量',fmt(l.value)],['占来源流出',(l.value/l.source.sourceLinks.reduce((s,l)=>s+l.value,0)*100).toFixed(2)+'%'],['占目标流入',(l.value/l.target.targetLinks.reduce((s,l)=>s+l.value,0)*100).toFixed(2)+'%']]:[['流入',fmt(n.targetLinks.reduce((s,l)=>s+l.value,0))],['流出',fmt(n.sourceLinks.reduce((s,l)=>s+l.value,0))]];const heading=document.createElement('div');heading.className='sankey-detail-title';heading.textContent=kind==='link'?l.source.name+' → '+l.target.name:n.name;tip.replaceChildren(heading);const body=document.createElement('div');body.className='sankey-detail-rows';tip.append(body);for(const [a,v] of rows){const row=document.createElement('div');row.className='sankey-detail-row';const text=document.createElement('span'),value=document.createElement('span');text.textContent=a;value.textContent=v;row.append(text,value);body.append(row);}if(kind==='node'&&n.description){const text=document.createElement('div');text.className='sankey-detail-description';text.textContent=n.description;tip.append(text);}const button=kind==='node'?labels[index]:keys[index];button?.setAttribute('aria-describedby',tip.id);tip.hidden=false;\n placeTip(kind,index,anchor||button,event);}\n\n // 按实际曲线与带宽避让,而不是用整条曲线的包围盒近似。\n function placeTip(kind,index,anchor,event){\n const rr=root.getBoundingClientRect(),ar=anchor.getBoundingClientRect(),sr=svg.getBoundingClientRect(),scale=sr.width/svg.viewBox.baseVal.width;\n tip.style.width=Math.min(200,root.clientWidth-16)+'px';const w=tip.offsetWidth,h=tip.offsetHeight,pointer=event?.type?.startsWith('pointer'),x=(pointer?event.clientX:ar.left)-rr.left,y=(pointer?event.clientY:ar.top)-rr.top,aw=pointer?0:ar.width,ah=pointer?0:ar.height;\n const segments=[];let radius=0;\n bands.forEach((path,i)=>{const link=graph.links[i],selected=kind==='link'?i===index:link.source===graph.nodes[index]||link.target===graph.nodes[index];if(!selected)return;const r=link.width*scale/2+2;radius=Math.max(radius,r);if(!path.flowSamples){const length=path.getTotalLength(),steps=Math.max(2,Math.ceil(length/4));path.flowSamples=Array.from({length:steps+1},(_,j)=>path.getPointAtLength(length*j/steps));}let previous;for(const p of path.flowSamples){const point={x:sr.left-rr.left+p.x*scale,y:sr.top-rr.top+p.y*scale};if(previous)segments.push({a:previous,b:point,r});previous=point;}});\n const crosses=(a,b,box)=>{let lo=0,hi=1;for(const [start,delta,min,max]of [[a.x,b.x-a.x,box.x,box.x+box.w],[a.y,b.y-a.y,box.y,box.y+box.h]]){if(Math.abs(delta)<1e-9){if(start<min||start>max)return false;}else{const p=(min-start)/delta,q=(max-start)/delta;lo=Math.max(lo,Math.min(p,q));hi=Math.min(hi,Math.max(p,q));if(lo>hi)return false;}}return true;};\n const candidates=[],add=(left,top)=>{const c={x:Math.max(4,Math.min(root.clientWidth-w-4,left)),y:Math.max(4,Math.min(root.clientHeight-h-4,top))};c.crossings=segments.reduce((sum,s)=>sum+(crosses(s.a,s.b,{x:c.x-s.r,y:c.y-s.r,w:w+s.r*2,h:h+s.r*2})?Math.hypot(s.b.x-s.a.x,s.b.y-s.a.y):0),0);c.distance=Math.hypot(Math.max(x-c.x-w,c.x-x-aw,0),Math.max(y-c.y-h,c.y-y-ah,0));candidates.push(c);};\n for(const gap of [12,20,28,Math.ceil(radius+12)])for(const [left,top]of [[x+aw+gap,y+(ah-h)/2],[x-w-gap,y+(ah-h)/2],[x+(aw-w)/2,y-h-gap],[x+(aw-w)/2,y+ah+gap],[x+aw+gap,y-h-gap],[x-w-gap,y-h-gap],[x+aw+gap,y+ah+gap],[x-w-gap,y+ah+gap]])add(left,top);\n // 图的上下留白也是浮层候选,不改变图表高度或追加详情面板。\n add(x+(aw-w)/2,4);add(x+(aw-w)/2,root.clientHeight-h-4);\n candidates.sort((a,b)=>a.crossings-b.crossings||a.distance-b.distance);const best=candidates[0];tip.style.left=best.x+'px';tip.style.top=best.y+'px';tip.dataset.flowOverlap=String(best.crossings);tip.dataset.placement='flow-clear';\n }\n\n function focus(kind,index){pending={kind,index};const b=kind==='node'?labels[index]:keys[index],br=b.getBoundingClientRect(),vr=viewport.getBoundingClientRect();if(!view.expanded){if(br.left<vr.left+8)viewport.scrollLeft-=vr.left+8-br.left;else if(br.right>vr.right-8)viewport.scrollLeft+=br.right-vr.right+8;}const ticket=++epoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(ticket===epoch){pending=null;show(kind,index,null,b);}}));}\n function cancel(){cancelAnimationFrame(frame);frame=0;if(mask)mask.setAttribute('width',scene.offsetWidth);}\n function draw(){cancel();clear();svg.replaceChildren();labels.forEach(b=>b.remove());labels=[];list.replaceChildren();keys=[];bands=[];bars=[];\n const measure=document.createElement('button');measure.className='sankey-label';measure.style.visibility='hidden';scene.append(measure);let labelHeight=0;for(const n of data.nodes){measure.textContent=n.name;if(data.showValues){const v=document.createElement('span');v.className='value';v.textContent=fmt(data.links.filter(l=>l.source===n.id||l.target===n.id).reduce((a,l)=>a+l.value,0));measure.append(v);}labelHeight=Math.max(labelHeight,measure.offsetHeight);}measure.remove();\n const depth=new Map(data.nodes.map(n=>[n.id,0]));for(let k=0;k<data.nodes.length;k++)for(const l of data.links)depth.set(l.target,Math.max(depth.get(l.target),depth.get(l.source)+1));const columns=Math.max(...depth.values())+1,width=Math.max(viewport.clientWidth,view.expanded?(columns===2?560:736+Math.max(0,columns-3)*220):(columns===2?736:384+(columns-1)*340)),count=Math.max(...Array.from({length:columns},(_,i)=>data.nodes.filter(n=>depth.get(n.id)===i).length)),height=Math.max(420,count*160),padding=Math.max(72,labelHeight+20),top=Math.max(72,labelHeight+20),bottom=labelHeight/2+20;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';svg.setAttribute('role','img');svg.setAttribute('aria-label','桑基图:'+data.links.map(l=>l.source+' → '+l.target+' '+fmt(l.value)).join(';'));\n const align={left:d3.sankeyLeft,right:d3.sankeyRight,center:d3.sankeyCenter,justify:d3.sankeyJustify};graph=d3.sankey().nodeId(n=>n.id).nodeWidth(10).nodePadding(padding).nodeAlign(align[data.alignment]||d3.sankeyJustify).nodeSort(null).extent([[184,top],[width-200,height-bottom]])({nodes:data.nodes.map(n=>({...n})),links:data.links.map(l=>({...l}))});\n const defs=add('defs',{}),id=root.id+'-sankey-mask';clip=add('clipPath',{id},defs);mask=add('rect',{x:0,y:0,width,height},clip);const layer=add('g',{'clip-path':`url(#${id})`});\n graph.links.forEach((l,i)=>{let stroke=color(l.source.index);if(data.linkColor==='target')stroke=color(l.target.index);else if(data.linkColor==='gradient'){const g=add('linearGradient',{id:root.id+'-flow-'+i,gradientUnits:'userSpaceOnUse',x1:l.source.x1,x2:l.target.x0,y1:0,y2:0},defs);add('stop',{offset:0,'stop-color':color(l.source.index)},g);add('stop',{offset:1,'stop-color':color(l.target.index)},g);stroke=`url(#${root.id}-flow-${i})`;}const path=add('path',{d:d3.sankeyLinkHorizontal()(l),fill:'none',stroke,'stroke-opacity':'.22','stroke-width':l.width,'data-flow':l.id,'data-value':l.value},layer);bands.push(path);const hit=add('path',{d:path.getAttribute('d'),fill:'none',stroke:'transparent','stroke-width':Math.max(12,l.width),'data-flow-hit':l.id},layer);hit.onpointerenter=hit.onpointermove=hit.onclick=e=>{if(!frame)show('link',i,e,hit);};hit.onpointerleave=clear;const key=document.createElement('button');key.type='button';key.className='sankey-flow-key';key.textContent=fmt(l.value);key.setAttribute('aria-label',l.source.name+' → '+l.target.name+','+fmt(l.value));const mid=path.getPointAtLength(path.getTotalLength()/2);key.style.left=(mid.x-12)+'px';key.style.top=(mid.y-12)+'px';list.append(key);keys.push(key);key.onpointerenter=key.onclick=e=>show('link',i,e);key.onfocus=()=>focus('link',i);key.onpointerleave=clear;key.onblur=()=>{pending=null;clear();};});\n graph.nodes.forEach((n,i)=>{const bar=add('rect',{x:n.x0,y:n.y0,width:n.x1-n.x0,height:n.y1-n.y0,fill:color(i),'fill-opacity':'.7','data-sankey-node':n.id},layer);bars.push(bar);bar.onpointerenter=bar.onclick=e=>show('node',i,e,bar);bar.onpointerleave=clear;const b=document.createElement('button');b.type='button';b.className='sankey-label';b.dataset.sankeyLabel=n.id;const name=document.createElement('span');name.textContent=n.name;b.append(name);if(data.showValues){const value=document.createElement('span');value.className='value';value.textContent=fmt(n.value);b.append(value);}b.setAttribute('aria-label',n.name+',流入 '+fmt(n.targetLinks.reduce((s,l)=>s+l.value,0))+',流出 '+fmt(n.sourceLinks.reduce((s,l)=>s+l.value,0)));scene.append(b);if(n.layer===0){b.style.left=(n.x0-184)+'px';b.style.top=((n.y0+n.y1)/2-b.offsetHeight/2)+'px';b.style.textAlign='right';}else if(n.layer===columns-1){b.style.left=(n.x1+8)+'px';b.style.top=((n.y0+n.y1)/2-b.offsetHeight/2)+'px';}else{b.style.left=(n.x0-83)+'px';b.style.top=(n.y0-b.offsetHeight-10)+'px';b.style.textAlign='center';}labels.push(b);b.onpointerenter=b.onclick=e=>show('node',i,e);b.onfocus=()=>focus('node',i);b.onpointerleave=clear;b.onblur=()=>{pending=null;clear();};});scene.append(list);cue.hidden=view.expanded?viewport.clientWidth>=560:width<=viewport.clientWidth+1;cue.textContent=view.expanded?'整体概览 · 选择节点或流带查看明细;退出放大后可横向阅读完整文字':'横向滚动查看其余阶段 →';view.refresh();}\n function replay(){cancel();if(reduced.matches||view.expanded)return;mask.setAttribute('width',0);const start=performance.now();function step(now){const t=Math.min(1,(now-start)/1000);mask.setAttribute('width',scene.offsetWidth*(1-(1-t)**4));frame=t<1?requestAnimationFrame(step):0;}frame=requestAnimationFrame(step);}\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(width!==root.clientWidth){width=root.clientWidth;draw();}})).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting){armed=true;cancel();clear();}else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else if(armed){const r=root.getBoundingClientRect();if(r.bottom>0&&r.top<innerHeight){armed=false;replay();}}});reduced.addEventListener('change',cancel);viewport.addEventListener('scroll',()=>{if(pending){const p=pending;focus(p.kind,p.index);}else clear();},{passive:true});document.addEventListener('keydown',e=>{if(e.key==='Escape'){pending=null;clear();}});window.addEventListener('blur',clear);\n}\n\n// 解析层保留维度顺序、曲线名称、量表与网格选项。\nasync function readMermaidRadarModel(db,descriptions={}) {\n if(!db.getAxes||!db.getCurves||!db.getOptions)return {reason:'Radar 数据接口不兼容'};\n const axes=structuredClone(db.getAxes()),curves=structuredClone(db.getCurves()),options=structuredClone(db.getOptions());\n if(axes.length<3||!curves.length||curves.some(c=>c.entries.length!==axes.length||c.entries.some(v=>!Number.isFinite(v))))return {reason:'雷达维度或数值保留原生语义'};\n if([...axes,...curves].some(d=>hasRichMermaidLabel(d.label))||Object.keys(descriptions).length)return {reason:'雷达富文本与额外说明保留原生语义'};\n const min=options.min??0,max=options.max??Math.max(...curves.flatMap(c=>c.entries));\n if(!Number.isFinite(min)||!Number.isFinite(max)||max<=min||curves.some(c=>c.entries.some(v=>v<min||v>max)))return {reason:'雷达量表边界保留原生语义'};\n return {data:{kind:'radar',axes:axes.map(d=>({id:d.name,name:d.label})),series:curves.map(d=>({id:d.name,name:d.label,values:d.entries})),min,max,ticks:options.ticks,graticule:options.graticule,showLegend:options.showLegend,title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidRadar(root,data) {\n if(data.min===0&&data.graticule==='polygon'&&data.ticks===5&&data.showLegend&&data.axes.length<=8&&data.series.length<=4&&data.axes.every(a=>a.name.length<=12)){\n root.dataset.renderer='qv-radar';root.dataset.preset='radar-chart';\n await renderMermaidRadarPreset(root,{title:root.querySelector('h3').textContent||data.title,dimensions:data.axes.map(a=>a.name),series:data.series.map(s=>({name:s.name,values:s.values})),maxValue:data.max,valuePrefix:'',valueSuffix:'',decimals:2},data.series.map(s=>s.name+':'+s.values.join(',')).join(';'));root.dataset.state='enhanced';return;\n }\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');root.dataset.state='enhanced';root.dataset.renderer='qv-radar';root.classList.add('qv-radar');viewport.setAttribute('aria-label','雷达图,可横向滚动');root.querySelector('.edge-key').hidden=true;svg.removeAttribute('aria-hidden');\n const title=root.querySelector('h3');if(!title.textContent)title.textContent=data.title;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .wires{pointer-events:auto}\n #${root.id} .radar-axis-label{position:absolute;border:0;background:transparent;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;text-align:center;padding:8px;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .radar-legend{display:flex;justify-content:center;flex-wrap:wrap;gap:8px 22px;margin-top:20px}\n #${root.id} .radar-series-key{display:flex;align-items:center;gap:7px;border:0;background:transparent;color:var(--foreground);font:inherit;font-size:12px;line-height:20px;padding:4px 0;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .radar-swatch{width:10px;height:10px;background:currentColor}\n #${root.id} .radar-detail{position:absolute;z-index:10;display:grid;gap:12px;width:188px;box-sizing:border-box;max-width:min(260px,calc(100% - 16px));padding:12px 14px;border:1px solid var(--border);border-radius:12px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:18px;box-shadow:0 6px 18px color-mix(in srgb,var(--foreground) 10%,transparent);pointer-events:none}\n #${root.id} .radar-detail[hidden]{display:none}\n #${root.id} .radar-detail-title{display:flex;align-items:center;gap:8px;font-size:14px;line-height:20px;font-weight:600;overflow-wrap:anywhere}\n #${root.id} .radar-detail-rows{display:grid;gap:4px}\n #${root.id} .radar-detail-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:6px}\n #${root.id} .radar-detail[data-mode=\"series\"] .radar-detail-row{grid-template-columns:minmax(0,1fr) auto}\n #${root.id} .radar-detail-name{color:var(--popover-foreground);overflow-wrap:anywhere}\n #${root.id} .radar-detail[data-mode=\"series\"] .radar-detail-name{color:var(--popover-foreground);font-weight:500}\n #${root.id} .radar-detail-value{color:light-dark(color-mix(in srgb,var(--popover-foreground) 64%,var(--popover)),color-mix(in srgb,var(--popover-foreground) 68%,var(--popover)));font-variant-numeric:tabular-nums;white-space:nowrap}\n #${root.id} .radar-detail .radar-swatch{flex-shrink:0}\n\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',add=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},color=i=>`var(--viz-series-${i%6+1})`,fmt=v=>new Intl.NumberFormat(undefined,{maximumFractionDigits:2}).format(v);\n const legend=document.createElement('div');legend.className='radar-legend';legend.hidden=!data.showLegend;root.append(legend);const tip=document.createElement('div');tip.className='radar-detail';tip.setAttribute('role','tooltip');tip.id=root.id+'-radar-detail';tip.hidden=true;root.append(tip);\n let groups=[],geometry,frame=0,initialized=false,pendingFocus=null;const reduced=matchMedia('(prefers-reduced-motion:reduce)'),view=createMermaidViewport(root);\n const keys=data.series.map((s,i)=>{const b=document.createElement('button');b.type='button';b.className='radar-series-key';const sw=document.createElement('span');sw.className='radar-swatch';sw.style.color=color(i);sw.style.borderRadius=i%3===0?'50%':'0';if(i%3===2)sw.style.transform='rotate(45deg) scale(.8)';sw.setAttribute('aria-hidden','true');b.append(sw,document.createTextNode(s.name));legend.append(b);b.onpointerenter=b.onclick=b.onfocus=()=>{clear();highlight(i);};b.onpointerleave=b.onblur=clear;return b;});\n let labels=[],detailEpoch=0;\n function openAxis(i,event){pendingFocus=i;const b=labels[i],r=b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(!view.expanded){if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;}const epoch=++detailEpoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(epoch===detailEpoch){pendingFocus=null;show('axis',i,event?.type==='focus'?null:event);}}));}\n function clear(){++detailEpoch;tip.hidden=true;tip.style.position='absolute';tip.style.marginTop='0';[...keys,...labels].forEach(b=>b.removeAttribute('aria-describedby'));groups.forEach(g=>{g.layer.style.opacity='1';g.shape.style.strokeOpacity='';g.shape.style.fillOpacity='';g.markers.forEach(m=>m.style.opacity='');});}\n function highlight(index){groups.forEach((g,j)=>{g.shape.style.strokeOpacity=j===index?'1':'.1';g.shape.style.fillOpacity=j===index?'.035':'.003';g.markers.forEach(m=>m.style.opacity=j===index?'1':'.1');});}\n function seriesSwatch(index){return keys[index].querySelector('.radar-swatch').cloneNode(true);}\n function show(kind,index,event){clear();const objectMode=kind==='series',rows=objectMode?data.axes.map((a,j)=>({name:a.name,value:data.series[index].values[j]})):data.series.map((s,j)=>({name:s.name,value:s.values[index],series:j}));tip.dataset.mode=kind;const header=document.createElement('div');header.className='radar-detail-title';header.textContent=objectMode?data.series[index].name:data.axes[index].name;if(objectMode)header.prepend(seriesSwatch(index));tip.replaceChildren(header);const list=document.createElement('div');list.className='radar-detail-rows';for(const d of rows){const row=document.createElement('div');row.className='radar-detail-row';const name=document.createElement('span');name.className='radar-detail-name';name.textContent=d.name;const value=document.createElement('span');value.className='radar-detail-value';value.textContent=fmt(d.value);if(!objectMode)row.append(seriesSwatch(d.series));row.append(name,value);list.append(row);}tip.append(list);if(objectMode)highlight(index);const b=(objectMode?keys:labels)[index];b?.setAttribute('aria-describedby',tip.id);tip.hidden=false;placeMermaidDetail(root,tip,b,event,188);}\n\n\n function cancel(){cancelAnimationFrame(frame);frame=0;groups.forEach(g=>{g.shape.setAttribute('points',g.points.map(p=>p.join(',')).join(' '));g.markers.forEach((m,j)=>m.setAttribute('transform',`translate(${g.points[j][0]} ${g.points[j][1]})`));});}\n function draw(){cancel();clear();svg.replaceChildren();labels.forEach(b=>b.remove());labels=[];const dense=data.axes.length>6||data.axes.some(a=>a.name.length>8),width=Math.max(viewport.clientWidth,dense?640:560),height=dense?480:450,cx=width/2,cy=dense?240:220,radius=dense?150:142;geometry={cx,cy,radius};scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';svg.setAttribute('role','img');svg.setAttribute('aria-label',`${data.title},量表${data.min}至${data.max}。`+data.series.map(s=>s.name+':'+s.values.map((v,j)=>data.axes[j].name+' '+v).join(',')).join(';'));\n const point=(i,r)=>{const a=-Math.PI/2+i*Math.PI*2/data.axes.length;return[cx+Math.cos(a)*r,cy+Math.sin(a)*r];},outline=r=>data.axes.map((a,i)=>point(i,r).join(',')).join(' ');\n for(let k=1;k<=data.ticks;k++){const r=radius*k/data.ticks;add(data.graticule==='circle'?'circle':'polygon',data.graticule==='circle'?{cx,cy,r,fill:'none',stroke:'var(--border)'}:{points:outline(r),fill:'none',stroke:'var(--border)'});}\n data.axes.forEach((a,i)=>{const p=point(i,radius);add('line',{x1:cx,y1:cy,x2:p[0],y2:p[1],stroke:'var(--border)'});const lp=point(i,radius+(dense?56:34));const b=document.createElement('button');b.className='radar-axis-label';b.type='button';b.textContent=a.name;const labelWidth=dense?168:128;b.style.width=labelWidth+'px';scene.append(b);b.style.left=(lp[0]-labelWidth/2)+'px';b.style.top=(lp[1]-b.offsetHeight/2)+'px';b.onpointerenter=b.onclick=e=>show('axis',i,e);b.onfocus=e=>openAxis(i,e);b.onpointerleave=clear;b.onblur=()=>{pendingFocus=null;clear();};labels.push(b);});\n groups=data.series.map((s,i)=>{const layer=add('g',{'data-series':s.id}),points=s.values.map((v,j)=>point(j,(v-data.min)/(data.max-data.min)*radius)),shape=add('polygon',{points:points.map(p=>p.join(',')).join(' '),fill:color(i),'fill-opacity':'.035',stroke:color(i),'stroke-width':1.7,'stroke-linejoin':'round'},layer);if(i%3)shape.setAttribute('stroke-dasharray',i%3===1?'5 3':'2 3');shape.onpointerenter=shape.onpointermove=e=>show('series',i,e);shape.onpointerleave=clear;const markers=points.map((p,j)=>{const m=add(i%3===0?'circle':i%3===1?'rect':'path',i%3===0?{cx:0,cy:0,r:3,fill:color(i)}:i%3===1?{x:-3,y:-3,width:6,height:6,fill:color(i)}:{d:'M0 -4L4 0L0 4L-4 0Z',fill:color(i)},layer);m.setAttribute('transform',`translate(${p[0]} ${p[1]})`);m.onpointerenter=m.onpointermove=e=>show('series',i,e);m.onpointerleave=clear;return m;});return{layer,shape,points,markers};});geometry.axes=data.axes.map((a,i)=>point(i,radius));view.refresh();if(!initialized&&!view.expanded)viewport.scrollLeft=Math.max(0,(width-viewport.clientWidth)/2);initialized=true;}\n function replay(){cancel();if(reduced.matches||view.expanded)return;const {cx,cy}=geometry,start=performance.now();function step(now){const t=Math.min(1,(now-start)/900),k=1-(1-t)**4;groups.forEach(g=>{const points=g.points.map(p=>[cx+(p[0]-cx)*k,cy+(p[1]-cy)*k]);g.shape.setAttribute('points',points.map(p=>p.join(',')).join(' '));g.markers.forEach((m,j)=>m.setAttribute('transform',`translate(${points[j][0]} ${points[j][1]})`));});frame=t<1?requestAnimationFrame(step):0;}frame=requestAnimationFrame(step);}\n\n const segmentDistance=(p,a,b)=>{const dx=b[0]-a[0],dy=b[1]-a[1],t=Math.max(0,Math.min(1,((p[0]-a[0])*dx+(p[1]-a[1])*dy)/(dx*dx+dy*dy||1)));return Math.hypot(p[0]-a[0]-t*dx,p[1]-a[1]-t*dy);};\n function inspect(event){if(frame||!geometry)return;const r=svg.getBoundingClientRect(),scale=svg.viewBox.baseVal.width/r.width,p=[(event.clientX-r.left)*scale,(event.clientY-r.top)*scale],center=[geometry.cx,geometry.cy];const points=groups.flatMap(g=>g.points.map((q,j)=>({index:groups.indexOf(g),distance:Math.hypot(p[0]-q[0],p[1]-q[1])}))).sort((a,b)=>a.distance-b.distance);if(points[0]?.distance<=12*scale){show('series',points[0].index,event);return;}if(Math.hypot(p[0]-center[0],p[1]-center[1])<14*scale){clear();return;}const axes=geometry.axes.map((q,i)=>({index:i,distance:segmentDistance(p,center,q)})).sort((a,b)=>a.distance-b.distance);if(axes[0]?.distance<=8*scale)show('axis',axes[0].index,event);}\n svg.addEventListener('pointermove',inspect);svg.addEventListener('click',inspect);svg.addEventListener('pointerleave',clear);\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(width!==root.clientWidth){width=root.clientWidth;draw();}})).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting){armed=true;cancel();clear();}else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else if(armed){const r=root.getBoundingClientRect();if(r.bottom>0&&r.top<innerHeight){armed=false;replay();}}});reduced.addEventListener('change',cancel);viewport.addEventListener('scroll',()=>{if(pendingFocus!==null){const i=pendingFocus;openAxis(i,null);}else clear();},{passive:true});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!tip.hidden){pendingFocus=null;clear();}});window.addEventListener('blur',clear);\n}\n\n// 使用真实坐标值与解析后域,不从源码猜测点序列。\nasync function readMermaidXYModel(db,descriptions={}) {\n if(!db.getXYChartData||!db.getChartConfig)return {reason:'XY 数据接口不兼容'};\n const raw=structuredClone(db.getXYChartData()),config=db.getChartConfig(),counts={bar:0,line:0};\n const plots=raw.plots.map((p,i)=>({id:'series-'+(i+1),type:p.type,name:(p.type==='bar'?'柱形':'折线')+(++counts[p.type]),points:p.data.map(([x,y],index)=>({x,y,index}))}));\n if(!plots.length||plots.some(p=>!['bar','line'].includes(p.type)||p.points.some(d=>!Number.isFinite(d.y))))return {reason:'XY 系列数据保留原生语义'};\n if(hasRichMermaidLabel(raw.xAxis.title)||hasRichMermaidLabel(raw.yAxis.title)||raw.xAxis.categories?.some(hasRichMermaidLabel))return {reason:'富文本坐标轴保留原生语义'};\n if(Object.keys(descriptions).length)return {reason:'XY 额外说明保留原生语义'};\n return {data:{kind:'xy',plots,xAxis:raw.xAxis,yAxis:raw.yAxis,horizontal:config.chartOrientation==='horizontal',title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidXY(root,data) {\n const d3=window.__mcxQVRuntime.d3,scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';svg.removeAttribute('aria-hidden');root.classList.add('qv-xy');root.dataset.state='enhanced';root.dataset.renderer='qv-xy';root.querySelector('.edge-key').hidden=true;\n const style=document.createElement('style');style.textContent=`#${root.id} .wires{pointer-events:auto}#${root.id} .xy-label{position:absolute;border:0;background:transparent;padding:4px;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}#${root.id} .xy-tip{position:absolute;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);box-sizing:border-box;font-size:12px;line-height:18px;pointer-events:none;z-index:10;box-shadow:0 3px 12px #0002}#${root.id} .xy-tip-title{font-size:14px;line-height:20px;font-weight:600;overflow-wrap:anywhere;padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--border)}#${root.id} .xy-tip-row{display:grid;grid-template-columns:8px minmax(0,1fr) auto;gap:8px;align-items:center;margin-top:6px}#${root.id} .xy-tip-name{color:var(--popover-foreground);overflow-wrap:anywhere}#${root.id} .xy-tip-value{color:var(--muted-foreground);font-variant-numeric:tabular-nums;font-weight:500}#${root.id} .xy-key{display:flex;flex-wrap:wrap;gap:18px;margin-top:20px;justify-content:center;font-size:12px}#${root.id} .xy-key span{display:flex;gap:6px;align-items:center}`;root.append(style);\n const el=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},text=(value,attrs)=>{const n=el('text',{fill:'var(--foreground)','font-size':12,...attrs});n.textContent=value;return n;},color=i=>`var(--viz-series-${i%6+1})`;\n const reference=document.createElement('div');reference.style.cssText='position:absolute;left:0;width:64px;background:var(--background);pointer-events:none;z-index:2;font-size:12px';root.append(reference);const caption=document.createElement('div');caption.className='axis-title';caption.dataset.axis='x';caption.style.cssText='text-align:center;font-size:12px;margin-top:12px;color:var(--foreground)';caption.textContent=data.horizontal?data.yAxis.title:data.xAxis.title;root.append(caption);\n const tip=document.createElement('div');tip.className='xy-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);const view=createMermaidViewport(root),reduced=matchMedia('(prefers-reduced-motion:reduce)');let buttons=[],motions=[],animations=[],state;\n const categories=data.xAxis.type==='band'?data.xAxis.categories:[...new Set(data.plots.flatMap(p=>p.points.map(d=>d.x)))];\n function clear(){tip.hidden=true;root.querySelectorAll('[data-guide]').forEach(n=>n.remove());root.querySelectorAll('[data-category]').forEach(n=>n.style.opacity='');}\n // 以当前分类的真实柱子和点为避让对象;侧向放置并缩窄浮层,禁止盖住选中标记。\n function placeTip(index,event){\n const rr=root.getBoundingClientRect(),vr=viewport.getBoundingClientRect(),marks=[...svg.querySelectorAll(`[data-category=\"${index}\"]`)].map(n=>n.getBoundingClientRect()).filter(b=>b.right>=vr.left&&b.left<=vr.right&&b.bottom>=vr.top&&b.top<=vr.bottom);\n const fallback=buttons[index].getBoundingClientRect(),left=Math.max(vr.left,marks.length?Math.min(...marks.map(b=>b.left)):fallback.left)-rr.left,right=Math.min(vr.right,marks.length?Math.max(...marks.map(b=>b.right)):fallback.right)-rr.left,top=Math.max(vr.top,marks.length?Math.min(...marks.map(b=>b.top)):fallback.top)-rr.top,bottom=Math.min(vr.bottom,marks.length?Math.max(...marks.map(b=>b.bottom)):fallback.bottom)-rr.top;\n const gap=12,spaces={left:Math.max(0,left-gap-4),right:Math.max(0,root.clientWidth-right-gap-4)},side=spaces.right>=Math.min(240,spaces.left)?'right':'left',width=Math.min(240,spaces[side]);\n tip.style.width=width+'px';tip.style.maxWidth=width+'px';tip.style.overflowWrap='anywhere';\n const pointer=event?.type?.startsWith('pointer'),y=pointer?Math.max(top,Math.min(bottom,event.clientY-rr.top)):(top+bottom)/2,h=tip.offsetHeight;\n tip.style.left=(side==='right'?right+gap:left-gap-width)+'px';tip.style.top=Math.max(4,Math.min(root.clientHeight-h-4,y-h/2))+'px';tip.dataset.placement=side;\n }\n function show(index,event){clear();const value=categories[index];root.querySelectorAll('[data-category]').forEach(n=>n.style.opacity=+n.dataset.category===index?'1':'.18');tip.replaceChildren();const name=document.createElement('div');name.className='xy-tip-title';name.textContent=value;tip.append(name);for(const [j,p]of data.plots.entries()){const d=data.xAxis.type==='band'?p.points[index]:p.points.find(d=>d.x===value);if(!d)continue;const row=document.createElement('div');row.className='xy-tip-row';const swatch=document.createElement('span');swatch.style.cssText=`width:8px;height:${p.type==='line'?2:8}px;border-radius:2px;background:${color(j)}`;swatch.setAttribute('aria-hidden','true');const label=document.createElement('span');label.className='xy-tip-name';label.textContent=p.name;const number=document.createElement('span');number.className='xy-tip-value';number.textContent=d.y;row.append(swatch,label,number);tip.append(row);}tip.hidden=false;placeTip(index,event);if(state.horizontal)el('line',{x1:state.left,x2:state.right,y1:state.position(value,index),y2:state.position(value,index),stroke:'var(--border)','data-guide':true});else el('line',{x1:state.position(value,index),x2:state.position(value,index),y1:state.top,y2:state.bottom,stroke:'var(--border)','data-guide':true});}\n\n function cancel(){animations.forEach(a=>a.cancel());animations=[];}\n function draw(){cancel();clear();svg.replaceChildren();buttons.forEach(n=>n.remove());buttons=[];motions=[];const minSlot=Math.min(120,Math.max(56,...categories.map(v=>String(v).length*12+8))),width=Math.max(viewport.clientWidth,data.horizontal?480:84+categories.length*minSlot),horizontal=data.horizontal,left=horizontal?138:64,right=width-20,top=32,bottom=horizontal?top+Math.max(260,categories.length*Math.max(40,...categories.map(v=>Math.ceil(String(v).length/9)*18+12))):292,plotWidth=right-left;\n const axis=data.yAxis,values=data.plots.flatMap(p=>p.points.map(d=>d.y));let min=axis.min,max=axis.max;if(!Number.isFinite(min)||!Number.isFinite(max)){min=Math.min(0,...values);max=Math.max(0,...values);}if(min===max){min-=1;max+=1;}\n const categoryBand=(horizontal?(bottom-top):plotWidth)/Math.max(1,categories.length);const x=data.xAxis.type==='band'?d3.scalePoint(categories.map((_,i)=>i),horizontal?[top+categoryBand/2,bottom-categoryBand/2]:[left+categoryBand/2,right-categoryBand/2]):d3.scaleLinear([data.xAxis.min,data.xAxis.max],horizontal?[top+16,bottom-16]:[left+16,right-16]);const y=d3.scaleLinear([min,max],horizontal?[left,right]:[bottom,top]);const position=(value,index)=>x(data.xAxis.type==='band'?index:value);state={horizontal,left,right,top,bottom,x,y,position};reference.hidden=horizontal||view.expanded;reference.replaceChildren();reference.style.top=viewport.offsetTop+'px';reference.style.height=(bottom+8)+'px';const unit=document.createElement('span');unit.textContent=data.yAxis.title;unit.style.cssText='position:absolute;top:0;left:4px;max-width:60px;overflow-wrap:anywhere';reference.append(unit);for(const v of y.ticks(width<400?4:6)){const label=document.createElement('span');label.textContent=v;label.style.cssText='position:absolute;right:12px;top:'+(y(v)-8)+'px';reference.append(label);}\n svg.setAttribute('viewBox',`0 0 ${width} 380`);svg.style.width=width+'px';svg.style.height='380px';svg.setAttribute('role','img');svg.setAttribute('aria-label',data.plots.map(p=>p.name+':'+p.points.map(d=>d.x+' '+d.y).join(',')).join(';'));scene.style.width=scene.style.minWidth=width+'px';scene.style.height='400px';\n for(const v of y.ticks(width<400?4:6)){if(horizontal){el('line',{x1:y(v),x2:y(v),y1:top,y2:bottom,stroke:'var(--border)'});text(v,{x:y(v),y:bottom+22,'text-anchor':'middle'});}else{el('line',{x1:left,x2:right,y1:y(v),y2:y(v),stroke:'var(--border)'});text(v,{x:left-12,y:y(v)+4,'text-anchor':'end'});}}\n const axisTitle=text(horizontal?data.yAxis.title:data.xAxis.title,{x:(left+right)/2,y:370,'text-anchor':'middle','data-axis':'x',class:'axis-title'});text(horizontal?data.xAxis.title:data.yAxis.title,{x:horizontal?8:left,y:18,'data-axis':'y',class:'axis-title',visibility:(!horizontal&&!view.expanded)?'hidden':'visible'});\n const bars=data.plots.filter(p=>p.type==='bar'),band=(horizontal?(bottom-top):plotWidth)/Math.max(1,categories.length),barWidth=Math.min(24,band*.6/Math.max(1,bars.length));let barIndex=0;\n data.plots.forEach((p,j)=>{if(p.type==='bar'){const offset=(barIndex++-(bars.length-1)/2)*barWidth;for(const d of p.points){const a=position(d.x,d.index)+offset,b=y(d.y),zero=y(Math.max(min,Math.min(max,0)));const n=el('rect',{x:horizontal?Math.min(b,zero):a-barWidth/2,y:horizontal?a-barWidth/2:Math.min(b,zero),width:horizontal?Math.abs(b-zero):barWidth,height:horizontal?barWidth:Math.abs(b-zero),fill:color(j),'data-mark':p.id,'data-category':data.xAxis.type==='band'?d.index:categories.indexOf(d.x)});n.style.transformOrigin=horizontal?`${zero}px 0px`:`0px ${zero}px`;motions.push({n,bar:true,zero,horizontal});}}else{const points=p.points.map(d=>horizontal?[y(d.y),position(d.x,d.index)]:[position(d.x,d.index),y(d.y)]),path=el('path',{d:d3.line()(points),fill:'none',stroke:color(j),'stroke-width':1.5,'data-mark':p.id});motions.push({n:path,line:true});for(const [i,[a,b]]of points.entries())el('circle',{cx:a,cy:b,r:3,fill:color(j),'data-category':data.xAxis.type==='band'?p.points[i].index:categories.indexOf(p.points[i].x)});}});\n categories.forEach((value,i)=>{const b=document.createElement('button');b.type='button';b.className='xy-label';b.textContent=value;b.style.left=(horizontal?0:Math.max(left,Math.min(right-Math.max(24,band-4),position(value,i)-band/2)))+'px';b.style.top=(horizontal?position(value,i)-18:bottom+12)+'px';b.style.width=(horizontal?left-14:Math.max(24,band-4))+'px';b.onpointerenter=b.onclick=()=>show(i);b.onfocus=()=>{closed=false;focused=i;const v=viewport.getBoundingClientRect(),r=b.getBoundingClientRect(),margin=data.horizontal?12:68;if(r.left<v.left+margin)viewport.scrollLeft-=v.left+margin-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;show(i);restore();};b.onpointerleave=clear;b.onblur=()=>{focused=null;++epoch;clear();};scene.append(b);buttons.push(b);});\n const labelBottom=Math.max(bottom+24,...buttons.map(b=>b.offsetTop+b.offsetHeight));const figureHeight=labelBottom+16;axisTitle.setAttribute('y',labelBottom+32);axisTitle.setAttribute('visibility','hidden');svg.setAttribute('viewBox',`0 0 ${width} ${figureHeight}`);svg.style.height=figureHeight+'px';scene.style.height=figureHeight+'px';\n const hit=el('rect',{x:left,y:top,width:Math.max(1,plotWidth),height:bottom-top,fill:'transparent','data-chart-hit':true,'data-chart-hover-overlay':'cross-series'});hit.onpointermove=e=>{const r=svg.getBoundingClientRect(),coordinate=(horizontal?e.clientY-r.top:e.clientX-r.left)/view.scale;let best=0;categories.forEach((v,i)=>{if(Math.abs(position(v,i)-coordinate)<Math.abs(position(categories[best],best)-coordinate))best=i;});show(best,e);};hit.onpointerleave=clear;\n const key=root.querySelector('.xy-key')||document.createElement('div');key.className='xy-key';key.replaceChildren();key.hidden=data.plots.length<2;data.plots.forEach((p,i)=>{const item=document.createElement('span'),sw=document.createElement('span');sw.style.width='12px';sw.style.height=p.type==='line'?'2px':'8px';sw.style.background=color(i);item.append(sw,document.createTextNode(p.name));key.append(item);});if(!key.parentElement)root.append(key);view.refresh();}\n function replay(){cancel();if(reduced.matches||view.expanded)return;motions.forEach(m=>{if(m.line){const length=m.n.getTotalLength();animations.push(m.n.animate([{strokeDasharray:length,strokeDashoffset:length},{strokeDasharray:length,strokeDashoffset:0}],{duration:1200}));}else animations.push(m.n.animate([{transform:m.horizontal?'scaleX(0)':'scaleY(0)'},{transform:m.horizontal?'scaleX(1)':'scaleY(1)'}],{duration:650}));});}\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let last=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(root.clientWidth!==last){last=root.clientWidth;draw();}})).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);let focused=null,closed=false,epoch=0;function restore(){const id=++epoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(id===epoch&&!closed&&focused!==null&&document.activeElement===buttons[focused])show(focused);}));}document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else{const r=root.getBoundingClientRect(),visibleHeight=Math.max(0,Math.min(r.bottom,innerHeight)-Math.max(r.top,0));if(armed&&visibleHeight>=r.height*.2){armed=false;replay();}}});reduced.addEventListener('change',cancel);root.addEventListener('keydown',e=>{if(e.key==='Escape'){closed=true;++epoch;clear();}});viewport.addEventListener('scroll',()=>{clear();restore();},{passive:true});root.addEventListener('qv:display-mode',()=>{focused=null;++epoch;clear();});\n}\n\n// 固定版本返回 Map;保留标签顺序,不重排或合并小项。\nasync function readMermaidPieModel(db,descriptions={}) {\n if(!db.getSections||!db.getShowData)return {reason:'Pie 数据接口不兼容'};\n const items=[...db.getSections()].map(([name,value],i)=>({id:'slice-'+(i+1),name,value,description:descriptions[name]||''}));\n const total=items.reduce((s,d)=>s+d.value,0);\n if(!items.length||!Number.isFinite(total)||total<=0||items.some(d=>!Number.isFinite(d.value)||d.value<0))return {reason:'饼图需要有效的非负数与正总量'};\n if(items.some(d=>hasRichMermaidLabel(d.name)))return {reason:'富文本饼图保留原生语义'};\n const unknown=Object.keys(descriptions).filter(name=>!items.some(d=>d.name===name));if(unknown.length)throw Error('说明引用了未知分类:'+unknown.join('、'));\n return {data:{kind:'pie',items,total,showData:db.getShowData(),title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidDonutPreset(presetRoot,config,summary){\n const d3=window.__mcxQVRuntime.d3;\n class PresetResizeObserver{constructor(callback){let frame=0;this.observer=new ResizeObserver(()=>{if(!frame)frame=requestAnimationFrame(()=>{frame=0;callback();});});}observe(target){this.observer.observe(target);}}\n const escape=s=>String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\"','"');\n presetRoot.innerHTML=\"\\n\\u003cstyle\\u003e\\n#@@ID@@{position:relative;padding-top:32px;width:100%;color:var(--foreground);--ring-enter:650}\\n#@@ID@@ h3{margin:0 90px 32px 0;font-weight:500}\\n#@@ID@@ .plot{position:relative}\\n#@@ID@@ .direct-label{position:absolute;transform:translate(-50%,-50%);background:transparent;border:0;padding:5px;color:var(--foreground);font:inherit;font-size:14px;text-align:center;white-space:nowrap;cursor:var(--cursor-interaction,pointer)}\\n#@@ID@@ .direct-label span{display:block;margin-top:4px;color:var(--muted-foreground);font-size:13px;font-variant-numeric:tabular-nums}\\n#@@ID@@ .legend[hidden]{display:none}\\n#@@ID@@ .chart{width:100%;display:block}\\n#@@ID@@ .center{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;pointer-events:none;width:140px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px}\\n#@@ID@@ .center-value{line-height:1;white-space:nowrap;font-size:calc(var(--font-size-base,14px)*2.3);font-weight:500;letter-spacing:-.04em;font-variant-numeric:tabular-nums}\\n#@@ID@@ .center-label{line-height:1.4;font-size:12px;color:var(--muted-foreground)}\\n#@@ID@@ .legend{display:flex;flex-wrap:wrap;justify-content:center;gap:8px 22px;margin-top:20px}\\n#@@ID@@ .legend button{font:inherit;font-size:12px;color:var(--foreground);background:transparent;border:0;border-radius:0;display:inline-flex;align-items:center;gap:7px;padding:4px 0;cursor:var(--cursor-interaction,pointer)}\\n#@@ID@@ .swatch{width:9px;height:9px;border-radius:2px;display:inline-block;flex-shrink:0}\\n#@@ID@@ .percent{color:var(--muted-foreground);font-variant-numeric:tabular-nums}\\n#@@ID@@ .slice{transition:opacity 160ms ease;cursor:var(--cursor-interaction,pointer)}\\n#@@ID@@ .bubble{position:absolute;z-index:2;width:176px;max-width:calc(100% - 16px);padding:12px 14px;box-sizing:border-box;background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);border-radius:12px;box-shadow:0 4px 16px color-mix(in srgb,var(--foreground) 10%,transparent);pointer-events:none;font-size:12px}\\n#@@ID@@ .bubble[hidden]{display:none}\\n#@@ID@@ .bubble-title{font-weight:500;margin-bottom:10px}\\n#@@ID@@ .bubble-row{display:flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums}\\n#@@ID@@ .bubble-percent{margin-left:auto;color:var(--muted-foreground)}\\n@media(prefers-reduced-motion:reduce){#@@ID@@ .slice{transition:none}}\\n@media(pointer:coarse){#@@ID@@ .legend button{min-height:44px}}\\n\\u003c/style\\u003e\\n\\u003ch3 id=\\\"@@ID@@-title\\\"\\u003e@@TITLE@@\\u003c/h3\\u003e\\n\\u003cdiv class=\\\"plot\\\"\\u003e\\u003csvg class=\\\"chart\\\" role=\\\"img\\\" aria-label=\\\"@@SUMMARY@@\\\"\\u003e\\u003c/svg\\u003e\\u003cdiv class=\\\"center\\\" aria-hidden=\\\"true\\\"\\u003e\\u003cspan class=\\\"center-value\\\"\\u003e\\u003c/span\\u003e\\u003cspan class=\\\"center-label\\\"\\u003e\\u003c/span\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\n\\u003cdiv class=\\\"legend\\\" aria-label=\\\"来源及占比\\\"\\u003e\\u003c/div\\u003e\\n\\u003cdiv class=\\\"bubble\\\" role=\\\"tooltip\\\" hidden\\u003e\\u003cdiv class=\\\"bubble-title\\\"\\u003e\\u003c/div\\u003e\\u003cdiv class=\\\"bubble-row\\\"\\u003e\\u003cspan class=\\\"swatch\\\"\\u003e\\u003c/span\\u003e\\u003cspan class=\\\"bubble-value\\\"\\u003e\\u003c/span\\u003e\\u003cspan class=\\\"bubble-percent\\\"\\u003e\\u003c/span\\u003e\\u003c/div\\u003e\\u003cdiv class=\\\"description text-small\\\" style=\\\"margin-top:8px\\\" hidden\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\n\\n\\n\".replaceAll('@@ID@@',presetRoot.id).replaceAll('@@TITLE@@',escape(config.title)).replaceAll('@@SUMMARY@@',escape(summary));\n \n(()=>{\nconst root=presetRoot,plot=root.querySelector('.plot'),svg=d3.select(root.querySelector('.chart')),legend=root.querySelector('.legend'),bubble=root.querySelector('.bubble');\n\nconst numberFormat=new Intl.NumberFormat(undefined,{minimumFractionDigits:config.decimals,maximumFractionDigits:config.decimals});const formatValue=v=>config.valuePrefix+(config.exactValues?String(v):numberFormat.format(v))+config.valueSuffix;\nconst data=config.items;\nconst total=d3.sum(data,d=>d.value),color=i=>`var(--viz-series-${i%6+1})`,pct=i=>`${Number((data[i].value/total*100).toFixed(1))}%`;\nconst reduced=window.matchMedia('(prefers-reduced-motion: reduce)');let layout=null,entered=false,lastWidth=0;\nlet direct=false;const eligibleDirect=data.length<=4&&d3.min(data,d=>d.value/total)>=.08&&data.every(d=>d.label.length<=12);\nfunction clear(){svg.selectAll('.slice').attr('opacity',1);root.querySelectorAll('.legend button,.direct-label').forEach(b=>b.style.opacity=1);root.querySelector('.center-value').textContent=config.valuePrefix+numberFormat.format(total);root.querySelector('.center-label').textContent='总计'+(config.valueSuffix.trim()?' · '+config.valueSuffix.trim():'');bubble.hidden=true;}\nfunction show(i){if(!layout)return;svg.selectAll('.slice').attr('opacity',d=>d.index===i?1:.25);root.querySelectorAll('.legend button,.direct-label').forEach((b,j)=>b.style.opacity=j===i?1:.55);root.querySelector('.center-value').textContent=pct(i);root.querySelector('.center-label').textContent=data[i].label;\nif(config.showTooltip===false)return;\nroot.querySelector('.bubble-title').textContent=data[i].label;const description=root.querySelector('.description');description.textContent=data[i].description||'';description.hidden=!data[i].description;root.querySelector('.bubble-value').textContent=formatValue(data[i].value);root.querySelector('.bubble-percent').textContent=pct(i);root.querySelector('.bubble .swatch').style.background=color(i);bubble.hidden=false;\nconst d=layout.arcs[i],angle=(d.startAngle+d.endAngle)/2,px=layout.cx+Math.sin(angle)*layout.radius,py=layout.cy-Math.cos(angle)*layout.radius;\nconst rb=root.getBoundingClientRect(),pb=plot.getBoundingClientRect(),bw=bubble.offsetWidth,bh=bubble.offsetHeight;\nbubble.style.left=Math.max(8,Math.min(rb.width-bw-8,px>=layout.cx?px+14:px-bw-14))+'px';bubble.style.top=Math.max(pb.top-rb.top+8,Math.min(pb.top-rb.top+py-bh/2,pb.bottom-rb.top-bh-8))+'px';}\ndata.forEach((d,i)=>{const b=document.createElement('button');b.type='button';b.innerHTML=`<span class=\"swatch\" style=\"background:${color(i)}\" aria-hidden=\"true\"></span><span class=\"name\"></span><span class=\"percent\"></span>`;b.querySelector('.name').textContent=d.label;b.querySelector('.percent').textContent=pct(i);b.addEventListener('pointerenter',()=>show(i));b.addEventListener('pointerleave',clear);b.addEventListener('focus',()=>show(i));b.addEventListener('blur',clear);legend.append(b);});\nfunction draw(animate=false){const width=Math.round(plot.getBoundingClientRect().width);if(width<1)return;lastWidth=width;direct=eligibleDirect&&width>=420;const height=direct?340:280,cx=width/2,cy=height/2,radius=direct?Math.min(104,width/2-50):Math.min(118,width/2-16),inner=radius*.75;\n// 间隔不参与角度分配;随最小正值缩小,避免零值和极小值撑大缺口。\nconst arcs=d3.pie().sort(null).value(d=>d.value)(data),positiveArcs=arcs.filter(d=>d.data.value>0),smallestAngle=d3.min(positiveArcs,d=>d.endAngle-d.startAngle),gap=positiveArcs.length>1?Math.min(.025,smallestAngle*.2):0;\n// 保留接近整圆时的微小端点差,避免三位小数将两端舍入为同一点。\nconst arc=d3.arc().digits(9).innerRadius(inner).outerRadius(radius).padAngle(gap).cornerRadius(Math.min(3,radius*smallestAngle*.2)),enter=parseFloat(getComputedStyle(root).getPropertyValue('--ring-enter'));layout={width,cx,cy,radius,arcs};\nsvg.selectAll('*').interrupt();svg.selectAll('*').remove();svg.attr('viewBox',`0 0 ${width} ${height}`);const g=svg.append('g').attr('transform',`translate(${cx},${cy})`);\nconst slices=g.selectAll('.slice').data(positiveArcs).join('path').attr('class','slice').attr('fill',d=>color(d.index)).attr('d',arc).on('pointerenter pointermove',(_,d)=>show(d.index)).on('pointerleave pointercancel',clear).on('pointerup',e=>{if(e.pointerType==='touch')clear();});\nplot.querySelectorAll('.direct-label').forEach(e=>{e.getAnimations().forEach(a=>a.cancel());e.remove();});legend.hidden=direct;\nif(direct){arcs.forEach((d,i)=>{const a=(d.startAngle+d.endAngle)/2,side=Math.sin(a)>0?1:-1,narrow=width<460;\nconst peers=arcs.filter(q=>(Math.sin((q.startAngle+q.endAngle)/2)>0?1:-1)===side).sort((p,q)=>Math.cos((q.startAngle+q.endAngle)/2)-Math.cos((p.startAngle+p.endAngle)/2));const rank=peers.indexOf(d);\nconst x=narrow?cx+(i<2?1:-1)*width*.28:cx+Math.sin(a)*(radius+64),y=narrow?(i===0||i===3?32:height-32):cy-Math.cos(a)*(radius+48);\nconst b=document.createElement('button');b.type='button';b.className='direct-label';b.style.left=x+'px';b.style.top=y+'px';b.append(document.createTextNode(data[i].label));const percent=document.createElement('span');percent.textContent=(config.showData?data[i].value+' · ':'')+pct(i);b.append(percent);b.onpointerenter=()=>show(i);b.onpointerleave=clear;b.onfocus=()=>show(i);b.onblur=clear;plot.append(b);\nif(animate&&!reduced.matches){\nconst delay=enter*(.2+i*.05);\nb.animate([{opacity:0},{opacity:1}],{delay,duration:enter*.4,easing:'ease',fill:'backwards'});\n}\nconst px=cx+Math.sin(a)*(radius+7),py=cy-Math.cos(a)*(radius+7);\n});}\nif(animate&&!reduced.matches){\n// 细分遮罩只控制显现节奏,不改变实际数据分段。\nconst id=root.id+'-reveal',mask=svg.append('defs').append('mask').attr('id',id).attr('maskUnits','userSpaceOnUse').attr('x',-radius-2).attr('y',-radius-2).attr('width',radius*2+4).attr('height',radius*2+4);\nconst segments=d3.range(120),unit=Math.PI*2/120;\nconst reveal=d3.arc().innerRadius(inner-2);\nconst cells=mask.selectAll('path').data(segments).join('path').attr('fill','white').attr('d',i=>reveal.outerRadius(inner-2)({startAngle:i*unit-.002,endAngle:(i+1)*unit+.002}));\ng.attr('mask',`url(#${id})`);\ncells.transition().delay(i=>i/(segments.length-1)*enter*.55).duration(enter*.45).ease(d3.easeCubicOut).attrTween('d',i=>t=>reveal.outerRadius(inner-2+(radius-inner+4)*t)({startAngle:i*unit-.002,endAngle:(i+1)*unit+.002}));\ng.transition().delay(enter).on('end',()=>{g.attr('mask',null);mask.remove();});\n}clear();}\nroot.addEventListener('pointerleave',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});window.addEventListener('blur',clear);\ndraw();if(!reduced.matches)svg.attr('visibility','hidden');let inView=false,armed=true;\nfunction replayIfReady(){if(inView&&armed&&!document.hidden){armed=false;entered=true;svg.attr('visibility','visible');draw(true);}}\nconst observer=new IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){inView=false;armed=true;}else if(e.intersectionRatio>=.2){inView=true;replayIfReady();}},{threshold:[0,.2]});\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else replayIfReady();});\nwindow.addEventListener('pageshow',e=>{if(e.persisted){armed=true;replayIfReady();}});observer.observe(root);\nnew PresetResizeObserver(()=>{if(Math.round(plot.getBoundingClientRect().width)!==lastWidth)draw();}).observe(plot);\nroot.dispatchEvent(new Event('qv:ready',{bubbles:true}));\n})();\n\n}\nasync function renderMermaidRadarPreset(presetRoot,config,summary){\n \n class PresetResizeObserver{constructor(callback){let frame=0;this.observer=new ResizeObserver(()=>{if(!frame)frame=requestAnimationFrame(()=>{frame=0;callback();});});}observe(target){this.observer.observe(target);}}\n const escape=s=>String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\"','"');\n presetRoot.innerHTML=\"\\n \\u003cstyle\\u003e\\n #@@ID@@ {\\n position: relative;\\n display: grid;\\n gap: 0;\\n padding-top:32px;\\n width: 100%;\\n color: var(--foreground);\\n }\\n\\n #@@ID@@ .quick-radar-title {\\n margin: 0 90px 32px 0;\\n color: var(--foreground);\\n font-weight: 500;\\n }\\n\\n #@@ID@@ .quick-radar-legend {\\n display: flex;\\n flex-wrap: wrap;\\n gap: 8px 22px;\\n justify-content:center;\\n margin-top:20px;\\n }\\n\\n #@@ID@@ .quick-radar-legend[hidden] {\\n display: none;\\n }\\n\\n #@@ID@@ .quick-radar-legend-button {\\n display: inline-flex;\\n align-items: center;\\n gap: 6px;\\n margin: 0;\\n padding: 2px 0;\\n color: var(--foreground);\\n font: inherit;\\n background: transparent;\\n border: 0;\\n cursor: var(--cursor-interaction, pointer);\\n }\\n\\n #@@ID@@ .quick-radar-legend-button.is-dimmed {\\n color: var(--muted-foreground);\\n opacity: 0.32;\\n }\\n\\n #@@ID@@ .quick-radar-swatch {\\n width: 9px;\\n height: 9px;\\n flex: 0 0 auto;\\n background: currentColor;\\n }\\n\\n #@@ID@@ .quick-radar-swatch[data-shape=\\\"circle\\\"] {\\n border-radius: 50%;\\n }\\n\\n #@@ID@@ .quick-radar-swatch[data-shape=\\\"diamond\\\"] {\\n transform: rotate(45deg) scale(0.78);\\n }\\n\\n #@@ID@@ .quick-radar-swatch[data-shape=\\\"triangle\\\"] {\\n clip-path: polygon(50% 0, 100% 100%, 0 100%);\\n }\\n\\n #@@ID@@ .quick-radar-plot {\\n position: relative;\\n min-width: 0;\\n }\\n\\n #@@ID@@ .quick-radar-svg {\\n display: block;\\n width: 100%;\\n height: auto;\\n overflow: visible;\\n }\\n\\n #@@ID@@ .quick-radar-grid,\\n #@@ID@@ .quick-radar-axis {\\n fill: transparent;\\n stroke: var(--border);\\n vector-effect: non-scaling-stroke;\\n }\\n\\n #@@ID@@ .quick-radar-grid {\\n stroke-opacity: 0.72;\\n }\\n\\n #@@ID@@ .quick-radar-axis {\\n stroke-opacity: 0.58;\\n }\\n\\n #@@ID@@ .quick-radar-axis-label,\\n #@@ID@@ .quick-radar-scale-label {\\n fill: var(--foreground);\\n font-size: 12px;\\n font-weight: 400;\\n }\\n\\n #@@ID@@ .quick-radar-scale-label {\\n fill: var(--muted-foreground);\\n }\\n\\n #@@ID@@ .quick-radar-series-polygon,\\n #@@ID@@ .quick-radar-marker {\\n transition: fill-opacity 160ms ease, stroke-opacity 160ms ease, opacity 160ms ease, stroke-width 160ms ease;\\n vector-effect: non-scaling-stroke;\\n }\\n\\n #@@ID@@ .quick-radar-series-polygon {\\n fill: currentColor;\\n stroke: currentColor;\\n stroke-linejoin: round;\\n }\\n\\n #@@ID@@ .quick-radar-marker {\\n fill: currentColor;\\n stroke: var(--background);\\n stroke-width: 1.5;\\n }\\n\\n #@@ID@@ .quick-radar-hit {\\n fill: transparent;\\n cursor: crosshair;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip {\\n position: absolute;\\n z-index: 2;\\n display: grid;\\n gap: 12px;\\n width:188px;\\n box-sizing:border-box;\\n max-width: min(260px, calc(100% - 16px));\\n padding: 12px 14px;\\n color: var(--popover-foreground);\\n background: var(--popover);\\n border: 1px solid var(--border);\\n border-radius: 12px;\\n box-shadow: 0 6px 18px color-mix(in srgb, var(--foreground) 10%, transparent);\\n pointer-events: none;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip[hidden] {\\n display: none;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip[data-mode=\\\"object\\\"] .quick-radar-tooltip-row {grid-template-columns:minmax(0,1fr) auto;}\\n\\n #@@ID@@ .quick-radar-tooltip-dimension {\\n display:flex;\\n align-items:center;\\n gap:8px;\\n color: var(--popover-foreground);\\n font-size:14px;\\n line-height:20px;\\n font-weight:600;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-rows {\\n display: grid;\\n gap: 4px;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-row {\\n display: grid;\\n grid-template-columns: auto minmax(0, 1fr) auto;\\n align-items: center;\\n gap: 6px;\\n font-size: 12px;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-row.is-active .quick-radar-tooltip-name {\\n color: var(--popover-foreground);\\n font-weight: 500;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-name {\\n color: var(--popover-foreground);\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-value {\\n color: light-dark(color-mix(in srgb,var(--popover-foreground) 64%,var(--popover)),color-mix(in srgb,var(--popover-foreground) 68%,var(--popover)));\\n font-variant-numeric: tabular-nums;\\n }\\n\\n @media (prefers-reduced-motion: reduce) {\\n #@@ID@@ .quick-radar-series-polygon,\\n #@@ID@@ .quick-radar-marker {\\n transition: none;\\n }\\n }\\n \\u003c/style\\u003e\\n\\n \\u003ch3 class=\\\"quick-radar-title\\\" id=\\\"@@ID@@-title\\\"\\u003e@@TITLE@@\\u003c/h3\\u003e\\n \\u003cp class=\\\"sr-only\\\" id=\\\"@@ID@@-summary\\\"\\u003e@@SUMMARY@@\\u003c/p\\u003e\\n\\n \\u003cdiv class=\\\"quick-radar-plot\\\" id=\\\"@@ID@@-plot\\\"\\u003e\\n \\u003csvg class=\\\"quick-radar-svg\\\" id=\\\"@@ID@@-svg\\\" role=\\\"img\\\" aria-label=\\\"@@SUMMARY@@\\\"\\u003e\\n \\u003cdesc id=\\\"@@ID@@-svg-desc\\\"\\u003e@@SUMMARY@@\\u003c/desc\\u003e\\n \\u003c/svg\\u003e\\n \\u003c/div\\u003e\\n\\n \\u003cdiv class=\\\"quick-radar-legend\\\" id=\\\"@@ID@@-legend\\\" aria-label=\\\"数据系列\\\"\\u003e\\u003c/div\\u003e\\n\\n \\u003cdiv class=\\\"quick-radar-tooltip\\\" id=\\\"@@ID@@-tooltip\\\" role=\\\"tooltip\\\" hidden\\u003e\\n \\u003cspan class=\\\"quick-radar-tooltip-dimension\\\"\\u003e\\u003c/span\\u003e\\n \\u003cdiv class=\\\"quick-radar-tooltip-rows\\\"\\u003e\\u003c/div\\u003e\\n \\u003c/div\\u003e\\n\\n \\n\".replaceAll('@@ID@@',presetRoot.id).replaceAll('@@TITLE@@',escape(config.title)).replaceAll('@@SUMMARY@@',escape(summary));\n \n (() => {\n const SVG_NS = 'http://www.w3.org/2000/svg';\n const root = presetRoot;\n const plot = root.querySelector(`#${presetRoot.id}-plot`);\n const svg = root.querySelector(`#${presetRoot.id}-svg`);\n const legend = root.querySelector(`#${presetRoot.id}-legend`);\n const tooltip = root.querySelector(`#${presetRoot.id}-tooltip`);\n const tooltipDimension = tooltip.querySelector('.quick-radar-tooltip-dimension');\n const tooltipRows = tooltip.querySelector('.quick-radar-tooltip-rows');\n \n const shapes = ['circle', 'square', 'diamond', 'triangle'];\n const numberFormat = new Intl.NumberFormat(undefined, {\n minimumFractionDigits: config.decimals,\n maximumFractionDigits: config.decimals,\n });\n const valueText = (value) => `${config.valuePrefix}${numberFormat.format(value)}${config.valueSuffix}`;\n const colorFor = (index) => `var(--viz-series-${index + 1})`;\n let focusedIndex = null;\n let hoveredIndex = null;\n let pointLocations = [];\n let seriesGroups = [];\n let geometry=null,motionFrame=0;\n\n const createSvg = (name, attributes = {}) => {\n const element = document.createElementNS(SVG_NS, name);\n Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, value));\n return element;\n };\n\n const activeIndex = () => hoveredIndex ?? focusedIndex;\n\n const updateLegend = () => {\n const active = activeIndex();\n legend.querySelectorAll('button').forEach((button) => {\n const index = Number(button.dataset.seriesIndex);\n\n button.classList.toggle('is-dimmed', active !== null && active !== index);\n });\n };\n\n const updateSeries = () => {\n const active = activeIndex();\n seriesGroups.forEach((entry, index) => {\n const isActive = active === index;\n const isDimmed = active !== null && !isActive;\n entry.polygon.setAttribute('fill-opacity', isActive ? '0.09' : isDimmed ? '0.003' : '0.035');\n entry.polygon.setAttribute('stroke-opacity', isDimmed ? '0.1' : '1');\n entry.polygon.setAttribute('stroke-width', isActive ? '2.2' : '1.7');\n entry.markers.forEach((marker) => {\n marker.setAttribute('opacity', isDimmed ? '0.1' : isActive ? '1' : '0.78');\n marker.setAttribute('stroke-width', isActive ? '2' : '1.5');\n });\n });\n if (active !== null && seriesGroups[active]) {\n seriesGroups[active].layer.append(seriesGroups[active].group);\n }\n updateLegend();\n };\n\n const setHover = (index) => {\n if (hoveredIndex === index) return;\n hoveredIndex = index;\n updateSeries();\n };\n\n const clearHover = () => {\n if (hoveredIndex === null) return;\n hoveredIndex = null;\n updateSeries();\n };\n\n if (config.series.length <= 1) {\n legend.hidden = true;\n } else {\n config.series.forEach((series, index) => {\n const button = document.createElement('button');\n button.className = 'quick-radar-legend-button';\n button.type = 'button';\n button.dataset.seriesIndex = String(index);\n\n\n\n const swatch = document.createElement('span');\n swatch.className = 'quick-radar-swatch';\n swatch.dataset.shape = shapes[index];\n swatch.style.color = colorFor(index);\n swatch.setAttribute('aria-hidden', 'true');\n const label = document.createElement('span');\n label.textContent = series.name;\n button.append(swatch, label);\n\n button.addEventListener('mouseenter', () => setHover(index));\n button.addEventListener('mouseleave', clearHover);\n button.addEventListener('focus', () => setHover(index));\n button.addEventListener('blur', clearHover);\n\n legend.append(button);\n });\n }\n\n const markerPath = (shape, radius) => {\n if (shape === 'circle') {\n return `M ${-radius},0 A ${radius},${radius} 0 1,0 ${radius},0 A ${radius},${radius} 0 1,0 ${-radius},0`;\n }\n if (shape === 'diamond') {\n return `M 0,${-radius * 1.25} L ${radius},0 L 0,${radius * 1.25} L ${-radius},0 Z`;\n }\n if (shape === 'triangle') {\n return `M 0,${-radius * 1.2} L ${radius * 1.05},${radius} L ${-radius * 1.05},${radius} Z`;\n }\n return `M ${-radius},${-radius} L ${radius},${-radius} L ${radius},${radius} L ${-radius},${radius} Z`;\n };\n\n const splitLabel = (label, narrow) => {\n const characters = Array.from(label);\n const chunkSize = narrow ? 4 : 9;\n const lines = [];\n for (let index = 0; index < characters.length; index += chunkSize) {\n lines.push(characters.slice(index, index + chunkSize).join(''));\n }\n return lines;\n };\n\n const polygonPoints = (cx, cy, radius, ratio = 1) => config.dimensions.map((_, index) => {\n const angle = -Math.PI / 2 + index * Math.PI * 2 / config.dimensions.length;\n return [cx + Math.cos(angle) * radius * ratio, cy + Math.sin(angle) * radius * ratio];\n });\n\n const pointsAttribute = (points) => points.map(([x, y]) => `${x},${y}`).join(' ');\n\n const showTooltip = (event, dimensionIndex, nearestSeriesIndex) => {\n const objectMode=dimensionIndex===null;\n tooltip.dataset.mode=objectMode?'object':'dimension';\n tooltipDimension.textContent = objectMode?config.series[nearestSeriesIndex].name:config.dimensions[dimensionIndex];\n if(objectMode){const swatch=document.createElement('span');swatch.className='quick-radar-swatch';swatch.dataset.shape=shapes[nearestSeriesIndex];swatch.style.color=colorFor(nearestSeriesIndex);swatch.setAttribute('aria-hidden','true');tooltipDimension.prepend(swatch);}\n tooltipRows.replaceChildren();\n const rows=objectMode?config.dimensions.map((name,i)=>({name,score:config.series[nearestSeriesIndex].values[i],color:nearestSeriesIndex})):config.series.map((series,i)=>({name:series.name,score:series.values[dimensionIndex],color:i}));\n rows.forEach((series, index) => {\n const row = document.createElement('div');\n row.className = 'quick-radar-tooltip-row';\n row.classList.toggle('is-active', objectMode);\n const swatch = document.createElement('span');\n swatch.className = 'quick-radar-swatch';\n swatch.dataset.shape = shapes[series.color];\n swatch.style.color = colorFor(series.color);\n swatch.setAttribute('aria-hidden', 'true');\n const name = document.createElement('span');\n name.className = 'quick-radar-tooltip-name';\n name.textContent = series.name;\n const value = document.createElement('span');\n value.className = 'quick-radar-tooltip-value';\n value.textContent = valueText(series.score);\n if(objectMode)row.append(name,value);else row.append(swatch,name,value);\n tooltipRows.append(row);\n });\n tooltip.hidden = false;\n\n const rootBounds = root.getBoundingClientRect();\n const plotBounds = plot.getBoundingClientRect();\n const tooltipBounds = tooltip.getBoundingClientRect();\n const bw=tooltipBounds.width,bh=tooltipBounds.height,pad=8,gap=16;\n const ox=plotBounds.left-rootBounds.left,oy=plotBounds.top-rootBounds.top;\n const sx=plotBounds.width/svg.viewBox.baseVal.width,sy=plotBounds.height/svg.viewBox.baseVal.height;\n const polygons=seriesGroups.map(e=>e.points.map(p=>({x:ox+p.x*sx,y:oy+p.y*sy})));\n const minY=root.querySelector('h3').getBoundingClientRect().bottom-rootBounds.top+pad,maxY=Math.max(minY,rootBounds.height-bh-pad),maxX=Math.max(pad,rootBounds.width-bw-pad);\n const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));\n const px=event.clientX-rootBounds.left,py=event.clientY-rootBounds.top;\n // 所有候选都紧邻光标,边界约束后再比较附近的遮挡。\n const positions=[];\n for(const offset of [16,28,40]){\n for(const fraction of [0,.25,.5,.75,1])positions.push([px+offset,py-bh*fraction],[px-offset-bw,py-bh*fraction],[px-bw*fraction,py-offset-bh],[px-bw*fraction,py+offset]);\n positions.push([px+offset,py+offset],[px+offset,py-offset-bh],[px-offset-bw,py+offset],[px-offset-bw,py-offset-bh]);\n }\n const candidates=positions.map(([x,y],i)=>({x:clamp(x,pad,maxX),y:clamp(y,minY,maxY),order:i}));\n const overlap=(poly,box)=>{\n let pts=poly;\n for(const [axis,edge,sign] of [['x',box.x,1],['x',box.x+bw,-1],['y',box.y,1],['y',box.y+bh,-1]]){\n const out=[];for(let i=0;i<pts.length;i++){const a=pts[i],b=pts[(i+1)%pts.length],ai=sign*(a[axis]-edge)>=0,bi=sign*(b[axis]-edge)>=0;if(ai)out.push(a);if(ai!==bi){const t=(edge-a[axis])/(b[axis]-a[axis]);out.push({x:a.x+t*(b.x-a.x),y:a.y+t*(b.y-a.y)});}}pts=out;if(!pts.length)return 0;\n }\n return Math.abs(pts.reduce((sum,p,i)=>{const q=pts[(i+1)%pts.length];return sum+p.x*q.y-q.x*p.y;},0))/2;\n };\n const labels=Array.from(svg.querySelectorAll('.quick-radar-axis-label')).map(e=>{const r=e.getBoundingClientRect();return {x:r.left-rootBounds.left,y:r.top-rootBounds.top,w:r.width,h:r.height};});\n candidates.forEach(c=>{\n c.distance=Math.hypot(Math.max(c.x-px,0,px-c.x-bw),Math.max(c.y-py,0,py-c.y-bh));\n c.coversPointer=c.distance<gap/2?1:0;\n c.area=polygons.reduce((sum,p)=>sum+overlap(p,c),0);\n c.labels=labels.reduce((sum,r)=>sum+Math.max(0,Math.min(c.x+bw,r.x+r.w)-Math.max(c.x,r.x))*Math.max(0,Math.min(c.y+bh,r.y+r.h)-Math.max(c.y,r.y)),0);\n });\n candidates.sort((a,b)=>a.coversPointer-b.coversPointer||Math.max(0,a.distance-40)-Math.max(0,b.distance-40)||a.area-b.area||a.labels-b.labels||Math.abs(a.distance-gap)-Math.abs(b.distance-gap)||a.order-b.order);\n tooltip.style.left=candidates[0].x+'px';tooltip.style.top=candidates[0].y+'px';\n };\n\n const hideTooltip = () => {\n tooltip.hidden = true;\n clearHover();updateSeries();\n svg.querySelectorAll('.quick-radar-axis').forEach(e=>{e.style.stroke='var(--border)';e.style.strokeOpacity='';e.setAttribute('stroke-opacity','.58');});\n };\n\n const draw = (animate=false) => {\n cancelAnimationFrame(motionFrame);motionFrame=0;hideTooltip();\n const width = Math.max(1, Math.round(plot.getBoundingClientRect().width));\n if (width <= 1) return;\n const narrow = width < 480;\n const denseNarrow = narrow && (\n config.dimensions.length > 6\n || config.dimensions.some((dimension) => Array.from(dimension).length > 4)\n );\n const height = narrow ? 330 : 380;\n const cx = width / 2;\n const cy = height / 2;\n const radius = Math.min(140, (width-132)/2, height * 0.36);\n svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n geometry={cx,cy,radius,axes:polygonPoints(cx,cy,radius)};\n seriesGroups.forEach(e=>e.group.getAnimations().forEach(a=>a.cancel()));\n plot.querySelectorAll('.dimension-key').forEach(e=>e.remove());\n svg.replaceChildren();\n\n const description = createSvg('desc', { id: `${presetRoot.id}-svg-desc` });\n description.textContent = config.summary;\n svg.append(description);\n\n const gridLayer = createSvg('g');\n [0.25, 0.5, 0.75, 1].forEach((ratio) => {\n gridLayer.append(createSvg('polygon', {\n class: 'quick-radar-grid',\n points: pointsAttribute(polygonPoints(cx, cy, radius, ratio)),\n }));\n const scaleLabel = createSvg('text', {\n class: 'quick-radar-scale-label',\n x: String(cx + 5),\n y: String(cy - radius * ratio + 13),\n });\n scaleLabel.textContent = numberFormat.format(config.maxValue * ratio);\n\n });\n\n polygonPoints(cx, cy, radius).forEach(([x, y]) => {\n gridLayer.append(createSvg('line', {\n class: 'quick-radar-axis',\n x1: String(cx),\n y1: String(cy),\n x2: String(x),\n y2: String(y),\n }));\n });\n svg.append(gridLayer);\n\n const seriesLayer = createSvg('g');\n seriesGroups = [];\n pointLocations = [];\n config.series.forEach((series, seriesIndex) => {\n const group = createSvg('g', { style: `color:${colorFor(seriesIndex)}` });\n const points = series.values.map((value, dimensionIndex) => {\n const angle = -Math.PI / 2 + dimensionIndex * Math.PI * 2 / config.dimensions.length;\n const distance = radius * value / config.maxValue;\n return {\n x: cx + Math.cos(angle) * distance,\n y: cy + Math.sin(angle) * distance,\n seriesIndex,\n dimensionIndex,\n };\n });\n const polygon = createSvg('polygon', {\n class: 'quick-radar-series-polygon',\n points: pointsAttribute(points.map(({ x, y }) => [x, y])),\n });\n group.append(polygon);\n const markers = points.map((point) => {\n const marker = createSvg('path', {\n class: 'quick-radar-marker',\n d: markerPath(shapes[seriesIndex], 2.8),\n transform: `translate(${point.x} ${point.y})`,\n });\n group.append(marker);\n pointLocations.push(point);\n return marker;\n });\n seriesLayer.append(group);\n seriesGroups.push({ group, polygon, markers, points, layer: seriesLayer });\n });\n svg.append(seriesLayer);\n\n const labelLayer = createSvg('g');\n config.dimensions.forEach((dimension, index) => {\n const angle = -Math.PI / 2 + index * Math.PI * 2 / config.dimensions.length;\n const directionX = Math.cos(angle);\n const directionY = Math.sin(angle);\n const labelDistance = radius + (denseNarrow ? 12 : narrow ? 18 : 24);\n const x = Math.min(width - 4, Math.max(4, cx + directionX * labelDistance));\n const y = cy + directionY * labelDistance;\n const text = createSvg('text', {\n class: 'quick-radar-axis-label',\n x: String(x),\n y: String(y),\n 'text-anchor': directionX > 0.28 ? 'start' : directionX < -0.28 ? 'end' : 'middle',\n });\n const lines = splitLabel(dimension, narrow);\n const firstLineOffset = directionY > 0.45\n ? 12\n : directionY < -0.45\n ? -5 - (lines.length - 1) * 14\n : 4 - (lines.length - 1) * 7;\n lines.forEach((line, lineIndex) => {\n const tspan = createSvg('tspan', {\n x: String(x),\n dy: lineIndex === 0 ? String(firstLineOffset) : '14',\n });\n tspan.textContent = line;\n text.append(tspan);\n });\n labelLayer.append(text);\n const key=document.createElement('button');key.type='button';key.className='dimension-key';key.setAttribute('aria-label',`查看${dimension},比较全部方案`);key.style.cssText=`position:absolute;left:${x}px;top:${y+firstLineOffset-6}px;transform:translate(${directionX>.28?'0':directionX<-.28?'-100%':'-50%'},-50%);width:${Math.max(48,Math.min(108,dimension.length*12))}px;height:32px;background:transparent;border:0;padding:0;`;key.onfocus=()=>{const r=root.getBoundingClientRect(),p=plot.getBoundingClientRect();showTooltip({clientX:r.left+x,clientY:p.top+y},index,null);};key.onpointerenter=e=>showDimension(e,index);key.onpointerleave=hideTooltip;key.onblur=hideTooltip;plot.append(key);\n });\n svg.append(labelLayer);\n\n const hit = createSvg('rect', {\n class: 'quick-radar-hit',\n x: '0',\n y: '0',\n width: String(width),\n height: String(height),\n 'data-chart-hit': '',\n });\n svg.append(hit);\n updateSeries();\n if(animate){\n // 顶点按数值向外展开,线宽与标记尺寸保持不变,避免整组缩放。\n const paint=t=>{const k=1-Math.pow(1-t,4);seriesGroups.forEach(entry=>{const points=entry.points.map(p=>({x:cx+(p.x-cx)*k,y:cy+(p.y-cy)*k}));entry.polygon.setAttribute('points',pointsAttribute(points.map(p=>[p.x,p.y])));entry.markers.forEach((m,i)=>{m.setAttribute('transform',`translate(${points[i].x} ${points[i].y})`);m.style.opacity=String(Math.min(1,t*5));});});};\n paint(0);const start=performance.now();const tick=now=>{const t=Math.min(1,(now-start)/900);paint(t);if(t<1)motionFrame=requestAnimationFrame(tick);else{motionFrame=0;seriesGroups.forEach(e=>e.markers.forEach(m=>m.style.opacity=''));}};motionFrame=requestAnimationFrame(tick);\n }\n };\n\n const segmentDistance=(p,a,b)=>{const dx=b.x-a.x,dy=b.y-a.y,t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(dx*dx+dy*dy||1)));return Math.hypot(p.x-a.x-t*dx,p.y-a.y-t*dy);};\n const contains=(p,points)=>{let inside=false;for(let i=0,j=points.length-1;i<points.length;j=i++){const a=points[i],b=points[j];if((a.y>p.y)!==(b.y>p.y)&&p.x<(b.x-a.x)*(p.y-a.y)/(b.y-a.y)+a.x)inside=!inside;}return inside;};\n function showDimension(event,index){hideTooltip();svg.querySelectorAll('.quick-radar-axis').forEach((e,i)=>{if(i===index){e.style.stroke='var(--foreground)';e.style.strokeOpacity='.4';}});seriesGroups.forEach(e=>e.markers.forEach((m,i)=>m.setAttribute('opacity',i===index?'1':'.2')));showTooltip(event,index,null);}\n function showObject(event,index){hideTooltip();setHover(index);showTooltip(event,null,index);}\n svg.addEventListener('pointermove',event=>{\n if(!geometry||motionFrame){hideTooltip();return;}\n const r=svg.getBoundingClientRect(),scale=svg.viewBox.baseVal.width/r.width,p={x:(event.clientX-r.left)*scale,y:(event.clientY-r.top)*scale};\n const nearest=pointLocations.reduce((best,q)=>{const distance=Math.hypot(p.x-q.x,p.y-q.y);return !best||distance<best.distance?{...q,distance}:best;},null);\n if(nearest&&nearest.distance<=9*scale){showObject(event,nearest.seriesIndex);return;}\n const center={x:geometry.cx,y:geometry.cy};if(Math.hypot(p.x-center.x,p.y-center.y)<12){hideTooltip();return;}\n const axes=geometry.axes.map(([x,y],i)=>({i,distance:segmentDistance(p,center,{x,y})})).sort((a,b)=>a.distance-b.distance);\n if(axes[0].distance<=6*scale){showDimension(event,axes[0].i);return;}\n const edges=seriesGroups.map((e,i)=>({i,inside:contains(p,e.points),distance:Math.min(...e.points.map((q,j)=>segmentDistance(p,q,e.points[(j+1)%e.points.length])))})).sort((a,b)=>a.distance-b.distance||a.i-b.i);\n const target=edges.find(e=>e.distance<=8*scale)||edges.find(e=>e.inside);\n if(target)showObject(event,target.i);else hideTooltip();\n });\n svg.addEventListener('pointerleave', hideTooltip);\n\n const reduced=matchMedia('(prefers-reduced-motion: reduce)');let visible=false,armed=true,lastWidth=0;\n function replay(){if(visible&&armed&&!document.hidden){armed=false;draw(!reduced.matches);}}\n const observer=new IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){visible=false;armed=true;}else if(e.intersectionRatio>=.2){visible=true;replay();}},{threshold:[0,.2]});\n document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else replay();});\n root.addEventListener('pointerleave',hideTooltip);window.addEventListener('blur',hideTooltip);\n const resizeObserver = new PresetResizeObserver(()=>{const w=Math.round(plot.getBoundingClientRect().width);if(w!==lastWidth){lastWidth=w;draw();}});\n resizeObserver.observe(plot);draw();observer.observe(root);\n root.dispatchEvent(new Event('qv:ready',{bubbles:true}));\n})();\n \n}\nasync function renderMermaidPie(root,data) {\n root.dataset.renderer='qv-pie';root.classList.add('qv-pie');\n const config={title:root.querySelector('h3').textContent||data.title,items:data.items.map(d=>({label:d.name,value:d.value,description:d.description})),decimals:data.items.every(d=>Number.isInteger(d.value))?0:2,showData:data.showData,exactValues:true,showTooltip:false,valuePrefix:'',valueSuffix:''};\n await renderMermaidDonutPreset(root,config,data.items.map(d=>`${d.name}:${d.value}`).join(';'));root.dataset.state='enhanced';\n const style=document.createElement('style');style.textContent=`#${root.id} .legend button{max-width:100%;min-width:0;text-align:left}#${root.id} .legend .name{min-width:0;overflow-wrap:anywhere}#${root.id} .legend .percent{flex:none}#${root.id} .center-value{line-height:1.2}#${root.id} .center-label{max-width:140px;overflow-wrap:anywhere}`;root.append(style);\n const number=root.querySelector('.center-value'),ctx=document.createElement('canvas').getContext('2d');ctx.font='500 32px '+getComputedStyle(root).fontFamily;const fit=()=>number.style.fontSize=Math.min(32,32*136/Math.max(1,ctx.measureText(number.textContent).width))+'px';fit();new MutationObserver(fit).observe(number,{childList:true,characterData:true,subtree:true});\n if(data.showData)root.querySelectorAll('.legend .percent').forEach((n,i)=>n.textContent=data.items[i].value+' · '+n.textContent);\n}\n\n// 使用固定版本的真实列与节点数据;不从源码重新编写看板。\nasync function readMermaidKanbanModel(db,descriptions={}) {\n if(!db.getData||!db.getSections)return {reason:'Kanban 数据接口不兼容'};\n const raw=db.getData(),nodes=raw.nodes;\n if(raw.config?.kanban?.ticketBaseUrl)return {reason:'票据外部链接保留原生行为'};\n if(nodes.some(n=>n.icon||hasRichMermaidLabel(n.label)))return {reason:'图标或富文本看板保留原生语义'};\n const columns=nodes.filter(n=>n.isGroup).map(n=>({id:n.id,name:n.label,tasks:nodes.filter(t=>t.parentId===n.id).map(t=>({id:t.id,name:t.label,assigned:t.assigned||'',ticket:t.ticket||'',priority:t.priority||'',description:descriptions[t.id]||''}))}));\n if(!columns.length||nodes.some(n=>!n.isGroup&&!columns.some(c=>c.id===n.parentId)))return {reason:'看板层级保留原生语义'};\n const tasks=columns.flatMap(c=>c.tasks),unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw Error('说明引用了未知卡片:'+unknown.join('、'));\n return {data:{kind:'kanban',columns}};\n}\n\nfunction renderMermaidKanban(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll');scene.querySelector('svg').setAttribute('hidden','');root.querySelector('.edge-key').hidden=true;\n if(!window.lucide)throw Error('看板的 Lucide 图标尚未就绪');\n root.classList.add('qv-kanban');root.dataset.state='enhanced';root.dataset.renderer='qv-kanban';viewport.setAttribute('aria-label','看板:按源码阶段排列的任务,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} .kanban-column{position:absolute;top:0;box-sizing:border-box;padding:12px;border-radius:12px;background:color-mix(in srgb,var(--kanban-tone) 6%,var(--background))}\n #${root.id} .kanban-heading{display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:13px;line-height:20px;font-weight:500}\n #${root.id} .kanban-category{display:flex;align-items:center;gap:6px;min-width:0;max-width:calc(100% - 32px);padding:3px 9px;border-radius:999px;background:color-mix(in srgb,var(--kanban-tone) 15%,var(--background));color:color-mix(in srgb,var(--kanban-tone) 55%,var(--foreground))}\n #${root.id} .kanban-category-dot{width:7px;height:7px;border-radius:50%;background:var(--kanban-tone);flex:none}\n #${root.id} .kanban-category-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n #${root.id} .kanban-heading .count{color:color-mix(in srgb,var(--kanban-tone) 55%,var(--foreground));font-size:12px;font-weight:400;flex:none;font-variant-numeric:tabular-nums}\n #${root.id} .kanban-task{display:block;width:100%;box-sizing:border-box;margin:10px 0 0;padding:14px;text-align:left;border:1px solid transparent;border-radius:9px;background:var(--background);color:var(--foreground);font:inherit;cursor:var(--cursor-interaction,pointer);box-shadow:none}\n #${root.id} .kanban-task[data-active=true],#${root.id} .kanban-task:focus-visible{border-color:var(--viz-series-1);outline:none}\n #${root.id} .kanban-name{display:block;font-size:16px;line-height:24px;font-weight:500;overflow-wrap:anywhere}\n #${root.id} .kanban-description,#${root.id} .kanban-ticket{display:block;margin-top:10px;font-size:12px;line-height:18px;color:color-mix(in srgb,var(--foreground) 64%,var(--background));overflow-wrap:anywhere}\n #${root.id} .kanban-meta{display:flex;align-items:center;gap:10px 12px;flex-wrap:wrap;font-size:12px;line-height:18px;color:color-mix(in srgb,var(--foreground) 76%,var(--background));margin-top:12px}\n #${root.id} .kanban-assignee{display:flex;align-items:center;gap:6px;min-width:0;max-width:100%;overflow-wrap:anywhere}\n #${root.id} .kanban-assignee svg{--icon-size:14px;width:14px;height:14px;flex:none;stroke-width:1.6}\n #${root.id} .kanban-assignee-name{min-width:0;overflow-wrap:anywhere}\n #${root.id} .kanban-priority{display:flex;align-items:center;gap:6px;flex:none;max-width:100%;overflow-wrap:anywhere}\n #${root.id} .kanban-priority-meter{display:flex;gap:2px;flex:none}\n #${root.id} .kanban-priority-segment{width:6px;height:10px;border-radius:2px;background:color-mix(in srgb,var(--foreground) 12%,var(--background))}\n #${root.id} .kanban-priority-segment[data-filled=true]{background:var(--viz-series-1)}\n `;root.append(style);\n const make=(tag,className,text)=>{const n=document.createElement(tag);n.className=className;if(text!==undefined)n.textContent=text;return n;};\n const tones=[3,6,2,1,4,5],levels={'Very Low':1,'Low':2,'High':3,'Very High':4},objects=[];\n const columns=data.columns.map((c,column)=>{\n const box=make('div','kanban-column');box.dataset.column=c.id;box.setAttribute('role','group');box.setAttribute('aria-label',c.name);box.style.setProperty('--kanban-tone',`var(--viz-series-${tones[column%tones.length]})`);\n const header=make('div','kanban-heading'),category=make('span','kanban-category'),letters=Array.from(c.name);\n // 展示限制不改源码类别或列归属,完整名称保留在可访问标签。\n category.setAttribute('aria-label',c.name);category.append(make('span','kanban-category-dot'),make('span','kanban-category-name',letters.length>12?letters.slice(0,11).join('')+'…':c.name));header.append(category,make('span','count',String(c.tasks.length)));box.append(header);scene.append(box);\n c.tasks.forEach((t,i)=>{\n const b=make('button','kanban-task');b.type='button';b.dataset.task=t.id;b.append(make('span','kanban-name',t.name));\n if(t.description)b.append(make('span','kanban-description',t.description));\n if(t.ticket)b.append(make('span','kanban-ticket',t.ticket));\n const meta=make('span','kanban-meta');\n if(t.assigned){const person=make('span','kanban-assignee'),icon=window.lucide.createElement(window.lucide.icons.UserRound);icon.classList.add('lucide');icon.setAttribute('aria-hidden','true');person.append(icon,make('span','kanban-assignee-name',t.assigned));meta.append(person);}\n if(t.priority){const priority=make('span','kanban-priority'),level=levels[t.priority];priority.dataset.priority=t.priority;priority.setAttribute('aria-label','优先级:'+t.priority);if(level){const meter=make('span','kanban-priority-meter');meter.setAttribute('aria-hidden','true');for(let j=0;j<4;j++){const segment=make('span','kanban-priority-segment');segment.dataset.filled=String(j<level);meter.append(segment);}priority.append(meter);}priority.append(make('span','kanban-priority-name',t.priority));meta.append(priority);}\n if(meta.childNodes.length)b.append(meta);\n b.setAttribute('aria-label',[c.name,t.name,t.description,t.ticket,t.assigned&&'负责人:'+t.assigned,t.priority&&'优先级:'+t.priority].filter(Boolean).join(' · '));box.append(b);objects.push({b,t,i,column});\n });return {box,header,c};\n });\n const view=createMermaidViewport(root);\n function layout(){const width=view.expanded?248:272,gap=16;columns.forEach((c,i)=>{c.box.style.width=width+'px';c.box.style.left=12+i*(width+gap)+'px';c.header.style.height='auto';});const headerHeight=Math.max(...columns.map(c=>c.header.offsetHeight));columns.forEach(c=>c.header.style.height=headerHeight+'px');scene.style.width=scene.style.minWidth=24+columns.length*(width+gap)-gap+'px';scene.style.height=Math.max(...columns.map(c=>c.box.offsetHeight))+12+'px';view.refresh();}\n function clear(){objects.forEach(o=>o.b.removeAttribute('data-active'));}\n function show(o){clear();o.b.dataset.active='true';}\n objects.forEach(o=>{o.b.onpointerenter=o.b.onclick=()=>show(o);o.b.onpointerleave=()=>{if(!o.b.matches(':focus-visible'))o.b.removeAttribute('data-active');};o.b.onfocus=()=>{const v=viewport.getBoundingClientRect(),r=o.b.getBoundingClientRect();if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;show(o);};o.b.onblur=()=>o.b.removeAttribute('data-active');});viewport.addEventListener('scroll',clear,{passive:true});root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n layout();root.addEventListener('qv:display-mode',()=>{clear();layout();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let armed=true,running=[];\n function cancel(){running.forEach(a=>a.cancel());running=[];}\n function replay(){cancel();if(reduced.matches||view.expanded)return;columns.forEach((c,i)=>running.push(c.header.animate([{opacity:0,transform:'translateY(8px)'},{opacity:1,transform:'translateY(0)'}],{duration:300,delay:i*120,fill:'backwards'})));objects.forEach(o=>running.push(o.b.animate([{opacity:0,transform:'translateY(8px)'},{opacity:1,transform:'translateY(0)'}],{duration:240,delay:o.column*120+o.i*70+80,fill:'backwards'})));root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});root.addEventListener('qv:display-mode',cancel);reduced.addEventListener('change',cancel);\n let lastWidth=root.clientWidth,frame=0;new ResizeObserver(()=>{if(lastWidth===root.clientWidth||frame)return;frame=requestAnimationFrame(()=>{frame=0;const width=root.clientWidth;if(!root.isConnected||lastWidth===width)return;lastWidth=width;cancel();clear();layout();});}).observe(root);\n}\n\nfunction readMermaidGanttModel(db,descriptions={}){\n if(!db.getTasks)return {reason:'Gantt 数据接口不兼容'};\n if(db.getExcludes?.().length||db.getIncludes?.().length||db.endDatesAreInclusive?.()||Object.keys(db.getLinks?.()||{}).length)return {reason:'Gantt 排除日期、含结束日期或交互链接保留原生语义'};\n const marker=db.getTodayMarker?.();if((marker&&marker!=='off')||/[HhmsXx]/.test(db.getDateFormat?.()||''))return {reason:'Gantt 自定义今日标记或时分秒排期保留原生语义'};\n const tasks=db.getTasks().map(t=>({...t,task:t.task.trim(),startTime:new Date(t.startTime),endTime:new Date(t.endTime),description:descriptions[t.id]||'',dependencies:t.raw?.startTime?.startData?.startsWith('after ')?t.raw.startTime.startData.slice(6).trim().split(/\\s+/):[]}));\n if(!tasks.length||tasks.some(t=>!Number.isFinite(+t.startTime)||!Number.isFinite(+t.endTime)||t.endTime<t.startTime))return {reason:'Gantt 任务日期无法确定'};\n if(tasks.some(t=>hasRichMermaidLabel(t.task)||hasRichMermaidLabel(t.section)||t.classes?.length||t.dependencies.some(id=>!tasks.some(n=>n.id===id))))return {reason:'Gantt 富文本、样式或未匹配依赖保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw Error('说明引用了未知任务:'+unknown.join('、'));\n return {data:{kind:'gantt',tasks,title:db.getDiagramTitle?.()||'',today:marker!=='off',sections:[...new Set(tasks.map(t=>t.section))]}};\n}\n\n// 接入已审视的时间尺、阶段条与里程碑胶囊,不复制模拟数据。\nasync function renderMermaidGantt(root,data){\n const d3=window.__mcxQVRuntime.d3;\n const heading=root.querySelector('h3');if(heading.hidden&&data.title){heading.textContent=data.title;heading.hidden=false;}\n root.dataset.state='enhanced';root.dataset.renderer='qv-gantt';root.querySelector('.edge-key').hidden=true;\n const viewport=root.querySelector('.diagram-scroll'),plot=root.querySelector('.scene');plot.replaceChildren();\n viewport.setAttribute('aria-label','甘特图:任务起止日期与里程碑,可横向滚动');\n const chart=document.createElementNS('http://www.w3.org/2000/svg','svg');chart.classList.add('chart');const hits=document.createElement('div');hits.className='hits';plot.append(chart,hits);const svg=d3.select(chart);\n const bubble=document.createElement('div');bubble.className='bubble';bubble.setAttribute('role','tooltip');bubble.hidden=true;bubble.innerHTML='<div class=\"bubble-head\"><i class=\"swatch\" aria-hidden=\"true\"></i><span></span></div><div class=\"bubble-rows\"></div><div class=\"bubble-dependency\"></div>';root.append(bubble);\n const style=document.createElement('style');style.textContent=`\n#${root.id}{position:relative;width:100%;padding-top:32px;color:var(--foreground);--tip-radius:12px;--title-gap:32px}\n#${root.id} h3{margin:0 90px var(--title-gap) 0;font-weight:500}\n#${root.id} .plot{position:relative}\n#${root.id} .chart .group-label{font-size:12px;font-weight:400;fill:var(--muted-foreground)}\n#${root.id} .chart .month-label{font-size:14px;font-weight:500}\n#${root.id} .dependency{fill:none;stroke:color-mix(in srgb,var(--muted-foreground) 38%,transparent);stroke-width:1;transition:opacity 160ms ease}\n#${root.id} .dependency-arrow{fill:color-mix(in srgb,var(--muted-foreground) 38%,transparent)}\n#${root.id} .milestone-guide{stroke:color-mix(in srgb,var(--muted-foreground) 35%,transparent);stroke-width:1;stroke-dasharray:3 5}\n#${root.id} .chart{display:block;width:100%;overflow:visible}\n#${root.id} .chart text{font:inherit;font-size:12px;fill:var(--foreground);font-variant-numeric:tabular-nums}\n#${root.id} .chart .tick-label{fill:var(--muted-foreground)}\n#${root.id} .task{transition:opacity 160ms ease}\n#${root.id} .swatch{display:inline-block;width:9px;height:9px;border-radius:2px;flex-shrink:0}\n#${root.id} .bubble{position:absolute;z-index:3;pointer-events:none;width:206px;max-width:calc(100% - 16px);box-sizing:border-box;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);border:1px solid color-mix(in srgb,var(--border) 70%,transparent);border-radius:var(--tip-radius);box-shadow:0 4px 16px color-mix(in srgb,var(--foreground) 10%,transparent)}\n#${root.id} .bubble[hidden]{display:none}\n#${root.id} .bubble-head{display:flex;gap:8px;align-items:center;margin-bottom:10px;font-weight:500}\n#${root.id} .bubble-row{display:flex;justify-content:space-between;gap:14px;margin-top:7px;font-size:12px}\n#${root.id} .bubble-row span{color:var(--muted-foreground)}\n#${root.id} .bubble-row strong{font-weight:400;font-variant-numeric:tabular-nums;text-align:right}\n#${root.id} .bubble-dependency{border-top:1px solid var(--border);margin-top:10px;padding-top:10px;font-size:12px}\n#${root.id} .bubble-dependency[hidden]{display:none}\n#${root.id} .hits{position:absolute;inset:0;pointer-events:none}\n#${root.id} .task-hit{position:absolute;pointer-events:auto;opacity:0;margin:0;cursor:var(--cursor-interaction,pointer)}\n#${root.id} .task-hit:focus-visible{opacity:1;background:transparent;outline:2px solid var(--ring);outline-offset:2px}\n@media(prefers-reduced-motion:reduce){#${root.id} .task{transition:none}}\n\n#${root.id} .scene{margin:0;min-width:0} #${root.id} .bubble-head span{overflow-wrap:anywhere} #${root.id} .task-hit{border:0;background:none;min-height:0;padding:0}\n`;root.append(style);\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),measure=document.createElement('canvas').getContext('2d'),view=createMermaidViewport(root,{expandedSizing:'hug'});\n const day=86400000,date=d=>`${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}`,tasks=data.tasks,sections=data.sections;\n let layout=null,lastWidth=0,resizeFrame=0,hover=null,inView=false,armed=true;\n const color=t=>`var(--viz-series-${sections.indexOf(t.section)%6+1})`;\nfunction clear(){hover=null;paint();}\nfunction linesFor(text,width){\n if(measure.measureText(text).width<=width)return [text];\n if(width<measure.measureText('…').width)return [];\n const chars=Array.from(text),lines=[];\n for(let line=0;line<2&&chars.length;line++){\n let value='';while(chars.length&&measure.measureText(value+chars[0]).width<=width)value+=chars.shift();\n if(line===1&&chars.length){while(value&&measure.measureText(value+'…').width>width)value=value.slice(0,-1);value+='…';}\n if(!value){lines.push('…');break;}lines.push(value);\n }\n return lines;\n}\nfunction paint(){if(!layout)return;svg.selectAll('.task').style('opacity',t=>hover!==null?(t.id===hover?1:.45):1);svg.selectAll('.dependency-group').style('opacity',e=>hover===null||e.from.id===hover||e.to.id===hover?1:.16);if(hover===null)bubble.hidden=true;}\nfunction show(t,event){\n hover=t.id;paint();root.querySelector('.bubble-head span').textContent=t.task;root.querySelector('.bubble-head .swatch').style.background=color(t);\n const rows=root.querySelector('.bubble-rows');rows.replaceChildren();\n const values=t.milestone?[['日期',date(t.startTime)],['类型','里程碑']]:[['开始',date(t.startTime)],['结束',date(t.endTime)],['时长',`${Math.round((t.endTime-t.startTime)/day)} 自然日`],['状态',t.done?'已完成':t.active?'进行中':'未开始']];\n if(t.description)values.push(['说明',t.description]);\n if(t.crit)values.push(['标记','关键任务']);\n values.forEach(([label,value])=>{const row=document.createElement('div');row.className='bubble-row';const name=document.createElement('span'),valueNode=document.createElement('strong');name.textContent=label;valueNode.textContent=value;row.append(name,valueNode);rows.append(row);});\n const dependency=root.querySelector('.bubble-dependency');dependency.hidden=!t.dependencies.length;dependency.textContent=t.dependencies.length?'前置任务:'+t.dependencies.map(id=>tasks.find(q=>q.id===id).task).join('、'):'';\n bubble.hidden=false;\n const box=layout.boxes.get(t.id),rb=root.getBoundingClientRect(),pb=plot.getBoundingClientRect(),bw=bubble.offsetWidth,bh=bubble.offsetHeight,hit=[...hits.children].find(n=>n.dataset.task===t.id),hb=hit.getBoundingClientRect();\n const px=event?.clientX===undefined?(hb.left+hb.right)/2-rb.left:event.clientX-rb.left,py=hb.top-rb.top;\n const candidates=[{left:px+16,top:py-bh-14},{left:px-bw-16,top:py-bh-14},{left:px+16,top:hb.bottom-rb.top+12},{left:px-bw-16,top:hb.bottom-rb.top+12}];\n const own={left:hb.left-rb.left,right:hb.right-rb.left,top:hb.top-rb.top,bottom:hb.bottom-rb.top};\n const overlap=(a,b)=>Math.max(0,Math.min(a.right,b.right)-Math.max(a.left,b.left))*Math.max(0,Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top));\n const best=candidates.map(c=>{const left=Math.max(8,Math.min(rb.width-bw-8,c.left)),top=Math.max(0,Math.min(root.offsetHeight-bh-8,c.top));return {left,top,score:overlap({left,right:left+bw,top,bottom:top+bh},own)};}).sort((a,b)=>a.score-b.score)[0];\n bubble.style.left=best.left+'px';bubble.style.top=best.top+'px';\n}\nfunction draw(animate=false){\n const width=Math.max(320,viewport.clientWidth);plot.style.width=plot.style.minWidth=width+'px';if(!width)return;lastWidth=width;clear();\n measure.font='12px '+getComputedStyle(root).fontFamily;\n const narrow=width<500,row=narrow?60:44,left=8,right=14,top=56;\n const x=d3.scaleTime().domain([d3.min(tasks,t=>t.startTime),d3.max(tasks,t=>t.endTime)]).range([left,width-right]);\n const milestoneBoxes=new Map(),tiers=[];for(const t of tasks.filter(t=>t.milestone)){const lines=linesFor(t.task,Math.min(160,width-left-right-16)),bw=Math.min(176,Math.max(32,...lines.map(line=>measure.measureText(line).width))+16),bx=Math.max(left,Math.min(width-right-bw,x(t.startTime)-bw/2));let tier=tiers.findIndex(end=>end+8<=bx);if(tier<0)tier=tiers.length;tiers[tier]=bx+bw;milestoneBoxes.set(t.id,{lines,bw,bx,y:top+4+tier*44});}\n const placed=new Map(),groupRows=[];let cursor=top+Math.max(56,tiers.length*44+12);\n sections.forEach((section,index)=>{const entries=tasks.filter(t=>t.section===section&&!t.milestone);groupRows.push({section,y:cursor,index});cursor+=16;entries.forEach(t=>{const a=x(t.startTime),b=x(t.endTime),lines=linesFor(t.task,Math.max(0,b-a-16)),barHeight=lines.length>1?40:32;placed.set(t.id,{y:cursor,a,b,lines,barHeight,center:cursor+barHeight/2});cursor+=row;});cursor+=16;});\n const base=cursor-4,height=base+14;\n layout={width,height,base,x,boxes:new Map()};svg.selectAll('*').interrupt();svg.selectAll('*').remove();hits.replaceChildren();svg.attr('viewBox',`0 0 ${width} ${height}`).attr('height',height);\n const start=x.domain()[0],end=x.domain()[1],span=Math.round((end-start)/day),ticks=[0];for(let d=7;d<span;d+=7)ticks.push(d);ticks.push(span);\n // 刻度随容器减量,保留实际项目开始与结束日期。\n const tickDates=[...new Set(ticks)].map(n=>new Date(+start+n*day));\n svg.append('text').attr('x',left).attr('y',14).attr('class','month-label').text(`${start.getFullYear()} 年 ${start.getMonth()+1} 月`+(start.getMonth()!==end.getMonth()||start.getFullYear()!==end.getFullYear()?` — ${end.getFullYear()} 年 ${end.getMonth()+1} 月`:''));\n const visibleDates=tickDates;const today=new Date();if(data.today&&today>=start&&today<=end){svg.append('line').attr('class','today-marker').attr('x1',x(today)).attr('x2',x(today)).attr('y1',top).attr('y2',base).attr('stroke','var(--muted-foreground)').attr('stroke-dasharray','3 5');svg.append('text').attr('x',Math.max(left,Math.min(width-right-24,x(today)+4))).attr('y',top+16).text('今天');}\n const days=d3.timeDay.range(start,d3.timeDay.offset(end,1)),majorDays=new Set([...visibleDates.map(Number),...tasks.filter(t=>t.milestone).map(t=>+t.startTime)]);\n svg.append('line').attr('class','ruler-baseline').attr('x1',left).attr('x2',width-right).attr('y1',top).attr('y2',top).attr('stroke','var(--border)').attr('stroke-opacity',.85);\n svg.selectAll('.day-tick').data(days).join('line').attr('class','day-tick').attr('x1',x).attr('x2',x).attr('y1',d=>majorDays.has(+d)?top-10:top-5).attr('y2',top).attr('stroke','var(--muted-foreground)').attr('stroke-opacity',d=>majorDays.has(+d)?.55:.28);\n svg.selectAll('.grid').data(visibleDates.filter(d=>+d!==+start&&+d!==+end)).join('line').attr('class','grid').attr('x1',x).attr('x2',x).attr('y1',top).attr('y2',base).attr('stroke','var(--border)').attr('stroke-opacity',.35);\n svg.selectAll('.date-label').data(visibleDates).join('text').attr('class','date-label').attr('x',x).attr('y',41).attr('text-anchor',d=>+d===+start?'start':+d===+end?'end':'middle').text(d=>String(d.getDate()).padStart(2,'0'));\n groupRows.forEach(g=>{if(g.index)svg.append('line').attr('x1',left).attr('x2',width-right).attr('y1',g.y-16).attr('y2',g.y-16).attr('stroke','var(--border)').attr('stroke-opacity',.45);svg.append('text').attr('class','group-label').attr('x',left).attr('y',g.y-1).text(g.section);});\n const edges=tasks.flatMap(to=>to.dependencies.map(id=>({from:tasks.find(t=>t.id===id),to}))).filter(e=>!e.from.milestone&&!e.to.milestone&&+e.to.startTime>+e.from.endTime);\n const wires=svg.append('g').attr('class','wires');edges.forEach(e=>{const from=placed.get(e.from.id),to=placed.get(e.to.id),sx=from.b,sy=from.center,ex=to.a-6,ey=to.center,bend=(ex-sx)*.55,g=wires.append('g').datum(e).attr('class','dependency-group');g.append('path').attr('class','dependency').attr('d',`M${sx},${sy}C${sx+bend},${sy} ${ex-bend},${ey} ${ex},${ey}`);g.append('path').attr('class','dependency-arrow').attr('d',`M${ex},${ey-2.2}L${ex+5},${ey}L${ex},${ey+2.2}Z`);});\n const groups=svg.selectAll('.task').data(tasks).join('g').attr('class','task').attr('data-task',t=>t.id);\n groups.each(function(t,i){\n const g=d3.select(this),a=x(t.startTime),b=x(t.endTime),label=t.task,labelWidth=measure.measureText(label).width;\n let box;\n if(t.milestone){\n const m=milestoneBoxes.get(t.id),bubbleWidth=m.bw,bubbleX=m.bx,bubbleY=m.y,bubbleHeight=m.lines.length>1?40:24;\n g.append('line').attr('class','milestone-guide').attr('x1',a).attr('x2',a).attr('y1',top+2).attr('y2',base);\n g.append('rect').attr('class','milestone-label-bubble').attr('x',bubbleX).attr('y',bubbleY).attr('width',bubbleWidth).attr('height',bubbleHeight).attr('rx',12).attr('fill',color(t)).attr('fill-opacity',.13);\n const caption=g.append('text').attr('class','task-label milestone-label').attr('x',bubbleX+8).attr('y',bubbleY+16);m.lines.forEach((line,i)=>caption.append('tspan').attr('x',bubbleX+8).attr('dy',i?14:0).text(line));\n g.append('path').attr('class','milestone').attr('transform',`translate(${a},${top-5})`).attr('d','M-4,-6H4V-1L0,4L-4,-1Z').attr('fill',color(t));\n box={left:bubbleX-4,right:Math.min(width,Math.max(a+6,bubbleX+bubbleWidth+4)),top:top-12,bottom:bubbleY+bubbleHeight+4};\n }else{\n const p=placed.get(t.id),y=p.y,labelX=a+8;\n const bar=g.append('rect').attr('class','bar').attr('x',a).attr('y',y).attr('height',p.barHeight).attr('rx',3).attr('width',animate&&!reduced.matches?0:b-a).attr('fill',color(t)).attr('fill-opacity',.13);\n g.append('line').attr('class','bar-start').attr('x1',a).attr('x2',a).attr('y1',y+1).attr('y2',y+p.barHeight-1).attr('stroke',color(t)).attr('stroke-width',2);\n const labelNode=g.append('text').attr('class','task-label').attr('x',labelX).attr('y',y+(p.lines.length>1?16:20)).attr('aria-label',t.task);\n p.lines.forEach((line,index)=>labelNode.append('tspan').attr('x',labelX).attr('dy',index?14:0).text(line));\n if(animate&&!reduced.matches){const f=i/Math.max(1,tasks.length-1),weight=f*f*(3-2*f);bar.transition().duration(580+100*weight).delay(0).ease(d3.easeCubicInOut).attr('width',b-a);}\n box={left:a-4,right:Math.min(width-right,b+4),top:p.y-5,bottom:y+p.barHeight+5};\n }\n layout.boxes.set(t.id,box);\n const hit=document.createElement('button');hit.type='button';hit.className='task-hit';hit.dataset.task=t.id;hit.setAttribute('aria-label',`${t.task},${date(t.startTime)}至${date(t.endTime)},${t.milestone?'里程碑':Math.round((t.endTime-t.startTime)/day)+'自然日'}`);Object.assign(hit.style,{left:box.left+'px',top:box.top+'px',width:Math.max(32,box.right-box.left)+'px',height:box.bottom-box.top+'px'});hits.append(hit);\n hit.addEventListener('pointerenter',e=>show(t,e));hit.addEventListener('pointermove',e=>show(t,e));hit.addEventListener('pointerleave',clear);hit.addEventListener('pointercancel',clear);hit.addEventListener('focus',()=>show(t));hit.addEventListener('blur',clear);hit.addEventListener('pointerup',e=>{if(e.pointerType==='touch')clear();});\n });\n paint();plot.style.height=height+'px';view.refresh();\n}\n\n function replayIfReady(){if(inView&&armed&&!document.hidden&&!view.expanded){armed=false;draw(!reduced.matches);root.dataset.replays=String((+root.dataset.replays||0)+1);}}\n draw();\n new IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){inView=false;armed=true;clear();}else if(e.intersectionRatio>=.2){inView=true;replayIfReady();}},{threshold:[0,.2]}).observe(root);\n new ResizeObserver(()=>{if(viewport.clientWidth===lastWidth||resizeFrame)return;resizeFrame=requestAnimationFrame(()=>{resizeFrame=0;if(Math.max(320,viewport.clientWidth)!==lastWidth)draw();});}).observe(viewport);\n root.addEventListener('qv:display-mode',()=>{svg.selectAll('*').interrupt();clear();draw();});\n reduced.addEventListener('change',()=>{svg.selectAll('*').interrupt();draw();});\n document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;clear();}else replayIfReady();});\n root.addEventListener('pointerleave',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});viewport.addEventListener('scroll',clear,{passive:true});view.refresh();\n}\n\nfunction readMermaidMindmapModel(db,descriptions={}){\n if(!db.getMindmap)return {reason:'Mindmap 数据接口不兼容'};\n const tree=db.getMindmap(),nodes=[];if(!tree)return {reason:'Mindmap 缺少根节点'};\n function visit(n,parent=null,depth=0){const id=String(n.id);nodes.push({id,sourceId:n.nodeId,label:String(n.descr||''),parent,depth,children:n.children||[],description:descriptions[n.nodeId]||descriptions[id]||'',icon:n.icon,classes:n.cssClasses});for(const c of n.children||[])visit(c,id,depth+1);}\n visit(tree);\n if(nodes.some(n=>n.icon||n.classes||hasRichMermaidLabel(n.label)))return {reason:'Mindmap 自定义图标、样式或富文本保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!nodes.some(n=>n.id===id||n.sourceId===id));if(unknown.length)throw Error('说明引用了未知主题:'+unknown.join('、'));\n return {data:{kind:'mindmap',nodes}};\n}\n\n// 复用已审视的双侧树布局;全部层级来自 Mermaid DB。\nfunction renderMermaidMindmap(root,data){\n root.dataset.state='enhanced';root.dataset.renderer='qv-mindmap';root.querySelector('.edge-key').hidden=true;\n const scene=root.querySelector('.scene'),svg=root.querySelector('.wires'),viewport=root.querySelector('.diagram-scroll'),tip=root.querySelector('.node-tip');\n viewport.setAttribute('aria-label','思维导图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n#${root.id}{position:relative;padding-top:32px;color:var(--foreground);width:100%}\n#${root.id} h3{font-weight:500;margin:0 90px 32px 0}\n#${root.id} .diagram-scroll{overflow-x:auto;width:100%;padding-bottom:8px}\n#${root.id} .scene{position:relative;margin:auto;height:364px}\n#${root.id} .wires{position:absolute;inset:0;overflow:visible;pointer-events:none}\n#${root.id} .wire{fill:none;stroke:color-mix(in srgb,var(--muted-foreground) 26%,var(--background));stroke-width:1;transition:opacity 140ms}\n#${root.id} button.node{position:absolute;display:flex;align-items:center;justify-content:center;font:inherit;font-size:12px;color:var(--foreground);background:var(--background);border:1px solid var(--border);border-radius:10px;padding:12px 14px;box-sizing:border-box;white-space:normal;overflow-wrap:anywhere;line-height:1.6;transition:opacity 140ms,border-color 140ms;cursor:var(--cursor-interaction,pointer)}\n#${root.id} button.node.root{font-size:14px;font-weight:500;background:color-mix(in srgb,var(--muted) 30%,var(--background))}\n#${root.id} button.node.leaf,#${root.id} button.node.twig{border:0;background:transparent;justify-content:flex-start;text-align:left;padding:6px 12px} #${root.id} button.node.twig{font-weight:500}\n#${root.id} button.node.leaf.left,#${root.id} button.node.twig.left{justify-content:flex-end;text-align:right}\n#${root.id} button.node:hover{border-color:var(--muted-foreground)}\n#${root.id} button.node.dim{opacity:.22}\n#${root.id} .wire.dim{opacity:.055}\n#${root.id} .wire.active{stroke:var(--muted-foreground)}\n#${root.id} .error{color:var(--muted-foreground)}\n@media(prefers-reduced-motion:reduce){#${root.id} button.node,#${root.id} .wire{transition:none}}\n\n#${root.id} button.node{min-width:0;transform:none;text-align:center} #${root.id} .scene{min-width:0;margin:0} #${root.id} button.node-tip{max-width:260px}\n`;root.append(style);\n const NS='http://www.w3.org/2000/svg',reduce=matchMedia('(prefers-reduced-motion:reduce)'),view=createMermaidViewport(root),isExpanded=()=>view.expanded;\n const model=data.nodes;let animations=[],visible=false,replayArmed=true,viewGeometry=null,inlineOffset=0;\nfunction highlight(id){const target=model.find(n=>n.id===id);if(target.description){tip.textContent=target.description;tip.hidden=false;tip.style.left='8px';tip.style.top='48px';}else tip.hidden=true;const active=new Set();let n=target;while(n){active.add(n.id);n=model.find(t=>t.id===n.parent);}const descendants=p=>{for(const c of model.filter(t=>t.parent===p)){active.add(c.id);descendants(c.id);}};descendants(id);scene.querySelectorAll('.node').forEach(b=>b.classList.toggle('dim',!active.has(b.dataset.id)));svg.querySelectorAll('.wire').forEach(p=>{const a=active.has(p.dataset.from)&&active.has(p.dataset.to);p.classList.toggle('dim',!a);p.classList.toggle('active',a);});}\nfunction clear(){tip.hidden=true;scene.querySelectorAll('.dim,.active').forEach(n=>n.classList.remove('dim','active'));}\nfunction replay(){if(reduce.matches||isExpanded())return;animations.forEach(a=>a.cancel());animations=[];scene.querySelectorAll('.node').forEach(b=>{animations.push(b.animate([{transform:'scale(.08)',opacity:0},{transform:'scale(1)',opacity:1}],{duration:240,delay:+b.dataset.depth*220,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));});svg.querySelectorAll('.wire').forEach(p=>{const length=p.getTotalLength();animations.push(p.animate([{strokeDasharray:`${length} ${length}`,strokeDashoffset:length},{strokeDasharray:`${length} ${length}`,strokeDashoffset:0}],{duration:650,delay:+p.dataset.depth*220,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));});root.dataset.replays=String(+(root.dataset.replays||0)+1);}\nfunction draw(){\n const viewOffset=viewGeometry?.mode==='inline'?viewport.scrollLeft+viewGeometry.viewportWidth/2-viewGeometry.rootCenter:inlineOffset;\n if(viewGeometry?.mode==='inline')inlineOffset=viewOffset;\n scene.style.transform='';scene.style.marginLeft='';viewport.style.height='';\n animations.forEach(a=>a.cancel());svg.replaceChildren();scene.querySelectorAll('.node').forEach(n=>n.remove());\n const byId=new Map(model.map(n=>[n.id,n])),buttons=new Map(),sizes=new Map(),spans=new Map(),positions=new Map();\n const canvas=document.createElement('canvas'),context=canvas.getContext('2d');context.font='12px '+getComputedStyle(root).fontFamily;\n for(const n of model){\n const leaf=n.children.length===0,b=document.createElement('button');b.type='button';b.className='node '+(n.depth===0?'root':n.depth===1?'branch':leaf?'leaf':'twig');b.textContent=n.label;b.dataset.id=n.id;b.dataset.depth=n.depth;b.setAttribute('aria-label',n.label+(n.description?' · '+n.description:''));\n const w=n.depth===0?116:n.depth===1?100:Math.min(140,Math.max(76,Math.ceil(context.measureText(n.label).width)+32));\n Object.assign(b.style,{width:w+'px',minHeight:'44px',height:'auto',visibility:'hidden'});scene.append(b);\n const h=Math.max(44,Math.ceil(b.offsetHeight));sizes.set(n.id,{w,h});buttons.set(n.id,b);\n b.addEventListener('mouseenter',()=>highlight(n.id));b.addEventListener('mouseleave',clear);b.addEventListener('focus',()=>highlight(n.id));b.addEventListener('blur',clear);\n }\n function span(id){const n=byId.get(id),h=n.children.length?n.children.reduce((s,c)=>s+span(String(c.id)),0)+(n.children.length-1)*6:sizes.get(id).h;spans.set(id,Math.max(sizes.get(id).h,h));return spans.get(id);}\n const top=model[0],branches=model.filter(n=>n.depth===1),sides={left:[],right:[]},totals={left:0,right:0};\n for(const n of branches){span(n.id);const side=totals.left<=totals.right?'left':'right';sides[side].push(n);totals[side]+=spans.get(n.id)+(sides[side].length>1?30:0);}\n const columns={left:[],right:[]};\n function measure(id,side){const n=byId.get(id);columns[side][n.depth]=Math.max(columns[side][n.depth]||0,sizes.get(id).w);for(const c of n.children)measure(String(c.id),side);}\n for(const side of ['left','right'])for(const n of sides[side])measure(n.id,side);\n const gap=28,margin=20,extent=side=>columns[side].slice(1).reduce((s,w)=>s+w+gap,0),leftExtent=extent('left'),rightExtent=extent('right'),rw=sizes.get(top.id).w;\n // 两侧预留相同画布空间,让根节点可停在可见窗口中心。\n const width=Math.max(viewport.clientWidth,2*Math.max(leftExtent,rightExtent)+rw+margin*2),height=Math.max(300,totals.left,totals.right)+32,cx=(width-rw)/2,cy=height/2;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);positions.set(top.id,{x:cx,y:cy-sizes.get(top.id).h/2,...sizes.get(top.id)});\n function place(id,side,start){const n=byId.get(id),s=sizes.get(id),center=start+spans.get(id)/2;\n const offset=gap+columns[side].slice(1,n.depth).reduce((sum,w)=>sum+w+gap,0);\n const x=side==='left'?cx-offset-s.w:cx+rw+offset;positions.set(id,{x,y:center-s.h/2,...s,side});\n if(n.children.length){const kidsHeight=n.children.reduce((sum,c)=>sum+spans.get(String(c.id)),0)+(n.children.length-1)*6;let cursor=center-kidsHeight/2;for(const c of n.children){place(String(c.id),side,cursor);cursor+=spans.get(String(c.id))+6;}}\n }\n for(const side of ['left','right']){let cursor=(height-totals[side])/2;for(const n of sides[side]){place(n.id,side,cursor);cursor+=spans.get(n.id)+30;}}\n for(const n of model){const p=positions.get(n.id),b=buttons.get(n.id);b.classList.toggle('left',p.side==='left');Object.assign(b.style,{left:p.x+'px',top:p.y+'px',height:p.h+'px',visibility:''});\n if(n.parent){const a=positions.get(n.parent),left=p.side==='left',sx=left?a.x:a.x+a.w,ex=left?p.x+p.w:p.x,sy=a.y+a.h/2,ey=p.y+p.h/2,mid=(sx+ex)/2;\n const path=document.createElementNS(NS,'path');path.setAttribute('d',`M${sx},${sy} C${mid},${sy} ${mid},${ey} ${ex},${ey}`);path.setAttribute('class','wire');Object.assign(path.dataset,{from:n.parent,to:n.id,depth:n.depth-1});svg.append(path);}\n }\n view.refresh();\n if(!view.expanded){viewport.scrollLeft=cx+rw/2+viewOffset-viewport.clientWidth/2;}\n viewGeometry={viewportWidth:viewport.clientWidth,rootCenter:cx+rw/2,mode:view.expanded?'fullscreen':'inline'};\n root.dataset.nodes=String(model.length);root.dataset.depth=String(Math.max(...model.map(n=>n.depth))+1);root.dataset.sides=JSON.stringify({left:sides.left.map(n=>n.label),right:sides.right.map(n=>n.label)});\n}\n\n\n draw();let lastWidth=root.clientWidth;\n new ResizeObserver(()=>{if(lastWidth===root.clientWidth)return;lastWidth=root.clientWidth;draw();}).observe(root);\n root.addEventListener('qv:display-mode',()=>{clear();draw();if(!view.expanded&&view.savedInlinePosition)viewport.scrollLeft=view.savedInlinePosition.left;});\n new IntersectionObserver(entries=>{const e=entries[0];if(!e.isIntersecting){visible=false;replayArmed=true;}else if(e.intersectionRatio>=.2){visible=true;if(replayArmed){replay();replayArmed=false;}}},{threshold:[0,.2]}).observe(root);\n document.addEventListener('visibilitychange',()=>{if(!document.hidden&&visible)replay();});\n viewport.addEventListener('scroll',()=>{clear();const focused=document.activeElement;if(scene.contains(focused)&&focused.dataset.id)highlight(focused.dataset.id);},{passive:true});\n root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n reduce.addEventListener('change',()=>{if(reduce.matches)animations.forEach(a=>a.cancel());});view.refresh();\n}\n\n// Journey getTasks 追加共享数组;只读一次,再复制源码的顺序、评分与角色。\nasync function readMermaidJourneyModel(db,descriptions={}) {\n if(!db.getTasks||!db.getActors)return {reason:'Journey 数据接口不兼容'};\n const tasks=db.getTasks().map((t,i)=>({id:'step-'+(i+1),name:t.task.trim(),section:t.section.trim(),score:t.score,actors:[...t.people].filter(Boolean),description:descriptions['step-'+(i+1)]||''}));\n if(!tasks.length||tasks.some(t=>!Number.isFinite(t.score)||t.score<1||t.score>5))return {reason:'体验评分超出1–5范围,保留原生语义'};\n if(tasks.some(t=>[t.name,t.section,...t.actors].some(hasRichMermaidLabel)))return {reason:'此用户旅程包含富文本,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw new Error('说明引用了未知步骤:'+unknown.join('、'));\n return {data:{kind:'journey',tasks,actors:[...db.getActors()].filter(Boolean),title:db.getDiagramTitle?.()||''}};\n}\n\nfunction renderMermaidJourney(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-journey');root.dataset.state='enhanced';root.dataset.renderer='qv-journey';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','用户旅程:步骤、体验曲线与角色参与,可横向滚动');\n if(data.title&&root.querySelector('h3').hidden){root.querySelector('h3').textContent=data.title;root.querySelector('h3').hidden=false;}\n if(!window.lucide)throw Error('用户旅程的 Lucide 图标尚未就绪');\n const frame=document.createElement('div'),reference=document.createElement('div'),axis=document.createElementNS(NS,'svg');frame.className='journey-frame';reference.className='journey-reference';axis.classList.add('journey-reference-axis');reference.append(axis);viewport.before(frame);frame.append(reference,viewport);\n const style=document.createElement('style');style.textContent=`\n #${root.id}{--journey-low:light-dark(#cf6f68,#e79b94);--journey-mid:light-dark(#b49a49,#d8c175);--journey-high:light-dark(#6b9b7a,#96c5a4);--journey-block:color-mix(in srgb,var(--muted) 18%,var(--background));--journey-role-bar:color-mix(in srgb,var(--foreground) 9%,var(--background))}\n #${root.id} .journey-frame{display:flex;align-items:flex-start}\n #${root.id} .journey-frame>.diagram-scroll{flex:1;min-width:0}\n #${root.id} .scene{margin:0}\n #${root.id} .journey-reference{position:relative;width:112px;flex:none;background:var(--background)}\n #${root.id} .journey-reference-axis{position:absolute;inset:0;width:112px;overflow:visible;pointer-events:none}\n #${root.id} .journey-column{position:absolute;top:0;z-index:2;min-width:0;min-height:0;margin:0;padding:0;border:0;border-radius:8px;background:transparent;color:var(--foreground);font:inherit;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .journey-column:focus-visible{outline:1px solid var(--ring);outline-offset:-2px}\n #${root.id} .journey-step{position:absolute;left:8px;right:8px;display:flex;align-items:center;flex-direction:column;gap:10px;font-size:12px;line-height:20px;font-weight:400;text-align:center;white-space:normal;overflow-wrap:anywhere}\n #${root.id} .journey-step-name{display:block;width:100%;text-wrap:balance}\n #${root.id} .journey-step-index{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:1px solid var(--border);border-radius:50%;font-size:11px;font-variant-numeric:tabular-nums;flex:none;color:var(--muted-foreground)}\n #${root.id} .journey-section{position:absolute;top:0;box-sizing:border-box;padding:10px 12px;border-radius:8px;background:linear-gradient(var(--journey-block),var(--journey-block)),var(--background);font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere}\n #${root.id} .journey-section span{position:relative;display:inline-block;overflow-wrap:anywhere}\n #${root.id} .journey-line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .journey-grid{stroke:var(--border);stroke-width:1;fill:none;opacity:.65}\n #${root.id} .journey-axis-label{font-size:12px;fill:var(--muted-foreground)}\n #${root.id} .journey-face{--icon-size:24px;position:absolute;left:50%;display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:var(--background);transform:translate(-50%,-50%);pointer-events:none}\n #${root.id} .journey-face svg{width:24px;height:24px;stroke-width:1.6}\n #${root.id} .journey-actor{position:absolute;left:8px;width:96px;display:flex;align-items:center;gap:8px;box-sizing:border-box;min-height:30px;color:var(--muted-foreground);font-size:12px;line-height:18px;overflow-wrap:anywhere}\n #${root.id} .journey-actor svg{width:16px;height:16px;flex:none;stroke-width:1.6}\n #${root.id} .journey-participation{position:absolute;height:14px;border-radius:5px;background:var(--journey-role-bar);pointer-events:none}\n #${root.id} .journey-column-mask{position:absolute;z-index:3;background:var(--background);opacity:0;pointer-events:none;transition:opacity 140ms ease}\n #${root.id}[data-active-column] .journey-column-mask{opacity:.72}\n #${root.id}[data-active-column] .journey-column-mask[data-selected=true]{opacity:0}\n @media(prefers-reduced-motion:reduce){#${root.id} .journey-column-mask{transition:none}}\n `;root.append(style);\n const icon=name=>{const n=window.lucide.createElement(window.lucide.icons[name]||window.lucide.icons.UserRound);n.setAttribute('aria-hidden','true');n.classList.add('lucide');n.dataset.icon=name;return n;};\n const make=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;};\n const defs=make('defs',{}),tone=make('linearGradient',{id:root.id+'-tone',gradientUnits:'userSpaceOnUse',x1:0,x2:0},defs);\n for(const [offset,color]of [['0%','var(--journey-high)'],['50%','var(--journey-mid)'],['100%','var(--journey-low)']])make('stop',{offset,'stop-color':color},tone);\n const fade=make('linearGradient',{id:root.id+'-fade',x1:0,x2:0,y1:0,y2:1},defs);make('stop',{offset:'0%','stop-color':'white','stop-opacity':'.22'},fade);make('stop',{offset:'55%','stop-color':'white','stop-opacity':'.10'},fade);make('stop',{offset:'100%','stop-color':'white','stop-opacity':'0'},fade);\n const mask=make('mask',{id:root.id+'-area-mask',maskContentUnits:'userSpaceOnUse'},defs),maskRect=make('rect',{fill:`url(#${root.id}-fade)`},mask);\n const grid=Array.from({length:5},(_,i)=>({path:make('path',{class:'journey-grid'}),label:make('text',{class:'journey-axis-label','text-anchor':'end'},axis)}));grid.forEach((g,i)=>g.label.textContent=5-i);\n const quantity=make('text',{class:'journey-axis-label'},axis);quantity.textContent='体验曲线';const stepCaption=make('text',{class:'journey-axis-label'},axis);stepCaption.textContent='用户旅程';\n const area=make('path',{class:'journey-area',fill:`url(#${root.id}-tone)`,mask:`url(#${root.id}-area-mask)`}),line=make('path',{class:'journey-line',stroke:`url(#${root.id}-tone)`});\n const objects=[],roles=[],sections=[];\n let start=0,phase=data.tasks[0].section;for(let i=1;i<=data.tasks.length;i++)if(i===data.tasks.length||data.tasks[i].section!==phase){if(phase){const box=document.createElement('div'),caption=document.createElement('span');box.className='journey-section';caption.textContent=phase;box.append(caption);scene.append(box);sections.push({box,caption,start,end:i});}if(i<data.tasks.length){start=i;phase=data.tasks[i].section;}}\n data.tasks.forEach((t,i)=>{const b=document.createElement('button');b.type='button';b.className='journey-column';b.dataset.step=t.id;b.dataset.score=String(t.score);b.setAttribute('aria-label',[`${i+1},${t.name}`,t.section,`体验评分${t.score}/5`,t.actors.length?'参与角色:'+t.actors.join('、'):'未指定角色',t.description].filter(Boolean).join(' · '));const header=document.createElement('span'),number=document.createElement('span'),name=document.createElement('span'),face=document.createElement('span'),columnMask=document.createElement('div');header.className='journey-step';number.className='journey-step-index';number.textContent=String(i+1);name.className='journey-step-name';name.textContent=t.name;header.append(number,name);face.className='journey-face';face.style.color=t.score===3?'var(--journey-mid)':t.score<3?'var(--journey-low)':'var(--journey-high)';face.append(icon(t.score===3?'Meh':t.score<3?'Frown':'Smile'));b.append(header,face);columnMask.className='journey-column-mask';columnMask.dataset.step=t.id;scene.append(b,columnMask);objects.push({t,b,header,face,mask:columnMask});});\n const iconRules=[[/编辑|作者|editor|writer/i,'PencilLine'],[/审核|review|check/i,'BadgeCheck'],[/运营|市场|market/i,'Megaphone'],[/设计|design/i,'Palette'],[/法务|legal/i,'Scale'],[/策略|strategy/i,'Compass']];\n data.actors.forEach(actor=>{const b=document.createElement('div'),name=document.createElement('span');b.className='journey-actor';b.dataset.actor=actor;name.textContent=actor;b.append(icon(iconRules.find(([re])=>re.test(actor))?.[1]||'UserRound'),name);reference.append(b);const runs=[];for(let i=0;i<objects.length;i++)if(objects[i].t.actors.includes(actor)){const last=runs.at(-1);if(last&&last.end===i)last.end=i+1;else runs.push({start:i,end:i+1});}runs.forEach(run=>{run.b=document.createElement('div');run.b.className='journey-participation';run.b.dataset.actor=actor;run.b.dataset.start=String(run.start);run.b.dataset.end=String(run.end);run.b.setAttribute('aria-hidden','true');scene.append(run.b);});roles.push({actor,b,runs});});\n const view=createMermaidViewport(root);let columnWidth=0,left=0,stepTop=0,plotTop=0,plotBottom=0,width=0,height=0,active=null;\n function clear(){active=null;delete root.dataset.activeColumn;objects.forEach(o=>o.mask.removeAttribute('data-selected'));}\n function select(id){if(active===id)return;active=id;root.dataset.activeColumn=id;objects.forEach(o=>o.mask.dataset.selected=String(o.t.id===id));}\n objects.forEach(o=>{o.b.onpointerenter=o.b.onfocus=o.b.onclick=()=>select(o.t.id);o.b.onblur=clear;});root.addEventListener('pointerleave',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n function placeSections(){sections.forEach(s=>s.caption.style.left=(view.expanded?0:Math.max(0,Math.min(s.box.offsetWidth-24-s.caption.offsetWidth,viewport.scrollLeft-s.left+8)))+'px');}\n function layout(){\n if(view.expanded)scene.append(reference);else frame.prepend(reference);reference.style.position=view.expanded?'absolute':'relative';reference.style.left=reference.style.top='0';\n columnWidth=view.expanded?120:Math.max(76,Math.min(144,(viewport.clientWidth-24)/objects.length));left=view.expanded?124:12;\n sections.forEach(s=>{s.left=left+s.start*columnWidth;s.box.style.left=s.left+4+'px';s.box.style.width=(s.end-s.start)*columnWidth-8+'px';s.caption.style.maxWidth=Math.max(32,Math.min(s.box.offsetWidth-24,viewport.clientWidth-32))+'px';});\n stepTop=Math.max(56,...sections.map(s=>s.box.offsetHeight+18));objects.forEach((o,i)=>{o.b.style.left=left+i*columnWidth+'px';o.b.style.width=columnWidth+'px';o.header.style.top=stepTop+'px';});\n const headerHeight=Math.max(...objects.map(o=>o.header.offsetHeight));plotTop=stepTop+headerHeight+42;plotBottom=plotTop+160;// 曲线块包含底部表情的15px半径;与人员块外缘保持24px。\n let roleBottom=plotBottom+15+24;\n roles.forEach(r=>{const rowHeight=Math.max(14,r.b.offsetHeight);r.y=roleBottom+rowHeight/2;roleBottom+=rowHeight+12;});\n width=left+objects.length*columnWidth+12;height=roles.length?roleBottom-12+16:plotBottom+31;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);reference.style.height=height+'px';axis.style.height=height+'px';axis.setAttribute('viewBox',`0 0 112 ${height}`);\n const x=i=>left+(i+.5)*columnWidth,y=score=>plotBottom-(score-1)*40;objects.forEach((o,i)=>{o.x=x(i);o.y=y(o.t.score);o.face.style.top=o.y+'px';o.b.style.height=height+'px';Object.assign(o.mask.style,{left:left+i*columnWidth+'px',top:stepTop-8+'px',width:columnWidth+'px',height:height-stepTop+8+'px'});});\n // 控制点停留在相邻真实评分之间,不产生超出1–5的过冲。\n const path=objects.map((o,i)=>{if(!i)return `M${o.x} ${o.y}`;const a=objects[i-1],mid=(a.x+o.x)/2;return `C${mid} ${a.y} ${mid} ${o.y} ${o.x} ${o.y}`;}).join(' ');line.setAttribute('d',path);area.setAttribute('d',path+`L${objects.at(-1).x} ${plotBottom}L${objects[0].x} ${plotBottom}Z`);tone.setAttribute('y1',plotTop);tone.setAttribute('y2',plotBottom);for(const [k,v]of Object.entries({x:left,y:plotTop,width:width-left,height:plotBottom-plotTop}))maskRect.setAttribute(k,v);\n grid.forEach((g,i)=>{const yy=plotTop+i*40;g.path.setAttribute('d',`M${view.expanded?112:0} ${yy}H${width-12}`);g.label.setAttribute('x',100);g.label.setAttribute('y',yy+4);});quantity.setAttribute('x',8);quantity.setAttribute('y',plotTop-22);stepCaption.setAttribute('x',8);stepCaption.setAttribute('y',22);\n roles.forEach(r=>{const cy=r.y;r.b.style.top=cy-r.b.offsetHeight/2+'px';r.runs.forEach(run=>Object.assign(run.b.style,{left:left+run.start*columnWidth+6+'px',top:cy-7+'px',width:(run.end-run.start)*columnWidth-12+'px'}));});placeSections();view.refresh();\n }\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let running=[],armed=true;\n function cancelMotion(){running.forEach(a=>a.cancel());running=[];line.style.strokeDasharray='';}\n function replay(){cancelMotion();if(reduced.matches||view.expanded)return;const length=line.getTotalLength();line.style.strokeDasharray=`${length} ${length}`;running.push(line.animate([{strokeDashoffset:length},{strokeDashoffset:0}],{duration:1000,fill:'backwards'}),area.animate([{opacity:0},{opacity:1}],{duration:500,fill:'backwards'}));objects.forEach((o,i)=>running.push(o.face.animate([{opacity:0},{opacity:1}],{duration:200,delay:i/Math.max(1,objects.length-1)*1000,fill:'backwards'})));root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n layout();viewport.addEventListener('scroll',()=>{placeSections();clear();if(document.activeElement?.classList.contains('journey-column'))select(document.activeElement.dataset.step);},{passive:true});root.addEventListener('qv:display-mode',()=>{cancelMotion();clear();layout();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});reduced.addEventListener('change',cancelMotion);\n let lastWidth=root.clientWidth,layoutFrame=0;new ResizeObserver(()=>{if(lastWidth===root.clientWidth||layoutFrame)return;layoutFrame=requestAnimationFrame(()=>{layoutFrame=0;const nextWidth=root.clientWidth;if(!root.isConnected||lastWidth===nextWidth)return;lastWidth=nextWidth;cancelMotion();clear();layout();});}).observe(root);\n}\n\n// Timeline DB 的 getTasks 会追加到共享数组;只读取一次并立即复制。\nasync function readMermaidTimelineModel(db,descriptions={}) {\n if(!db.getTasks||!db.getSections)return {reason:'Timeline 数据接口不兼容'};\n const tasks=db.getTasks().map(t=>({id:String(t.id),name:t.task.trim(),section:t.section.trim(),events:[...t.events].map(v=>v.trim()),description:descriptions[String(t.id)]||''}));\n if(tasks.some(t=>[t.name,t.section,...t.events].some(hasRichMermaidLabel)))return {reason:'此时间轴包含富文本,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw new Error('说明引用了未知时间节点:'+unknown.join('、'));\n return {data:{kind:'timeline',tasks,title:db.getCommonDb?.().getDiagramTitle?.()||''}};\n}\n\nfunction renderMermaidTimeline(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');\n root.classList.add('qv-timeline');root.dataset.state='enhanced';root.dataset.renderer='qv-timeline';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','按源码顺序排列的时间轴,可横向滚动');\n if(data.title&&root.querySelector('h3').hidden){root.querySelector('h3').textContent=data.title;root.querySelector('h3').hidden=false;}\n const style=document.createElement('style');style.textContent=`\n #${root.id} .timeline-column{position:absolute;width:216px;top:42px;box-sizing:border-box;padding:12px;border-radius:12px;background:transparent}\n #${root.id} .timeline-period{display:block;width:100%;min-height:38px;border:0;background:none;text-align:left;font:inherit;font-size:13px;font-weight:500;color:var(--foreground);padding:0 0 10px;line-height:20px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .timeline-events{margin-top:38px;padding-left:22px;display:flex;flex-direction:column;gap:12px}\n #${root.id} .timeline-event{display:block;width:100%;box-sizing:border-box;border:0;border-radius:4px;background:transparent;color:var(--foreground);text-align:left;font:inherit;font-size:14px;line-height:22px;min-height:30px;padding:4px 0;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .timeline-event[data-active=true],#${root.id} .timeline-event:focus-visible{color:var(--viz-series-1);outline:none}\n #${root.id} .timeline-event:focus-visible{outline:1px solid var(--viz-series-1);outline-offset:4px}\n #${root.id} .timeline-event-point{fill:var(--timeline-tone);stroke:var(--background);stroke-width:3}\n #${root.id} .timeline-section{position:absolute;top:0;font-size:12px;line-height:20px;color:color-mix(in srgb,var(--timeline-tone) 55%,var(--foreground))}\n #${root.id} .timeline-section>span{position:sticky;left:0;display:inline-block;box-sizing:border-box;max-width:100%;padding:4px 10px;border-radius:999px;background:color-mix(in srgb,var(--timeline-tone) 15%,var(--background));overflow-wrap:anywhere}\n #${root.id} .wires{z-index:1}\n #${root.id} .timeline-axis,#${root.id} .timeline-stem{stroke:var(--border);stroke-width:1;fill:none}\n #${root.id} .timeline-point{fill:var(--timeline-tone);stroke:var(--background);stroke-width:2}\n\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',el=(tag,attrs)=>{const n=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,v);svg.append(n);return n;};\n const tones=[3,6,2,1,4,5],groups=[...new Set(data.tasks.map(t=>t.section))],tone=section=>`var(--viz-series-${tones[groups.indexOf(section)%tones.length]})`;let gap=40,w=216;const left=24,width=left*2+data.tasks.length*(w+gap)-gap,objects=[];scene.style.width=width+'px';scene.style.minWidth=width+'px';\n let lastSection=null,sectionStart=0;data.tasks.forEach((t,i)=>{if(t.section!==lastSection){if(lastSection)renderSection(lastSection,sectionStart,i);lastSection=t.section;sectionStart=i;}if(i===data.tasks.length-1&&lastSection)renderSection(lastSection,sectionStart,i+1);});\n function renderSection(name,start,end){const n=document.createElement('div');n.className='timeline-section';n.style.setProperty('--timeline-tone',tone(name));const caption=document.createElement('span');caption.textContent=name;n.append(caption);n.dataset.sectionStart=left+start*(w+gap);n.dataset.sectionIndex=start;n.dataset.sectionCount=end-start;n.style.left=left+start*(w+gap)+'px';n.style.width=(end-start)*(w+gap)-gap+'px';scene.append(n);}\n const fadeId=root.id+'-timeline-tail',defs=el('defs',{}),fade=el('linearGradient',{id:fadeId,gradientUnits:'userSpaceOnUse'});defs.append(fade);[1,0].forEach((opacity,i)=>{const stop=el('stop',{offset:i?'100%':'0%','stop-opacity':opacity,style:'stop-color:var(--border)'});fade.append(stop);});\n const axis=el('path',{class:'timeline-axis',d:`M${left} 96H${left+(data.tasks.length-1)*(w+gap)}`});\n const tail=el('path',{class:'timeline-tail',fill:'none','stroke-width':1,stroke:`url(#${fadeId})`});\n data.tasks.forEach((t,i)=>{const x=left+i*(w+gap),column=document.createElement('div');column.className='timeline-column';column.dataset.period=t.id;column.style.setProperty('--timeline-tone',tone(t.section));column.style.left=x+'px';const b=document.createElement('button');b.type='button';b.className='timeline-period';b.textContent=t.name;column.append(b);const content=document.createElement('div');content.className='timeline-events';column.append(content);if(t.description){const d=document.createElement('div');d.className='timeline-description';d.textContent=t.description;d.style.cssText='font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere';content.append(d);}\n const stem=el('path',{class:'timeline-stem',d:`M${x} 101V116`}),point=el('circle',{class:'timeline-point',cx:x,cy:96,r:7,style:'--timeline-tone:'+tone(t.section)});\n const events=t.events.map((text,j)=>{const e=document.createElement('button');e.type='button';e.className='timeline-event';e.dataset.event=j;e.textContent=text;e.setAttribute('aria-label',[t.section,t.name,text].filter(Boolean).join(' · '));content.append(e);return e;});scene.append(column);const eventPoints=events.map(()=>el('circle',{class:'timeline-event-point',cx:x,cy:116,r:5.5,style:'--timeline-tone:'+tone(t.section)}));objects.push({t,column,b,events,eventPoints,point,stem});});\n const view=createMermaidViewport(root);\n // 完整图收紧列间距,文字在本列内换行,不改事件顺序。\n function layoutMode(){\n gap=view.expanded?24:40;w=view.expanded?184:216;\n const width=left*2+objects.length*(w+gap)-gap;\n root.style.maxWidth=view.expanded?width+'px':'';root.style.marginInline=view.expanded?'auto':'';\n objects.forEach((o,i)=>{o.column.style.width=w+'px';o.column.style.left=left+i*(w+gap)+'px';o.b.style.height='auto';o.b.style.fontSize='13px';});\n const ph=Math.max(38,...objects.map(o=>o.b.offsetHeight));let columnTop=42;\n root.querySelectorAll('.timeline-section').forEach(n=>{const start=Number(n.dataset.sectionIndex);n.style.left=left+start*(w+gap)+'px';n.style.width=Number(n.dataset.sectionCount)*(w+gap)-gap+'px';n.dataset.sectionStart=left+start*(w+gap);n.style.fontSize=view.expanded?'14px':'12px';});\n columnTop=Math.max(42,...[...root.querySelectorAll('.timeline-section')].map(n=>n.offsetHeight+12));const ay=columnTop+12+ph+16;\n objects.forEach((o,i)=>{const x=left+i*(w+gap)+12;o.column.style.top=columnTop+'px';o.b.style.height=ph+'px';o.point.setAttribute('cx',x);o.point.setAttribute('cy',ay);const centers=o.events.map(e=>columnTop+e.offsetTop+e.offsetHeight/2);o.eventPoints.forEach((p,j)=>{p.setAttribute('cx',x);p.setAttribute('cy',centers[j]);});o.stem.setAttribute('d',centers.length?`M${x} ${ay+8}V${centers.at(-1)}`:'');});\n const height=Math.max(180,...objects.map(o=>o.column.offsetHeight+columnTop+24));scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);axis.setAttribute('d',`M${left+12} ${ay}H${left+12+(objects.length-1)*(w+gap)}`);const end=left+12+(objects.length-1)*(w+gap);tail.setAttribute('d',`M${end} ${ay}h120`);fade.setAttribute('x1',end);fade.setAttribute('x2',end+120);fade.setAttribute('y1',ay);fade.setAttribute('y2',ay);[axis,...objects.map(o=>o.stem)].forEach(p=>{p.style.strokeDasharray='none';});view.refresh();\n }\n root.addEventListener('qv:display-mode',()=>{layoutMode();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});layoutMode();\n function clear(){root.querySelectorAll('[data-active]').forEach(n=>n.removeAttribute('data-active'));}\n const bind=(b,point)=>{b.onpointerenter=()=>{clear();b.dataset.active='true';if(point)point.dataset.active='true';};b.onpointerleave=()=>{if(!b.matches(':focus-visible')){b.removeAttribute('data-active');if(point)point.removeAttribute('data-active');}};b.onfocus=()=>{clear();b.dataset.active='true';if(point)point.dataset.active='true';const r=b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;};b.onblur=()=>{b.removeAttribute('data-active');if(point)point.removeAttribute('data-active');};};objects.forEach(o=>{bind(o.b);o.events.forEach((e,j)=>bind(e,o.eventPoints[j]));});\n // 原生sticky与滚动同步合成,避免scroll事件回写left慢一帧造成边缘抖动。\n viewport.addEventListener('scroll',clear,{passive:true});root.addEventListener('qv:display-mode',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let running=[],armed=true;function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;const length=axis.getTotalLength();axis.style.strokeDasharray=`${length} ${length}`;running.push(axis.animate([{strokeDashoffset:length},{strokeDashoffset:0}],{duration:Math.max(480,Math.min(objects.length-1,10)*160),fill:'backwards'}));running.push(tail.animate([{opacity:0},{opacity:1}],{duration:240,delay:Math.min(objects.length-1,10)*160,fill:'backwards'}));objects.forEach((o,i)=>{const delay=Math.min(i,10)*160;running.push(o.column.animate([{opacity:0,transform:'translateY(8px)'},{opacity:1,transform:'translateY(0)'}],{duration:300,delay,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'}));running.push(o.point.animate([{opacity:0,r:'.32px'},{opacity:1,r:'7px'}],{duration:240,delay,fill:'backwards'}));o.eventPoints.forEach(p=>running.push(p.animate([{opacity:0},{opacity:1}],{duration:240,delay,fill:'backwards'})));const stemLength=o.stem.getTotalLength();o.stem.style.strokeDasharray=`${stemLength} ${stemLength}`;running.push(o.stem.animate([{strokeDashoffset:stemLength},{strokeDashoffset:0}],{duration:240,delay,fill:'backwards'}));});root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\n// Mermaid 11.12 成员已拆分属性/方法,并给出可见性和分类符,不再解析源码签名。\nasync function readMermaidClassModel(db,descriptions={}) {\n const classes=[...db.getClasses()],raw=db.getData(),marks=new Set(['none','extension','composition','aggregation','dependency']);\n if(db.getNotes().length||db.styleClasses?.size||classes.some(([,n])=>n.styles?.length||n.cssClasses!=='default'||n.link||n.haveCallback||hasRichMermaidLabel(n.label)||[...n.members,...n.methods].some(a=>[a.id,a.parameters,a.returnType].some(hasRichMermaidLabel)))||raw.edges.some(e=>!marks.has(e.arrowTypeStart)||!marks.has(e.arrowTypeEnd)||hasRichMermaidLabel(e.label)))return {reason:'此类图包含尚未适配的注释、棒棒糖接口、链接、回调、富文本或自定义样式,保留原生语义'};\n const members=(list,kind)=>list.map(a=>({name:a.getDisplayDetails().displayText,kind,visibility:a.visibility,classifier:a.classifier}));\n const nodes=classes.map(([id,n])=>({id,name:(n.label||id)+(n.type?'<'+n.type+'>':''),technicalName:id,parent:n.parent||undefined,annotations:[...n.annotations],members:members(n.members,'attribute'),methods:members(n.methods,'method'),description:descriptions[id]||'',shape:'class'}));\n const groups=raw.nodes.filter(n=>n.isGroup).map(n=>({id:n.id,name:n.label||n.id,parent:n.parentId||undefined}));\n const unknown=Object.keys(descriptions).filter(id=>![...nodes,...groups].some(n=>n.id===id));if(unknown.length)throw new Error('说明引用了未知类或命名空间:'+unknown.join('、'));\n return {data:{kind:'class',direction:db.getDirection()||'TB',nodes,groups,layers:[],edges:raw.edges.map(e=>({id:e.id,from:e.start,to:e.end,label:e.label||'',startMark:e.arrowTypeStart,endMark:e.arrowTypeEnd,startMultiplicity:e.startLabelRight||'',endMultiplicity:e.endLabelLeft||'',async:e.pattern==='dashed'}))}};\n}\nasync function prepareMermaidClassLayout(data) {\n const measure=document.createElement('div');measure.style.cssText='position:absolute;left:-100000px;visibility:hidden;white-space:pre-wrap;overflow-wrap:anywhere;box-sizing:border-box';document.body.append(measure);\n const height=(text,width,font,padding)=>{measure.style.width=width+'px';measure.style.font=font;measure.style.padding=padding;measure.textContent=text;return measure.offsetHeight;};\n const mono='ui-monospace,SFMono-Regular,Menlo,Consolas,monospace',context=document.createElement('canvas').getContext('2d');context.font='12px '+mono;\n for(const n of data.nodes){\n n.layoutWidth=Math.max(240,Math.min(320,Math.max(context.measureText(n.name).width,...[...n.members,...n.methods].map(a=>context.measureText(a.name).width))+32));n.headerHeight=Math.max(52,height(n.name,n.layoutWidth,'600 14px/22px '+getComputedStyle(document.body).fontFamily,'14px 16px'))+(n.annotations.length?24:0);\n n.descriptionHeight=n.description?height(n.description,n.layoutWidth-20,'12px/18px '+getComputedStyle(document.body).fontFamily,'8px 10px')+8:0;\n for(const a of [...n.members,...n.methods])a.rowHeight=Math.max(30,height(a.name,n.layoutWidth-20,'12px/20px '+mono,'5px 10px'));\n n.layoutHeight=n.headerHeight+n.descriptionHeight+[n.members,n.methods].reduce((h,list)=>h+(list.length?44+list.reduce((sum,a)=>sum+a.rowHeight,0):0),0)+10;\n }\n context.font='12px '+getComputedStyle(document.body).fontFamily;for(const e of data.edges){e.labelWidth=Math.max(30,Math.min(180,context.measureText(e.label||'连接').width+12));e.labelHeight=height(e.label||'连接',e.labelWidth,'12px/18px '+getComputedStyle(document.body).fontFamily,'2px 6px');}\n measure.remove();\n for(const e of data.edges)if(e.from===e.to)e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';\n await prepareMermaidLayout(data);\n // 类图同样只在大量节点且明显长条时比较另一方向,不强制普通图趋近正方形。\n if(data.layout.width>(cfg.readingWidth||root.clientWidth)&&['LR','RL'].includes(data.direction)){\n const initial=structuredClone(data),ratio=d=>Math.abs(Math.log(d.layout.width/d.layout.height));data.direction=['LR','RL'].includes(data.direction)?'TB':'LR';\n for(const e of data.edges)if(e.from===e.to)e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';\n await prepareMermaidLayout(data);if(data.layout.width>=initial.layout.width)Object.assign(data,initial);\n }\n\n // 自关联单独留出阅读通道,保留源端点及 UML 方向。\n const map=new Map(data.nodes.map(n=>[n.id,n]));\n for(const e of data.edges.filter(e=>e.from===e.to)){\n const n=map.get(e.from),a=e.points[0],b=e.points.at(-1),labelWidth=e.labelWidth;\n if(e.fromSide==='EAST'){\n const x=n.x+n.w+88;e.points=[a,[x,a[1]],[x,b[1]],b];e.labelPosition={x:x+labelWidth/2+8,y:(a[1]+b[1])/2};\n const right=x+labelWidth+24;data.layout.width=Math.max(data.layout.width,right+28);\n for(let id=n.parent;id;){const g=data.layout.groups.find(g=>g.id===id);if(!g)break;g.w=Math.max(g.w,right-g.x);id=g.parent;}\n }else{\n const y=n.y+n.h+72;e.points=[a,[a[0],y],[b[0],y],b];e.labelPosition={x:(a[0]+b[0])/2,y:y+20};\n const bottom=y+42;data.layout.height=Math.max(data.layout.height,bottom+28);\n for(let id=n.parent;id;){const g=data.layout.groups.find(g=>g.id===id);if(!g)break;g.h=Math.max(g.h,bottom-g.y);id=g.parent;}\n }\n }\n\n}\n\nfunction renderMermaidClass(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-class');root.dataset.renderer='qv-class';root.dataset.state='enhanced';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','类结构图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id}{--class-wire:light-dark(#727a82,#a3aab3)}\n #${root.id} .wires{z-index:1}\n #${root.id} .class-entity{z-index:2;position:absolute;border:1px solid var(--border);border-radius:14px;background:var(--background);overflow:hidden;transition:opacity 140ms ease,border-color 140ms ease;--class-type:light-dark(#287d7b,#80c3ba);--class-method:light-dark(#35659f,#98b9ed)}\n #${root.id} .class-header{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;width:100%;border:0;background:none;color:var(--foreground);font:inherit;font-size:14px;font-weight:600;padding:0 16px;text-align:left;white-space:normal;overflow-wrap:anywhere;line-height:22px;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-header small{color:var(--muted-foreground);font-size:12px;font-weight:400}\n #${root.id} .class-field{display:block;width:100%;min-height:30px;padding:5px 10px;white-space:pre-wrap;overflow-wrap:anywhere;box-sizing:border-box;line-height:20px;border:0;background:none;color:var(--foreground);font:12px/20px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-section-caption{height:28px;box-sizing:border-box;padding:7px 10px 3px;font-size:11px;color:var(--muted-foreground)}\n #${root.id} .class-panel{margin:0 10px 8px;padding:4px 0;border-radius:8px;background:color-mix(in srgb,var(--foreground) 4%,var(--background))}\n #${root.id} .class-description{margin:0 10px 8px;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--foreground) 4%,var(--background));font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere}\n #${root.id} .class-token-type{color:var(--class-type)}\n #${root.id} .class-token-method{color:var(--class-method)}\n #${root.id} .class-token-muted{color:var(--muted-foreground)}\n #${root.id} .class-field[data-classifier=abstract]{font-style:italic}\n #${root.id} .class-field[data-classifier=static]{text-decoration:underline}\n #${root.id} .class-methods{border:0}\n #${root.id} .class-namespace{position:absolute;z-index:0;border:1px dashed color-mix(in srgb,var(--foreground) 18%,var(--background));border-radius:12px;background:var(--background);pointer-events:none}\n #${root.id} .class-namespace>span{position:absolute;left:14px;top:8px;padding:0 4px;font-size:12px;color:var(--muted-foreground);background:transparent}\n #${root.id} .class-marker path{stroke:currentColor;stroke-width:1.4;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke}\n #${root.id} .class-multiplicity{position:absolute;z-index:3;transform:translate(-50%,-50%);border:0;background:var(--background);padding:2px 4px;font:inherit;font-size:12px;line-height:18px;min-height:0;color:var(--muted-foreground);cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-field[data-active=true]{background:var(--accent)}\n #${root.id} .class-markers{color:var(--class-wire)}\n #${root.id} .class-wire{fill:none;stroke:var(--class-wire);stroke-width:1.4;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke}\n #${root.id} .class-label{position:absolute;z-index:3;transform:translate(-50%,-50%);padding:2px 6px;border:0;border-radius:4px;background:var(--background);color:var(--muted-foreground);font:inherit;font-size:12px;line-height:18px;min-height:0;max-width:180px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-tip{position:absolute;z-index:10;border-radius:12px;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:none}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='class-tip';tip.id=root.id+'-class-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);\n const element=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const width=data.layout.width,height=data.layout.height,offset=(width-data.layout.width)/2;\n scene.style.cssText=`width:${width}px;min-width:${width}px;height:${height}px`;svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\n const nodes=data.nodes.map(n=>({...n,x:n.x+offset})),groups=data.layout.groups.map(n=>({...n,x:n.x+offset})),map=new Map([...nodes,...groups].map(n=>[n.id,n])),objects=[],edges=[];\n for(const g of groups){const box=document.createElement('div');box.className='class-namespace';box.dataset.namespace=g.id;box.style.cssText=`left:${g.x}px;top:${g.y}px;width:${g.w}px;height:${g.h}px`;const caption=document.createElement('span');caption.textContent=g.name;box.append(caption);scene.append(box);}\n for(const n of nodes){\n const box=document.createElement('div');box.className='class-entity';box.dataset.classEntity=n.id;box.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;\n const b=document.createElement('button');b.type='button';b.className='class-header';b.style.height=n.headerHeight+'px';b.dataset.classNode=n.id;if(n.annotations.length){const stereotype=document.createElement('small');stereotype.textContent=n.annotations.map(a=>'«'+a+'»').join(' ');b.append(stereotype);}const title=document.createElement('span');title.textContent=n.name;b.append(title);box.append(b);if(n.description){const d=document.createElement('div');d.className='class-description';d.textContent=n.description;box.append(d);}\n for(const [list,kind] of [[n.members,'attribute'],[n.methods,'method']]){const area=document.createElement('div');area.className='class-panel'+(kind==='method'?' class-methods':'');if(list.length){const caption=document.createElement('div');caption.className='class-section-caption';caption.textContent=(kind==='attribute'?'属性':'方法')+' · '+list.length;area.append(caption);}for(const member of list){const row=document.createElement('button');row.type='button';row.className='class-field';row.dataset.member=member.name;row.dataset.kind=kind;row.dataset.classifier=member.classifier==='*'?'abstract':member.classifier==='$'?'static':'';row.style.height=member.rowHeight+'px';syntax(row,member);row.setAttribute('aria-label',`${n.name} · ${kind==='attribute'?'属性':'方法'} · ${member.name}${member.classifier==='*'?' · 抽象':member.classifier==='$'?' · 静态':''}`);area.append(row);}if(list.length)box.append(area);}\n scene.append(box);objects.push({n,b,box});\n }\n function syntax(row,member){const text=member.name,span=(value,token)=>{if(!value)return;const s=document.createElement('span');s.className='class-token-'+token;s.textContent=value;row.append(s);};let offset=/^[+#~\\-]/.test(text)?1:0;span(text.slice(0,offset),'muted');const body=text.slice(offset),paren=body.indexOf('(');if(member.kind==='method'&&paren>=0){span(body.slice(0,paren),'method');const close=body.lastIndexOf(')');span(body.slice(paren,close+1),'muted');span(body.slice(close+1),'type');}else{const split=body.indexOf(' ');if(split>=0){span(body.slice(0,split),'type');row.append(document.createTextNode(body.slice(split)));}else row.append(document.createTextNode(body));}}\n const rounded=points=>{let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],l1=Math.hypot(b[0]-a[0],b[1]-a[1]),l2=Math.hypot(c[0]-b[0],c[1]-b[1]),r=Math.min(8,l1/2,l2/2);if(!l1||!l2)continue;d+=`L${b[0]-(b[0]-a[0])*r/l1} ${b[1]-(b[1]-a[1])*r/l1}Q${b[0]} ${b[1]} ${b[0]+(c[0]-b[0])*r/l2} ${b[1]+(c[1]-b[1])*r/l2}`;}return d+`L${points.at(-1)[0]} ${points.at(-1)[1]}`};\n const relationKind=e=>[e.startMark,e.endMark].includes('composition')?'组合':[e.startMark,e.endMark].includes('aggregation')?'聚合':[e.startMark,e.endMark].includes('extension')?(e.async?'实现':'继承'):[e.startMark,e.endMark].includes('dependency')?(e.async?'依赖':'关联'):'连接';\n function marker(point,next,kind,parent){const angle=Math.atan2(next[1]-point[1],next[0]-point[0]),size=kind==='extension'?10.392304845:12,scale=['extension','composition','aggregation'].includes(kind)?Math.min(1,Math.hypot(next[0]-point[0],next[1]-point[1])/size):1,g=element('g',{class:'class-marker','data-mark':kind,transform:`translate(${point[0]} ${point[1]}) rotate(${angle*180/Math.PI}) scale(${scale})`},parent);\n if(kind==='extension')element('path',{d:'M0 0L10.392304845 -6V6Z',fill:'var(--background)'},g);\n if(kind==='composition'||kind==='aggregation')element('path',{d:'M0 0L6 -6L12 0L6 6Z',fill:kind==='composition'?'currentColor':'var(--background)'},g);\n if(kind==='dependency')element('path',{d:'M6 -3L0 0L6 3',fill:'none'},g);return g;\n }\n for(const e of data.edges){const pts=e.points.map(([x,y])=>[x+offset,y]),line=pts.map(p=>[...p]);for(const [at,next,mark]of [[0,1,e.startMark],[line.length-1,line.length-2,e.endMark]])if(mark==='extension'||mark==='composition'||mark==='aggregation'){const p=line[at],n=pts[next],length=Math.hypot(n[0]-p[0],n[1]-p[1]),trim=Math.min(mark==='extension'?10.392304845:12,length);line[at]=[p[0]+(n[0]-p[0])*trim/length,p[1]+(n[1]-p[1])*trim/length];}\n const path=element('path',{d:rounded(line),class:'class-wire','data-class-edge':e.id,'data-from':e.from,'data-to':e.to,...(e.async?{'stroke-dasharray':'4 4'}:{})}),head=element('g',{class:'class-markers'});marker(pts[0],pts[1],e.startMark,head);marker(pts.at(-1),pts.at(-2),e.endMark,head);\n const detail=`${map.get(e.from).name}${e.startMultiplicity?' '+e.startMultiplicity:''} — ${map.get(e.to).name}${e.endMultiplicity?' '+e.endMultiplicity:''} · ${relationKind(e)}${e.label?' · '+e.label:''}`,p=e.labelPosition||path.getPointAtLength(path.getTotalLength()/2),label=document.createElement('button');label.type='button';label.className='class-label';label.textContent=e.label||relationKind(e);label.style.left=(e.labelPosition?p.x+offset:p.x)+'px';label.style.top=p.y+'px';label.setAttribute('aria-label',detail);scene.append(label);\n const multiplicities=[];for(const [point,next,text]of [[pts[0],pts[1],e.startMultiplicity],[pts.at(-1),pts.at(-2),e.endMultiplicity]])if(text){const length=Math.hypot(next[0]-point[0],next[1]-point[1]),ux=(next[0]-point[0])/length,uy=(next[1]-point[1])/length,sign=ux>=0?-1:1,b=document.createElement('button');b.type='button';b.className='class-multiplicity';b.textContent=text;b.setAttribute('aria-label',detail);b.style.left=(point[0]+ux*24-uy*sign*14)+'px';b.style.top=(point[1]+uy*24+ux*sign*14)+'px';scene.append(b);multiplicities.push(b);}\n edges.push({...e,path,head,label,multiplicities});\n }\n // 标签沿所属路径法向留8px间距,不能用背板擦断方向证据。\n for(const e of edges)if(e.label){const b=e.label,px=parseFloat(b.style.left),py=parseFloat(b.style.top),length=e.path.getTotalLength();let nearest,distance=Infinity,at=0;for(let d=0;d<=length;d+=3){const p=e.path.getPointAtLength(d),delta=Math.hypot(px-p.x,py-p.y);if(delta<distance){nearest=p;distance=delta;at=d;}}const a=e.path.getPointAtLength(Math.max(0,at-1)),c=e.path.getPointAtLength(Math.min(length,at+1));if(Math.abs(c.x-a.x)>=Math.abs(c.y-a.y)){const gap=b.offsetHeight/2+8;if(Math.abs(py-nearest.y)<gap)b.style.top=(nearest.y+(py<nearest.y?-gap:gap))+'px';}else{const gap=b.offsetWidth/2+8;if(Math.abs(px-nearest.x)<gap)b.style.left=(nearest.x+(px<nearest.x?-gap:gap))+'px';}}\n // 在所属路径旁寻找空位,保留多重性与卡片的阅读空间。\n const overlap=(a,b)=>Math.max(0,Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x))*Math.max(0,Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y));\n const labelBox=b=>({x:parseFloat(b.style.left)-b.offsetWidth/2-4,y:parseFloat(b.style.top)-b.offsetHeight/2-4,w:b.offsetWidth+8,h:b.offsetHeight+8});\n const occupied=[...nodes.map(n=>({x:n.x-4,y:n.y-4,w:n.w+8,h:n.h+8})),...edges.flatMap(e=>e.multiplicities.map(labelBox)),...groups.map(g=>({x:g.x,y:g.y,w:g.w,h:28}))];\n for(const e of edges){const b=e.label,w=b.offsetWidth,h=b.offsetHeight,origin={x:parseFloat(b.style.left),y:parseFloat(b.style.top)},length=e.path.getTotalLength(),candidates=[origin];\n for(let i=1;i<40;i++){const distance=length*i/40,p=e.path.getPointAtLength(distance),a=e.path.getPointAtLength(Math.max(0,distance-1)),z=e.path.getPointAtLength(Math.min(length,distance+1));if(Math.abs(z.x-a.x)>=Math.abs(z.y-a.y)){for(const sign of [-1,1])candidates.push({x:p.x,y:p.y+sign*(h/2+9)});}else for(const sign of [-1,1])candidates.push({x:p.x+sign*(w/2+9),y:p.y});}\n for(const c of candidates){const box={x:c.x-w/2-4,y:c.y-h/2-4,w:w+8,h:h+8};c.score=occupied.reduce((sum,o)=>sum+overlap(box,o),0)*1000+Math.hypot(c.x-origin.x,c.y-origin.y);if(box.x<0||box.y<0||box.x+box.w>width||box.y+box.h>height)c.score+=1e8;}\n candidates.sort((a,b)=>a.score-b.score);b.style.left=candidates[0].x+'px';b.style.top=candidates[0].y+'px';occupied.push(labelBox(b));\n }\n const view=createMermaidViewport(root);\n let hoverTimer=0,leaveTimer=0,focusSuppressed=false;const focusActions=new WeakMap();\n function clear(){clearTimeout(hoverTimer);clearTimeout(leaveTimer);tip.hidden=true;root.querySelectorAll('[data-active]').forEach(n=>n.removeAttribute('data-active'));objects.forEach(({box})=>{box.style.opacity='';box.style.borderColor='';});edges.forEach(e=>{e.path.style.opacity=e.head.style.opacity='';e.path.style.stroke=e.head.style.color='';e.multiplicities.forEach(b=>b.style.opacity='');e.label.style.opacity='';});}\n function select(trigger,id,edge){clear();trigger.dataset.active='true';const selected=edge?new Set([edge.from,edge.to]):new Set([id]);objects.forEach(({n,box})=>{box.style.opacity=selected.has(n.id)?'1':'.26';box.style.borderColor=selected.has(n.id)?'var(--viz-series-1)':'';});edges.forEach(e=>{const hit=edge?e===edge:e.from===id||e.to===id;e.path.style.opacity=e.head.style.opacity=hit?'1':'.14';e.path.style.stroke=e.head.style.color=hit?'var(--viz-series-1)':'';e.label.style.opacity=hit?'1':'.25';e.multiplicities.forEach(b=>b.style.opacity=hit?'1':'.25');});}\n function showRelation(trigger,edge){select(trigger,null,edge);}\n const bind=(b,activate)=>{focusActions.set(b,activate);b.onpointerenter=()=>{clearTimeout(hoverTimer);clearTimeout(leaveTimer);hoverTimer=setTimeout(activate,70);};b.onfocus=()=>{focusSuppressed=false;activate();};b.onclick=activate;};\n objects.forEach(({n,b,box})=>{bind(box,()=>select(b,n.id));bind(b,()=>select(b,n.id));box.querySelectorAll('.class-field').forEach(row=>bind(row,()=>select(row,n.id)));});edges.forEach(e=>{bind(e.label,()=>showRelation(e.label,e));e.multiplicities.forEach(b=>bind(b,()=>select(b,null,e)));});\n root.addEventListener('focusout',e=>{if(!root.contains(e.relatedTarget))clear();});\n scene.addEventListener('pointerout',e=>{if(e.relatedTarget&&scene.contains(e.relatedTarget))return;clearTimeout(hoverTimer);leaveTimer=setTimeout(clear,120);});viewport.addEventListener('scroll',()=>{clear();const active=document.activeElement;if(!focusSuppressed&&active?.matches(':focus-visible')&&focusActions.has(active))requestAnimationFrame(()=>{if(!focusSuppressed&&document.activeElement===active)focusActions.get(active)();});},{passive:true});root.addEventListener('qv:display-mode',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape'){focusSuppressed=true;clear();}});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),defs=element('defs',{});let running=[],armed=true;\n edges.forEach((e,i)=>{const length=e.path.getTotalLength(),id=root.id+'-class-mask-'+i,mask=element('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs);e.reveal=element('path',{d:e.path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);e.path.setAttribute('mask',`url(#${id})`)});\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;objects.forEach(({n,box})=>running.push(box.animate([{opacity:0,transform:'scale(.08)'},{opacity:1,transform:'scale(1)'}],{duration:240,delay:(n.layer||0)*180,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'})));edges.forEach(e=>{const delay=(map.get(e.from).layer||0)*180+110;running.push(e.reveal.animate([{strokeDashoffset:e.path.getTotalLength()},{strokeDashoffset:0}],{duration:650,delay,fill:'backwards'}));running.push(e.head.animate([{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'}));e.multiplicities.forEach(b=>running.push(b.animate([{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'})));if(e.label)running.push(e.label.animate([{opacity:0},{opacity:1}],{duration:100,delay:delay+600,fill:'backwards'}))});root.dataset.replays=String(+(root.dataset.replays||0)+1)}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\nroot.setAttribute('aria-label',cfg.title||'架构图');\nlet mermaidRuntime,data,sourceModel,drilldownActive=true,navigationEnabled=false,currentScope=null,navigationBusy=false;const scopeScroll=new Map();\nconst viewport=root.querySelector('.diagram-scroll');\nlet diagramViewport;\nconst finishLoading=()=>{viewport.setAttribute('aria-busy','false');root.dispatchEvent(new Event('qv:ready',{bubbles:true}));};\nconst notice=root.querySelector('.qv-notice');\ntry {\n const mermaid=window.__mcxQVRuntime.mermaid;\n mermaidRuntime=mermaid;mermaid.initialize({startOnLoad:false,securityLevel:'strict',suppressErrorRendering:true,secure:['securityLevel','startOnLoad','suppressErrorRendering','maxTextSize'],maxTextSize:50000});\n if(!window.lucide&&document.readyState!=='complete')await new Promise(resolve=>window.addEventListener('load',resolve,{once:true}));\n const model=await readMermaidModel(mermaid,cfg.source,cfg.descriptions);\n if(model.data?.kind==='block'){renderMermaidBlock(root,model.data);finishLoading();return;}\n if(model.data?.kind==='c4'){renderMermaidC4(root,model.data);finishLoading();return;}\n if(model.data?.kind==='quadrant'){renderMermaidQuadrant(root,model.data);finishLoading();return;}\n if(model.data?.kind==='sankey'){await renderMermaidSankey(root,model.data);finishLoading();return;}\n if(model.data?.kind==='radar'){await renderMermaidRadar(root,model.data);finishLoading();return;}\n if(model.data?.kind==='xy'){await renderMermaidXY(root,model.data);finishLoading();return;}\n if(model.data?.kind==='pie'){await renderMermaidPie(root,model.data);finishLoading();return;}\n if(model.data?.kind==='kanban'){renderMermaidKanban(root,model.data);finishLoading();return;}\n if(model.data?.kind==='gantt'){await renderMermaidGantt(root,model.data);finishLoading();return;}\n if(model.data?.kind==='mindmap'){renderMermaidMindmap(root,model.data);finishLoading();return;}\n if(model.data?.kind==='journey'){renderMermaidJourney(root,model.data);finishLoading();return;}\n if(model.data?.kind==='timeline'){renderMermaidTimeline(root,model.data);finishLoading();return;}\n if(model.data?.kind==='class'){await prepareMermaidClassLayout(model.data);renderMermaidClass(root,model.data);finishLoading();return;}\n if(model.data?.kind==='er'){await prepareMermaidErLayout(model.data);renderMermaidEr(root,model.data);finishLoading();return;}\n if(model.data?.kind==='state'){await prepareMermaidStateLayout(model.data);renderMermaidState(root,model.data);finishLoading();return;}\n if(model.data?.kind==='sequence'){renderMermaidSequence(root,model.data,cfg);finishLoading();return;}\n if(!model.data){\n const result=await mermaid.render('mcx-qv-root-native',cfg.source);\n finishLoading();notice.hidden=false;scene.innerHTML=result.svg;root.querySelector('.edge-key').hidden=true;\n root.dataset.state='native';notice.textContent=model.reason+';当前显示 Mermaid 原生图。';return;\n }\n diagramViewport=createMermaidViewport(root);\n data=model.data;sourceModel=data;root.dataset.architecture=String(!!data.architecture);root.dataset.kind=data.kind||'';\n const iconNodes=data.nodes.filter(n=>!n.junction),explicitIcons=iconNodes.some(n=>n.explicitIcon),requiredIcons=cfg.icons==='required'||explicitIcons||(data.architecture&&cfg.icons!=='none');\n if(requiredIcons){const missing=iconNodes.filter(n=>!n.icon);if(missing.length)throw new Error('这些服务需要在 Mermaid 源码中指定语义图标:'+missing.map(n=>n.name).join('、'));if(!window.lucide)throw new Error('节点图标运行时尚未就绪');}\n navigationEnabled=!!data.groups?.length&&!data.architecture;drilldownActive=navigationEnabled&&cfg.navigation!=='expanded';root.dataset.hasGroups=String(!!data.groups?.length);\n if(drilldownActive){data=mermaidScopeView(sourceModel);root.dataset.navigation='true';root.dataset.scope='overview';}\n if(data.kind==='flowchart'&&!data.navigation)await prepareFlowchartLayout(data,mermaid,cfg.source);else if(data.nativeGeometry)await prepareNativeGeometry(data,mermaid,cfg.source,data.db,data.architecture);else if(data.navigation)await prepareNavigationView(data);else if(data.generic)await prepareMermaidLayout(data);scene.style.minWidth=(data.layout?data.layout.width:data.grouped?Math.max(736,Math.max(...data.layers.map((_,i)=>data.nodes.filter(n=>n.layer===i).length))*150-18):Math.max(560,data.layers.length*160+(data.layers.length-1)*48))+'px';root.dataset.state='enhanced';root.dataset.renderer='qv-architecture';notice.hidden=true;notice.textContent='';\n const missing=Object.keys(cfg.descriptions).filter(id=>![...sourceModel.nodes,...sourceModel.groups||[]].some(n=>n.id===id));\n if(missing.length){notice.hidden=false;notice.textContent='未匹配节点说明:'+missing.join('、');}\n} catch(error){finishLoading();notice.hidden=false;root.dataset.state='error';notice.setAttribute('role','alert');notice.textContent='无法绘制 Mermaid:'+String(error.message||error).slice(0,800);root.querySelector('.edge-key').hidden=true;return;}\n\nconst useIcons=!!window.lucide&&(sourceModel.nodes.some(n=>n.explicitIcon)||(cfg.icons!=='none'&&sourceModel.nodes.filter(n=>!n.junction).every(n=>n.icon)));\nlet architectureReader;\nconst tip=root.querySelector('.node-tip');let tipTimer,activeNode,leaveTimer,hoverTimer,pendingHover;\nconst NS='http://www.w3.org/2000/svg';function el(t,a,parent=svg){const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);parent.append(n);return n;}\n// 绘制结果始终可见,入场动画不作为显示的前置条件。\nlet nodes=[],arrows=[],edges=[],lastWidth=0,visible=false,armed=true,replayTimer,finishTimer;let motionSpecs=[],runningAnimations=[];root.dataset.motion='complete';\nconst legend=cfg.legend||{};\nfunction updateLegend(){for(const [selector,label,exists] of [['.solid-key',legend.solid,data.edges.some(e=>!e.async)],['.async-key',legend.dotted,data.edges.some(e=>e.async)]]){const item=root.querySelector(selector);item.hidden=!label||!exists;item.querySelector('.key-label').textContent=label||'';}\nroot.querySelector('.edge-key').hidden=[...root.querySelector('.edge-key').children].every(item=>item.hidden);}\nconst containmentLevels=new Map();\nfunction containmentLevel(id){\n if(containmentLevels.has(id))return containmentLevels.get(id);\n const group=sourceModel.groups?.find(g=>g.id===id);if(!group){containmentLevels.set(id,0);return 0;}\n const children=[...sourceModel.nodes,...sourceModel.groups].filter(n=>n.parent===id);\n const level=1+Math.max(0,...children.map(n=>containmentLevel(n.id)));containmentLevels.set(id,level);return level;\n}\nfunction setContainmentSurface(element,id){if(sourceModel.kind!=='flowchart'&&!sourceModel.architecture)return;const level=containmentLevel(id);element.dataset.containmentLevel=level;element.style.setProperty('--node-surface',level%2?'color-mix(in srgb,var(--muted) 18%,var(--background))':'var(--background)');}\nconst parents=new Map([...sourceModel.nodes,...sourceModel.groups||[]].map(n=>[n.id,n.parent]));\nfunction belongs(id,endpoint){while(id){if(id===endpoint)return true;id=parents.get(id);}return false;}\nfunction touches(e,id){return belongs(id,e.from)||belongs(id,e.to);}\nfunction relatedTo(id){const linked=data.edges.filter(e=>touches(e,id));return new Set(nodes.filter(n=>n.id===id||linked.some(e=>belongs(n.id,e.from)||belongs(n.id,e.to))).map(n=>n.id));}\nfunction styleArrows(id){arrows.forEach(a=>{const active=a.edges.some(e=>touches(e,id));a.el.style.fill=active?'var(--viz-series-1)':'';a.el.style.opacity=id?(active?.85:.055):1;});}\nfunction clear(){clearTimeout(hoverTimer);pendingHover=null;root.dataset.hoverNode='';clearTimeout(leaveTimer);architectureReader?.clear();root.querySelectorAll('.preview-item,.preview-link,.preview-arrow').forEach(el=>{el.style.opacity='';el.style.stroke='';el.style.fill='';});styleArrows(null);clearTimeout(tipTimer);tip.hidden=true;activeNode=null;nodes.forEach(n=>n.button.removeAttribute('aria-describedby'));nodes.forEach(n=>{n.button.style.opacity=1;n.button.style.borderColor='';n.button.style.removeProperty('--node-highlight');});edges.forEach(e=>{e.path.style.opacity='1';e.path.style.stroke='';e.path.style.strokeWidth=e.thick?'2':'';if(e.labelElement)e.labelElement.style.opacity='1';if(e.countElement)e.countElement.style.opacity='1';});}\nfunction focus(id){root.dataset.hoverNode=id;styleArrows(id);const related=relatedTo(id);nodes.forEach(n=>{n.button.style.opacity=related.has(n.id)?1:.22;n.button.style.borderColor=n.id===id?'var(--viz-series-1)':'';n.button.style.setProperty('--node-highlight',n.id===id?'var(--viz-series-1)':'var(--border)');});edges.forEach(e=>{const active=touches(e,id);e.path.style.opacity=active?.85:.055;e.path.style.stroke=active?'var(--viz-series-1)':'';e.path.style.strokeWidth=active?(e.thick?'2':'1.2'):(e.thick?'2':'');if(e.labelElement)e.labelElement.style.opacity=active?'1':'.22';if(e.countElement)e.countElement.style.opacity=active?'1':'.22';});}\nfunction rounded(points,radius=7){let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],u=Math.hypot(b[0]-a[0],b[1]-a[1]),v=Math.hypot(c[0]-b[0],c[1]-b[1]);if(!u||!v)continue;const r=Math.min(radius,u/2,v/2),pre=[b[0]+(a[0]-b[0])*r/u,b[1]+(a[1]-b[1])*r/u],post=[b[0]+(c[0]-b[0])*r/v,b[1]+(c[1]-b[1])*r/v];d+=`L${pre[0]} ${pre[1]}Q${b[0]} ${b[1]} ${post[0]} ${post[1]}`;}const last=points.at(-1);return d+`L${last[0]} ${last[1]}`;}\n// 快速掠过不切换焦点;跨节点保留旧状态,停稳后直接过渡到新状态。\nfunction queueNodeHover(n,event){\n if(sourceModel.kind!=='flowchart'){showTip(n,event);return;}\n clearTimeout(leaveTimer);\n clearTimeout(hoverTimer);pendingHover=null;if(activeNode===n.id)return;pendingHover=n.id;\n const pointer={clientX:event.clientX,clientY:event.clientY};\n hoverTimer=setTimeout(()=>{pendingHover=null;showTip(n,pointer);},140);\n}\nfunction leaveNodeHover(){\n clearTimeout(hoverTimer);pendingHover=null;clearTimeout(leaveTimer);\n if(!architectureReader?.focused)leaveTimer=setTimeout(clear,220);\n}\nfunction showTip(n,event){clearTimeout(leaveTimer);if(activeNode===n.id&&!architectureReader)return;if(root.dataset.motion==='running')return;if(architectureReader){clear();activeNode=n.id;architectureReader.choose(n.id,n.button,!event);return;}clearTimeout(tipTimer);tip.hidden=true;nodes.forEach(m=>m.button.removeAttribute('aria-describedby'));focus(n.id);activeNode=n.id;if(!n.description?.trim())return;tipTimer=setTimeout(()=>{if(activeNode!==n.id)return;tip.textContent=n.description;tip.style.maxWidth=Math.min(260,root.clientWidth-16)+'px';tip.hidden=false;n.button.setAttribute('aria-describedby','mcx-qv-root-tip');const rr=root.getBoundingClientRect(),nr=n.button.getBoundingClientRect(),sx=scene.getBoundingClientRect().left-rr.left,sy=scene.getBoundingClientRect().top-rr.top,scale=diagramViewport.scale,box={x:nr.left-rr.left,y:nr.top-rr.top,w:nr.width,h:nr.height},w=tip.offsetWidth,h=tip.offsetHeight,px=event?.clientX?event.clientX-rr.left:box.x+box.w/2,py=event?.clientY?event.clientY-rr.top:box.y+box.h/2,gap=10;\nconst candidates=[[box.x+box.w+10,box.y-h-10],[box.x-w-10,box.y-h-10],[box.x+box.w+10,box.y+box.h+10],[box.x-w-10,box.y+box.h+10],[box.x+box.w+gap,py-h/2],[box.x-w-gap,py-h/2],[px-w/2,box.y-h-gap],[px-w/2,box.y+box.h+gap],[box.x,box.y-h-gap],[box.x+box.w-w,box.y+box.h+gap],...([22,38,54].flatMap(g=>[[box.x,box.y+box.h+g],[box.x+box.w-w,box.y+box.h+g],[box.x,box.y-h-g],[box.x+box.w-w,box.y-h-g]]))].map(([x,y])=>({x:Math.max(4,Math.min(root.clientWidth-w-4,x)),y:Math.max(0,Math.min(root.clientHeight-h,y))}));\nfunction overlap(a,b){return Math.max(0,Math.min(a.x+w,b.x+b.w)-Math.max(a.x,b.x))*Math.max(0,Math.min(a.y+h,b.y+b.h)-Math.max(a.y,b.y));}\nconst relatedNodes=relatedTo(n.id);\nfor(const c of candidates){c.score=nodes.reduce((sum,m)=>sum+overlap(c,{x:m.x*scale+sx,y:m.y*scale+sy,w:m.w*scale,h:m.h*scale})*(relatedNodes.has(m.id)?1000:1),0);for(const e of edges){const related=touches(e,n.id);for(const pt of e.samples){if(pt.x*scale+sx>c.x&&pt.x*scale+sx<c.x+w&&pt.y*scale+sy>c.y&&pt.y*scale+sy<c.y+h)c.score+=related?500:2;}}const dx=Math.max(c.x-px,0,px-c.x-w),dy=Math.max(c.y-py,0,py-c.y-h);c.score+=Math.hypot(dx,dy)*.5;}candidates.sort((a,b)=>a.score-b.score);tip.style.left=candidates[0].x+'px';tip.style.top=candidates[0].y+'px';},120);}\n\nfunction markerPath(x,y,ux,uy,type){const bx=x-5*ux,by=y-5*uy;\n if(type==='circle'){const cx=x-2.5*ux,cy=y-2.5*uy;return `M${cx-2.5} ${cy}a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0`;}\n if(type==='cross'){const cx=x-2.5*ux,cy=y-2.5*uy;return `M${cx-3} ${cy-2}l1 -1 5 5 -1 1Z M${cx+2} ${cy-3}l1 1 -5 5 -1 -1Z`;}\n return `M${x} ${y}L${bx-uy*2.2} ${by+ux*2.2}L${bx+uy*2.2} ${by-ux*2.2}Z`;\n}\nfunction drawNodeShape(n,button){\n button.classList.add('shaped');const svg=el('svg',{class:'node-outline',viewBox:`0 0 ${n.w} ${n.h}`},button),w=n.w,h=n.h;\n if(n.shape==='circle'||n.shape==='doublecircle'){el('ellipse',{cx:w/2,cy:h/2,rx:w/2-1,ry:h/2-1},svg);if(n.shape==='doublecircle')el('ellipse',{cx:w/2,cy:h/2,rx:w/2-6,ry:h/2-6,fill:'none'},svg);return;}\n if(n.shape==='stadium'){el('rect',{x:1,y:1,width:w-2,height:h-2,rx:h/2-1},svg);return;}\n if(n.shape==='cylinder'){el('path',{d:`M1 9C1 -2 ${w-1} -2 ${w-1} 9V${h-9}C${w-1} ${h+2} 1 ${h+2} 1 ${h-9}Z`,fill:'var(--node-surface,var(--background))'},svg);el('path',{d:`M1 9C1 20 ${w-1} 20 ${w-1} 9`,fill:'none'},svg);return;}\n const paths={diamond:`M${w/2} 1L${w-1} ${h/2}L${w/2} ${h-1}L1 ${h/2}Z`,hexagon:`M18 1H${w-18}L${w-1} ${h/2}L${w-18} ${h-1}H18L1 ${h/2}Z`,subroutine:`M1 1H${w-1}V${h-1}H1Z M9 1V${h-1} M${w-9} 1V${h-1}`,'lean-left':`M17 1H${w-1}L${w-17} ${h-1}H1Z`,'lean-right':`M1 1H${w-17}L${w-1} ${h-1}H17Z`,trapezoid:`M17 1H${w-17}L${w-1} ${h-1}H1Z`,'inv-trapezoid':`M1 1H${w-1}L${w-17} ${h-1}H17Z`,odd:`M17 1H${w-1}V${h-1}H17L1 ${h/2}Z`};el('path',{d:paths[n.shape]},svg);\n}\nfunction drawGroupPreview(n,button){\n button.classList.add('preview-group');\n const marks=el('svg',{class:'preview-wires',viewBox:`0 0 ${n.w} ${n.h}`},button),map=new Map(n.preview.rows.map(r=>[r.id,r]));\n n.preview.edges.forEach(e=>{const a=map.get(e.from),b=map.get(e.to),y1=a.y+9,y2=b.y+9,x=22,mid=Math.min(y1,y2)-8;\n const path=el('path',{d:`M${x} ${y1} C6 ${y1} 6 ${y2} ${x} ${y2}`,fill:'none',class:'preview-link','data-from':e.from,'data-to':e.to,...(e.async?{'stroke-dasharray':'3 3'}:{})},marks);\n el('path',{d:`M${x} ${y2}l-4 -2v4Z`,class:'preview-arrow','data-from':e.from,'data-to':e.to},marks);\n });\n n.preview.rows.forEach(row=>{const item=document.createElement('span');item.className=row.heading?'preview-section':'preview-item';item.style.top=row.y+'px';if((sourceModel.architecture||sourceModel.kind==='flowchart')&&row.h)item.style.height=row.h+'px';item.dataset.previewNode=row.id;\n if(!row.heading&&row.icon&&useIcons){const icon=document.createElement('i');icon.setAttribute('data-lucide',row.icon);icon.setAttribute('aria-hidden','true');item.append(icon);}if(sourceModel.kind==='flowchart'){const text=document.createElement('span');text.className='flowchart-name';text.textContent=row.name;item.append(text);}else item.append(document.createTextNode(row.name));\n if(!row.heading){item.onpointerenter=e=>{e.stopPropagation();clear();activeNode=n.id;const linked=sourceModel.edges.filter(e=>e.from===row.id||e.to===row.id),ids=new Set([row.id,...linked.flatMap(e=>[e.from,e.to])]);\n root.querySelectorAll('.preview-item').forEach(el=>el.style.opacity=ids.has(el.dataset.previewNode)?1:.28);\n root.querySelectorAll('.preview-link,.preview-arrow').forEach(el=>{const active=el.dataset.from===row.id||el.dataset.to===row.id;el.style.opacity=active?1:.1;if(active)el.style[el.classList.contains('preview-link')?'stroke':'fill']='var(--viz-series-1)';});\n const matches=e=>(e.members||[]).some(i=>sourceModel.edges[i].from===row.id||sourceModel.edges[i].to===row.id);\n edges.forEach(e=>{const active=matches(e);e.path.style.opacity=active?1:.12;e.path.style.stroke=active?'var(--viz-series-1)':'';if(e.labelElement)e.labelElement.style.opacity=active?1:.25;if(e.countElement)e.countElement.style.opacity=active?1:.25;});\n arrows.forEach(a=>{const active=a.edges.some(matches);a.el.style.opacity=active?1:.12;a.el.style.fill=active?'var(--viz-series-1)':'';});};item.onpointerleave=()=>{clear();focus(n.id);activeNode=n.id;};}\n button.append(item);});\n const detail=button.querySelector('.group-detail');if(n.preview.remaining)detail.firstChild.textContent='另有 '+n.preview.remaining+' 个节点';else detail.remove();\n}\nfunction draw(){root.dataset.balanced=String(!!data.balanced);updateLegend();motionSpecs=[];runningAnimations.forEach(a=>a.cancel());runningAnimations=[];clear();clearTimeout(tipTimer);tip.hidden=true;const width=scene.clientWidth;if(!width)return;lastWidth=width;\nscene.querySelectorAll('.node,.layer,.edge-label,.edge-count,.qv-group').forEach(n=>{n.getAnimations().forEach(a=>a.cancel());n.remove();});svg.querySelectorAll('*').forEach(n=>n.getAnimations().forEach(a=>a.cancel()));root.querySelector('.edge-key').getAnimations().forEach(a=>a.cancel());svg.replaceChildren();nodes=[];arrows=[];edges=[];\nconst horizontal=!data.grouped&&['LR','RL'].includes(data.direction),complex=!horizontal;let height=0;\nconst layoutOffset=data.generic?Math.max(0,(width-data.layout.width)/2):0;\nif(data.generic){nodes=data.nodes.map(n=>({...n,x:n.x+layoutOffset}));for(const g of (sourceModel.kind==='flowchart'?[...data.layout.groups].sort((a,b)=>containmentLevel(b.id)-containmentLevel(a.id)):data.layout.groups)){const box=document.createElement('div');box.className='qv-group';box.dataset.group=g.id;box.style.cssText=`left:${g.x+layoutOffset}px;top:${g.y}px;width:${g.w}px;height:${g.h}px`;setContainmentSurface(box,g.id);const title=document.createElement(sourceModel.architecture?'button':'span');title.textContent=g.name;if(sourceModel.architecture){title.type='button';title.className='architecture-group-title';title.dataset.group=g.id;title.setAttribute('aria-label',g.name+',分组详情');title.onpointerenter=()=>architectureReader.choose(g.id,title);title.onpointerleave=leaveNodeHover;title.onfocus=()=>architectureReader.choose(g.id,title,true);title.onblur=clear;title.onclick=()=>architectureReader.choose(g.id,title);}box.append(title);scene.append(box);}}else if(horizontal){\n const count=data.layers.length,w=Math.min(160,(width-(count-1)*48)/count),gap=count>1?(width-w*count)/(count-1):0;\n const maxRows=Math.max(...data.layers.map((_,layer)=>data.nodes.filter(n=>n.layer===layer).length));\n const extent=(maxRows-1)*76+48;\n nodes=data.nodes.map(n=>{const peers=data.nodes.filter(m=>m.layer===n.layer),i=peers.indexOf(n),column=data.direction==='RL'?count-1-n.layer:n.layer;return {...n,x:column*(w+gap),y:8+(extent-((peers.length-1)*76+48))/2+i*76,w,h:48};});\n} else {\n let top=0;const order=data.layers.map((_,i)=>i);if(data.direction==='BT')order.reverse();\n order.forEach(layer=>{const label=data.layers[layer],members=data.nodes.filter(n=>n.layer===layer);\n if(label){const caption=document.createElement('div');caption.className='layer';caption.textContent=label;caption.style.top=top+'px';caption.style.setProperty('--rule-width',Math.max(0,width-64)+'px');scene.append(caption);top+=36;}\n const w=Math.min(154,(width-(members.length-1)*18)/members.length),h=48;\n {const gap=members.length>1?18:0,total=w*members.length+gap*(members.length-1);members.forEach((n,i)=>nodes.push({...n,x:(width-total)/2+i*(w+gap),y:top,w,h}));top+=h+70;}\n });\n}\nheight=data.layout?.height||Math.max(...nodes.map(n=>n.y+n.h));scene.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\nconst defs=el('defs',{});\nconst map=Object.fromEntries([...(data.layout?.groups||[]).map(g=>({...g,x:g.x+layoutOffset})),...nodes].map(n=>[n.id,n]));\n// 同一侧同时承担出入时分成两个端口;同向连接仍共用端口。\nconst sides=new Map();\nfunction side(n,other){return complex?(other.y<n.y?'top':'bottom'):(other.x<n.x?'left':'right');}\nfor(const e of data.edges){for(const [id,other,incoming] of [[e.from,e.to,false],[e.to,e.from,true]]){const key=id+':'+side(map[id],map[other]);if(!sides.has(key))sides.set(key,new Set());sides.get(key).add(incoming?'in':'out');}}\nfunction port(n,other,incoming){const face=side(n,other),split=sides.get(n.id+':'+face).size===2,offset=split?(incoming?-7:7):0,gap=incoming?1:0;\n if(face==='top')return [n.x+n.w/2+offset,n.y-gap];\n if(face==='bottom')return [n.x+n.w/2+offset,n.y+n.h+gap];\n if(face==='left')return [n.x-gap,n.y+n.h/2+offset];\n return [n.x+n.w+gap,n.y+n.h/2+offset];\n}\ndata.edges.forEach((e,i)=>{const a=map[e.from],b=map[e.to],[sx,sy]=port(a,b,false),[tx,ty]=port(b,a,true);let d;\nif(e.curve){const c=e.curve.map((v,j)=>j%2===0?v+layoutOffset:v);d=`M${c[0]} ${c[1]}C${c[2]} ${c[3]} ${c[4]} ${c[5]} ${c[6]} ${c[7]}`;}else if(e.points){d=rounded(e.points.map(([x,y])=>[x+layoutOffset,y]),12);}else if(!complex){const mid=(sx+tx)/2;d=`M${sx} ${sy}C${mid} ${sy} ${mid} ${ty} ${tx} ${ty}`;}\nelse {const mid=(sy+ty)/2;d=`M${sx} ${sy}C${sx} ${mid} ${tx} ${mid} ${tx} ${ty}`;}\nconst path=el('path',{d,class:'wire','stroke-linejoin':'round','stroke-linecap':'round','data-edge':e.from+'-'+e.to,'data-from':e.from,'data-to':e.to});if(e.invisible)path.style.visibility='hidden';if(e.async)path.setAttribute('stroke-dasharray','4 4');if(e.thick)path.style.strokeWidth='2';const fullLength=path.getTotalLength(),tip=path.getPointAtLength(fullLength),near=path.getPointAtLength(Math.max(0,fullLength-.1)),rawAngle=Math.atan2(tip.y-near.y,tip.x-near.x),axis=Math.round(rawAngle/(Math.PI/2))*Math.PI/2,angle=Math.abs(rawAngle-axis)<.15?axis:rawAngle,ux=Math.cos(angle),uy=Math.sin(angle),bx=tip.x-(e.arrow?5:0)*ux,by=tip.y-(e.arrow?5:0)*uy;\npath.setAttribute('d',d.replace(/[-\\d.]+ [-\\d.]+$/,`${bx} ${by}`));\nconst length=path.getTotalLength();if(e.async)path.setAttribute('stroke-dashoffset',2-length%8);const samples=Array.from({length:Math.ceil(length/5)+1},(_,j)=>path.getPointAtLength(Math.min(length,j*5)));const edge={...e,path,samples};edges.push(edge);\nif(e.arrow&&!e.invisible){const key=[tip.x.toFixed(2),tip.y.toFixed(2),Math.round(angle*100),e.marker||'point'].join(':');let arrow=arrows.find(a=>a.key===key);if(!arrow){const head=el('path',{class:'arrow-head',d:markerPath(tip.x,tip.y,ux,uy,e.marker)});arrow={key,el:head,edges:[]};arrows.push(arrow);}arrow.edges.push(edge);}\n\nif(e.startArrow&&!e.invisible){const first=path.getPointAtLength(0),next=path.getPointAtLength(.1),angle=Math.atan2(first.y-next.y,first.x-next.x),ux=Math.cos(angle),uy=Math.sin(angle),head=el('path',{class:'arrow-head',d:markerPath(first.x,first.y,ux,uy,e.marker)});arrows.push({key:'start'+i,el:head,edges:[edge]});path.setAttribute('d',path.getAttribute('d').replace(/^M[-\\d.]+ [-\\d.]+/,`M${first.x-ux*5} ${first.y-uy*5}`));}\nif(e.bundleCount){const pt=path.getPointAtLength(length/2),badge=document.createElement('span');badge.className='edge-count';badge.textContent=e.bundleCount;badge.setAttribute('aria-label',e.bundleCount+' 条连接');badge.style.left=pt.x+'px';badge.style.top=pt.y+'px';scene.append(badge);edge.countElement=badge;}\nif(e.label){const pt=e.label&&typeof e.label==='string'&&e.labelPosition?{x:e.labelPosition.x+layoutOffset,y:e.labelPosition.y}:path.getPointAtLength(length/2),label=document.createElement('span');label.className='edge-label';label.textContent=e.label;label.style.left=pt.x+'px';label.style.top=(pt.y+(e.bundleCount?24:0))+'px';scene.append(label);edge.labelElement=label;}\nif(!reduced.matches){const maskId='mcx-qv-root-reveal-'+i,mask=el('mask',{id:maskId,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs),reveal=el('path',{d:path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-linecap':'butt','stroke-dasharray':`${length} ${length}`},mask);path.setAttribute('mask',`url(#${maskId})`);motionSpecs.push(()=>reveal.animate([{strokeDashoffset:length},{strokeDashoffset:0}],{duration:650,delay:(a.layer||0)*220+110,easing:'linear',fill:'backwards'}));}\n});\nif(!reduced.matches)arrows.forEach(a=>{const layer=Math.min(...a.edges.map(e=>(map[e.from].layer||0)));motionSpecs.push(()=>a.el.animate([{opacity:0},{opacity:1}],{duration:50,delay:layer*220+760,fill:'backwards'}));});\nnodes.forEach(n=>{const button=document.createElement('button');button.type='button';button.className='node';button.dataset.layer=n.layer;button.dataset.shape=n.shape||'rect';if(n.junction)button.classList.add('junction');button.dataset.node=n.id;if(n.groupCard)button.classList.add('group-card');if(n.external)button.classList.add('external');button.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;setContainmentSurface(button,n.targetGroup!==undefined?n.targetGroup:n.id);button.setAttribute('aria-label',n.name);const label=document.createElement('span');label.className='name';if(useIcons&&!n.junction){const icon=document.createElement('i');icon.setAttribute('data-lucide',n.icon);icon.setAttribute('aria-hidden','true');label.append(icon);}if(!n.junction){if(sourceModel.architecture||sourceModel.kind==='flowchart'){const text=document.createElement('span');text.className=sourceModel.architecture?'architecture-name':'flowchart-name';text.textContent=n.name;label.append(text);}else label.append(document.createTextNode(n.name));}if(n.textWidth&&!n.groupCard){label.style.width=n.textWidth+'px';label.style.lineHeight=n.labelLine+'px';}button.append(label);if(n.groupCard){const detail=document.createElement('span');detail.className='group-detail';detail.append(document.createTextNode(n.count+' 个节点'));const arrow=document.createElement('i');arrow.setAttribute('data-lucide','chevron-right');arrow.setAttribute('aria-hidden','true');const enter=document.createElement('span');enter.className='group-enter';enter.append(arrow);label.append(enter);button.append(detail);if(n.preview)drawGroupPreview(n,button);button.setAttribute('aria-label',n.name+','+n.count+' 个节点,进入分组');button.onclick=e=>navigateScope(n.targetGroup,e.detail===0,n.button);}\nif(n.external){if(n.targetGroup!==undefined){button.classList.add('external-group');const arrow=document.createElement('i');arrow.setAttribute('data-lucide','arrow-up-right');arrow.setAttribute('aria-hidden','true');label.append(arrow);label.style.width='100%';button.setAttribute('aria-label',n.name+','+(n.targetGroup===null?'返回总览':'进入此分组'));button.onclick=e=>navigateScope(n.targetGroup,e.detail===0,n.button);}else{const context=document.createElement('span');context.className='external-label';context.textContent='来自其他分组';button.append(context);}}\nif(n.shape&& !['rect','round'].includes(n.shape))drawNodeShape(n,button);button.onpointerenter=e=>queueNodeHover(n,e);button.onpointermove=e=>{if(activeNode!==n.id&&root.dataset.motion!=='running')queueNodeHover(n,e);};button.onpointerleave=leaveNodeHover;button.onfocus=()=>showTip(n);button.onblur=clear;if(sourceModel.architecture&&!button.onclick)button.onclick=()=>showTip(n,{type:'click'});scene.append(button);n.button=button;if(!reduced.matches)motionSpecs.push(()=>button.animate([{opacity:0,transform:'scale(.08)',offset:0},{opacity:1,transform:'scale(.7)',offset:.55},{opacity:1,transform:'scale(1)',offset:1}],{duration:240,delay:n.layer*220,easing:'cubic-bezier(.2,.65,.3,1)',fill:'backwards'}));});\nif(window.lucide)window.lucide.createIcons({attrs:{width:16,height:16}});\narchitectureReader?.refresh();diagramViewport.refresh();\n}\nlet crumbPanel,crumbAnchor,crumbCloseTimer,crumbOpenTimer;\nfunction closeCrumbPanel(){clearTimeout(crumbOpenTimer);clearTimeout(crumbCloseTimer);if(crumbPanel?.matches(':popover-open'))crumbPanel.hidePopover();crumbAnchor?.setAttribute('aria-expanded','false');}\nfunction openCrumbPanel(anchor,group,keyboard=false){\n if(navigationBusy||!drilldownActive||group.id===null)return;closeCrumbPanel();crumbAnchor=anchor;\n if(!crumbPanel){crumbPanel=document.createElement('div');crumbPanel.className='scope-peers';crumbPanel.setAttribute('popover','auto');crumbPanel.setAttribute('aria-label','同层级分组');root.append(crumbPanel);\n crumbPanel.onpointerenter=()=>clearTimeout(crumbCloseTimer);crumbPanel.onpointerleave=()=>crumbCloseTimer=setTimeout(closeCrumbPanel,180);\n crumbPanel.addEventListener('toggle',()=>{if(!crumbPanel.matches(':popover-open'))crumbAnchor?.setAttribute('aria-expanded','false');});\n crumbPanel.onkeydown=e=>{const items=[...crumbPanel.querySelectorAll('button')],i=items.indexOf(document.activeElement);if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)){e.preventDefault();items[e.key==='Home'?0:e.key==='End'?items.length-1:(i+(e.key==='ArrowDown'?1:-1)+items.length)%items.length]?.focus();}else if(e.key==='Escape'){e.preventDefault();closeCrumbPanel();crumbAnchor?.focus({preventScroll:true});}};\n }\n crumbPanel.replaceChildren();const peers=sourceModel.groups.filter(g=>(g.parent||null)===(group.id===null?null:group.parent||null));\n for(const peer of peers){const item=document.createElement('button');item.type='button';item.textContent=peer.name;item.dataset.peer=peer.id;if(peer.id===group.id)item.setAttribute('aria-current','location');item.onclick=e=>{closeCrumbPanel();navigateScope(peer.id,e.detail===0);};crumbPanel.append(item);}\n if(!peers.length)return;\n crumbPanel.style.visibility='hidden';crumbPanel.showPopover();anchor.setAttribute('aria-expanded','true');const box=anchor.getBoundingClientRect(),w=crumbPanel.offsetWidth,h=crumbPanel.offsetHeight;\n crumbPanel.style.left=Math.max(8,Math.min(innerWidth-w-8,box.left))+'px';crumbPanel.style.top=(box.bottom+6+h<=innerHeight-8?box.bottom+6:Math.max(8,box.top-h-6))+'px';crumbPanel.style.visibility='';if(keyboard)crumbPanel.querySelector('button')?.focus({preventScroll:true});\n}\nfunction drawNavigation(){\n closeCrumbPanel();const nav=root.querySelector('.scope-nav');nav.hidden=!navigationEnabled;if(!navigationEnabled)return;nav.replaceChildren();\n const groups=new Map(sourceModel.groups.map(g=>[g.id,g])),path=[];for(let g=groups.get(currentScope);g;g=groups.get(g.parent))path.unshift(g);\n function button(name,target,icon){const b=document.createElement('button');b.type='button';b.disabled=navigationBusy;b.dataset.scope=target||'';if(icon){const i=document.createElement('i');i.setAttribute('data-lucide',icon);i.setAttribute('aria-hidden','true');b.append(i);b.setAttribute('aria-label',name);}else b.textContent=name;b.onclick=e=>navigateScope(target,e.detail===0);nav.append(b);return b;}\n\n const crumbs=drilldownActive?[{id:null,name:'总览'},...path]:[];crumbs.forEach((g,index)=>{\n if(index){const sep=document.createElement('span');sep.textContent='/';sep.setAttribute('aria-hidden','true');nav.append(sep);}\n const crumb=button(g.name,g.id);crumb.dataset.crumb=g.id||'overview';if(index===crumbs.length-1)crumb.setAttribute('aria-current','location');\n if(drilldownActive&&g.id!==null){crumb.setAttribute('aria-haspopup','true');crumb.setAttribute('aria-expanded','false');crumb.onpointerenter=()=>{clearTimeout(crumbCloseTimer);clearTimeout(crumbOpenTimer);crumbOpenTimer=setTimeout(()=>openCrumbPanel(crumb,g),120);};crumb.onpointerleave=()=>{clearTimeout(crumbOpenTimer);crumbCloseTimer=setTimeout(closeCrumbPanel,180);};crumb.onkeydown=e=>{if(e.key==='ArrowDown'){e.preventDefault();openCrumbPanel(crumb,g,true);}};}\n });\n const control=document.createElement('label');control.className='form-check form-switch scope-toggle';const input=document.createElement('input');input.type='checkbox';input.className='form-check-input';input.setAttribute('role','switch');input.checked=drilldownActive;input.disabled=navigationBusy;input.onchange=()=>setNavigationMode(input.checked);const text=document.createElement('span');text.className='form-check-label';text.textContent='节点组';control.append(text,input);nav.append(control);\n if(window.lucide)window.lucide.createIcons({attrs:{width:16,height:16}});\n}\nfunction capturePositions(){return new Map(nodes.map(n=>[n.id,n.button.getBoundingClientRect()]));}\nfunction transitionView(before,anchor,keyboard){\n runningAnimations.forEach(a=>a.cancel());runningAnimations=[];clearTimeout(finishTimer);root.dataset.motion='complete';\n if(reduced.matches||keyboard||diagramViewport.expanded)return;\n const ease='cubic-bezier(.22,1,.36,1)';\n if(anchor){const target=scene.getBoundingClientRect(),ghost=document.createElement('div');ghost.className='scope-envelope';ghost.style.cssText=`left:0;top:0;width:${target.width}px;height:${target.height}px`;scene.append(ghost);const dx=anchor.x-target.x,dy=anchor.y-target.y;const anim=ghost.animate([{transform:`translate(${dx}px,${dy}px) scale(${anchor.width/target.width},${anchor.height/target.height})`,opacity:.7},{transform:'translate(0,0) scale(1)',opacity:0}],{duration:360,easing:ease});runningAnimations.push(anim);anim.finished.catch(()=>{}).finally(()=>ghost.remove());}\n\n nodes.forEach(n=>{const end=n.button.getBoundingClientRect(),old=before.get(n.id)||anchor,dx=old?old.x+old.width/2-end.x-end.width/2:0,dy=old?old.y+old.height/2-end.y-end.height/2:12;\n runningAnimations.push(n.button.animate([{transform:`translate(${dx}px,${dy}px) scale(${before.has(n.id)?1:.94})`,opacity:before.has(n.id)?1:0},{transform:'translate(0,0) scale(1)',opacity:1}],{duration:360,easing:ease}));\n });\n edges.forEach(e=>{const path=e.path,length=path.getTotalLength(),mask=svg.querySelector('#'+CSS.escape(path.getAttribute('mask')?.slice(5,-1)||''));if(mask){const reveal=mask.querySelector('path');runningAnimations.push(reveal.animate([{strokeDashoffset:length},{strokeDashoffset:0}],{duration:380,delay:60,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));}});\n arrows.forEach(a=>runningAnimations.push(a.el.animate([{opacity:0},{opacity:1}],{duration:140,delay:220,fill:'backwards'})));\n}\nasync function setNavigationMode(enabled){if(sourceModel.architecture)return;\n if(navigationBusy)return;navigationBusy=true;clear();drawNavigation();viewport.setAttribute('aria-busy','true');\n try{\n const next=enabled?mermaidScopeView(sourceModel):{...sourceModel,nodes:sourceModel.nodes.map(n=>({...n})),edges:sourceModel.edges.map(e=>({...e})),groups:(sourceModel.groups||[]).map(g=>({...g}))};\n if(enabled)await prepareNavigationView(next);else if(next.kind==='flowchart')await prepareFlowchartLayout(next,mermaidRuntime,cfg.source);else if(next.nativeGeometry)await prepareNativeGeometry(next,mermaidRuntime,cfg.source,next.db,next.architecture);else if(next.generic)await prepareMermaidLayout(next);\n data=next;drilldownActive=enabled;currentScope=null;root.dataset.navigation=String(enabled);root.dataset.scope=enabled?'overview':'expanded';\n scene.style.minWidth=(data.layout?data.layout.width:data.grouped?Math.max(736,Math.max(...data.layers.map((_,i)=>data.nodes.filter(n=>n.layer===i).length))*150-18):Math.max(560,data.layers.length*160+(data.layers.length-1)*48))+'px';\n draw();viewport.scrollLeft=0;playMotion();root.querySelector('.scope-status').textContent=enabled?'节点组':'完整架构';\n }catch(error){notice.hidden=false;notice.textContent='无法切换视图:'+error.message;}\n finally{navigationBusy=false;viewport.setAttribute('aria-busy','false');drawNavigation();root.querySelector('.scope-toggle input')?.focus({preventScroll:true});}\n}\nasync function navigateScope(scope,keyboard=false,origin=null){\n if(navigationBusy||!drilldownActive||scope===currentScope)return;\n navigationBusy=true;clear();drawNavigation();viewport.setAttribute('aria-busy','true');scopeScroll.set(currentScope,viewport.scrollLeft);\n try{const before=capturePositions(),anchor=origin?.getBoundingClientRect();const next=mermaidScopeView(sourceModel,scope);await prepareCompleteScope(next,root.clientWidth);data=next;currentScope=scope;root.dataset.scope=scope||'overview';scene.style.minWidth=data.layout.width+'px';draw();viewport.scrollLeft=scopeScroll.get(scope)||0;transitionView(before,anchor,keyboard);root.querySelector('.scope-status').textContent='已进入'+(sourceModel.groups.find(g=>g.id===scope)?.name||'总览');}\n catch(error){notice.hidden=false;notice.textContent='无法进入分组:'+error.message;}\n finally{navigationBusy=false;viewport.setAttribute('aria-busy','false');drawNavigation();if(keyboard)root.querySelector('.scope-nav button')?.focus({preventScroll:true});}\n}\nif(sourceModel.architecture)architectureReader=createArchitectureReader(root,sourceModel,viewport,diagramViewport,focus,()=>clearTimeout(leaveTimer),leaveNodeHover);\ndrawNavigation();\nnew ResizeObserver(()=>requestAnimationFrame(()=>{if(architectureReader)architectureReader.refresh();else clear();})).observe(root);viewport.addEventListener('scroll',()=>{if(architectureReader)architectureReader.onScroll();else clear();},{passive:true});viewport.addEventListener('pointermove',e=>{if(sourceModel.kind==='flowchart'&&activeNode&&!e.target.closest('button.node')){clearTimeout(leaveTimer);leaveTimer=setTimeout(clear,220);}},{passive:true});root.onpointerleave=leaveNodeHover;document.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});function playMotion(){if(diagramViewport.expanded){runningAnimations.forEach(a=>a.cancel());root.dataset.motion='complete';return;}clear();runningAnimations.forEach(a=>a.cancel());root.dataset.motion=reduced.matches?'complete':'running';root.dataset.replays=String(+(root.dataset.replays||0)+1);runningAnimations=reduced.matches?[]:motionSpecs.map(start=>start());clearTimeout(finishTimer);finishTimer=setTimeout(()=>root.dataset.motion='complete',Math.max(...data.nodes.map(n=>n.layer))*220+810);}\nfunction scheduleEntry(){clearTimeout(replayTimer);replayTimer=setTimeout(()=>{replayTimer=null;if(visible&&!document.hidden){playMotion();}else armed=true;},80);}\nfunction replay(){if(visible&&armed&&!document.hidden){armed=false;scheduleEntry();}}\nreduced.addEventListener('change',()=>{runningAnimations.forEach(a=>a.cancel());root.dataset.motion='complete';});\nnew IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){visible=false;armed=true;}else if(e.intersectionRatio>=.2||(e.rootBounds&&e.boundingClientRect.height>e.rootBounds.height)){visible=true;replay();}},{threshold:[0,.2]}).observe(root);\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else replay();});new ResizeObserver(()=>{if(scene.clientWidth!==lastWidth){draw();if(replayTimer)scheduleEntry();}}).observe(scene);draw();finishLoading();\nroot.addEventListener('qv:display-mode',()=>{closeCrumbPanel();clear();architectureReader?.refresh();clearTimeout(replayTimer);clearTimeout(finishTimer);runningAnimations.forEach(a=>a.cancel());root.dataset.motion='complete';});\n})();\n"}
|
|
1
|
+
{"markup":"<div id=\"mcx-qv-root\" class=\"qv-mermaid\" data-state=\"loading\" role=\"figure\" aria-label=\"\">\n<style>\n#mcx-qv-root{position:relative;padding-top:32px;width:100%;color:var(--foreground)}\n#mcx-qv-root h3{margin:0 90px 32px 0;font-weight:500}\n#mcx-qv-root .scope-nav{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 24px;font-size:12px;color:var(--muted-foreground)}\n#mcx-qv-root .scope-nav button{font:inherit;color:inherit;border:0;background:none;padding:5px 4px;cursor:var(--cursor-interaction,pointer)}\n#mcx-qv-root .scope-nav button:hover{color:var(--foreground)}\n#mcx-qv-root .scope-nav [aria-current]{color:var(--foreground);font-weight:500}\n#mcx-qv-root .scope-nav .scope-count{margin-left:auto;font-variant-numeric:tabular-nums}\n#mcx-qv-root[data-navigation=\"true\"] button.node{justify-content:flex-start;text-align:left;background:var(--background);color:var(--foreground)}\n#mcx-qv-root[data-navigation=\"true\"] button.node.group-card{flex-direction:column;align-items:flex-start;justify-content:center;gap:9px;border-color:color-mix(in srgb,var(--viz-series-1) 20%,var(--border));background:color-mix(in srgb,var(--viz-series-1) 3%,var(--background));padding:14px 16px}\n#mcx-qv-root button.node.group-card .group-detail{display:flex;align-items:center;justify-content:space-between;width:100%;color:var(--muted-foreground);font-size:12px}\n#mcx-qv-root button.node.group-card:hover{border-color:var(--viz-series-1)}\n#mcx-qv-root button.node.external{flex-direction:column;align-items:flex-start;justify-content:center;gap:6px;color:var(--muted-foreground)}\n#mcx-qv-root button.node .external-label{font-size:11px;color:var(--muted-foreground)}\n#mcx-qv-root[data-navigation=\"true\"] .edge-label{font-size:11px}\n#mcx-qv-root .scope-nav svg{width:14px;height:14px;flex:none}\n#mcx-qv-root .scene{position:relative;width:100%}\n#mcx-qv-root .diagram-scroll{width:100%;overflow-x:auto;overscroll-behavior-x:contain;padding-bottom:4px}\n#mcx-qv-root .qv-group{position:absolute;border:1px solid var(--border);border-radius:12px;box-sizing:border-box;pointer-events:none;background:color-mix(in srgb,var(--muted) 16%,transparent)}\n#mcx-qv-root .qv-group>span{position:absolute;left:16px;top:10px;color:var(--muted-foreground);font-size:12px}\n#mcx-qv-root button.node .name{position:relative;z-index:1}\n#mcx-qv-root button.node .node-outline{position:absolute;inset:0;width:100%;height:100%;overflow:visible;fill:var(--background);stroke:var(--node-highlight,var(--border));stroke-width:1}\n#mcx-qv-root button.node.shaped{border:0}\n#mcx-qv-root button.node.junction{border:0;background:var(--muted-foreground);border-radius:50%;padding:0}\n#mcx-qv-root .wires{display:block;width:100%;position:absolute;inset:0;overflow:visible;pointer-events:none}\n#mcx-qv-root .arrow-head{fill:color-mix(in srgb,var(--muted-foreground) 26%,var(--background));pointer-events:none}\n#mcx-qv-root .wire{fill:none;stroke:color-mix(in srgb,var(--muted-foreground) 26%,var(--background));stroke-width:1;opacity:1;transition:opacity 140ms ease}\n#mcx-qv-root button.node{position:absolute;display:flex;flex-direction:row;align-items:center;justify-content:center;padding:12px 14px;text-align:left;font:inherit;color:var(--foreground);background:var(--background);border:1px solid var(--border);border-radius:10px;box-sizing:border-box;cursor:var(--cursor-interaction,pointer);transition:opacity 140ms ease,border-color 140ms ease}\n#mcx-qv-root .edge-label{position:absolute;transform:translate(-50%,-50%);padding:2px 5px;background:var(--background);color:var(--muted-foreground);font-size:12px;pointer-events:none}\n#mcx-qv-root .qv-notice{font-size:12px;color:var(--muted-foreground)}\n#mcx-qv-root button.node .name{display:flex;align-items:center;gap:8px;font-weight:500;white-space:nowrap}\n#mcx-qv-root button.node[data-layer=\"1\"]{background:color-mix(in srgb,var(--viz-series-1) 6%,var(--background))}\n#mcx-qv-root button.node[data-layer=\"3\"]{background:var(--secondary);color:var(--secondary-foreground)}\n#mcx-qv-root .layer{position:absolute;left:0;color:var(--muted-foreground);font-size:12px;pointer-events:none}\n#mcx-qv-root .layer:after{content:'';position:absolute;left:64px;top:50%;width:var(--rule-width);height:1px;background:var(--border);opacity:.55}\n#mcx-qv-root .edge-key{display:flex;justify-content:center;gap:22px;margin-top:20px;padding:16px 0;color:var(--muted-foreground);font-size:12px}\n#mcx-qv-root [hidden]{display:none!important}\n#mcx-qv-root .edge-key span{display:inline-flex;align-items:center;gap:8px}\n#mcx-qv-root .edge-key i{width:20px;border-top:1px solid var(--muted-foreground)}\n#mcx-qv-root .edge-key .async{border-top-style:dashed}\n@media(prefers-reduced-motion:reduce){#mcx-qv-root button.node,#mcx-qv-root .wire{transition:none}}\n#mcx-qv-root .route-controls{display:flex;justify-content:flex-end;margin:-16px 0 24px;gap:8px;align-items:center}\n#mcx-qv-root .route-controls label{white-space:nowrap;flex:none}\n#mcx-qv-root .node-tip{position:absolute;z-index:10;max-width:260px;width:max-content;box-sizing:border-box;padding:10px 12px;border-radius:7px;background:#202020;color:#fff;font-size:12px;line-height:1.6;pointer-events:none;box-shadow:0 3px 12px #0002}\n\n#mcx-qv-root[data-navigation=\"true\"] button.node.preview-group{padding:18px 16px;justify-content:flex-start;gap:0;background:color-mix(in srgb,var(--muted) 18%,var(--background));border-color:var(--border);border-radius:14px}\n#mcx-qv-root button.preview-group>.name{font-size:14px;gap:8px}\n#mcx-qv-root button.preview-group>.name svg{color:var(--muted-foreground)}\n#mcx-qv-root button.node.preview-group .group-detail{position:absolute;bottom:12px;left:16px;width:calc(100% - 32px);font-size:11px}\n#mcx-qv-root .preview-item{position:absolute;left:26px;right:12px;display:flex;gap:8px;align-items:center;font-size:12px;font-weight:400;white-space:nowrap;height:20px;overflow:hidden;text-overflow:ellipsis}\n#mcx-qv-root .preview-item svg{width:14px;height:14px;flex:none;color:var(--muted-foreground)}\n#mcx-qv-root .preview-section{position:absolute;left:24px;right:12px;font-size:11px;font-weight:500;color:var(--muted-foreground);text-align:left}\n#mcx-qv-root .preview-wires{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}\n#mcx-qv-root .preview-link{stroke:color-mix(in srgb,var(--muted-foreground) 22%,transparent);stroke-width:.8}\n#mcx-qv-root .preview-arrow{fill:color-mix(in srgb,var(--muted-foreground) 30%,transparent)}\n#mcx-qv-root .preview-group:hover .preview-link{stroke:color-mix(in srgb,var(--viz-series-1) 45%,transparent)}\n#mcx-qv-root .preview-group:hover .preview-arrow{fill:var(--viz-series-1)}\n\n#mcx-qv-root .scope-toggle{margin-left:auto}\n#mcx-qv-root button.node.group-card>.name{width:100%}\n#mcx-qv-root button.node .group-enter{display:flex;margin-left:auto;width:14px;height:14px;color:var(--muted-foreground)}\n#mcx-qv-root .edge-count{position:absolute;transform:translate(-50%,-50%);width:22px;height:22px;display:grid;place-items:center;border-radius:50%;border:1px solid var(--border);background:var(--background);color:var(--muted-foreground);font-size:11px;font-variant-numeric:tabular-nums;pointer-events:none}\n#mcx-qv-root[data-has-groups=\"true\"] button.node:not(.group-card){background:color-mix(in srgb,var(--muted) 18%,var(--background));color:var(--foreground);border-radius:12px;font-size:12px;padding:12px 16px}\n#mcx-qv-root[data-has-groups=\"true\"] button.node .name svg{width:14px;height:14px;color:var(--muted-foreground)}\n#mcx-qv-root[data-has-groups=\"true\"] .qv-group{background:color-mix(in srgb,var(--muted) 12%,transparent);border-radius:14px}\n\n#mcx-qv-root .scope-envelope{position:absolute;box-sizing:border-box;border:1px solid var(--border);border-radius:14px;transform-origin:0 0;pointer-events:none}\n#mcx-qv-root button.node:active{filter:brightness(.98)}\n#mcx-qv-root button.node.shaped[data-shape]{background:transparent}\n\n#mcx-qv-root[data-architecture=true] button.node>.name{width:100%;align-items:flex-start;white-space:normal;line-height:20px}\n#mcx-qv-root[data-architecture=true] .architecture-name{flex:1;min-width:0;font-size:14px;line-height:20px;overflow-wrap:anywhere;white-space:normal}\n#mcx-qv-root[data-architecture=true] button.node>.name>svg{flex:none;width:16px;height:16px;position:relative;top:2px}\n#mcx-qv-root[data-architecture=true] button.preview-group>.name{position:absolute;left:16px;top:16px;width:calc(100% - 32px)}\n#mcx-qv-root[data-architecture=true] button.preview-group>.name .group-enter{position:static;flex:none;align-self:flex-start}\n#mcx-qv-root[data-architecture=true] .preview-item{height:auto;white-space:normal;overflow:visible;line-height:18px;align-items:flex-start}\n#mcx-qv-root[data-architecture=true] .preview-item>svg{flex:none}\n#mcx-qv-root[data-architecture=true] .preview-section{white-space:normal;line-height:18px}\n#mcx-qv-root[data-architecture=true] .qv-group>span{max-width:calc(100% - 32px);line-height:18px;overflow-wrap:anywhere}\n\n#mcx-qv-root[data-architecture=true] button.node.junction{padding:0;min-width:0;min-height:0;border:0;border-radius:50%;background:var(--muted-foreground)}\n#mcx-qv-root .architecture-detail{position:absolute;z-index:20;box-sizing:border-box;background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);border-radius:12px;padding:14px 16px;font-size:12px;line-height:18px;white-space:normal;overflow-wrap:anywhere;max-height:min(380px,calc(100vh - 32px));overflow-y:auto;pointer-events:auto;box-shadow:0 8px 24px #00000024}\n#mcx-qv-root .architecture-detail-name{font-size:14px;line-height:20px;font-weight:500;margin-bottom:4px}\n#mcx-qv-root .architecture-detail-label{color:var(--muted-foreground);font-size:11px;line-height:16px}\n#mcx-qv-root .architecture-detail-section{border-top:1px solid var(--border);padding-top:10px;margin-top:10px}\n#mcx-qv-root .architecture-detail-body{margin-top:4px}\n#mcx-qv-root .architecture-detail-link{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);gap:10px;margin-top:8px;align-items:start}\n#mcx-qv-root .architecture-detail-arrow{color:var(--muted-foreground);padding-top:1px}\n#mcx-qv-root .architecture-group-title{position:absolute;left:16px;top:10px;max-width:calc(100% - 32px);padding:0;border:0;background:none;color:var(--muted-foreground);font:inherit;font-size:12px;line-height:18px;text-align:left;overflow-wrap:anywhere;pointer-events:auto;cursor:var(--cursor-interaction,pointer)}\n\n#mcx-qv-root[data-kind=flowchart] button.node:not(.group-card)>.name{margin:auto;justify-content:center;max-width:100%;white-space:normal;flex:none}\n#mcx-qv-root[data-kind=flowchart] .flowchart-name{flex:1;min-width:0;text-align:center;white-space:normal;overflow-wrap:anywhere}\n#mcx-qv-root[data-kind=flowchart] button.node>.name>svg{flex:none}\n#mcx-qv-root[data-kind=flowchart] .preview-item{height:auto;white-space:normal;overflow:visible;line-height:18px;align-items:flex-start}\n#mcx-qv-root[data-kind=flowchart] .preview-section{white-space:normal;line-height:18px}\n#mcx-qv-root[data-kind=flowchart] .preview-item>svg{flex:none}\n#mcx-qv-root[data-kind=flowchart] .preview-item .flowchart-name{text-align:left}\n#mcx-qv-root[data-kind=flowchart]{--flow-wire:light-dark(color-mix(in srgb,var(--muted-foreground) 26%,var(--background)),color-mix(in srgb,var(--muted-foreground) 36%,var(--background)))}\n#mcx-qv-root[data-kind=flowchart] .wire{stroke:var(--flow-wire)}\n#mcx-qv-root[data-kind=flowchart] .arrow-head{fill:var(--flow-wire)}\n#mcx-qv-root .scope-peers{position:fixed;inset:auto;margin:0;padding:6px;width:240px;max-width:calc(100vw - 16px);max-height:min(320px,calc(100vh - 16px));overflow:auto;box-sizing:border-box;border:1px solid var(--border);border-radius:10px;background:var(--background);color:var(--foreground);box-shadow:0 8px 24px #0002;font:inherit}\n#mcx-qv-root .scope-peers::backdrop{background:transparent;pointer-events:none}\n#mcx-qv-root .scope-peers button{display:block;width:100%;padding:9px 10px;text-align:left;border:0;border-radius:6px;background:none;color:inherit;font:inherit;font-size:12px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n#mcx-qv-root .scope-peers button:hover,#mcx-qv-root .scope-peers button:focus-visible{background:var(--secondary)}\n#mcx-qv-root .scope-peers button[aria-current]{color:var(--viz-series-1);font-weight:500}\n#mcx-qv-root button.node.external-group{flex-direction:row;align-items:center}\n#mcx-qv-root button.node.external-group>.name{margin:0;width:100%;justify-content:flex-start}\n#mcx-qv-root button.node.external-group>.name>svg:last-child{width:14px;height:14px;margin-left:8px;color:var(--muted-foreground);flex:none}\n#mcx-qv-root button.node.external-group .flowchart-name{text-align:left}\n#mcx-qv-root[data-kind=flowchart] .qv-group{background:linear-gradient(var(--node-surface,var(--background)),var(--node-surface,var(--background))),var(--background)}\n#mcx-qv-root[data-kind=flowchart] button.node:not(.junction){background:linear-gradient(var(--node-surface,var(--background)),var(--node-surface,var(--background))),var(--background);color:var(--foreground)}\n#mcx-qv-root[data-kind=flowchart] button.node.shaped[data-shape]{background:transparent}\n#mcx-qv-root[data-kind=flowchart] .node-outline{fill:var(--node-surface,var(--background))}\n#mcx-qv-root[data-kind=flowchart] .wire{transition:opacity 140ms ease,stroke 140ms ease,stroke-width 140ms ease}\n#mcx-qv-root[data-kind=flowchart] .arrow-head{transition:opacity 140ms ease,fill 140ms ease}\n#mcx-qv-root[data-kind=flowchart] .edge-label,#mcx-qv-root[data-kind=flowchart] .edge-count{transition:opacity 140ms ease}\n#mcx-qv-root[data-kind=flowchart] .node-outline{transition:stroke 140ms ease}\n@media(prefers-reduced-motion:reduce){#mcx-qv-root[data-kind=flowchart] .wire,#mcx-qv-root[data-kind=flowchart] .arrow-head,#mcx-qv-root[data-kind=flowchart] .edge-label,#mcx-qv-root[data-kind=flowchart] .edge-count,#mcx-qv-root[data-kind=flowchart] .node-outline{transition:none}}\n#mcx-qv-root[data-kind=flowchart]{--flow-container-surface:color-mix(in srgb,var(--muted) 18%,var(--background))}\n#mcx-qv-root[data-kind=flowchart] .qv-group{z-index:0}\n#mcx-qv-root[data-kind=flowchart] .wires{z-index:1}\n#mcx-qv-root[data-kind=flowchart] button.node{z-index:2}\n#mcx-qv-root[data-kind=flowchart] .edge-label,#mcx-qv-root[data-kind=flowchart] .edge-count{z-index:3}\n\n#mcx-qv-root[data-architecture=true] .qv-group{z-index:0;background:var(--node-surface,var(--background))}\n#mcx-qv-root[data-architecture=true] .wires{z-index:1}\n#mcx-qv-root[data-architecture=true] button.node{z-index:2}\n#mcx-qv-root[data-architecture=true] button.node:not(.junction){background:var(--node-surface,var(--background))}\n#mcx-qv-root[data-architecture=true] .edge-label{z-index:3}\n#mcx-qv-root[data-architecture=true] .wire{stroke:light-dark(color-mix(in srgb,var(--muted-foreground) 36%,var(--background)),color-mix(in srgb,var(--muted-foreground) 48%,var(--background)))}\n#mcx-qv-root[data-architecture=true] .arrow-head{fill:light-dark(color-mix(in srgb,var(--muted-foreground) 36%,var(--background)),color-mix(in srgb,var(--muted-foreground) 48%,var(--background)))}\n</style>\n<h3 hidden></h3><nav class=\"scope-nav\" aria-label=\"架构层级\" hidden></nav><div class=\"qv-notice\" role=\"status\" hidden></div><div class=\"node-tip\" id=\"mcx-qv-root-tip\" role=\"tooltip\" hidden></div><div class=\"diagram-scroll\" role=\"region\" aria-label=\"架构图,可横向滚动\" aria-busy=\"true\"><div class=\"scene\"><svg class=\"wires\" aria-hidden=\"true\"></svg></div></div>\n<div class=\"edge-key\"><span class=\"solid-key\"><i></i><span class=\"key-label\"></span></span><span class=\"async-key\"><i class=\"async\"></i><span class=\"key-label\"></span></span></div>\n\n</div>\n<style>#mcx-qv-root.qv-mermaid{--mcx-ease-out:cubic-bezier(.23,1,.32,1);--mcx-tip-shadow:0 4px 16px light-dark(color-mix(in srgb,var(--foreground) 9%,transparent),rgb(0 0 0/.5));--mcx-tip-surface:light-dark(var(--popover),color-mix(in srgb,var(--popover-foreground) 6%,var(--popover)))}\n#mcx-qv-root.qv-mermaid :is(.node-tip,.seq-tip,.er-tip,.class-tip,.state-tip,.state-peers,.xy-tip,.sankey-detail,.radar-detail,.quadrant-detail,.architecture-detail,.bubble,.quick-radar-tooltip){background:var(--mcx-tip-surface);color:var(--popover-foreground);border:1px solid var(--border);border-radius:12px;padding:12px 14px;font-size:12px;line-height:18px;box-shadow:var(--mcx-tip-shadow);transition:opacity 140ms var(--mcx-ease-out),scale 140ms var(--mcx-ease-out)}\n@starting-style{#mcx-qv-root.qv-mermaid :is(.node-tip,.seq-tip,.er-tip,.class-tip,.state-tip,.state-peers,.xy-tip,.sankey-detail,.radar-detail,.quadrant-detail,.architecture-detail,.bubble,.quick-radar-tooltip){opacity:0;scale:.97}}\n#mcx-qv-root.qv-mermaid :is(.xy-tip-title,.sankey-detail-title,.radar-detail-title,.c4-tip-title,.state-tip-name,.bubble-title,.quick-radar-tooltip-dimension){font-size:14px;line-height:20px;font-weight:500}\n#mcx-qv-root.qv-mermaid :is(.xy-tip-name,.radar-detail-name,.quick-radar-tooltip-name){color:var(--muted-foreground);font-weight:400}\n#mcx-qv-root.qv-mermaid :is(.xy-tip-value,.radar-detail-value,.quick-radar-tooltip-value,.bubble-value){color:var(--popover-foreground);font-weight:500;font-variant-numeric:tabular-nums}\n#mcx-qv-root.qv-mermaid :is(.state-tip-caption,.architecture-detail-label){font-size:12px;line-height:18px}\n@media (prefers-reduced-motion:reduce){#mcx-qv-root.qv-mermaid :is(.node-tip,.seq-tip,.er-tip,.class-tip,.state-tip,.state-peers,.xy-tip,.sankey-detail,.radar-detail,.quadrant-detail,.architecture-detail,.bubble,.quick-radar-tooltip){transition:none}}\n\n/* 规则 37:标题、正文与辅助信息保持稳定层级。 */\n#mcx-qv-root.qv-mermaid h3{font-size:16px;line-height:24px}\n#mcx-qv-root.qv-mermaid .state-node{font-size:14px;line-height:20px}\n#mcx-qv-root.qv-mermaid .timeline-period{font-size:14px}\n#mcx-qv-root.qv-mermaid .er-type{color:var(--muted-foreground)}\n\n/* 内容画布在宽容器内居中,超宽时保留左侧可达的滚动起点。 */\n#mcx-qv-root.qv-mermaid[data-state=\"enhanced\"]:not([data-renderer=\"qv-journey\"]) .scene{margin-inline:auto}\n/* Journey 的固定参考轴与步骤画布一起居中,溢出时只滚动步骤。 */\n#mcx-qv-root.qv-mermaid[data-renderer=\"qv-journey\"] .journey-frame{max-width:100%;margin-inline:auto}\n#mcx-qv-root.qv-mermaid[data-state=\"enhanced\"]>h3{margin-inline:0;text-align:center;overflow-wrap:anywhere}\n\n#mcx-qv-root.qv-mermaid[data-state=\"enhanced\"]>h3[data-title-wrapped]{text-align:left}\n/* 预设替换 root 内容后不再含原视口样式,包装层仍需保留横向滚动。 */\n#mcx-qv-root.qv-mermaid[data-state=\"enhanced\"]:is([data-preset],[data-renderer=\"qv-pie\"])>.diagram-scroll{overflow-x:auto}\n</style>","script":"// 规则 32–33:渲染器显式接入,不修改宿主或 iframe 的全局原型。\nconst mermaidEntryEase='cubic-bezier(.23,1,.32,1)',mermaidEntryBudget=1200;\nlet mermaidEntryBatch=null;\nfunction animateMermaidEntry(element,frames,options){\n const timing=typeof options==='number'?{duration:options}:{...options};\n timing.easing=mermaidEntryEase;\n if(Array.isArray(frames))frames=frames.map(frame=>{\n const match=typeof frame.transform==='string'&&frame.transform.match(/^scale\\((0|0?\\.\\d+)\\)$/);\n if(!match||+match[1]>=.9)return frame;\n return {...frame,transform:`scale(${+match[1]<.5?.92:.97})`,opacity:frame.opacity??(+match[1]<.5?0:1)};\n });\n const animation=element.animate(frames,timing);\n if(!mermaidEntryBatch){mermaidEntryBatch=[];queueMicrotask(()=>{\n const batch=mermaidEntryBatch;mermaidEntryBatch=null;\n const end=Math.max(0,...batch.map(a=>{const t=a.effect.getTiming();return Number(t.delay)+Number(t.duration);}));\n if(end>mermaidEntryBudget){const ratio=mermaidEntryBudget/end;for(const a of batch){const t=a.effect.getTiming();a.effect.updateTiming({delay:Number(t.delay)*ratio,duration:Number(t.duration)*ratio});}}\n });}\n mermaidEntryBatch.push(animation);return animation;\n}\n// rAF 与 D3 几何动画采用同一条曲线,二分求解 Bézier 的时间参数。\nfunction mermaidEntryProgress(time){\n const t=Math.max(0,Math.min(1,time));let low=0,high=1;\n for(let i=0;i<16;i++){const u=(low+high)/2,x=3*(1-u)*(1-u)*u*.23+3*(1-u)*u*u*.32+u*u*u;if(x<t)low=u;else high=u;}\n const u=(low+high)/2;return 1-(1-u)**3;\n}\nfunction nearbyRadarDetails(candidates) {\n const unobscured = candidates.filter(candidate => !(candidate.pointer ?? candidate.coversPointer));\n const available = unobscured.length ? unobscured : candidates;\n const nearest = Math.min(...available.map(candidate => candidate.distance));\n return available.filter(candidate => candidate.distance <= Math.max(32, nearest + 16));\n}\n\nwindow.__mcxQVReady=(async()=>{document.addEventListener(\"qv:ready\",event=>queueMicrotask(()=>{const root=event.target.closest?.(\".qv-mermaid\");if(root&&[\"enhanced\",\"native\"].includes(root.dataset.state))createMermaidViewport(root);}));document.addEventListener(\"qv:ready\",e=>{const r=e.target.closest?.(\".qv-mermaid\");if(r)requestAnimationFrame(()=>alignMermaidLegends(r));});\nconst root=document.getElementById('mcx-qv-root'),scene=root.querySelector('.scene'),svg=root.querySelector('.wires'),cfg=JSON.parse(JSON.stringify(window.__mcxDiagramConfig));\nconst reduced=matchMedia('(prefers-reduced-motion: reduce)');\n\nfunction declaredMermaidIcon(value){\n if(!value)return null;\n const name=value.startsWith('lucide:')?value.slice(7):'',key=name.replace(/(^|-)([a-z0-9])/g,(_,dash,c)=>c.toUpperCase());\n if(!name||!window.lucide?.icons?.[key])throw new Error('图标不可用:'+value+';请使用宿主已有的 lucide 图标');\n return name;\n}\nconst hasRichMermaidLabel=value=>/<[^>]+>/.test(String(value||''));\n// 只在这一层访问固定版本的 Mermaid 数据接口,不解析源码字符串。\nasync function readSimpleMermaidModel(mermaid, source, descriptions) {\n const parsed = await mermaid.parse(source);\n if (!['flowchart-v2', 'flowchart', 'flowchart-elk'].includes(parsed.diagramType)) {\n return {reason: `${parsed.diagramType} 尚未适配 qv 绘制`,diagramType:parsed.diagramType};\n }\n // Mermaid 11.12.0 没有公开通用 AST;将版本相关接口隔离在此处。\n const diagram = await mermaid.mermaidAPI.getDiagramFromText(source);\n const db = diagram.db;\n if (!db.getVertices || !db.getEdges || !db.getSubGraphs || !db.getDirection) {\n return {reason: 'Mermaid 数据接口不兼容'};\n }\n const vertices = [...db.getVertices().values()];\n const groups = db.getSubGraphs();\n const links = db.getEdges();\n const ids = new Set(vertices.map(v => v.id));\n const supportedShapes = new Set(['square', 'round']);\n if (!vertices.length || vertices.some(v => (!supportedShapes.has(v.type || 'square') || v.icon&&v.form==='circle') || v.labelType === 'markdown' || hasRichMermaidLabel(v.text))) {\n return {reason: '此图包含尚未适配的节点形状或富文本'};\n }\n if (groups.some(g=>hasRichMermaidLabel(g.title)) || links.some(e=>hasRichMermaidLabel(e.text))) return {reason:'富文本标签尚未适配 qv 绘制'};\n if (vertices.some(v => v.styles?.length || v.classes?.length) || links.some(e => e.style?.length || e.classes?.length)) {\n return {reason: '此图包含自定义样式,保留原始语义'};\n }\n if (links.some(e => !ids.has(e.start) || !ids.has(e.end) || !['arrow_point', 'arrow_open'].includes(e.type))) {\n return {reason: '此图包含分组端点或尚未适配的箭头类型'};\n }\n if (groups.some(g => g.nodes.some(id => !ids.has(id)))) {\n return {reason: '嵌套分组尚未适配 qv 绘制'};\n }\n const direction = db.getDirection() || 'TB';\n if (groups.length && (!['TB','BT'].includes(direction) || groups.some(g => g.dir))) return {reason: '此分组方向尚未适配 qv 布局'};\n if (links.some(e => e.start === e.end)) return {reason: '自环尚未适配 qv 连线'};\n const iconFor = label => {\n const rules = [\n [/网关|gateway/i, 'network'], [/web|网页|浏览器/i, 'monitor'],\n [/移动|手机|app\\b/i, 'smartphone'], [/认证|登录|auth/i, 'key-round'],\n [/内容|文档|content/i, 'file-text'], [/搜索索引|index/i, 'scan-search'],\n [/搜索|search/i, 'search'], [/后台|任务|worker/i, 'workflow'],\n [/postgres|mysql|数据库|database|\\bdb\\b/i, 'database'], [/redis|缓存|cache/i, 'zap'],\n [/对象|文件存储|storage|object/i, 'archive'], [/队列|queue/i, 'list-tree'],\n [/api|服务|server/i, 'server']\n ];\n return rules.find(([pattern]) => pattern.test(label))?.[1] || null;\n };\n const nodes = vertices.map(v => ({id: v.id, name: v.text || v.id, icon: declaredMermaidIcon(v.icon)||iconFor(v.text || v.id), explicitIcon:!!v.icon,\n description: descriptions[v.id] || '', layer: 0}));\n const byId = new Map(nodes.map(n => [n.id, n]));\n let layers;\n if (groups.length) {\n const memberships = new Map();\n for (const g of groups) for (const id of g.nodes) {\n if (memberships.has(id)) return {reason: '重叠分组尚未适配 qv 绘制'};\n memberships.set(id, g.id);\n }\n if (nodes.some(n => !memberships.has(n.id))) return {reason: '部分分组尚未适配 qv 绘制'};\n layers = groups.map(g => g.title || g.id);\n groups.forEach((g, i) => g.nodes.forEach(id => {byId.get(id).layer = i;}));\n } else {\n const degree = new Map(nodes.map(n => [n.id, 0]));\n links.forEach(e => degree.set(e.end, degree.get(e.end) + 1));\n const queue = nodes.filter(n => degree.get(n.id) === 0);\n for (let i = 0; i < queue.length; i++) {\n const n = queue[i];\n for (const e of links.filter(e => e.start === n.id)) {\n const next = byId.get(e.end);\n next.layer = Math.max(next.layer, n.layer + 1);\n degree.set(e.end, degree.get(e.end) - 1);\n if (degree.get(e.end) === 0) queue.push(next);\n }\n }\n if (queue.length !== nodes.length) return {reason: '无分组回路尚未适配 qv 布局'};\n layers = Array.from({length: Math.max(...nodes.map(n => n.layer)) + 1}, () => '');\n }\n if (links.some(e => Math.abs(byId.get(e.start).layer - byId.get(e.end).layer) !== 1)) return {reason: '同层或跨多层连线尚未适配 qv 路由'};\n for(const g of groups)for(const id of g.nodes)byId.get(id).parent=g.id;\n return {data: {nodes, groups:groups.map(g=>({id:g.id,name:g.title||g.id,description:descriptions[g.id]||''})), layers, direction, grouped: !!groups.length,\n edges: links.map(e => ({from: e.start, to: e.end, async: e.stroke === 'dotted',\n thick: e.stroke === 'thick', arrow: e.type !== 'arrow_open', label: e.text || ''}))}};\n}\n\nasync function readMermaidModel(mermaid,source,descriptions) {\n const simple=await readSimpleMermaidModel(mermaid,source,descriptions);\n if(simple.diagramType==='block')return readMermaidBlockModel(mermaid,source,descriptions);\n if(simple.diagramType==='c4')return readMermaidC4Model(mermaid,source,descriptions);\n if(simple.diagramType==='quadrantChart')return readMermaidQuadrantModel(mermaid,source,descriptions);\n if(simple.data){simple.data.kind='flowchart';simple.data.generic=true;return simple;}\n const parsed=await mermaid.parse(source),diagram=await mermaid.mermaidAPI.getDiagramFromText(source,{title:window.__mcxQVRuntime.readTitle(source)}),db=diagram.db;\n if(parsed.diagramType==='sequence')return readMermaidSequenceModel(mermaid,source,descriptions);\n if(parsed.diagramType==='sankey')return readMermaidSankeyModel(db,descriptions);\n if(parsed.diagramType==='radar')return readMermaidRadarModel(db,descriptions);\n if(parsed.diagramType==='xychart')return readMermaidXYModel(db,descriptions);\n if(parsed.diagramType==='pie')return readMermaidPieModel(db,descriptions);\n if(parsed.diagramType==='kanban')return readMermaidKanbanModel(db,descriptions);\n if(parsed.diagramType==='gantt')return readMermaidGanttModel(db,descriptions);\n if(parsed.diagramType==='mindmap')return readMermaidMindmapModel(db,descriptions);\n if(parsed.diagramType==='journey')return readMermaidJourneyModel(db,descriptions);\n if(parsed.diagramType==='timeline')return readMermaidTimelineModel(db,descriptions);if(parsed.diagramType==='gitGraph')return readMermaidGitModel(db,descriptions);\n if(parsed.diagramType==='class')return readMermaidClassModel(db,descriptions);\n if(parsed.diagramType==='er')return readMermaidErModel(db,descriptions);\n if(parsed.diagramType==='stateDiagram')return readMermaidStateModel(db,descriptions);\n const iconFor=name=>{const rules=[[/web|客户端|client/i,'monitor'],[/app|移动/i,'smartphone'],[/网关|gateway/i,'network'],[/数据库|存储|database|disk|postgres|mysql/i,'database'],[/缓存|redis/i,'zap'],[/队列|queue/i,'list-tree'],[/cloud|云/i,'cloud'],[/server|服务|api/i,'server'],[/internet|网络/i,'globe'],[/auth|认证/i,'key-round']];return rules.find(([re])=>re.test(name))?.[1]||null;};\n if(parsed.diagramType==='architecture'){\n const groups=db.getGroups().map(g=>({id:g.id,name:g.title||g.id,description:descriptions[g.id]||'',parent:g.in,icon:({cloud:'cloud',database:'database',disk:'hard-drive',internet:'globe',server:'server'}[g.icon]||'layers')})),all=db.getNodes();\n if(all.some(n=>n.iconText||(n.icon&&!['cloud','database','disk','internet','server'].includes(n.icon))))return {reason:'自定义图标或图标内文字尚未适配 qv 绘制'};\n const nodes=all.map(n=>({id:n.id,name:n.title||n.id,resource:n.icon||'',parent:n.in,icon:({cloud:'cloud',database:'database',disk:'hard-drive',internet:'globe',server:'server'}[n.icon]||iconFor(n.title||n.id)),explicitIcon:!!n.icon,junction:n.type==='junction',description:descriptions[n.id]||'',layer:0}));\n const dirs={T:'NORTH',B:'SOUTH',L:'WEST',R:'EAST'};\n const edges=db.getEdges().map(e=>({from:e.lhsGroup?(all.find(n=>n.id===e.lhsId).in||e.lhsId):e.lhsId,to:e.rhsGroup?(all.find(n=>n.id===e.rhsId).in||e.rhsId):e.rhsId,sourceFrom:e.lhsId,sourceTo:e.rhsId,fromGroup:!!e.lhsGroup,toGroup:!!e.rhsGroup,fromSide:dirs[e.lhsDir],toSide:dirs[e.rhsDir],arrow:!!e.rhsInto,startArrow:!!e.lhsInto,label:e.title||''}));\n return {data:{nodes,groups,edges,generic:true,nativeGeometry:true,architecture:true,db,layers:[],direction:'LR',grouped:true}};\n }\n if(!['flowchart','flowchart-v2','flowchart-elk'].includes(parsed.diagramType))return simple;\n const vertices=[...db.getVertices().values()],rawGroups=db.getSubGraphs(),links=db.getEdges();\n if(vertices.some(n=>n.styles?.length||n.classes?.length)||links.some(e=>e.style?.length||e.classes?.length))return {reason:'此图包含自定义样式,保留 Mermaid 原始外观'};\n const shapes={square:'rect',rect:'rect',round:'round',rounded:'round',diamond:'diamond',question:'diamond',hexagon:'hexagon',hex:'hexagon',circle:'circle',doublecircle:'doublecircle',stadium:'stadium',cylinder:'cylinder',cyl:'cylinder',subroutine:'subroutine','fr-rect':'subroutine',lean_left:'lean-left',lean_right:'lean-right',trapezoid:'trapezoid',inv_trapezoid:'inv-trapezoid',odd:'odd'};\n if(vertices.some(n=>!shapes[n.type||'square']||n.labelType==='markdown'||hasRichMermaidLabel(n.text)))return simple;\n if(rawGroups.some(g=>hasRichMermaidLabel(g.title))||links.some(e=>hasRichMermaidLabel(e.text)))return {reason:'富文本标签尚未适配 qv 绘制'};\n const parents=new Map();for(const g of rawGroups)for(const id of g.nodes)parents.set(id,g.id);\n const groupIds=new Set(rawGroups.map(g=>g.id));\n const groups=rawGroups.map(g=>({id:g.id,name:g.title||g.id,description:descriptions[g.id]||'',parent:parents.get(g.id),direction:g.dir}));\n const nodes=vertices.filter(n=>!groupIds.has(n.id)).map(n=>({id:n.id,name:n.text||n.id,shape:n.icon?(n.form==='circle'?'circle':'round'):shapes[n.type||'square'],parent:parents.get(n.id),icon:declaredMermaidIcon(n.icon)||iconFor(n.text||n.id),explicitIcon:!!n.icon,description:descriptions[n.id]||'',layer:0}));\n const edges=links.map(e=>({from:e.start,to:e.end,label:e.text||'',arrow:e.type!=='arrow_open',startArrow:e.type?.startsWith('double_'),marker:e.type?.includes('circle')?'circle':e.type?.includes('cross')?'cross':'point',async:e.stroke==='dotted',invisible:e.stroke==='invisible',thick:e.stroke==='thick'}));\n return {data:{nodes,groups,edges,kind:'flowchart',generic:true,nativeGeometry:groups.some(g=>g.direction),db,layers:[],direction:db.getDirection()||'TB',grouped:!!groups.length}};\n}\n\n// 只访问固定版本的时序图 DB;不从源码或 SVG 猜测消息语义。\nasync function readMermaidSequenceModel(mermaid,source,descriptions={}) {\n const db=(await mermaid.mermaidAPI.getDiagramFromText(source)).db;\n const rawActors=[...db.getActors()],boxes=db.getBoxes?.();\n const actors=rawActors.map(([id,a])=>({id,name:a.description||a.name,type:a.type,description:descriptions[id]||''}));\n const messages=db.getMessages().map(m=>({...m}));\n const supported=new Set([0,1,2,3,4,5,6,10,11,12,13,14,15,16,17,18,19,20,21,24,25,26,27,28,29,30,31,33,34]);\n if(!actors.length||actors.some(a=>!['participant','actor'].includes(a.type)||hasRichMermaidLabel(a.name))||messages.some(m=>!supported.has(m.type)||typeof m.message==='string'&&hasRichMermaidLabel(m.message))||(boxes?.size||boxes?.length)||rawActors.some(([,a])=>a.rectData||a.wrap||Object.keys(a.links||{}).length||Object.keys(a.properties||{}).length))return {reason:'此时序图包含未适配的参与方、富文本或自定义区域,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!actors.some(a=>a.id===id));if(unknown.length)throw new Error('说明引用了未知参与方:'+unknown.join('、'));\n let number=1,step=1,numbered=false;\n for(const m of messages){if(m.type===26){numbered=m.message.visible!==false;if(Number.isFinite(m.message.start))number=m.message.start;if(Number.isFinite(m.message.step))step=m.message.step;}else if([0,1,3,4,5,6,24,25,33,34].includes(m.type)&&numbered){m.number=number;number+=step;}}\n return {data:{kind:'sequence',actors,messages}};\n}\n\n// ELK 只负责复杂拓扑的布局;节点、边、提示和动效仍由 qv 绘制。\nlet elkLoad;\nasync function loadMermaidLayoutEngine() {if(!window.ELK)throw new Error(\"QV ELK unavailable\");}\nasync function prepareMermaidLayout(data){\n // 流程图没有显式回流标记:先排一次,按主轴位置找出指向上游的连线,再改端口重排。\n for(const e of data.edges)if(e.__qvBack){delete e.fromSide;delete e.toSide;delete e.__qvBack;}\n await __qvBaseLayout(data);\n if(data.kind!=='flowchart')return;\n const map=new Map(data.nodes.map(n=>[n.id,n])),lr=['LR','RL'].includes(data.direction),flipped=['RL','BT'].includes(data.direction);\n const at=n=>lr?n.x+n.w/2:n.y+n.h/2;let changed=false;\n for(const e of data.edges){if(e.fromSide||e.toSide||e.from===e.to)continue;const a=map.get(e.from),b=map.get(e.to);if(!a||!b)continue;\n const d=at(b)-at(a);if(!(flipped?d>0:d<0))continue;\n // 同组回流与正向线并行;跨组回流走组外侧的单条通道,避免层级布线绕大圈。\n if((a.parent||null)===(b.parent||null)){const lr=['LR','RL'].includes(data.direction),flip=['RL','BT'].includes(data.direction);\ne.fromSide=lr?(flip?'EAST':'WEST'):(flip?'SOUTH':'NORTH');e.toSide=lr?(flip?'WEST':'EAST'):(flip?'NORTH':'SOUTH');}else{e.fromSide=e.toSide=lr?'SOUTH':'EAST';}\n e.__qvBack=true;changed=true;}\n if(changed)await __qvBaseLayout(data);\n}\nasync function __qvBaseLayout(data) {\n await loadMermaidLayoutEngine();\n const direction={TB:'DOWN',BT:'UP',LR:'RIGHT',RL:'LEFT'};\n const graph={id:'qv-layout',layoutOptions:{'elk.algorithm':'layered',...(data.kind==='flowchart'?{'elk.layered.cycleBreaking.strategy':'MODEL_ORDER'}:{}),'elk.layered.nodePlacement.bk.fixedAlignment':'BALANCED','elk.direction':direction[data.direction]||'DOWN','elk.hierarchyHandling':'INCLUDE_CHILDREN','elk.edgeRouting':'ORTHOGONAL','elk.json.edgeCoords':'ROOT','elk.spacing.nodeNode':'38','elk.layered.spacing.nodeNodeBetweenLayers':data.navigation?'56':'84','elk.padding':'[top=28,left=28,bottom=28,right=28]',...(['er','class'].includes(data.kind)?{'elk.spacing.edgeNode':'28','elk.layered.spacing.edgeNodeBetweenLayers':'28'}:{}),...(data.kind==='state'?{'elk.layered.cycleBreaking.strategy':'MODEL_ORDER','elk.layered.considerModelOrder.strategy':'NODES_AND_EDGES','elk.layered.spacing.nodeNodeBetweenLayers':'48'}:{})},children:[],edges:[]};\n const byId=new Map([[null,graph]]);\n for(const g of data.groups||[])byId.set(g.id,{id:g.id,children:[],layoutOptions:{'elk.padding':'[top=44,left=24,bottom=24,right=24]',...(g.direction?{'elk.direction':direction[g.direction]}:{}),...(data.kind==='state'?{'elk.layered.cycleBreaking.strategy':'MODEL_ORDER'}:{})}});\n for(const g of data.groups||[])(byId.get(g.parent)||graph).children.push(byId.get(g.id));\n for(const n of data.nodes){const shaped=['diamond','hexagon','circle','doublecircle'].includes(n.shape),w=n.junction?10:Math.max(shaped?128:154,Math.min(260,[...n.name].reduce((s,c)=>s+(c.charCodeAt(0)>255?14:8),0)+52)),h=n.junction?10:['circle','doublecircle'].includes(n.shape)?w:shaped?94:48;\n const node={id:n.id,width:n.layoutWidth??(n.groupCard?Math.max(184,w):w),height:n.layoutHeight??(n.groupCard?(n.preview?.height||76):n.external?64:h),ports:[],layoutOptions:{'elk.portConstraints':'FIXED_SIDE',...(data.kind==='state'&&['stateStart','stateEnd'].includes(n.shape)?{'elk.layered.layering.layerConstraint':n.shape==='stateStart'?'FIRST':'LAST'}:{})}};\n if(data.kind==='flowchart'&&!n.groupCard){const measured=measureFlowchartNode(n,root.dataset.hasGroups==='true');node.width=measured.w;node.height=measured.h;}\n n.w=node.width;n.h=node.height;byId.set(n.id,node);(byId.get(n.parent)||graph).children.push(node);\n }\n const port=(id,face,incoming,edgeIndex)=>{const n=byId.get(id),pid=id+'::'+face+'::'+(incoming?'in':'out')+(['er','class'].includes(data.kind)?'::'+edgeIndex:'');n.ports??=[];if(!n.ports.some(p=>p.id===pid))n.ports.push({id:pid,width:0,height:0,layoutOptions:{'elk.port.side':face}});return pid;};\n const defaultFaces={TB:['SOUTH','NORTH'],BT:['NORTH','SOUTH'],LR:['EAST','WEST'],RL:['WEST','EAST']}[data.direction]||['SOUTH','NORTH'];\n data.edges.forEach((e,i)=>{const label=e.label?{id:'label'+i,text:e.label,width:e.labelWidth??Math.max(30,[...e.label].length*12+12),height:e.labelHeight??22}:null;graph.edges.push({id:'edge'+i,sources:[port(e.from,e.fromSide||defaultFaces[0],false,i)],targets:[port(e.to,e.toSide||defaultFaces[1],true,i)],...(label?{labels:[label]}:{})});});\n if(data.order)for(const node of byId.values())if(node.children)node.children.sort((a,b)=>(data.order.get(a.id)||0)-(data.order.get(b.id)||0));\n const result=await new window.ELK().layout(graph),positions=new Map(),groups=[];\n function visit(g,x=0,y=0){const gx=x+(g.x||0),gy=y+(g.y||0);positions.set(g.id,{x:gx,y:gy,w:g.width,h:g.height});if(g!==result&&g.children)groups.push({...data.groups.find(n=>n.id===g.id),x:gx,y:gy,w:g.width,h:g.height});for(const c of g.children||[])visit(c,gx,gy);}\n visit(result);\n const routes=new Map();function collect(g,ox=0,oy=0){const x=ox+(g.x||0),y=oy+(g.y||0);for(const e of g.edges||[]){const points=(e.sections||[]).flatMap(s=>[s.startPoint,...s.bendPoints||[],s.endPoint].map(p=>[p.x,p.y]));routes.set(e.id,{points,labelPosition:e.labels?.[0]?{x:e.labels[0].x+e.labels[0].width/2,y:e.labels[0].y+e.labels[0].height/2}:null});}for(const c of g.children||[])collect(c,x,y);}collect(result);\n data.nodes.forEach(n=>Object.assign(n,positions.get(n.id)));\n data.edges.forEach((e,i)=>{Object.assign(e,routes.get('edge'+i));if(!e.points?.length)throw new Error('布局缺少连线:'+e.from+' → '+e.to);});\n data.layout={width:result.width,height:result.height,groups};\n // 动效按空间层级推进,回路不会导致无限拓扑遍历。\n const ys=[...new Set(data.nodes.map(n=>Math.round(data.direction==='LR'||data.direction==='RL'?n.x:n.y)))].sort((a,b)=>['RL','BT'].includes(data.direction)?b-a:a-b);\n data.nodes.forEach(n=>n.layer=Math.min(6,ys.indexOf(Math.round(data.direction==='LR'||data.direction==='RL'?n.x:n.y))));\n}\n\n// 使用 Mermaid 自己的方向约束布局;只读取显式节点 ID 对应的几何,不从图形猜测关系。\nasync function prepareArchitectureLayout(data){const measure=(name,width)=>{const t=document.createElement('span');t.style.cssText='position:absolute;left:-100000px;width:'+width+'px;font:500 14px/20px '+getComputedStyle(root).fontFamily+';white-space:normal;overflow-wrap:anywhere';t.textContent=name;root.append(t);const h=t.offsetHeight;t.remove();return h;};for(const n of data.nodes){if(n.junction){n.layoutWidth=n.layoutHeight=10;}else{n.layoutWidth=240;n.layoutHeight=Math.max(48,26+measure(n.name,184));}}const sideways=data.edges.filter(e=>['EAST','WEST'].includes(e.fromSide)).length;data.direction=sideways*2>=data.edges.length?'LR':'TB';root.dataset.mcxLayout='arch-elk';await prepareMermaidLayout(data);}\nasync function prepareNativeGeometry(data,mermaid,source,db,architecture) {let gridFailed=false;\n if(window.lucide){const icons={};for(const name of new Set(data.nodes.map(n=>n.icon).filter(Boolean))){const key=name.replace(/(^|-)([a-z0-9])/g,(_,dash,c)=>c.toUpperCase()),definition=window.lucide.icons[key];if(!definition)continue;const svg=window.lucide.createElement(definition),group=document.createElementNS(svg.namespaceURI,'g');for(const a of svg.attributes)if(!['width','height','viewBox','xmlns','class'].includes(a.name))group.setAttribute(a.name,a.value);for(const child of [...svg.children])group.append(child);icons[name]={body:group.outerHTML};}mermaid.registerIconPacks([{name:'lucide',icons:{prefix:'lucide',width:24,height:24,icons}}]);}\n const id='qv-measure-'+Math.random().toString(36).slice(2),result=await mermaid.render(id,source),host=document.createElement('div');host.style.cssText='position:absolute;left:-100000px;visibility:hidden;pointer-events:none';host.innerHTML=result.svg;document.body.append(host);\n try {\n const svg=host.querySelector('svg'),view=svg.viewBox.baseVal;svg.style.cssText=`width:${view.width}px;height:${view.height}px;max-width:none`;const sr=svg.getBoundingClientRect();\n const scaleX=architecture?1.8:1.65,scaleY=architecture?1.2:1.45;\n const rect=(id,service=false)=>{const el=host.querySelector('#'+CSS.escape(id));if(!el)throw new Error('缺少布局节点:'+id);let b=el.getBoundingClientRect();if(service){const matrix=el.getScreenCTM(),size=db.getConfigField('iconSize'),p=new DOMPoint(0,0).matrixTransform(matrix);b={x:p.x,y:p.y,width:size*matrix.a,height:size*matrix.d};}return {x:(b.x-sr.x)*scaleX+24,y:(b.y-sr.y)*scaleY+24,w:b.width*scaleX,h:b.height*scaleY};};\n const measureText=(name,width,size=14)=>{const text=document.createElement('span');text.style.cssText='position:absolute;left:-100000px;width:'+width+'px;font:'+'500 '+size+'px/'+(size===14?20:18)+'px '+getComputedStyle(root).fontFamily+';white-space:normal;overflow-wrap:anywhere';text.textContent=name;root.append(text);const height=text.offsetHeight;text.remove();return height;};\n for(const n of data.nodes){const b=rect(architecture?(n.junction?'node-':'service-')+n.id:db.lookUpDomId(n.id),architecture&&!n.junction);n.w=n.junction?10:Math.max(154,Math.min(260,b.w));n.h=n.junction?10:['circle','doublecircle'].includes(n.shape)?n.w:['diamond','hexagon'].includes(n.shape)?94:48;if(architecture&&!n.junction){n.w=240;n.h=Math.max(48,26+measureText(n.name,n.w-56));}if(data.kind==='flowchart')Object.assign(n,measureFlowchartNode(n,root.dataset.hasGroups==='true'));n.x=b.x+b.w/2-n.w/2;n.y=b.y+b.h/2-n.h/2;}\n if(architecture){const snap=(key,size)=>{const items=data.nodes.map(n=>({n,c:n[key]+n[size]/2})).sort((a,b)=>a.c-b.c),levels=[];for(const it of items){const last=levels.at(-1);if(last&&it.c-last.c<=24)last.items.push(it);else levels.push({c:it.c,items:[it]});}return levels;};const cols=snap('x','w'),rows=snap('y','h'),depth=Math.max(0,...data.groups.map(g=>{let d=1,p=g.parent;while(p){d++;p=data.groups.find(x=>x.id===p)?.parent;}return d;})),colGap=48+48*depth,rowGap=48+68*depth,colOf=new Map(),rowOf=new Map(),cells=new Set();cols.forEach((l,i)=>l.items.forEach(it=>colOf.set(it.n,i)));rows.forEach((l,i)=>l.items.forEach(it=>rowOf.set(it.n,i)));for(const n of data.nodes)cells.add(colOf.get(n)+':'+rowOf.get(n));if(cells.size!==data.nodes.length)gridFailed=true;else{const colW=cols.map(l=>Math.max(...l.items.map(it=>it.n.w))),rowH=rows.map(l=>Math.max(...l.items.map(it=>it.n.h))),colX=[],rowY=[];colW.reduce((x,w,i)=>(colX[i]=x,x+w+colGap),24);rowH.reduce((y,h,i)=>(rowY[i]=y,y+h+rowGap),68);for(const n of data.nodes){const c=colOf.get(n),r=rowOf.get(n);n.x=colX[c]+(colW[c]-n.w)/2;n.y=rowY[r]+(rowH[r]-n.h)/2;}}}const groups=data.groups.map(g=>({...g,...rect(architecture?'group-'+g.id:g.id)}));\n // 将原生图标布局转换成卡片后重新包住后代,并为标题留空间。\n const children=id=>[...data.nodes.filter(n=>n.parent===id),...groups.filter(g=>g.parent===id)];\n function bounds(g){for(const child of groups.filter(n=>n.parent===g.id))bounds(child);const list=children(g.id);if(list.length){const x=Math.min(...list.map(n=>n.x))-24,y=Math.min(...list.map(n=>n.y))-44;g.w=Math.max(Math.max(...list.map(n=>n.x+n.w))+24-x,architecture?240:0);g.h=Math.max(...list.map(n=>n.y+n.h))+24-y;g.x=x;g.y=y;}}\n for(const g of groups.filter(g=>!g.parent))bounds(g);if(architecture){const inside=(n,g)=>{for(let p=n.parent;p;p=groups.find(x=>x.id===p)?.parent)if(p===g.id)return true;return false;};if(gridFailed||groups.some(g=>data.nodes.some(n=>!inside(n,g)&&n.x<g.x+g.w&&g.x<n.x+n.w&&n.y<g.y+g.h&&g.y<n.y+n.h)))return prepareArchitectureLayout(data);}\n const all=[...groups,...data.nodes],dx=24-Math.min(...all.map(n=>n.x)),dy=24-Math.min(...all.map(n=>n.y));for(const n of all){n.x+=dx;n.y+=dy;}\n if(architecture){try{finishMermaidGeometry(data,groups,architecture);root.dataset.mcxLayout='arch-grid';}catch(error){return prepareArchitectureLayout(data);}}else finishMermaidGeometry(data,groups,architecture);\n } finally{host.remove();}\n}\nfunction routeAroundNodes(start,end,nodes){\n const blocks=nodes.filter(n=>!n.junction).map(n=>({l:n.x-8,r:n.x+n.w+8,t:n.y-8,b:n.y+n.h+8}));\n const xs=[...new Set([start[0],end[0],...blocks.flatMap(b=>[b.l-8,b.r+8])])].sort((a,b)=>a-b),ys=[...new Set([start[1],end[1],...blocks.flatMap(b=>[b.t-8,b.b+8])])].sort((a,b)=>a-b);\n const cross=(a,b)=>blocks.some(r=>a[0]===b[0]?a[0]>r.l&&a[0]<r.r&&Math.max(a[1],b[1])>r.t&&Math.min(a[1],b[1])<r.b:a[1]>r.t&&a[1]<r.b&&Math.max(a[0],b[0])>r.l&&Math.min(a[0],b[0])<r.r);\n const id=(x,y)=>y*xs.length+x,first=id(xs.indexOf(start[0]),ys.indexOf(start[1])),last=id(xs.indexOf(end[0]),ys.indexOf(end[1])),dist=new Map([[first,0]]),prev=new Map(),open=new Set([first]);\n while(open.size){let current,score=Infinity;for(const k of open){const d=dist.get(k)+Math.abs(xs[k%xs.length]-end[0])+Math.abs(ys[Math.floor(k/xs.length)]-end[1]);if(d<score){score=d;current=k;}}if(current===last){const path=[];for(let k=last;k!==undefined;k=prev.get(k))path.push([xs[k%xs.length],ys[Math.floor(k/xs.length)]]);return path.reverse();}open.delete(current);const x=current%xs.length,y=Math.floor(current/xs.length);for(const [nx,ny] of [[x-1,y],[x+1,y],[x,y-1],[x,y+1]]){if(nx<0||ny<0||nx>=xs.length||ny>=ys.length)continue;const a=[xs[x],ys[y]],b=[xs[nx],ys[ny]],next=id(nx,ny);if(cross(a,b))continue;const cost=dist.get(current)+Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1])+1;if(cost<(dist.get(next)??Infinity)){dist.set(next,cost);prev.set(next,current);open.add(next);}}}\n throw new Error('没有找到避开节点的连线路径');\n}\n\nfunction finishMermaidGeometry(data,groups,architecture=false){\n const all=[...groups,...data.nodes],map=new Map(all.map(n=>[n.id,n])),face=(a,b,out)=>{if(architecture)return out.fromSide;if(data.balanced){const dx=b.x+b.w/2-a.x-a.w/2,dy=b.y+b.h/2-a.y-a.h/2;return Math.abs(dx)>Math.abs(dy)?(dx>=0?'EAST':'WEST'):(dy>=0?'SOUTH':'NORTH');}const vertical=['TB','BT'].includes(data.direction);return vertical?(b.y>a.y?'SOUTH':'NORTH'):(b.x>a.x?'EAST':'WEST');};\n const dir={NORTH:[0,-1],SOUTH:[0,1],EAST:[1,0],WEST:[-1,0]},usages=new Map();\n for(const e of data.edges){const a=map.get(e.from),b=map.get(e.to);e.fs=e.fromSide||(data.balanced&&a===b?'EAST':face(a,b,e));e.ts=e.toSide||(data.balanced&&a===b?'SOUTH':({NORTH:'SOUTH',SOUTH:'NORTH',EAST:'WEST',WEST:'EAST'}[e.fs]));for(const [id,s,role] of [[e.from,e.fs,'out'],[e.to,e.ts,'in']]){const key=id+':'+s;if(!usages.has(key))usages.set(key,new Set());usages.get(key).add(role);}}\n function point(n,s,incoming){const split=usages.get(n.id+':'+s).size>1,offset=split?(incoming?-7:7):0;return s==='NORTH'?[n.x+n.w/2+offset,n.y]:s==='SOUTH'?[n.x+n.w/2+offset,n.y+n.h]:s==='WEST'?[n.x,n.y+n.h/2+offset]:[n.x+n.w,n.y+n.h/2+offset];}\n for(const e of data.edges){const a=map.get(e.from),b=map.get(e.to),start=point(a,e.fs,false),end=point(b,e.ts,true),u=dir[e.fs],v=dir[e.ts],s=[start[0]+u[0]*20,start[1]+u[1]*20],t=[end[0]+v[0]*20,end[1]+v[1]*20];delete e.labelPosition;e.points=[start,...routeAroundNodes(s,t,data.nodes),end];}\n const width=Math.max(...all.map(n=>n.x+n.w),...data.edges.flatMap(e=>e.points.map(p=>p[0])))+32,height=Math.max(...all.map(n=>n.y+n.h),...data.edges.flatMap(e=>e.points.map(p=>p[1])))+32;\n data.layout={width,height,groups};const coordinate=n=>Math.round(!architecture&&['LR','RL'].includes(data.direction)?n.x:n.y),levels=[...new Set(data.nodes.map(coordinate))].sort((a,b)=>['RL','BT'].includes(data.direction)?b-a:a-b);data.nodes.forEach(n=>n.layer=Math.min(6,levels.indexOf(coordinate(n))));\n}\n\n// 以真实字体测量换行高度;菱形与圆形只使用轮廓中央的安全区域。\nfunction measureFlowchartNode(n,grouped){\n const size=grouped?12:parseFloat(getComputedStyle(root).fontSize),line=grouped?18:Math.ceil(size*1.4),icon=(!!window.lucide&&(n.explicitIcon||(cfg.icons!=='none'&&n.icon)))?((grouped?14:16)+8):0,portal=n.external&&n.targetGroup!==undefined?22:0;\n const text=document.createElement('span');text.style.cssText=`position:absolute;left:-100000px;font:500 ${size}px/${line}px ${getComputedStyle(root).fontFamily};white-space:pre`;text.textContent=n.name;root.append(text);\n const natural=Math.ceil(text.getBoundingClientRect().width),limit=228-icon-portal,textWidth=Math.min(natural,limit)+icon+portal;\n text.style.width=(textWidth-icon-portal)+'px';text.style.whiteSpace='normal';text.style.overflowWrap='anywhere';const textHeight=text.offsetHeight;text.remove();\n const shaped=['diamond','circle','doublecircle'].includes(n.shape),w=Math.max(shaped?128:154,shaped?Math.ceil((textWidth+16)/.7):textWidth+34);\n let h=Math.max(48,textHeight+26);if(n.shape==='diamond')h=Math.max(94,Math.ceil((textHeight+12)/.3));else if(n.shape==='hexagon')h=Math.max(94,h);else if(['circle','doublecircle'].includes(n.shape))h=Math.max(w,Math.ceil((textHeight+20)/.7));\n if(n.external&&n.targetGroup===undefined)h+=22;\n n.textWidth=textWidth;n.labelLine=line;n.labelWrapped=natural>limit;return {w,h};\n}\nasync function prepareFlowchartLayout(data,mermaid,source){\n data.balanced=false;\n if(data.nativeGeometry)await prepareNativeGeometry(data,mermaid,source,data.db,false);else await prepareMermaidLayout(data);\n {const limit=cfg.readingWidth||root.clientWidth;\nif(data.navigation||data.layout.width<=limit){root.dataset.mcxLayout=\"fit\";return;}\nif(!data.nativeGeometry){const original=data.direction,first=data.layout.width,swap=['LR','RL'].includes(original)?'TB':'LR';\ndata.direction=swap;await prepareMermaidLayout(data);\nif(data.layout.width<=limit){root.dataset.mcxLayout=\"axis\";return;}\nif(data.layout.width<first){root.dataset.mcxLayout=\"axis-scroll\";return;}\ndata.direction=original;await prepareMermaidLayout(data);}\nroot.dataset.mcxLayout=\"scroll\";return;}}\n\nfunction mermaidScopeView(model, scope = null) {\n const groups = new Map((model.groups || []).map(g => [g.id, g]));\n const all = new Map([...groups.values(), ...model.nodes].map(n => [n.id, n]));\n if (scope !== null && !groups.has(scope)) throw new Error('分组不存在:' + scope);\n const inside = (id, parent) => {\n if (parent === null) return true;\n for (let n = all.get(id); n; n = all.get(n.parent)) if (n.id === parent) return true;\n return false;\n };\n // 每个端点映射到当前层的直接孩子,外部端点映射到最近的外部边界。\n function representative(id) {\n if (id === scope) return {id, external:true};\n if (inside(id, scope)) {\n let n = all.get(id);\n while (n.parent && n.parent !== scope) n = all.get(n.parent);\n return {id:n.id, external:false};\n }\n let n = all.get(id);\n const ancestors = new Set();\n for (let p = all.get(scope); p; p = all.get(p.parent)) ancestors.add(p.id);\n while (n.parent && !ancestors.has(n.parent)) n = all.get(n.parent);\n return {id:n.id, external:true};\n }\n const visible = new Map();\n function add(id, external = false) {\n const key = external ? 'qv-external:' + id : id;\n if (visible.has(key)) return key;\n const raw = all.get(id), group = groups.has(id);\n const count = group ? model.nodes.filter(n => !n.junction && inside(n.id,id)).length : 0;\n let preview;\n if(group&&!external){\n const rows=[];let shown=0;\n function visit(parent,depth=0){for(const child of all.values())if(child.parent===parent){\n if(shown>=8)break;\n if(groups.has(child.id)){rows.push({id:child.id,name:child.name,heading:true,depth});visit(child.id,depth+1);}\n else if(!child.junction){rows.push({id:child.id,name:child.name,icon:child.icon,depth});shown++;}\n }}\n visit(id);let y=model.architecture?96:62;rows.forEach(row=>{row.y=y;if(model.architecture||model.kind==='flowchart'){const text=document.createElement('span');text.style.cssText='position:absolute;left:-100000px;width:248px;font:12px/18px '+getComputedStyle(root).fontFamily+';white-space:normal;overflow-wrap:anywhere';text.textContent=row.name;root.append(text);row.h=Math.max(18,text.offsetHeight);text.remove();y+=row.h+14;}else y+=row.heading?26:32;});\n const ids=new Set(rows.filter(r=>!r.heading).map(r=>r.id));\n preview={rows,edges:model.edges.filter(e=>ids.has(e.from)&&ids.has(e.to)&&e.from!==e.to),remaining:count-shown,height:y+(count>shown?28:10)};\n }\n visible.set(key, {...raw, id:key, parent:undefined, shape:group?'round':raw.shape,\n groupCard:group&&!external, external, count, preview, icon:group?(raw.icon||'layers'):raw.icon,\n targetGroup:group ? (id===scope?raw.parent||null:id) : undefined,\n description:raw.description||'',\n layoutWidth:model.architecture||group&&!external&&model.kind==='flowchart'?320:undefined,layoutHeight:model.architecture?(preview?.height||64):undefined,\n layer:0});\n return key;\n }\n for (const n of all.values()) if ((n.parent||null)===scope) add(n.id);\n const edges = [], bundles = new Map();\n model.edges.forEach((edge,index) => {\n if (!inside(edge.from,scope)&&!inside(edge.to,scope)) return;\n const a=representative(edge.from), b=representative(edge.to);\n // 折叠组内部的边留在原模型;真实叶节点自环仍显示。\n if (a.id===b.id && !(edge.from===edge.to&&!groups.has(a.id))) return;\n const from=add(a.id,a.external),to=add(b.id,b.external);\n const key=JSON.stringify([from,to,edge.async,edge.thick,edge.arrow,edge.startArrow,edge.marker,edge.invisible,edge.label||'']);\n if(bundles.has(key)){bundles.get(key).members.push(index);return;}\n const projected={...edge,from,to,points:undefined,labelPosition:undefined,\n fromSide:from===edge.from?edge.fromSide:undefined,toSide:to===edge.to?edge.toSide:undefined,members:[index]};\n edges.push(projected);bundles.set(key,projected);\n });\n for(const edge of edges)if(edge.members.length>1)edge.bundleCount=edge.members.length;\n return {nodes:[...visible.values()],edges,groups:[],layers:[],generic:true,navigation:true,\n direction:model.direction||'LR',grouped:false,scope,kind:model.kind,architectureCards:!!model.architecture||model.kind==='flowchart'};\n}\n// 详情浮层独立于缩放场景,保留完整源身份和真实端口关系。\nfunction createArchitectureReader(root, model, viewport, controller, highlight, keepAlive, leave) {\n const entries=[...(model.groups||[]).map(g=>({...g,isGroup:true})),...model.nodes],byId=new Map(entries.map(n=>[n.id,n]));\n const detail=document.createElement('div');detail.className='architecture-detail';detail.id=root.id+'-architecture-detail';detail.setAttribute('role','tooltip');detail.hidden=true;root.append(detail);\n let selected=null,epoch=0,scrollTimer;\n const sides={NORTH:'上',SOUTH:'下',WEST:'左',EAST:'右'};\n function path(n){const chain=[];for(let parent=byId.get(n.parent);parent;parent=byId.get(parent.parent))chain.unshift(parent.name);return chain.join(' / ');}\n const row=(cls,text)=>{const n=document.createElement('div');n.className=cls;n.textContent=text;return n;};\n function section(title){const el=document.createElement('section');el.className='architecture-detail-section';el.append(row('architecture-detail-label',title));detail.append(el);return el;}\n function endpoint(id,side){const el=document.createElement('div');el.append(row('architecture-detail-endpoint',byId.get(id)?.name||id));el.append(row('architecture-detail-label',sides[side]?'端口 · '+sides[side]:''));return el;}\n function fill(id){\n const n=byId.get(id);detail.replaceChildren();detail.append(row('architecture-detail-name',n.name),row('architecture-detail-label',(n.isGroup?'分组':n.junction?'汇接点':'资源'+(n.resource?' · '+n.resource:''))+' · '+n.id));\n if(n.description)section('说明').append(row('architecture-detail-body',n.description));\n if(n.parent)section('所属分组').append(row('architecture-detail-body',path(n)));\n if(n.isGroup){const children=entries.filter(c=>c.parent===id);if(children.length){const list=section('直接成员');for(const child of children)list.append(row('architecture-detail-body',child.name));}}\n const links=model.edges.filter(e=>e.from===id||e.to===id);\n if(links.length){const list=section('直接连接');for(const e of links){const link=document.createElement('div');link.className='architecture-detail-link';const arrow=e.startArrow&&e.arrow?'↔':e.startArrow?'←':e.arrow?'→':'—';link.append(endpoint(e.from,e.fromSide),row('architecture-detail-arrow',arrow),endpoint(e.to,e.toSide));list.append(link);if(e.fromGroup||e.toGroup)list.append(row('architecture-detail-label','源端点:'+e.sourceFrom+' '+arrow+' '+e.sourceTo));}}\n }\n function clear(){epoch++;clearTimeout(scrollTimer);selected=null;detail.hidden=true;root.querySelectorAll('[aria-describedby=\"'+detail.id+'\"]').forEach(n=>n.removeAttribute('aria-describedby'));}\n function reveal(pending){if(pending!==epoch||!selected||!selected.anchor?.isConnected)return;highlight(selected.id);}\n function choose(id,anchor,ensure=false){\n if(!byId.has(id))return;\n clear();const pending=epoch;selected={id,anchor};\n if(ensure&&anchor&&!controller.expanded){const a=anchor.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(a.width<=v.width){if(a.left<v.left+8)viewport.scrollLeft-=v.left+8-a.left;else if(a.right>v.right-8)viewport.scrollLeft+=a.right-v.right+8;}}\n requestAnimationFrame(()=>requestAnimationFrame(()=>reveal(pending)));\n }\n function onScroll(){if(!selected)return;detail.hidden=true;const pending=epoch;clearTimeout(scrollTimer);scrollTimer=setTimeout(()=>reveal(pending),100);}\n function refresh(){if(selected)reveal(epoch);}\n detail.addEventListener('pointerenter',keepAlive);detail.addEventListener('pointerleave',leave);\n return {choose,clear,onScroll,refresh,get focused(){return !!selected&&selected.anchor===document.activeElement;}};\n}\n\nfunction createMermaidViewport(root) {\n if (root.__mcxViewport) return root.__mcxViewport;\n let viewport = root.querySelector('.diagram-scroll'), scene = root.querySelector('.scene');\n if (!viewport || !scene) {\n viewport = document.createElement('div'); scene = document.createElement('div');\n viewport.className = 'diagram-scroll'; scene.className = 'scene'; scene.style.width = root.clientWidth + 'px';\n for (const child of [...root.children]) if (!['STYLE','H3'].includes(child.tagName)) scene.append(child);\n viewport.append(scene); root.append(viewport); scene.style.position = 'relative'; viewport.style.width = '100%';\n }\n const positions = new Map(), inlinePositions = new Map();\n let mode = window.openai?.displayMode || 'inline', restorePending = null, frame = 0, drag = null, suppressClick = false;\n const scope = () => root.dataset.scope || 'diagram';\n const expanded = () => mode === 'viewer';\n const send = detail => root.dispatchEvent(new CustomEvent('mcx:qv-viewer-input',{bubbles:true,detail}));\n function refresh() {\n root.dataset.displayMode = mode;\n if (expanded()) {\n viewport.style.paddingBottom = '0'; viewport.style.height = scene.offsetHeight + 'px';\n viewport.style.overflow = 'hidden'; viewport.style.cursor = 'grab'; viewport.style.touchAction = 'none'; viewport.scrollLeft = 0;\n } else {\n for (const key of ['height','overflow','touchAction','cursor','paddingBottom']) viewport.style[key] = '';\n if (restorePending) {\n viewport.scrollLeft = restorePending.left;\n if (viewport.scrollLeft === restorePending.left || viewport.clientWidth === restorePending.width) restorePending = null;\n }\n }\n }\n const schedule = () => { if (!frame) frame = requestAnimationFrame(() => { frame=0;refresh(); }); };\n const captureInline = () => { if (!expanded()) inlinePositions.set(scope(),{left:viewport.scrollLeft,width:viewport.clientWidth}); };\n captureInline();\n viewport.addEventListener('scroll',() => {const saved=inlinePositions.get(scope());if (!expanded()&&!restorePending&&(!saved||saved.width===viewport.clientWidth)) captureInline();},{passive:true});\n window.addEventListener('openai:set_globals',event => {\n const next = event.detail?.globals?.displayMode || window.openai?.displayMode || mode;\n if (next === mode) { schedule();return; }\n if (next === 'viewer') {const saved=inlinePositions.get(scope());positions.set(scope(),saved&&saved.width!==viewport.clientWidth?saved:{left:viewport.scrollLeft,width:viewport.clientWidth});}\n else restorePending = positions.get(scope()) || {left:0,width:viewport.clientWidth};\n mode = next; refresh();\n if (expanded()) root.getAnimations?.({subtree:true}).forEach(animation=>animation.cancel());\n // 只让关闭后的正文恢复原版几何,缩放与拖拽不触发图型重绘。\n if (!expanded()) root.dispatchEvent(new Event('qv:display-mode'));\n });\n viewport.addEventListener('wheel',event => {\n if (!expanded()) {captureInline();return;}\n event.preventDefault();event.stopPropagation();send({kind:'wheel',deltaY:event.deltaY,deltaMode:event.deltaMode,x:event.clientX,y:event.clientY});\n },{passive:false});\n viewport.addEventListener('pointerdown',event => {\n if (!expanded()) {captureInline();return;}\n if (event.button!==0 || event.target.closest('input,a')) return;\n suppressClick=false;drag={id:event.pointerId,x:event.clientX,y:event.clientY,moved:false};send({kind:'start'});\n },true);\n viewport.addEventListener('pointermove',event => {\n if (!drag || drag.id!==event.pointerId) return;\n const dx=event.clientX-drag.x,dy=event.clientY-drag.y;\n if (!drag.moved && Math.hypot(dx,dy)<5) return;\n if (!drag.moved) {drag.moved=true;viewport.setPointerCapture(event.pointerId);}\n event.preventDefault();event.stopImmediatePropagation();viewport.style.cursor='grabbing';send({kind:'drag',dx,dy});\n },true);\n const end=event => {\n if (!drag || drag.id!==event.pointerId) return;\n suppressClick=drag.moved;\n if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);\n if (drag.moved) {event.preventDefault();event.stopImmediatePropagation();}\n drag=null;viewport.style.cursor=expanded()?'grab':'';send({kind:'end'});\n };\n viewport.addEventListener('pointerup',end,true);viewport.addEventListener('pointercancel',end,true);\n viewport.addEventListener('click',event => {if(suppressClick){suppressClick=false;event.preventDefault();event.stopImmediatePropagation();}},true);\n document.addEventListener('keydown',event => {\n if (!expanded() || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.target.closest('input,textarea,[contenteditable=true]')) return;\n if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key)) {event.preventDefault();send({kind:'pan',dx:event.key==='ArrowLeft'?-40:event.key==='ArrowRight'?40:0,dy:event.key==='ArrowUp'?-40:event.key==='ArrowDown'?40:0});}\n });\n const observer=new ResizeObserver(schedule);[root,viewport,scene].forEach(e=>observer.observe(e));\n root.__mcxViewport={refresh,get expanded(){return expanded();},get scale(){return 1;},get savedInlinePosition(){const saved=positions.get(scope());return saved?{...saved}:null;}};\n refresh();return root.__mcxViewport;\n}\n// 浮层避让只在触发对象附近进行,局部遮挡优先于失去关联。\nfunction alignMermaidLegends(root){if(!document.getElementById('mcx-legend-style')){const st=document.createElement('style');st.id='mcx-legend-style';st.textContent='[data-legend-stacked]{display:grid!important;grid-template-columns:minmax(0,max-content);justify-content:center;justify-items:start;max-width:100%}'+'[data-legend-stacked]>*{max-width:100%;min-width:0}[data-legend-stacked] .mcx-legend-text{display:block!important;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}';document.head.append(st);}const check=el=>{if(el.hidden||!el.children.length)return;el.removeAttribute('data-legend-stacked');const items=[...el.children].filter(n=>n.offsetParent);if(new Set(items.map(n=>n.offsetTop)).size<2)return;for(const item of items){item.title||=item.textContent.trim();for(const t of [...item.childNodes])if(t.nodeType===3&&t.textContent.trim()){const sp=document.createElement('span');sp.className='mcx-legend-text';t.replaceWith(sp);sp.append(t);}for(const span of item.querySelectorAll('span'))if(!span.children.length&&span.textContent.trim()&&!span.classList.contains('percent'))span.classList.add('mcx-legend-text');}el.setAttribute('data-legend-stacked','');};const refresh=()=>{if(root.dataset.state==='enhanced'){const title=root.querySelector('h3');if(title){const line=parseFloat(getComputedStyle(title).lineHeight);title.toggleAttribute('data-title-wrapped',!!title.offsetHeight&&title.scrollHeight>line*1.5);}}root.querySelectorAll('.radar-legend,.xy-key,.legend,.quick-radar-legend').forEach(check);};refresh();if(!root.__mcxLegendObserver){let last=root.clientWidth;root.__mcxLegendObserver=new ResizeObserver(()=>{if(root.clientWidth===last)return;last=root.clientWidth;requestAnimationFrame(refresh);});root.__mcxLegendObserver.observe(root);}}\nfunction placeMermaidDetail(root,tip,anchor,event,preferredWidth=280){\n const rr=root.getBoundingClientRect(),r=anchor?.getBoundingClientRect();\n if(!r)return;\n tip.style.position='absolute';tip.style.marginTop='0';tip.style.width=Math.min(preferredWidth,root.clientWidth-16)+'px';tip.style.maxWidth='calc(100% - 8px)';tip.style.boxSizing='border-box';tip.style.overflowWrap='anywhere';\n const w=tip.offsetWidth,h=tip.offsetHeight,pointer=event?.type?.startsWith('pointer'),x=pointer?event.clientX-rr.left:r.left-rr.left,y=pointer?event.clientY-rr.top:r.top-rr.top,aw=pointer?0:r.width,ah=pointer?0:r.height;\n const candidates=[];\n for(const gap of [12,20,28])for(const [left,top]of [[x+aw+gap,y+(ah-h)/2],[x-w-gap,y+(ah-h)/2],[x+(aw-w)/2,y-h-gap],[x+(aw-w)/2,y+ah+gap]]){\n const c={x:Math.max(4,Math.min(root.clientWidth-w-4,left)),y:Math.max(4,Math.min(root.clientHeight-h-4,top))};\n c.distance=Math.hypot(Math.max(x-c.x-w,c.x-x-aw,0),Math.max(y-c.y-h,c.y-y-ah,0));if(c.distance<=32)candidates.push(c);\n }\n const overlap=(c,o)=>Math.max(0,Math.min(c.x+w,o.x+o.w)-Math.max(c.x,o.x))*Math.max(0,Math.min(c.y+h,o.y+o.h)-Math.max(c.y,o.y));\n const obstacles=[...root.querySelectorAll('button,h3')].filter(n=>n!==anchor&&!tip.contains(n)).map(n=>{const b=n.getBoundingClientRect();return{x:b.left-rr.left,y:b.top-rr.top,w:b.width,h:b.height};});\n for(const c of candidates){c.anchor=overlap(c,{x,y,w:aw,h:ah});c.score=obstacles.reduce((sum,o)=>sum+overlap(c,o),0)+c.distance*8;}\n candidates.sort((a,b)=>a.anchor-b.anchor||a.score-b.score);const c=candidates[0]||{x:Math.max(4,Math.min(root.clientWidth-w-4,x)),y:y+ah+12};\n tip.style.left=c.x+'px';tip.style.top=c.y+'px';tip.dataset.placement='near';tip.style.transformOrigin=`${Math.max(0,Math.min(w,x+aw/2-c.x))}px ${Math.max(0,Math.min(h,y+ah/2-c.y))}px`;\n}\nfunction createMermaidHover(root,clear){\n let enter=0,leave=0,current=null;\n const cancel=()=>{clearTimeout(enter);clearTimeout(leave);};const reset=()=>{current=null;clear();};\n root.addEventListener('keydown',e=>{if(e.key==='Escape'){cancel();current=null;}});root.addEventListener('qv:display-mode',cancel);\n return (element,activate)=>{\n const queue=()=>{cancel();if(current!==element)enter=setTimeout(()=>{current=element;activate();},current?0:100);};element.onpointerenter=queue;element.onpointermove=queue;\n element.onpointerleave=()=>{clearTimeout(enter);leave=setTimeout(reset,160);};\n element.onfocus=()=>{cancel();current=element;activate(true);};element.onblur=()=>{cancel();reset();};element.onclick=()=>{cancel();current=element;activate(true);};\n };\n}\nfunction mermaidContainmentSurface(data,id){\n const groups=new Map(data.groups.map(n=>[n.id,n]));const height=id=>{const children=data.nodes.filter(n=>n.parent===id).concat(data.groups.filter(n=>n.parent===id));return children.length?1+Math.max(...children.map(n=>groups.has(n.id)?height(n.id):0)):1;};\n return height(id)%2?'color-mix(in srgb,var(--muted) 18%,var(--background))':'var(--background)';\n}\n\nfunction renderMermaidSequence(root,data,cfg) {\n root.classList.add('qv-sequence');root.dataset.renderer='qv-sequence';root.dataset.state='enhanced';\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n viewport.setAttribute('aria-label','时序图,消息顺序从上到下,可横向滚动');root.querySelector('.edge-key').hidden=true;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .seq-header{position:absolute;display:flex;align-items:center;justify-content:center;gap:8px;min-height:48px;border:1px solid var(--border);border-radius:10px;background:var(--background);color:var(--foreground);font:inherit;font-weight:500;padding:12px;line-height:20px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .seq-life{stroke:var(--border);stroke-width:1;stroke-dasharray:3 5}\n #${root.id} .seq-message{fill:none;stroke:light-dark(#727a82,#a3aab3);stroke-width:1}\n #${root.id} .seq-arrow{fill:light-dark(#4b5259,#c4cad1);stroke:none} #${root.id} .seq-badge circle{fill:light-dark(#4b5259,#c4cad1)} #${root.id} .seq-badge text{fill:var(--background);font-size:10px;font-weight:600;font-variant-numeric:tabular-nums} #${root.id} .seq-hit.is-related .seq-badge circle{fill:var(--viz-series-1)}\n #${root.id} .seq-label{position:absolute;border:0;background:var(--background);color:var(--foreground);font:inherit;font-size:12px;line-height:18px;min-height:0;padding:2px 7px;border-radius:4px;white-space:normal;overflow-wrap:anywhere;text-align:center;cursor:var(--cursor-interaction,pointer);transform:translateX(-50%)}\n #${root.id} .seq-number{font-variant-numeric:tabular-nums;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));margin-right:8px}\n #${root.id} .seq-frame{fill:transparent;stroke:color-mix(in srgb,var(--muted-foreground) 48%,var(--background));stroke-width:1}\n #${root.id} .seq-branch{position:absolute;font-size:12px;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));padding:0 8px;background:var(--background)}\n #${root.id} .seq-note{position:absolute;padding:10px 12px;border-left:2px solid var(--border);color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font-size:12px;background:var(--background)}\n #${root.id} .seq-activation{fill:light-dark(color-mix(in srgb,var(--viz-series-1) 16%,var(--background)),color-mix(in srgb,var(--viz-series-1) 26%,var(--background)));stroke:color-mix(in srgb,var(--viz-series-1) 65%,var(--background));stroke-width:1}\n #${root.id} .seq-tip{position:absolute;z-index:10;max-width:280px;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);box-shadow:0 3px 12px #0002;font-size:12px;pointer-events:none}\n #${root.id} .seq-hit{transition:opacity 140ms ease} #${root.id} .seq-hit.is-related .seq-message{stroke:var(--viz-series-1);stroke-width:1.4} #${root.id} .seq-hit.is-related .seq-arrow{fill:var(--viz-series-1)}\n #${root.id} .seq-open{fill:none;stroke:var(--muted-foreground)} #${root.id} .seq-hit.is-related .seq-open{fill:none;stroke:var(--viz-series-1)}\n @media(prefers-reduced-motion:reduce){#${root.id} .seq-hit{transition:none}}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='seq-tip';tip.hidden=true;tip.setAttribute('role','tooltip');root.append(tip);\n const el=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const text=(cls,value,x,y)=>{const e=document.createElement('div');e.className=cls;e.textContent=value;e.style.left=x+'px';e.style.top=y+'px';scene.append(e);return e;};\n const metrics=document.createElement('canvas').getContext('2d');metrics.font='12px '+getComputedStyle(root).fontFamily;\n const measure=value=>Math.ceil(metrics.measureText(String(value)).width);\n const spacing=Math.max(220,...data.messages.filter(m=>[0,1,3,4,5,6,24,25,33,34].includes(m.type)&&m.from!==m.to).map(m=>Math.min(288,measure(m.message))+48));\n const pad=100,notes=data.messages.filter(m=>m.type===2),selfs=data.messages.filter(m=>m.from===m.to&&[0,1,3,4,5,6,24,25,33,34].includes(m.type));\n const leftExtra=Math.max(0,...notes.filter(m=>m.placement===0&&m.from===data.actors[0].id).map(m=>Math.min(280,measure(m.message)+28)-48));\n const rightExtra=Math.max(0,...notes.filter(m=>m.placement===1&&m.from===data.actors.at(-1).id).map(m=>Math.min(280,measure(m.message)+28)-48),...selfs.filter(m=>m.from===data.actors.at(-1).id).map(m=>Math.min(288,measure(m.message))+32));\n const core=Math.max(root.clientWidth-leftExtra-rightExtra,pad*2+(data.actors.length-1)*spacing+120),width=core+leftExtra+rightExtra,step=(core-pad*2-120)/Math.max(1,data.actors.length-1),xs=new Map(data.actors.map((a,i)=>[a.id,leftExtra+pad+60+i*step]));\n // 固定几何宽度,展开时由共享视口整体缩放,HTML 和 SVG 不单独伸展。\n scene.style.width=width+'px';scene.style.minWidth=width+'px';const headers=[],marks=[],labels=[],frames=[],active=new Map();const probe=document.createElement('span');probe.style.cssText='position:absolute;left:-100000px;font:12px/18px '+getComputedStyle(root).fontFamily+';white-space:pre-wrap;overflow-wrap:anywhere';root.append(probe);const textHeight=(text,width)=>{probe.style.width=width+'px';probe.textContent=text;return probe.offsetHeight;};let y=Math.max(92,...data.actors.map(a=>textHeight(a.name,156)+60));\n const activationLayer=el('g',{class:'seq-activations'});svg.prepend(activationLayer);let lastMessageY=y;\n const activation=(actor,start,end,depth)=>{const bar=el('rect',{x:xs.get(actor)-5+depth*4,y:start,width:10,height:Math.max(12,end-start),class:'seq-activation','data-actor':actor,'data-depth':depth},activationLayer);activationLayer.prepend(bar);};\n const starts={10:'loop',12:'alt',15:'opt',19:'par',27:'critical',30:'break'},ends=new Set([11,14,16,21,29,31]),branches=new Set([13,20,28]);\n for(const [messageIndex,m] of data.messages.entries()){\n if(m.type===26)continue;\n if(starts[m.type]){frames.push({kind:starts[m.type],label:m.message,y:y-10,depth:frames.length});text('seq-branch',starts[m.type]+' · '+m.message,24+frames.length*8,y-17);y+=34;continue;}\n if(branches.has(m.type)){const f=frames.at(-1);el('line',{x1:24+f.depth*8,y1:y-10,x2:width-24-f.depth*8,y2:y-10,class:'seq-life'});text('seq-branch',(m.type===13?'else':'and')+' · '+m.message,32+f.depth*8,y-17);y+=34;continue;}\n if(ends.has(m.type)){const f=frames.pop();el('rect',{x:24+f.depth*8,y:f.y,width:width-48-f.depth*16,height:y-f.y-6,rx:6,class:'seq-frame'});y+=22;continue;}\n if(m.type===17){const list=active.get(m.from)||[];list.push(lastMessageY);active.set(m.from,list);continue;}\n if(m.type===18){const list=active.get(m.from)||[],start=list.pop();activation(m.from,start,lastMessageY,list.length);continue;}\n if(m.type===2){const from=xs.get(m.from),to=xs.get(m.to||m.from),nw=m.placement===2?Math.max(140,Math.abs(to-from)+96):Math.min(280,measure(m.message)+28),nx=m.placement===0?from-nw-12:m.placement===1?from+12:(from+to-nw)/2,note=text('seq-note',m.message,nx,y-10);note.style.width=nw+'px';note.dataset.placement=m.placement;y+=Math.max(60,note.offsetHeight+24);continue;}\n const labelWidth=Math.min(288,Math.max(48,measure(m.message)+14)),labelHeight=textHeight(m.message,labelWidth-14)+4;y+=Math.max(0,labelHeight-22);\n // 激活条跟随真实消息位置;线和箭头位于柱子上层,端点接到柱子边缘。\n const sourceX=xs.get(m.from),targetX=xs.get(m.to),self=sourceX===targetX,direction=targetX>=sourceX?1:-1,next=data.messages[messageIndex+1],sourceDepth=(active.get(m.from)||[]).length,targetDepth=(active.get(m.to)||[]).length+(next?.type===17&&next.from===m.to?1:0),a=sourceX+(sourceDepth?(-5+(sourceDepth-1)*4+(direction>0||self?10:0)):0),b=targetX+(targetDepth?(-5+(targetDepth-1)*4+(direction<0||self?10:0)):0),g=el('g',{class:'seq-hit','data-message':m.id});\n const end=b-(self?0:8*direction),path=el('path',{d:self?`M${a} ${y}h46q8 0 8 8v12q0 8 -8 8H${b}`:`M${a} ${y}H${end}`,class:'seq-message',...( [1,4,6,25,34].includes(m.type)?{'stroke-dasharray':'4 4'}:{})},g);\n const ey=self?y+28:y,dx=self?-1:direction;lastMessageY=ey;\n if([3,4].includes(m.type))el('path',{d:`M${b-3} ${ey-3}l6 6m0 -6l-6 6`,class:'seq-message'},g);\n else if(![5,6].includes(m.type)){const arrow=el('path',{d:[24,25].includes(m.type)?`M${b-8*dx} ${ey-4}L${b} ${ey}L${b-8*dx} ${ey+4}`:`M${b} ${ey}l${-8*dx} -4v8Z`,class:[24,25].includes(m.type)?'seq-arrow seq-open':'seq-arrow'},g);if([33,34].includes(m.type))el('path',{d:`M${a} ${y}l${8*dx} -4v8Z`,class:'seq-arrow'},g);}\n const label=document.createElement('button');label.type='button';label.className='seq-label';label.style.width=labelWidth+'px';label.style.left=(self?a+12+labelWidth/2:(a+b)/2)+'px';label.style.top=(y-labelHeight-10)+'px';const num=document.createElement('span');num.className='seq-number';num.textContent=m.number===undefined?'':String(m.number);if(m.number!==undefined){const bx=a+([33,34].includes(m.type)?18*dx:0),badge=el('g',{class:'seq-badge'},g);el('circle',{cx:bx,cy:y,r:String(m.number).length>2?10:8},badge);el('text',{x:bx,y:y+.5,'text-anchor':'middle','dominant-baseline':'central'},badge).textContent=String(m.number);}label.append(document.createTextNode(m.message));scene.append(label);label.dataset.message=m.id;label.setAttribute('aria-label',`${m.number??''} ${data.actors.find(n=>n.id===m.from).name} → ${data.actors.find(n=>n.id===m.to).name}:${m.message}`);marks.push({m,g,path,label});labels.push(label);y+=Math.max(self?88:60,labelHeight+36+(self?28:0));\n }\n for(const [actor,list] of active)list.forEach((start,depth)=>activation(actor,start,lastMessageY+24,depth));\n scene.style.height=(y+12)+'px';svg.style.height=(y+12)+'px';svg.setAttribute('viewBox',`0 0 ${width} ${y+12}`);\n for(const a of data.actors){const x=xs.get(a.id),life=el('line',{x1:x,y1:48,x2:x,y2:y,class:'seq-life'});svg.prepend(life);const h=document.createElement('button');h.type='button';h.className='seq-header';if(a.type==='actor'){const icon=document.createElement('i');icon.setAttribute('data-lucide','user-round');icon.setAttribute('aria-hidden','true');h.append(icon);}h.append(document.createTextNode(a.name));const hw=Math.min(240,Math.max(156,Math.ceil(measure(a.name)*7/6)+50));h.style.cssText=`left:${x-hw/2}px;top:0;width:${hw}px`;scene.append(h);headers.push({a,h});}\n probe.remove();window.lucide?.createIcons({attrs:{width:16,height:16}});\n const view=createMermaidViewport(root);\n tip.id=root.id+'-sequence-tip';\n function clear(){tip.hidden=true;for(const mark of marks){mark.g.style.opacity='';mark.label.style.opacity='';mark.g.classList.remove('is-related');mark.label.removeAttribute('aria-describedby');}headers.forEach(({h})=>{h.style.opacity='';h.removeAttribute('aria-describedby');});}\n function placeTip(trigger,value,related=[]){}\n function focus(m,label){for(const mark of marks){const related=mark.m.id===m.id;mark.g.classList.toggle('is-related',related);mark.g.style.opacity=related?'1':'.18';mark.label.style.opacity=related?'1':'.3';}headers.forEach(({a,h})=>h.style.opacity=a.id===m.from||a.id===m.to?'1':'.3');}\n let keyboardTarget=null;const bind=createMermaidHover(root,clear);marks.forEach(({m,label})=>bind(label,ensure=>{clear();keyboardTarget=ensure?label:null;focus(m,label);}));\n function focusActor(a,h){const related=marks.filter(n=>n.m.from===a.id||n.m.to===a.id);for(const mark of marks){const hit=related.includes(mark);mark.g.style.opacity=hit?'1':'.18';mark.label.style.opacity=hit?'1':'.3';mark.g.classList.toggle('is-related',hit);}headers.forEach(n=>n.h.style.opacity=n.a.id===a.id?'1':'.3');if(a.description)placeTip(h,a.description,related);}\n headers.forEach(({a,h})=>bind(h,ensure=>{clear();keyboardTarget=ensure?h:null;focusActor(a,h);}));\n let focusRestore=0;const cancelRestore=()=>{clearTimeout(focusRestore);keyboardTarget=null;clear();};\n viewport.addEventListener('scroll',()=>{clearTimeout(focusRestore);clear();const active=document.activeElement,mark=marks.find(n=>n.label===active),header=headers.find(n=>n.h===active);if(active===keyboardTarget&&(mark||header))focusRestore=setTimeout(()=>{if(document.activeElement!==active)return;if(mark)focus(mark.m,mark.label);else focusActor(header.a,header.h);},120);},{passive:true});root.addEventListener('qv:display-mode',cancelRestore);document.addEventListener('keydown',e=>{if(e.key==='Escape')cancelRestore();});\n const reduced=matchMedia('(prefers-reduced-motion: reduce)');let animations=[],armed=true;\n const defs=el('defs',{});marks.forEach(({m,path})=>{const id=root.id+'-seq-mask-'+m.id,length=path.getTotalLength(),mask=el('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height:y+12},defs),reveal=el('path',{d:path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);path.setAttribute('mask',`url(#${id})`);m.reveal=reveal;});\n function replay(){animations.forEach(a=>a.cancel());animations=[];if(reduced.matches||view.expanded)return;for(const [i,{m,g,path,label}] of marks.entries()){const length=path.getTotalLength(),delay=i*Math.min(140,900/Math.max(1,marks.length-1));animations.push(animateMermaidEntry(m.reveal,[{strokeDashoffset:length},{strokeDashoffset:0}],{duration:420,delay,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));animations.push(animateMermaidEntry(label,[{opacity:0},{opacity:1}],{duration:180,delay:delay+70,fill:'backwards'}));g.querySelectorAll('.seq-badge').forEach(n=>animations.push(animateMermaidEntry(n,[{opacity:0},{opacity:1}],{duration:180,delay,fill:'backwards'})));g.querySelectorAll('.seq-arrow').forEach(arrow=>animations.push(animateMermaidEntry(arrow,[{opacity:0},{opacity:1}],{duration:60,delay:delay+400,fill:'backwards'})));}root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);\n document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});root.addEventListener('qv:display-mode',()=>animations.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>animations.forEach(a=>a.cancel()));view.refresh();\n}\n\n// 固定 Mermaid 11.12 的状态 DB 已提供语义形状和 parentId,不按 ID 后缀猜起止点。\nasync function readMermaidStateModel(db,descriptions={}) {\n const raw=db.getData(),shapes=new Set(['rect','rectWithTitle','roundedWithTitle','stateStart','stateEnd','choice','fork','join']);\n if(!raw.nodes.length||Object.keys(db.getClasses?.()||{}).length||raw.nodes.some(n=>!shapes.has(n.shape)||n.cssStyles?.length||n.cssCompiledStyles?.length||hasRichMermaidLabel(n.label)||n.description?.some?.(hasRichMermaidLabel))||raw.edges.some(e=>hasRichMermaidLabel(e.label)))return {reason:'此状态图包含尚未适配的注释、并发区域、富文本或自定义样式,保留原生语义'};\n const all=raw.nodes.map(n=>({id:n.id,name:['stateStart','stateEnd'].includes(n.shape)?(n.shape==='stateStart'?'起点':'终点'):n.shape==='choice'?'选择':n.label||n.id,shape:n.shape,parent:n.parentId,description:descriptions[n.id]||'',lines:n.description||[],direction:n.dir,group:n.isGroup}));\n const unknown=Object.keys(descriptions).filter(id=>!all.some(n=>n.id===id));if(unknown.length)throw new Error('说明引用了未知状态:'+unknown.join('、'));\n return {data:{kind:'state',direction:raw.direction||'TB',nodes:all.filter(n=>!n.group),groups:all.filter(n=>n.group),edges:raw.edges.map((e,i)=>({id:e.id||String(i),from:e.start,to:e.end,label:e.label||'',arrow:true})),layers:[]}};\n}\n\nfunction wrapMermaidStateText(text){return String(text).split('\\n').flatMap(line=>{const chars=Array.from(line),rows=[];for(let i=0;i<chars.length;i+=20)rows.push(chars.slice(i,i+20).join(''));return rows.length?rows:[''];}).join('\\n');}\n\nasync function prepareMermaidStateLayout(data) {\n // 复用 qv 的布局引擎,状态符号保留独立尺寸,不转换成普通服务卡。\n const context=document.createElement('canvas').getContext('2d'),family=getComputedStyle(root).fontFamily;\n const measure=(text,font)=>{context.font=font+' '+family;return Math.max(0,...wrapMermaidStateText(text).split('\\n').map(row=>context.measureText(row).width));};\n for(const n of data.nodes){const terminal=['stateStart','stateEnd'].includes(n.shape),bar=['fork','join'].includes(n.shape),rows=wrapMermaidStateText(n.name).split('\\n').length;\n n.layoutWidth=terminal?20:n.shape==='choice'?32:bar?64:Math.max(n.groupCard?200:128,Math.ceil(Math.max(measure(n.name,'500 16px')+(n.groupCard||n.targetGroup!==undefined?22:0),...n.lines.map(line=>measure(line,'400 12px'))))+32);\n n.layoutHeight=terminal?20:n.shape==='choice'?32:bar?6:Math.max(48,24+24*rows+(n.lines.length?8+18*n.lines.reduce((sum,line)=>sum+wrapMermaidStateText(line).split('\\n').length,0):0)+(n.groupCard?24:0));\n }\n for(const e of data.edges)if(e.label){e.labelWidth=Math.max(30,Math.ceil(measure(e.label,'400 12px'))+12);e.labelHeight=4+18*wrapMermaidStateText(e.label).split('\\n').length;}\n const all=[...data.groups,...data.nodes],rank=new Map();\n // 每个复合状态内部独立从起点遍历;回流只改变布局通道,不改变转移。\n for(const parent of [undefined,...data.groups.map(g=>g.id)]){\n const siblings=all.filter(n=>n.parent===parent),ids=new Set(siblings.map(n=>n.id)),queue=siblings.filter(n=>n.shape==='stateStart').map(n=>n.id);let index=0;\n while(queue.length){const id=queue.shift();if(rank.has(id))continue;rank.set(id,index++);for(const e of data.edges.filter(e=>e.from===id&&ids.has(e.to)))queue.push(e.to);}\n for(const n of siblings)if(!rank.has(n.id))rank.set(n.id,index++);\n }\n data.order=rank;\n for(const e of data.edges){delete e.fromSide;delete e.toSide;if(e.from===e.to){e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';continue;}if(all.find(n=>n.id===e.from)?.parent===all.find(n=>n.id===e.to)?.parent&&rank.get(e.from)>rank.get(e.to)){{const lr=['LR','RL'].includes(data.direction),flip=['RL','BT'].includes(data.direction);e.fromSide=lr?(flip?'EAST':'WEST'):(flip?'SOUTH':'NORTH');e.toSide=lr?(flip?'WEST':'EAST'):(flip?'NORTH':'SOUTH');}}}\n data.balanced=false;await prepareMermaidLayout(data);\n for(const n of data.nodes)n.layer=Math.min(6,rank.get(n.id)||0);\n}\n\nfunction renderMermaidState(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-state');root.dataset.renderer='qv-state';root.dataset.state='enhanced';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','状态转移图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} .state-node{position:absolute;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 14px;border:1px solid var(--border);border-radius:10px;background:var(--background);color:var(--foreground);font-family:inherit;font-size:16px;line-height:24px;font-weight:500;white-space:pre-line;text-align:center;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-node small{font-weight:400;font-size:12px;line-height:18px;color:var(--muted-foreground)}\n #${root.id} .state-symbol{position:absolute;display:flex;align-items:center;justify-content:center;border:0;background:none;padding:0;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-symbol svg{width:100%;height:100%;overflow:visible}\n #${root.id} .state-symbol .state-mark{fill:var(--background);stroke:var(--muted-foreground);stroke-width:1}\n #${root.id} .state-symbol .state-solid{fill:var(--muted-foreground);stroke:none}\n #${root.id} .state-group{position:absolute;border:1px solid var(--border);border-radius:12px;background:var(--background)}\n #${root.id} .state-group>button{position:absolute;left:14px;top:8px;border:0;background:inherit;color:var(--muted-foreground);font:inherit;font-size:12px;display:flex;max-width:calc(100% - 28px);padding:0 4px;white-space:pre-line;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-wire{fill:none;stroke:light-dark(color-mix(in srgb,var(--muted-foreground) 55%,var(--background)),color-mix(in srgb,var(--muted-foreground) 68%,var(--background)));stroke-width:1}\n #${root.id} .state-arrow{fill:light-dark(color-mix(in srgb,var(--muted-foreground) 55%,var(--background)),color-mix(in srgb,var(--muted-foreground) 68%,var(--background)))}\n #${root.id} .state-label{position:absolute;transform:translate(-50%,-50%);padding:2px 6px;border:0;border-radius:4px;background:transparent;color:var(--muted-foreground);font:inherit;font-size:12px;line-height:18px;min-height:0;white-space:pre-line;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .state-tip,#${root.id} .state-peers{position:absolute;z-index:10;border-radius:12px;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:none;white-space:pre-line}\n #${root.id} .state-node svg,#${root.id} .state-group>button svg{width:14px;height:14px;flex:none}\n #${root.id} .state-tip-relation{display:grid;grid-template-columns:minmax(0,1fr) 14px minmax(0,1fr);align-items:center;gap:12px}\n #${root.id} .state-tip-relation svg{width:14px;height:14px;color:var(--muted-foreground)}\n #${root.id} .state-tip-endpoint{min-width:0}\n #${root.id} .state-tip-caption{display:block;font-size:11px;line-height:16px;color:var(--muted-foreground);font-weight:400;margin-bottom:4px}\n #${root.id} .state-tip-name{display:block;font-size:14px;line-height:20px;font-weight:600;white-space:normal;overflow-wrap:anywhere}\n #${root.id} .state-tip-body{margin-top:12px;padding-top:12px;border-top:1px solid var(--border);font-size:12px;line-height:20px;overflow-wrap:anywhere}\n #${root.id} .state-title{display:inline-flex;align-items:center;justify-content:center;gap:8px;max-width:100%;min-width:0}\n #${root.id} .state-title-name{min-width:0;white-space:pre-line}\n #${root.id} .state-node small:first-of-type{margin-top:8px}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='state-tip';tip.id=root.id+'-state-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);\n const element=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const source=structuredClone(data),sourceGroups=new Map(source.groups.map(g=>[g.id,g])),all=new Map([...source.groups,...source.nodes].map(n=>[n.id,n]));\n const level=id=>sourceGroups.has(id)?1+Math.max(0,...[...all.values()].filter(n=>n.parent===id).map(n=>level(n.id))):0;\n const surface=(el,id)=>{el.style.background=level(id)%2?'color-mix(in srgb,var(--muted) 18%,var(--background))':'var(--background)';};\n const icon=(el,name)=>{const i=document.createElement('i');i.dataset.lucide=name;i.setAttribute('aria-hidden','true');el.append(i);};\n function drawTip(value){\n tip.replaceChildren();const text=(parent,tag,cls,value)=>{const el=document.createElement(tag);el.className=cls;el.textContent=value;parent.append(el);return el;};\n if(value.from!==undefined){const relation=document.createElement('div');relation.className='state-tip-relation';tip.append(relation);for(const [caption,name]of [['起始状态',value.from],['目标状态',value.to]]){if(caption==='目标状态')icon(relation,'arrow-right');const endpoint=document.createElement('div');endpoint.className='state-tip-endpoint';text(endpoint,'span','state-tip-caption',caption);text(endpoint,'span','state-tip-name',name);relation.append(endpoint);}}\n else text(tip,'span','state-tip-name',value.title);\n const body=document.createElement('div');body.className='state-tip-body';tip.append(body);if(value.label)text(body,'span','state-tip-caption','转移说明'+(value.count>1?' · '+value.count+' 条转移':''));text(body,'div','state-tip-content',wrapMermaidStateText(value.label||value.description));if(window.lucide)window.lucide.createIcons({attrs:{width:14,height:14}});\n }\n const title=(el,name,arrow)=>{const row=document.createElement('span');row.className='state-title';const label=document.createElement('span');label.className='state-title-name';label.textContent=wrapMermaidStateText(name);row.append(label);if(arrow)icon(row,arrow);el.append(row);};\n const nav=root.querySelector('.scope-nav'),positions=new Map();let grouped=false,scope=null,busy=false,dispose=()=>{};\n const view=createMermaidViewport(root);\n async function navigate(target){\n const enabled=target!==null;\n if(busy)return;busy=true;positions.set(root.dataset.scope||'complete',viewport.scrollLeft);viewport.setAttribute('aria-busy','true');\n try{const next=enabled?mermaidScopeView(source,target):structuredClone(source);await prepareMermaidStateLayout(next);dispose();grouped=enabled;scope=target;root.dataset.scope=enabled?(target||'overview'):'complete';draw(next);navigation();view.refresh();viewport.scrollLeft=positions.get(root.dataset.scope)||0;}\n finally{busy=false;viewport.setAttribute('aria-busy','false');}\n }\n function navigation(){\n nav.hidden=!sourceGroups.size||scope===null;nav.setAttribute('aria-label','状态层级');nav.replaceChildren();if(nav.hidden)return;\n const path=[];for(let g=sourceGroups.get(scope);g;g=sourceGroups.get(g.parent))path.unshift(g);\n for(const [index,g]of [{id:null,name:'总览'},...(grouped?path:[])].entries()){\n if(index){const sep=document.createElement('span');sep.textContent='/';nav.append(sep);}\n const b=document.createElement('button');b.type='button';b.textContent=g.name;b.dataset.scope=g.id||'';b.onclick=()=>navigate(g.id);if(g.id===scope)b.setAttribute('aria-current','location');nav.append(b);\n if(g.id!==null){const peers=[...sourceGroups.values()].filter(p=>p.parent===g.parent&&p.id!==g.id);if(peers.length){\n const panel=document.createElement('div');panel.className='state-peers';panel.style.pointerEvents='auto';panel.hidden=true;b.setAttribute('aria-haspopup','true');b.setAttribute('aria-expanded','false');let close;\n const hide=()=>{panel.hidden=true;b.setAttribute('aria-expanded','false');};const open=()=>{clearTimeout(close);const r=b.getBoundingClientRect(),rr=root.getBoundingClientRect();panel.style.left=Math.max(0,r.left-rr.left)+'px';panel.style.top=(r.bottom-rr.top)+'px';panel.hidden=false;b.setAttribute('aria-expanded','true');};\n b.onpointerenter=open;b.onpointerleave=()=>close=setTimeout(hide,180);b.onkeydown=e=>{if(e.key==='ArrowDown'){e.preventDefault();open();panel.querySelector('button').focus();}};panel.onpointerenter=()=>clearTimeout(close);panel.onpointerleave=()=>close=setTimeout(hide,180);panel.onkeydown=e=>{if(e.key==='Escape'){hide();b.focus();}};\n for(const peer of peers){const item=document.createElement('button');item.type='button';item.textContent=peer.name;item.style.cssText='display:block;border:0;background:transparent;color:inherit;font:inherit;padding:6px 0';item.onclick=()=>navigate(peer.id);panel.append(item);}nav.append(panel);\n }}\n }\n\n }\n function draw(data){\n const abort=new AbortController(),signal=abort.signal;scene.querySelectorAll(':scope > :not(svg)').forEach(e=>e.remove());svg.replaceChildren();tip.hidden=true;\n const width=Math.max(root.clientWidth,data.layout.width),height=data.layout.height,offset=(width-data.layout.width)/2;\n scene.style.cssText=`width:${width}px;min-width:${width}px;height:${height}px`;svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\n const nodes=data.nodes.map(n=>({...n,x:n.x+offset})),groups=data.layout.groups.map(n=>({...n,x:n.x+offset})),map=new Map([...nodes,...groups].map(n=>[n.id,n])),objects=[],edges=[];\n for(const g of groups.sort((a,b)=>level(b.id)-level(a.id))){const box=document.createElement('div');box.className='state-group';box.style.cssText=`left:${g.x}px;top:${g.y}px;width:${g.w}px;height:${g.h}px`;const b=document.createElement('button');b.type='button';b.setAttribute('aria-label',g.name+',进入此层级');title(b,g.name,'chevron-right');surface(box,g.id);b.dataset.stateNode=g.id;box.append(b);scene.insertBefore(box,svg);objects.push({n:g,b,box});}\n for(const n of nodes){const b=document.createElement('button');b.type='button';b.className=['rect','rectWithTitle','roundedWithTitle','round'].includes(n.shape)?'state-node':'state-symbol';b.dataset.stateNode=n.id;b.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;b.setAttribute('aria-label',n.name);\n if(b.className==='state-node'){surface(b,n.targetGroup!==undefined?n.targetGroup:n.id);title(b,n.name,n.groupCard||n.targetGroup!==undefined?(n.external?'arrow-up-right':'chevron-right'):null);if(n.groupCard||n.targetGroup!==undefined){}for(const line of n.lines){const small=document.createElement('small');small.textContent=wrapMermaidStateText(line);b.append(small);}}\n else {const s=element('svg',{viewBox:`0 0 ${n.w} ${n.h}`,'aria-hidden':'true'},b);if(n.shape==='stateStart')element('circle',{cx:10,cy:10,r:5,class:'state-solid'},s);else if(n.shape==='stateEnd'){element('circle',{cx:10,cy:10,r:9,class:'state-mark'},s);element('circle',{cx:10,cy:10,r:5,class:'state-solid'},s);}else if(n.shape==='choice')element('path',{d:'M16 1L31 16L16 31L1 16Z',class:'state-mark'},s);else element('rect',{width:n.w,height:n.h,rx:2,class:'state-solid'},s);}\n scene.append(b);objects.push({n,b,box:b});\n }\n const rounded=points=>{let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],l1=Math.hypot(b[0]-a[0],b[1]-a[1]),l2=Math.hypot(c[0]-b[0],c[1]-b[1]),r=Math.min(8,l1/2,l2/2);if(!l1||!l2)continue;d+=`L${b[0]-(b[0]-a[0])*r/l1} ${b[1]-(b[1]-a[1])*r/l1}Q${b[0]} ${b[1]} ${b[0]+(c[0]-b[0])*r/l2} ${b[1]+(c[1]-b[1])*r/l2}`;}return d+`L${points.at(-1)[0]} ${points.at(-1)[1]}`};\n for(const e of data.edges){const pts=e.points.map(([x,y])=>[x+offset,y]),last=pts.at(-1),prev=pts.at(-2),a=Math.atan2(last[1]-prev[1],last[0]-prev[0]),ux=Math.cos(a),uy=Math.sin(a),line=pts.slice(0,-1).concat([[last[0]-5*ux,last[1]-5*uy]]),path=element('path',{d:rounded(line),class:'state-wire','data-state-edge':e.id,'data-from':e.from,'data-to':e.to}),head=element('path',{d:`M${last[0]} ${last[1]}L${last[0]-5*ux+2.2*uy} ${last[1]-5*uy-2.2*ux}L${last[0]-5*ux-2.2*uy} ${last[1]-5*uy+2.2*ux}Z`,class:'state-arrow'});let label;\n if(e.label){const p=e.labelPosition||path.getPointAtLength(path.getTotalLength()/2);label=document.createElement('button');label.type='button';label.className='state-label';label.textContent=wrapMermaidStateText(e.label);label.style.left=(e.labelPosition?p.x+offset:p.x)+'px';label.style.top=p.y+'px';label.setAttribute('aria-label',map.get(e.from).name+' → '+map.get(e.to).name+':'+e.label);scene.append(label);}\n edges.push({...e,labelText:e.label,path,head,label});\n }\n // 在所属路径附近寻找空白位置;折行后仍避开节点、其他条件和场景边界。\n const placed=[];\n for(const e of edges)if(e.label){\n const label=e.label,w=label.offsetWidth,h=label.offsetHeight,length=e.path.getTotalLength(),px=parseFloat(label.style.left),py=parseFloat(label.style.top),candidates=[];\n const obstacles=[...nodes.map(n=>({x:n.x,y:n.y,w:n.w,h:n.h})),...placed];\n for(let d=0;d<=length;d+=12){const p=e.path.getPointAtLength(d),a=e.path.getPointAtLength(Math.max(0,d-1)),b=e.path.getPointAtLength(Math.min(length,d+1)),horizontal=Math.abs(b.x-a.x)>=Math.abs(b.y-a.y);\n for(const clearance of [8,24,48,80])for(const side of [-1,1]){const x=horizontal?p.x:p.x+side*(w/2+clearance),y=horizontal?p.y+side*(h/2+clearance):p.y;\n if(x-w/2<4||x+w/2>width-4||y-h/2<4||y+h/2>height-4)continue;\n const overlap=obstacles.reduce((sum,o)=>sum+Math.max(0,Math.min(x+w/2,o.x+o.w)-Math.max(x-w/2,o.x))*Math.max(0,Math.min(y+h/2,o.y+o.h)-Math.max(y-h/2,o.y)),0);\n candidates.push({x,y,overlap,distance:Math.hypot(x-px,y-py)});\n }\n }\n candidates.sort((a,b)=>a.overlap-b.overlap||a.distance-b.distance);const p=candidates[0]||{x:Math.max(w/2+4,Math.min(width-w/2-4,px)),y:Math.max(h/2+4,Math.min(height-h/2-4,py))};label.style.left=p.x+'px';label.style.top=p.y+'px';placed.push({x:p.x-w/2,y:p.y-h/2,w,h});\n }\n function clear(){tip.hidden=true;objects.forEach(({b,box})=>{b.style.opacity='';box.style.borderColor='';b.removeAttribute('aria-describedby')});edges.forEach(e=>{e.path.style.opacity='';e.path.style.stroke='';e.head.style.opacity='';e.head.style.fill='';if(e.label){e.label.style.opacity='';e.label.removeAttribute('aria-describedby')}});}\n function show(trigger,value,ids,selected){clear();const relation=e=>selected?e.id===selected:e.from===ids||e.to===ids,related=edges.filter(relation),neighbors=new Set([ids,...related.flatMap(e=>[e.from,e.to])]);objects.forEach(({n,b})=>b.style.opacity=neighbors.has(n.id)?'1':'.3');edges.forEach(e=>{const hit=related.includes(e);e.path.style.opacity=e.head.style.opacity=hit?'1':'.18';e.path.style.stroke=e.head.style.fill=hit?'var(--viz-series-1)':'';if(e.label)e.label.style.opacity=hit?'1':'.3'});\n }\n if(window.lucide)window.lucide.createIcons({attrs:{width:14,height:14}});\n const bind=(b,show)=>{b.onpointerenter=show;b.onpointerleave=clear;b.onfocus=show;b.onblur=clear;b.onclick=show};objects.forEach(({n,b})=>bind(b,()=>show(b,n.description?{title:n.name,description:n.description}:null,n.id)));objects.forEach(({n,b})=>{if(sourceGroups.has(n.id)||n.targetGroup!==undefined)b.onclick=()=>navigate(n.targetGroup!==undefined?n.targetGroup:n.id);});edges.forEach(e=>{if(e.label)bind(e.label,()=>show(e.label,{from:map.get(e.from).name,to:map.get(e.to).name,label:e.labelText,count:e.bundleCount||1},null,e.id))});\n viewport.addEventListener('scroll',clear,{passive:true,signal});root.addEventListener('qv:display-mode',clear,{signal});document.addEventListener('keydown',e=>{if(e.key==='Escape')clear()},{signal});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),defs=element('defs',{});let running=[],armed=true;\n edges.forEach((e,i)=>{const length=e.path.getTotalLength(),id=root.id+'-state-mask-'+i,mask=element('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs);e.reveal=element('path',{d:e.path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);e.path.setAttribute('mask',`url(#${id})`)});\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;objects.forEach(({n,b})=>running.push(animateMermaidEntry(b,[{opacity:0,transform:'scale(.08)'},{opacity:1,transform:'scale(1)'}],{duration:240,delay:(n.layer||0)*180,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'})));edges.forEach(e=>{const delay=(map.get(e.from).layer||0)*180+110;running.push(animateMermaidEntry(e.reveal,[{strokeDashoffset:e.path.getTotalLength()},{strokeDashoffset:0}],{duration:650,delay,fill:'backwards'}));running.push(animateMermaidEntry(e.head,[{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'}));if(e.label)running.push(animateMermaidEntry(e.label,[{opacity:0},{opacity:1}],{duration:180,delay:delay+120,fill:'backwards'}))});root.dataset.replays=String(+(root.dataset.replays||0)+1)}\n const observer=new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]});observer.observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}},{signal});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()),{signal});reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()),{signal});dispose=()=>{abort.abort();observer.disconnect();running.forEach(a=>a.cancel());};view.refresh();\n }\n root.dataset.scope='complete';draw(data);navigation();\n}\n\n// 固定 Mermaid 11.12 DB;getData 的端点标记已处理 relSpec 左右命名反转。\nasync function readMermaidErModel(db,descriptions={}) {\n const entities=[...db.getEntities()],raw=db.getData(),cards=new Set(['only_one','zero_or_one','one_or_more','zero_or_more']);\n if(Object.keys(db.getClasses()).length||entities.some(([,n])=>n.cssStyles?.length||n.cssCompiledStyles?.length||n.cssClasses!=='default'||hasRichMermaidLabel(n.alias||n.label)||n.attributes.some(a=>[a.name,a.type,a.comment].some(hasRichMermaidLabel)))||raw.edges.some(e=>!cards.has(e.arrowTypeStart)||!cards.has(e.arrowTypeEnd)||hasRichMermaidLabel(e.label)))return {reason:'此实体关系图包含尚未适配的样式、富文本或关系标记,保留原生语义'};\n const nodes=entities.map(([id,n])=>({id,name:n.alias||n.label,technicalName:n.label,attributes:n.attributes.map(a=>({...a,keys:[...a.keys]})),description:descriptions[id]||'',shape:'er'})),ids=new Map(entities.map(([id,n])=>[n.id,id]));\n const unknown=Object.keys(descriptions).filter(id=>!nodes.some(n=>n.id===id));if(unknown.length)throw new Error('说明引用了未知实体:'+unknown.join('、'));\n return {data:{kind:'er',direction:db.getDirection()||'TB',nodes,groups:[],layers:[],edges:raw.edges.map(e=>({id:e.id,from:ids.get(e.start),to:ids.get(e.end),label:e.label||'',startCard:e.arrowTypeStart,endCard:e.arrowTypeEnd,async:e.pattern==='dashed'}))}};\n}\nasync function prepareMermaidErLayout(data) {\n const context=document.createElement('canvas').getContext('2d'),font=getComputedStyle(document.body).fontFamily;context.font='12px '+font;const measure=s=>Math.ceil(context.measureText(s||'').width);\n const probe=document.createElement('span');probe.style.cssText='position:absolute;left:-100000px;white-space:pre-wrap;overflow-wrap:anywhere;box-sizing:border-box';document.body.append(probe);\n const height=(text,width,size=12,line=18,padding=0)=>{probe.style.width=width+'px';probe.style.font=size+'px/'+line+'px '+font;probe.style.padding=padding+'px 0';probe.textContent=text;return probe.offsetHeight;};\n for(const n of data.nodes){\n n.layoutWidth=Math.min(360,Math.max(240,measure(n.name)+32,...n.attributes.map(a=>measure(a.name)+measure(a.type)+112)));\n const available=n.layoutWidth-40,key=46,type=Math.min(90,Math.max(52,...n.attributes.map(a=>measure(a.type))));n.columns={key,name:available-key-type-16,type};\n n.headerHeight=Math.max(52,height(n.name,n.layoutWidth-32,14,20)+24)+(n.name!==n.technicalName?height(n.technicalName,n.layoutWidth-32,12,18)+4:0);\n n.attributes.forEach(a=>a.rowHeight=Math.max(30,Math.max(height(a.name,n.columns.name),height(a.type,n.columns.type),height(a.keys.join(', '),key))+12));\n // 字段区上下内边距8px、底部留白10px,以及卡片上下边框2px。\n n.layoutHeight=n.headerHeight+n.attributes.reduce((sum,a)=>sum+a.rowHeight,0)+20;\n }\n for(const e of data.edges){e.labelWidth=Math.min(180,Math.max(30,measure(e.label)+12));e.labelHeight=Math.max(22,height(e.label,e.labelWidth-12)+4);}probe.remove();\n // 自关联独占侧面端口,避免与主轴上的关系基数叠合。\n for(const e of data.edges)if(e.from===e.to)e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';\n await prepareMermaidLayout(data);\n}\n\nfunction renderMermaidEr(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-er');root.dataset.renderer='qv-er';root.dataset.state='enhanced';const key=root.querySelector('.edge-key');key.hidden=false;key.replaceChildren();for(const [line,text] of [['solid','识别关系'],['dotted','非识别关系']])if(data.edges.some(e=>(e.async?'dotted':'solid')===line)){const item=document.createElement('span'),swatch=document.createElement('i');swatch.className='key-line';if(line==='dotted')swatch.style.borderTopStyle='dashed';item.append(swatch,document.createTextNode(text));key.append(item);}viewport.setAttribute('aria-label','实体关系图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} .er-entity{position:absolute;border:1px solid var(--border);border-radius:14px;background:var(--background);overflow:hidden;transition:opacity 140ms ease,border-color 140ms ease}\n #${root.id} .er-header{display:flex;align-items:center;justify-content:center;flex-direction:column;align-items:flex-start;gap:4px;width:100%;border:0;background:none;color:var(--foreground);font:inherit;font-weight:500;padding:12px 16px;overflow-wrap:anywhere;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .er-header small{color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font-size:12px;font-weight:400}\n #${root.id} .er-field{display:grid;gap:8px;align-items:center;width:100%;min-height:30px;padding:6px 10px;line-height:18px;white-space:pre-wrap;overflow-wrap:anywhere;border:0;background:none;color:var(--foreground);font:inherit;font-size:12px;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .er-field .er-key{color:var(--viz-series-1)} #${root.id} .er-field .er-type{color:light-dark(#287d7b,#80c3ba)} #${root.id} .er-body{margin:0 10px 10px;padding:4px 0;border-radius:8px;background:color-mix(in srgb,var(--foreground) 4%,var(--background))}\n #${root.id} .er-field[data-active=true]{background:var(--accent)}\n #${root.id} .er-markers{color:light-dark(#727a82,#a3aab3)}\n #${root.id} .er-cardinality{color:inherit}\n #${root.id} .er-cardinality .er-wire{stroke:currentColor}\n #${root.id} .er-zero{stroke:currentColor;stroke-width:1;fill:var(--background)}\n #${root.id} .er-wire{fill:none;stroke:light-dark(#727a82,#a3aab3);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .er-arrow{fill:light-dark(#727a82,#a3aab3)}\n #${root.id} .er-label{position:absolute;transform:translate(-50%,-50%);padding:2px 6px;border:0;border-radius:4px;background:var(--background);color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font:inherit;font-size:12px;line-height:18px;min-height:0;white-space:normal;overflow-wrap:anywhere;max-width:180px;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .er-tip{position:absolute;z-index:10;border-radius:12px;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:none}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='er-tip';tip.id=root.id+'-er-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);\n const element=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const width=data.layout.width,height=data.layout.height,offset=(width-data.layout.width)/2;\n scene.style.cssText=`width:${width}px;min-width:${width}px;height:${height}px`;svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\n const nodes=data.nodes.map(n=>({...n,x:n.x+offset})),groups=data.layout.groups.map(n=>({...n,x:n.x+offset})),map=new Map([...nodes,...groups].map(n=>[n.id,n])),objects=[],edges=[];\n for(const n of nodes){\n const box=document.createElement('div');box.className='er-entity';box.dataset.erEntity=n.id;box.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;\n const b=document.createElement('button');b.type='button';b.className='er-header';b.dataset.erNode=n.id;b.textContent=n.name;b.style.height=n.headerHeight+'px';if(n.name!==n.technicalName){const code=document.createElement('small');code.textContent=n.technicalName;b.append(code);}box.append(b);const body=document.createElement('div');body.className='er-body';box.append(body);\n for(const attribute of n.attributes){const row=document.createElement('button');row.type='button';row.className='er-field';row.dataset.field=attribute.name;row.style.height=attribute.rowHeight+'px';row.style.gridTemplateColumns=`${n.columns.key}px ${n.columns.name}px ${n.columns.type}px`;for(const [value,cl]of [[attribute.keys.join(', '),'er-key'],[attribute.name,'er-name'],[attribute.type,'er-type']]){const cell=document.createElement('span');cell.className=cl;cell.textContent=value;row.append(cell);}row.setAttribute('aria-label',`${n.name} · ${attribute.name} · ${attribute.type} · ${attribute.keys.join(', ')} · ${attribute.comment||''}`);body.append(row);}\n scene.append(box);objects.push({n,b,box});\n }\n const rounded=points=>{let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],l1=Math.hypot(b[0]-a[0],b[1]-a[1]),l2=Math.hypot(c[0]-b[0],c[1]-b[1]),r=Math.min(8,l1/2,l2/2);if(!l1||!l2)continue;d+=`L${b[0]-(b[0]-a[0])*r/l1} ${b[1]-(b[1]-a[1])*r/l1}Q${b[0]} ${b[1]} ${b[0]+(c[0]-b[0])*r/l2} ${b[1]+(c[1]-b[1])*r/l2}`;}return d+`L${points.at(-1)[0]} ${points.at(-1)[1]}`};\n const cardNames={only_one:'1',zero_or_one:'0..1',one_or_more:'1..*',zero_or_more:'0..*'};\n function cardinality(point,next,kind){const angle=Math.atan2(next[1]-point[1],next[0]-point[0]),g=element('g',{class:'er-cardinality',transform:`translate(${point[0]} ${point[1]}) rotate(${angle*180/Math.PI})`});\n const path=d=>element('path',{d,class:'er-wire'},g);\n if(kind==='only_one')path('M7 -5V5M13 -5V5');\n if(kind==='zero_or_one'){path('M7 -5V5');element('circle',{cx:19,cy:0,r:3.5,class:'er-zero'},g);}\n if(kind==='one_or_more'||kind==='zero_or_more'){path('M0 -5L12 0L0 5M0 0H12');if(kind==='one_or_more')path('M19 -5V5');else element('circle',{cx:20,cy:0,r:3.5,class:'er-zero'},g);}\n return g;\n }\n for(const e of data.edges){const pts=e.points.map(([x,y])=>[x+offset,y]),path=element('path',{d:rounded(pts),class:'er-wire','data-er-edge':e.id,'data-from':e.from,'data-to':e.to,...(e.async?{'stroke-dasharray':'4 4'}:{})}),start=cardinality(pts[0],pts[1],e.startCard),end=cardinality(pts.at(-1),pts.at(-2),e.endCard),head=element('g',{class:'er-markers'});head.append(start,end);let label;\n if(e.label){const p=e.labelPosition||path.getPointAtLength(path.getTotalLength()/2);label=document.createElement('button');label.type='button';label.className='er-label';label.textContent=e.label;label.style.left=(e.labelPosition?p.x+offset:p.x)+'px';label.style.top=p.y+'px';label.setAttribute('aria-label',`${map.get(e.from).name} ${cardNames[e.startCard]} — ${map.get(e.to).name} ${cardNames[e.endCard]} · ${e.label} · ${e.async?'非识别关系':'识别关系'}`);scene.append(label);}\n edges.push({...e,path,head,label});\n }\n // 标签沿所属路径法向留8px间距,不能用背板擦断方向证据。\n for(const e of edges)if(e.label){const b=e.label,px=parseFloat(b.style.left),py=parseFloat(b.style.top),length=e.path.getTotalLength();let nearest,distance=Infinity,at=0;for(let d=0;d<=length;d+=3){const p=e.path.getPointAtLength(d),delta=Math.hypot(px-p.x,py-p.y);if(delta<distance){nearest=p;distance=delta;at=d;}}const a=e.path.getPointAtLength(Math.max(0,at-1)),c=e.path.getPointAtLength(Math.min(length,at+1));if(Math.abs(c.x-a.x)>=Math.abs(c.y-a.y)){const gap=b.offsetHeight/2+8;if(Math.abs(py-nearest.y)<gap)b.style.top=(nearest.y+(py<nearest.y?-gap:gap))+'px';}else{const gap=b.offsetWidth/2+8;if(Math.abs(px-nearest.x)<gap)b.style.left=(nearest.x+(px<nearest.x?-gap:gap))+'px';}}\n const view=createMermaidViewport(root);\n function clear(){tip.hidden=true;root.querySelectorAll('[data-active]').forEach(n=>n.removeAttribute('data-active'));objects.forEach(({b,box})=>{box.style.opacity='';box.style.borderColor='';box.querySelectorAll('[aria-describedby]').forEach(n=>n.removeAttribute('aria-describedby'))});edges.forEach(e=>{e.path.style.opacity='';e.path.style.stroke='';e.head.style.opacity='';e.head.style.color='';if(e.label){e.label.style.opacity='';e.label.removeAttribute('aria-describedby')}});}\n function show(trigger,value,ids,selected,local=false){clear();if(local){trigger.dataset.active='true';objects.forEach(({n,box})=>{box.style.opacity=n.id===ids?'1':'.26';box.style.borderColor=n.id===ids?'var(--viz-series-1)':'';});}const relation=e=>selected?e.id===selected:e.from===ids||e.to===ids,related=edges.filter(relation),neighbors=new Set([ids,...related.flatMap(e=>[e.from,e.to])]);if(!local){objects.forEach(({n,box})=>box.style.opacity=neighbors.has(n.id)?'1':'.3');edges.forEach(e=>{const hit=related.includes(e);e.path.style.opacity=e.head.style.opacity=hit?'1':'.18';e.path.style.stroke=e.head.style.color=hit?'var(--viz-series-1)':'';if(e.label)e.label.style.opacity=hit?'1':'.3'});}\n }\n let focusedDetail=null,focusEpoch=0,escapeClosed=false;\n function restoreFocus(){const epoch=++focusEpoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(epoch===focusEpoch&&!escapeClosed&&focusedDetail&&document.activeElement===focusedDetail.b&&focusedDetail.b.matches(':focus-visible'))focusedDetail.show();}));}\n const stable=createMermaidHover(root,clear);const bind=(b,show)=>{const activate=()=>{escapeClosed=false;show()};stable(b,activate);b.onfocus=()=>{focusedDetail={b,show};escapeClosed=false;show();restoreFocus()};b.onblur=()=>{focusedDetail=null;++focusEpoch;clear()};b.onclick=activate};\n objects.forEach(({n,b,box})=>{bind(b,()=>show(b,n.description||n.name+' · '+n.attributes.length+' 个字段 · '+edges.filter(e=>e.from===n.id||e.to===n.id).length+' 条关系',n.id));box.querySelectorAll('.er-field').forEach(row=>bind(row,()=>show(row,n.attributes.find(a=>a.name===row.dataset.field)?.comment||'',n.id,null,true)));});edges.forEach(e=>{if(e.label)bind(e.label,()=>show(e.label,e.label.getAttribute('aria-label'),null,e.id))});\n viewport.addEventListener('scroll',()=>{clear();restoreFocus()},{passive:true});root.addEventListener('qv:display-mode',()=>{focusedDetail=null;++focusEpoch;clear()});document.addEventListener('keydown',e=>{if(e.key==='Escape'){escapeClosed=true;++focusEpoch;clear()}});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),defs=element('defs',{});let running=[],armed=true;\n edges.forEach((e,i)=>{const length=e.path.getTotalLength(),id=root.id+'-er-mask-'+i,mask=element('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs);e.reveal=element('path',{d:e.path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);e.path.setAttribute('mask',`url(#${id})`)});\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;objects.forEach(({n,box})=>running.push(animateMermaidEntry(box,[{opacity:0,transform:'scale(.08)'},{opacity:1,transform:'scale(1)'}],{duration:240,delay:(n.layer||0)*180,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'})));edges.forEach(e=>{const delay=(map.get(e.from).layer||0)*180+110;running.push(animateMermaidEntry(e.reveal,[{strokeDashoffset:e.path.getTotalLength()},{strokeDashoffset:0}],{duration:650,delay,fill:'backwards'}));running.push(animateMermaidEntry(e.head,[{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'}));if(e.label)running.push(animateMermaidEntry(e.label,[{opacity:0},{opacity:1}],{duration:100,delay:delay+600,fill:'backwards'}))});root.dataset.replays=String(+(root.dataset.replays||0)+1)}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\n// 11.12 只公开像素坐标;在原解析器回调中保存输入值,避免反推数值。\nasync function readMermaidQuadrantModel(mermaid,source,descriptions={}) {\n const seed=await mermaid.mermaidAPI.getDiagramFromText('quadrantChart'),db=seed.db;\n const names=['addClass','addPoint','setXAxisLeftText','setXAxisRightText','setYAxisBottomText','setYAxisTopText','setQuadrant1Text','setQuadrant2Text','setQuadrant3Text','setQuadrant4Text'];\n if(names.some(k=>typeof db[k]!=='function'))return {reason:'四象限解析接口不兼容'};\n const original=new Map(names.map(k=>[k,db[k]])),events=[];\n try {\n for(const name of names)db[name]=(...args)=>{events.push({name,args:structuredClone(args)});return original.get(name)(...args);};\n await mermaid.mermaidAPI.getDiagramFromText(source);\n } finally {for(const [name,fn]of original)db[name]=fn;}\n const text=name=>events.filter(e=>e.name===name).at(-1)?.args[0]?.text||'';\n if(events.some(e=>e.args[0]?.type==='markdown'||hasRichMermaidLabel(e.args[0]?.text)))return {reason:'四象限富文本保留原生语义'};\n const raw=events.filter(e=>e.name==='addPoint');\n const classes=new Map(events.filter(e=>e.name==='addClass').map(e=>[e.args[0],db.parseStyles(e.args[1])]));\n const styles=raw.map(e=>db.parseStyles(e.args[4]||[]));\n if([...classes.values(),...styles].some(s=>Object.keys(s).some(k=>k!=='color')))return {reason:'四象限自定义尺寸与描边保留原生语义'};\n const points=raw.map((e,i)=>({id:'point-'+(i+1),name:e.args[0].text,group:e.args[1]||'',color:styles[i].color||classes.get(e.args[1])?.color||'',x:Number(e.args[2]),y:Number(e.args[3]),description:descriptions['point-'+(i+1)]||''}));\n if(points.some(p=>!Number.isFinite(p.x)||!Number.isFinite(p.y)||p.x<0||p.x>1||p.y<0||p.y>1))return {reason:'四象限坐标保留原生语义'};\n const unknown=Object.keys(descriptions).filter(k=>!points.some(p=>p.id===k));if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n return {data:{kind:'quadrant',points,xLow:text('setXAxisLeftText'),xHigh:text('setXAxisRightText'),yLow:text('setYAxisBottomText'),yHigh:text('setYAxisTopText'),quadrants:[1,2,3,4].map(i=>text('setQuadrant'+i+'Text')),title:db.getDiagramTitle?.()||''}};\n}\n\nfunction renderMermaidQuadrant(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');root.dataset.state='enhanced';root.dataset.renderer='qv-quadrant';root.querySelector('.edge-key').hidden=true;svg.removeAttribute('aria-hidden');viewport.setAttribute('aria-label','四象限图,可横向滚动');const title=root.querySelector('h3');if(!title.textContent)title.textContent=data.title;if(title.textContent.trim())title.hidden=false;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .wires{pointer-events:auto}\n #${root.id} .quadrant-text{position:absolute;font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere} #${root.id} .quadrant-text.quadrant-caption{font-weight:500;color:color-mix(in srgb,var(--q-tone) 55%,var(--foreground))}\n #${root.id} .quadrant-axis{font-size:14px;font-weight:500;color:var(--foreground);box-sizing:border-box;padding:8px 12px;border-radius:999px;background:color-mix(in srgb,var(--muted-foreground) 8%,var(--background))}\n #${root.id} .quadrant-label{position:absolute;border:0;background:transparent;padding:4px 6px;max-width:144px;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .quadrant-detail{position:absolute;z-index:10;width:252px;max-width:calc(100% - 8px);box-sizing:border-box;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:auto;overflow-wrap:anywhere;overflow:auto;max-height:320px}\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',add=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},fmt=v=>new Intl.NumberFormat(undefined,{maximumFractionDigits:20}).format(v);\n const tip=document.createElement('div');tip.className='quadrant-detail';tip.id=root.id+'-quadrant-detail';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);const hint=document.createElement('div');hint.style.cssText='font-size:12px;line-height:18px;color:var(--muted-foreground);margin-top:12px';viewport.before(hint);\n let labels=[],texts=[],marks=[],leaders=[],animations=[],pending=null,epoch=0,plot,pointPositions=[],activePoint=null,leaveTimer=0;const view=createMermaidViewport(root),reduced=matchMedia('(prefers-reduced-motion:reduce)');\n const box=n=>({x:n.offsetLeft,y:n.offsetTop,w:n.offsetWidth,h:n.offsetHeight}),area=(a,b)=>Math.max(0,Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x))*Math.max(0,Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y));\n function clear(){clearTimeout(leaveTimer);pending=null;activePoint=null;epoch++;tip.hidden=true;root.style.paddingBottom='';tip.style.position='absolute';tip.style.marginTop='0';[...labels,...marks].forEach(b=>b.removeAttribute('aria-describedby'));marks.forEach(m=>m.style.opacity='1');leaders.forEach(l=>l.style.opacity='1');}\n function leave(){clearTimeout(leaveTimer);leaveTimer=setTimeout(clear,100);}\n tip.onpointerenter=()=>clearTimeout(leaveTimer);tip.onpointerleave=leave;\n function show(i,event,anchor,group=false){\n clear();activePoint=i;const p=data.points[i];\n const same=group?data.points.filter(q=>q.x===p.x&&q.y===p.y):[p];\n marks.forEach((m,j)=>m.style.opacity=same.includes(data.points[j])?'1':'.25');\n leaders.forEach((m,j)=>m.style.opacity=same.includes(data.points[j])?'1':'.25');\n const head=document.createElement('div');head.className='quadrant-detail-title';head.textContent=same.length>1?same.length+' 个方案位于同一坐标':p.name;tip.replaceChildren(head);\n if(same.length>1)same.forEach(q=>{const n=document.createElement('div');n.textContent=q.name;tip.append(n);});\n const row=document.createElement('div');row.style.color='var(--muted-foreground)';row.textContent='X '+fmt(p.x)+' · Y '+fmt(p.y);tip.append(row);\n const location=document.createElement('div');location.textContent=p.x===.5||p.y===.5?'位于象限分界线':data.quadrants[p.y>.5?(p.x>.5?0:1):(p.x>.5?3:2)];tip.append(location);\n const hasCategories=data.points.some(q=>q.group),categories=hasCategories?[...new Set(same.map(q=>q.group||''))]:[];\n categories.forEach(category=>{const n=document.createElement('div'),swatch=document.createElement('span');n.className='quadrant-detail-category';n.style.cssText='display:flex;align-items:center;gap:6px;margin-top:6px';swatch.style.cssText='width:7px;height:7px;flex-shrink:0;border-radius:50%';const member=same.find(q=>(q.group||'')===category);swatch.style.background=marks[data.points.indexOf(member)].getAttribute('fill');swatch.setAttribute('aria-hidden','true');n.append(swatch,document.createTextNode(category||'默认'));tip.append(n);});\n same.filter(q=>q.description).forEach(q=>{const d=document.createElement('div');d.style.marginTop='8px';d.textContent=(same.length>1?q.name+':':'')+q.description;tip.append(d);});\n const trigger=anchor||marks[i];trigger.setAttribute('aria-describedby',tip.id);tip.hidden=false;\n const rr=root.getBoundingClientRect(),br=trigger.getBoundingClientRect(),w=tip.offsetWidth;\n const top=Math.max(4,-rr.top+4),bottom=Math.min(root.clientHeight,innerHeight-rr.top-4);\n tip.style.maxHeight=Math.max(64,Math.min(320,bottom-top))+'px';\n const h=tip.offsetHeight,x=br.left+br.width/2-rr.left,y=br.top+br.height/2-rr.top;\n const candidates=[{x:x+14,y:y-h/2},{x:x-w-14,y:y-h/2},{x:x-w/2,y:y+14},{x:x-w/2,y:y-h-14}];\n candidates.forEach(c=>{c.x=Math.max(4,Math.min(root.clientWidth-w-4,c.x));c.y=Math.max(top,Math.min(bottom-h,c.y));c.cover=area({...c,w,h},{x:br.left-rr.left-6,y:br.top-rr.top-6,w:br.width+12,h:br.height+12});});\n candidates.sort((a,b)=>a.cover-b.cover);tip.style.left=candidates[0].x+'px';tip.style.top=candidates[0].y+'px';tip.dataset.placement='overlay';\n }\n function focus(i,ensureVisible=true){pending=i;const b=labels[i].hidden?marks[i]:labels[i],r=b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(ensureVisible&&!view.expanded){if(r.left<v.left+8)viewport.scrollLeft-=v.left+8-r.left;else if(r.right>v.right-8)viewport.scrollLeft+=r.right-v.right+8;}const ticket=++epoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(epoch===ticket){pending=null;show(i,null,marks[i],true);}}));}\n function cancel(){animations.forEach(a=>a.cancel());animations=[];}\n function label(text,x,y,w,align='left'){const n=document.createElement('div');n.className='quadrant-text';n.textContent=text;n.style.left=x+'px';n.style.top=y+'px';n.style.width=w+'px';n.style.textAlign=align;scene.append(n);texts.push(n);return n;}\n function draw(){\n cancel();clear();svg.replaceChildren();[...labels,...texts].forEach(n=>n.remove());labels=[];texts=[];marks=[];leaders=[];\n const width=Math.max(640,Math.min(736,viewport.clientWidth)),size=width-256;\n plot={x:128,y:104,w:size,h:size};const height=size+208;\n hint.textContent=!view.expanded&&viewport.clientWidth<width?'可横向滚动 · 密集区域悬浮点查看完整方案':'';hint.hidden=!hint.textContent;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';\n svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';svg.setAttribute('role','img');svg.setAttribute('aria-label',data.title+','+data.points.map(p=>p.name+' ['+p.x+', '+p.y+']').join(';'));\n const {x,y,w,h}=plot;\n [[x+w/2,y],[x,y],[x,y+h/2],[x+w/2,y+h/2]].forEach(([qx,qy],k)=>{const r=add('rect',{x:qx,y:qy,width:w/2,height:h/2});r.style.fill='color-mix(in srgb,var(--viz-series-'+(k+1)+') 6%,var(--background))';});add('rect',{x,y,width:w,height:h,fill:'none',stroke:'var(--border)'});add('line',{x1:x+w/2,y1:y,x2:x+w/2,y2:y+h,stroke:'var(--border)'});\n add('line',{x1:x,y1:y+h/2,x2:x+w,y2:y+h/2,stroke:'var(--border)'});\n const axis=(text,l,t,width)=>{const n=label(text,l,t,width,'center');n.classList.add('quadrant-axis');n.style.width='max-content';n.style.maxWidth=width+'px';n.style.left=l+(width-n.offsetWidth)/2+'px';return n;};\n const left=axis(data.xLow,0,0,112),right=axis(data.xHigh,x+w+16,0,112);[left,right].forEach(n=>n.style.top=y+h/2-n.offsetHeight/2+'px');\n const top=axis(data.yHigh,x+w/2-160,0,320);top.style.top=Math.max(0,y-20-top.offsetHeight)+'px';axis(data.yLow,x+w/2-160,y+h+20,320);\n // 轴说明按真实换行高度留白,不改变归一化坐标。\n const bottom=texts.at(-1);scene.style.height=Math.max(height,bottom.offsetTop+bottom.offsetHeight+16)+'px';\n const cx=x+w/2,cy=y+h/2,captionWidth=Math.min(176,w/2-24);\n data.quadrants.forEach((t,i)=>{const right=i===0||i===3,upper=i<2,n=label(t,right?x+w-captionWidth-12:x+12,0,captionWidth,right?'right':'left');n.classList.add('quadrant-caption');n.style.setProperty('--q-tone','var(--viz-series-'+(i+1)+')');n.style.top=(upper?y+10:y+h-10-n.offsetHeight)+'px';});\n const points=data.points.map(p=>({x:x+p.x*w,y:y+(1-p.y)*h})),occupied=texts.map(box);pointPositions=points;\n const groups=[...new Set(data.points.map(p=>p.group).filter(Boolean))];\n const color=p=>p.color||('var(--viz-series-'+(p.group?groups.indexOf(p.group)%5+2:1)+')');\n const measures=data.points.map(p=>{const b=document.createElement('button');b.className='quadrant-label';b.type='button';b.textContent=p.name;b.dataset.quadrantPoint=p.id;b.setAttribute('aria-label',p.name+',x '+fmt(p.x)+',y '+fmt(p.y));scene.append(b);return b;});\n data.points.forEach((p,i)=>{\n const pos=points[i],b=measures[i],bw=b.offsetWidth,bh=b.offsetHeight;\n const nearby=points.map((q,j)=>({q,j})).filter(({q})=>Math.hypot(q.x-pos.x,q.y-pos.y)<96);\n const load=nearby.reduce((sum,{j})=>sum+measures[j].offsetWidth*measures[j].offsetHeight,0);\n const dense=nearby.length>=3||nearby.length>=2&&load/(Math.PI*96*96)>.14;\n const candidates=[[pos.x+12,pos.y-bh/2],[pos.x-bw-12,pos.y-bh/2],[pos.x-bw/2,pos.y-bh-14],[pos.x-bw/2,pos.y+14],[pos.x+12,pos.y-bh-14],[pos.x-bw-12,pos.y+14]].map(([a,c])=>({x:Math.max(4,Math.min(width-bw-4,a)),y:Math.max(4,Math.min(scene.offsetHeight-bh-4,c)),w:bw,h:bh}));\n candidates.forEach(c=>{c.overlap=occupied.reduce((sum,r)=>sum+area({x:c.x-4,y:c.y-4,w:c.w+8,h:c.h+8},r),0);c.dotOverlap=points.reduce((sum,q)=>sum+(q.x>=c.x-6&&q.x<=c.x+c.w+6&&q.y>=c.y-6&&q.y<=c.y+c.h+6?1:0),0);});\n candidates.sort((a,b)=>a.dotOverlap-b.dotOverlap||a.overlap-b.overlap);const chosen=candidates[0];\n b.style.left=chosen.x+'px';b.style.top=chosen.y+'px';b.hidden=dense||chosen.overlap>0||chosen.dotOverlap>0;b.dataset.dense=String(dense);labels.push(b);if(!b.hidden)occupied.push(chosen);\n const lx=Math.max(chosen.x,Math.min(chosen.x+bw,pos.x)),ly=Math.max(chosen.y,Math.min(chosen.y+bh,pos.y));const leader=add('line',{x1:pos.x,y1:pos.y,x2:lx,y2:ly,stroke:'var(--border)','stroke-width':1},svg);leader.style.display=b.hidden?'none':'';leaders.push(leader);\n const dot=add('circle',{cx:pos.x,cy:pos.y,r:5,fill:color(p),'data-quadrant-mark':p.id,'data-x':p.x,'data-y':p.y,tabindex:b.hidden?'0':'-1',role:'button','aria-label':b.getAttribute('aria-label')});marks.push(dot);\n dot.onfocus=()=>focus(i);dot.onblur=leave;dot.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();focus(i);}};\n b.onpointerenter=()=>focus(i,false);b.onclick=e=>focus(i,e.detail===0);b.onfocus=()=>focus(i);b.onpointerleave=leave;b.onblur=leave;\n });\n svg.onpointermove=svg.onclick=e=>{const r=svg.getBoundingClientRect(),sx=(e.clientX-r.left)*width/r.width,sy=(e.clientY-r.top)*height/r.height;const near=pointPositions.map((p,i)=>({i,d:Math.hypot(p.x-sx,p.y-sy)})).sort((a,b)=>a.d-b.d);if(!near.length||near[0].d>16){leave();return;}clearTimeout(leaveTimer);const i=near[0].i;if(activePoint===i)return;show(i,e,marks[i],true);};svg.onpointerleave=leave;view.refresh();\n }\n function replay(){cancel();if(reduced.matches||view.expanded)return;marks.forEach((m,i)=>animations.push(animateMermaidEntry(m,[{transform:'scale(0)'},{transform:'scale(1)'}],{duration:240,delay:i*80,fill:'backwards',easing:'cubic-bezier(.25,1,.5,1)'})));marks.forEach(m=>{m.style.transformBox='fill-box';m.style.transformOrigin='center';});labels.forEach((b,i)=>{if(!b.hidden)animations.push(animateMermaidEntry(b,[{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:300,delay:i*80+120,fill:'backwards',easing:'cubic-bezier(.25,1,.5,1)'}));});}\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let width=root.clientWidth,resizeFrame=0;new ResizeObserver(()=>{if(width!==root.clientWidth&&!resizeFrame)resizeFrame=requestAnimationFrame(()=>{resizeFrame=0;if(width!==root.clientWidth){width=root.clientWidth;draw();}});}).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting){armed=true;cancel();clear();}else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else if(armed){const r=root.getBoundingClientRect();if(r.bottom>0&&r.top<innerHeight){armed=false;replay();}}});reduced.addEventListener('change',cancel);viewport.addEventListener('scroll',()=>{if(pending!==null)focus(pending,false);else clear();},{passive:true});document.addEventListener('keydown',e=>{if(e.key==='Escape'){pending=null;clear();}});window.addEventListener('blur',clear);\n}\n\n// 11.12 的 DB 保留网格树,但连线词法信息会丢失;从原解析器回调记录已解析的 token。\nasync function readMermaidBlockModel(mermaid,source,descriptions={}) {\n const seed=await mermaid.mermaidAPI.getDiagramFromText('block-beta\\n__qv_seed__'),db=seed.db;\n if(!db.getBlock||!db.getEdges||!db.edgeStrToEdgeData)return {reason:'Block 数据接口不兼容'};\n const original=db.edgeStrToEdgeData,tokens=[];\n try{db.edgeStrToEdgeData=token=>{tokens.push(String(token).trim());return original(token);};await mermaid.mermaidAPI.getDiagramFromText(source);}finally{db.edgeStrToEdgeData=original;}\n const rawRoot=db.getBlock('root'),rawEdges=db.getEdges(),classes=db.getClasses();\n if(classes?.size||[...db.getBlocksFlat()].some(n=>n.classes?.length||n.styles?.length))return {reason:'Block 自定义样式保留原生语义'};\n const shapes=new Set(['square','na','round','stadium','cylinder','diamond','circle','doublecircle','hexagon','subroutine','lean_left','lean_right','trapezoid','inv_trapezoid','rect_left_inv_arrow','block_arrow']);\n let reason='',nodes=[],groups=[];\n function copy(raw,parent=null,path='root'){\n const type=raw.type==='composite'?'group':raw.type==='space'?'space':raw.type||'na',span=type==='space'?1:raw.widthInColumns??1;\n if(!Number.isInteger(span)||span<1)reason='Block 跨列宽度保留原生语义';\n if(hasRichMermaidLabel(raw.label))reason='Block 富文本保留原生语义';\n const n={id:type==='space'?'space-'+path:raw.id,name:type==='space'?'':raw.label||'',type,span,parent,description:descriptions[raw.id]||'',directions:[...raw.directions||[]]};\n if(type==='group'){\n n.columns=raw.columns??-1;n.children=(raw.children||[]).map((child,i)=>copy(child,raw.id,path+'-'+i));\n if(n.columns===0||!Number.isInteger(n.columns))reason='Block 列数保留原生语义';\n let pos=0;for(const child of n.children){if(n.columns>0&&child.span>n.columns-pos%n.columns)reason='Block 跨行溢出保留原生布局';pos+=child.span;}\n if(raw.id!=='root')groups.push(n);\n }else if(type!=='space'){\n if(!shapes.has(type)||n.directions.some(d=>!['up','down','left','right','x','y'].includes(d)))reason='Block 形状保留原生语义';\n nodes.push(n);\n }\n return n;\n }\n const tree=copy(rawRoot);if(reason)return {reason};\n if(tokens.length!==rawEdges.length)return {reason:'Block 连线接口不兼容'};\n const ids=new Set([...nodes,...groups].map(n=>n.id)),marker=c=>c==='>'||c==='<'?'point':c==='o'?'circle':c==='x'?'cross':null;\n const edges=rawEdges.map((e,i)=>({id:e.id,from:e.start,to:e.end,label:e.label||'',startMarker:marker(tokens[i][0]),endMarker:marker(tokens[i].at(-1)),dotted:tokens[i].includes('.'),thick:tokens[i].includes('='),invisible:tokens[i].includes('~')}));\n if(edges.some(e=>!ids.has(e.from)||!ids.has(e.to)||hasRichMermaidLabel(e.label)))return {reason:'Block 特殊端点或富文本保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!ids.has(id));if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n return {data:{kind:'block',tree,nodes,groups,edges}};\n}\n\n// 保留原网格顺序与跨列;字号、行高和连接路由由 qv 决定。\nfunction layoutMermaidBlocks(data,measure,{cell=160,gap=24,padding=24}={}){\n const boxes=new Map(),rows=[];\n function minimum(n){\n if(n.type==='space')return {w:0,h:24};\n if(n.type!=='group')return {w:cell,h:measure(n,cell)};\n const cols=n.columns>0?n.columns:Math.max(1,n.children.reduce((s,c)=>s+c.span,0));let unit=cell;\n for(const c of n.children){const m=minimum(c);unit=Math.max(unit,(m.w-gap*(c.span-1))/c.span);}\n return {w:cols*unit+(cols-1)*gap+2*padding,h:0};\n }\n function place(n,x,y,width){\n if(n.type!=='group'){const h=n.type==='space'?24:measure(n,width),box={...n,x,y,w:width,h};boxes.set(n.id,box);return h;}\n const cols=n.columns>0?n.columns:Math.max(1,n.children.reduce((s,c)=>s+c.span,0)),unit=(width-2*padding-gap*(cols-1))/cols,head=n.id!=='root'&&n.name?Math.max(32,measure(n,width-2*padding)-12):0;\n let position=0;const grouped=[];\n for(const c of n.children){const col=position%cols,row=Math.floor(position/cols);(grouped[row]||=[]).push({n:c,col});position+=c.span;}\n let rowY=y+padding+head;\n for(let row=0;row<grouped.length;row++){\n let rowH=0;for(const c of grouped[row]||[]){const w=c.n.span*unit+(c.n.span-1)*gap;rowH=Math.max(rowH,place(c.n,x+padding+c.col*(unit+gap),rowY,w));}\n rows.push({parent:n.id,row,y:rowY,height:rowH});rowY+=rowH+gap;\n }\n const h=rowY-y-(grouped.length?gap:0)+padding;boxes.set(n.id,{...n,x,y,w:width,h,columns:cols});return h;\n }\n const width=minimum(data.tree).w,height=place(data.tree,24,24,width);return {boxes,rows,width:width+48,height:height+48};\n}\nfunction routeMermaidBlocks(layout,edges,{scoreRoute=()=>0,onRoute=()=>{}}={}){\n const all=[...layout.boxes.values()],leaves=all.filter(b=>!['space','group'].includes(b.type)),groups=all.filter(b=>b.type==='group'&&b.id!=='root'&&b.name);\n const obstacles=[...leaves.map(b=>({x:b.x-6,y:b.y-6,w:b.w+12,h:b.h+12})),...groups.map(b=>({x:b.x+12,y:b.y+10,w:b.w-24,h:b.captionHeight??24}))];\n const port=(b,side)=>side===0?{x:b.x+b.w/2,y:b.y,ox:b.x+b.w/2,oy:b.y-14}:side===1?{x:b.x+b.w,y:b.y+b.h/2,ox:b.x+b.w+14,oy:b.y+b.h/2}:side===2?{x:b.x+b.w/2,y:b.y+b.h,ox:b.x+b.w/2,oy:b.y+b.h+14}:{x:b.x,y:b.y+b.h/2,ox:b.x-14,oy:b.y+b.h/2};\n const ports=new Map(all.filter(b=>b.type!=='space'&&b.id!=='root').map(b=>[b.id,[0,1,2,3].map(s=>port(b,s))]));\n const xs=[8,layout.width-8],ys=[8,layout.height-8];for(const r of obstacles){xs.push(r.x-8,r.x+r.w+8);ys.push(r.y-8,r.y+r.h+8);}for(const ps of ports.values())for(const p of ps){xs.push(p.ox);ys.push(p.oy);}\n const X=[...new Set(xs)].sort((a,b)=>a-b),Y=[...new Set(ys)].sort((a,b)=>a-b),points=[],index=new Map();\n for(let iy=0;iy<Y.length;iy++)for(let ix=0;ix<X.length;ix++){const x=X[ix],y=Y[iy];if(obstacles.some(r=>x>r.x&&x<r.x+r.w&&y>r.y&&y<r.y+r.h))continue;const id=points.length;points.push({x,y,ix,iy,neighbors:[]});index.set(ix+','+iy,id);}\n const clear=(a,b)=>!obstacles.some(r=>a.x===b.x?a.x>r.x&&a.x<r.x+r.w&&Math.max(a.y,b.y)>r.y&&Math.min(a.y,b.y)<r.y+r.h:a.y>r.y&&a.y<r.y+r.h&&Math.max(a.x,b.x)>r.x&&Math.min(a.x,b.x)<r.x+r.w);\n for(const a of points)for(const [dx,dy]of [[1,0],[-1,0],[0,1],[0,-1]]){const j=index.get((a.ix+dx)+','+(a.iy+dy));if(j!==undefined&&clear(a,points[j]))a.neighbors.push({id:j,dir:dx?0:1,length:Math.abs(a.x-points[j].x)+Math.abs(a.y-points[j].y)});}\n const ids=p=>index.get(X.indexOf(p.ox)+','+Y.indexOf(p.oy));\n function shortest(start,end){\n if(start===undefined||end===undefined)return null;const costs=new Map(),prev=new Map(),heap=[];\n function push(v){let i=heap.length;heap.push(v);while(i){const p=(i-1)>>1;if(heap[p].score<=v.score)break;heap[i]=heap[p];i=p;}heap[i]=v;}\n function pop(){const first=heap[0],last=heap.pop();if(heap.length){let i=0;while(true){let c=i*2+1;if(c>=heap.length)break;if(c+1<heap.length&&heap[c+1].score<heap[c].score)c++;if(last.score<=heap[c].score)break;heap[i]=heap[c];i=c;}heap[i]=last;}return first;}\n const heuristic=i=>Math.abs(points[i].x-points[end].x)+Math.abs(points[i].y-points[end].y);costs.set(start*3+2,0);push({id:start,dir:2,cost:0,score:heuristic(start)});\n while(heap.length){const state=pop(),key=state.id*3+state.dir;if(state.cost!==costs.get(key))continue;if(state.id===end){const path=[];let k=key;while(k!==undefined){path.push(points[Math.floor(k/3)]);k=prev.get(k);}return {points:path.reverse().map(p=>[p.x,p.y]),cost:state.cost};}for(const n of points[state.id].neighbors){const nk=n.id*3+n.dir,cost=state.cost+n.length+(state.dir!==2&&state.dir!==n.dir?12:0);if(cost<(costs.get(nk)??Infinity)){costs.set(nk,cost);prev.set(nk,key);push({id:n.id,dir:n.dir,cost,score:cost+heuristic(n.id)});}}}\n return null;\n }\n const compact=raw=>{const simple=raw.filter((p,i)=>!i||p[0]!==raw[i-1][0]||p[1]!==raw[i-1][1]);return simple.filter((p,i)=>!i||i===simple.length-1||(simple[i-1][0]!==p[0]||p[0]!==simple[i+1][0])&&(simple[i-1][1]!==p[1]||p[1]!==simple[i+1][1]));};\n return edges.map(e=>{let best;const a=ports.get(e.from),b=ports.get(e.to);for(let i=0;i<4;i++)for(let j=0;j<4;j++){if(e.from===e.to&&i===j)continue;const route=shortest(ids(a[i]),ids(b[j]));if(route){const points=compact([[a[i].x,a[i].y],...route.points,[b[j].x,b[j].y]]),score=route.cost+scoreRoute({...e,points});if(Number.isFinite(score)&&(!best||score<best.score))best={...route,points,score,start:a[i],end:b[j]};}}if(!best)throw Error('无法在原网格中连接:'+e.from+' → '+e.to);const result={...e,points:best.points};onRoute(result);return result;});\n}\n\nfunction renderMermaidBlock(root,data){\n const scene=root.querySelector('.scene'),scroll=root.querySelector('.diagram-scroll'),tip=root.querySelector('.node-tip'),svg=root.querySelector('.wires'),reduced=matchMedia('(prefers-reduced-motion:reduce)'),ns='http://www.w3.org/2000/svg';\n root.dataset.state='enhanced';root.dataset.renderer='qv-block';root.querySelector('.edge-key').hidden=true;scroll.setAttribute('aria-label','方块图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} button.node.block-node{font-size:12px;padding:12px 14px;line-height:18px;background:transparent;border:0;transition:opacity 140ms ease}\n #${root.id} button.node.block-node .name{font-size:12px;display:block;white-space:normal;text-align:center;overflow-wrap:anywhere;font-weight:500;line-height:18px}\n #${root.id} .block-shape{position:absolute;inset:0;width:100%;height:100%;fill:var(--background);stroke:var(--border);stroke-width:1;overflow:visible}\n #${root.id} .block-group{position:absolute;border:1px solid var(--border);border-radius:12px;box-sizing:border-box;pointer-events:none}\n #${root.id} .block-group-label{max-width:calc(100% - 24px);white-space:normal;overflow-wrap:anywhere;position:absolute;left:12px;top:10px;font:inherit;font-size:12px;color:var(--muted-foreground);border:0;padding:2px 4px;background:transparent;pointer-events:auto;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .block-wire{pointer-events:stroke;stroke:light-dark(#727a82,#a3aab3);stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .wires{z-index:1} #${root.id} .block-group{z-index:0} #${root.id} button.node{z-index:2} #${root.id} .edge-label{z-index:3}\n #${root.id} .block-edge-label[data-empty=true]{width:32px;height:24px;padding:0;background:transparent}\n #${root.id} .block-edge-label{font:inherit;font-size:12px;line-height:18px;border:0;color:var(--muted-foreground);padding:2px 5px;background:var(--background);cursor:var(--cursor-interaction,pointer);pointer-events:auto;white-space:normal;text-align:center;max-width:200px}\n #${root.id} .node-tip{padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);box-shadow:none;border:1px solid var(--border);line-height:18px;max-width:260px;overflow-wrap:anywhere}\n #${root.id} .block-tip-title{font-weight:500;margin-bottom:4px}\n #${root.id} .block-tip-detail{color:var(--muted-foreground)}\n #${root.id} .block-node[data-active=true] .block-shape{stroke:var(--viz-series-1)}\n #${root.id} .block-node[data-active=true] .block-shape .block-body{fill:color-mix(in srgb,var(--viz-series-1) 6%,var(--background))}\n @media(prefers-reduced-motion:reduce){#${root.id} .block-node{transition:none}}\n `;root.append(style);\n const controller=createMermaidViewport(root),canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');ctx.font='500 12px '+getComputedStyle(root).fontFamily;\n let layout,routes,buttons=new Map(),groupButtons=new Map(),paths=[],labels=[],selected=null,epoch=0,animations=[],headTimers=[],motionEpoch=0,seen=false,exited=true;\n function element(name,attrs,parent){const el=document.createElementNS(ns,name);for(const [k,v]of Object.entries(attrs))el.setAttribute(k,String(v));parent.append(el);return el;}\n function textHeight(name,width){let count=1,used=0;for(const c of name){if(c==='\\n'){count++;used=0;continue;}const w=ctx.measureText(c).width;if(used+w>width&&used){count++;used=0;}used+=w;}return count*18;}\n const inset=n=>['diamond','hexagon','circle','doublecircle','block_arrow','rect_left_inv_arrow'].includes(n.type)?.56:.86;\n function measure(n,w){const text=textHeight(n.name,Math.max(24,w*inset(n)-12));return ['circle','doublecircle'].includes(n.type)?Math.max(w,text+40):n.type==='diamond'?Math.max(100,text*2+40):['hexagon','block_arrow','rect_left_inv_arrow'].includes(n.type)?Math.max(72,text+32):Math.max(48,text+24+(n.type==='cylinder'?20:0));}\n function shape(n,parent){\n const {w,h,type}=n,s=element('svg',{viewBox:`0 0 ${w} ${h}`,class:'block-shape','aria-hidden':'true'},parent),path=d=>element('path',{d,class:'block-body'},s),poly=p=>element('polygon',{points:p.map(a=>a.join(',')).join(' '),class:'block-body'},s);\n if(['circle','doublecircle'].includes(type)){element('ellipse',{cx:w/2,cy:h/2,rx:w/2-1,ry:h/2-1,class:'block-body'},s);if(type==='doublecircle')element('ellipse',{cx:w/2,cy:h/2,rx:w/2-6,ry:h/2-6,fill:'none'},s);}\n else if(type==='diamond')poly([[w/2,1],[w-1,h/2],[w/2,h-1],[1,h/2]]);\n else if(type==='hexagon')poly([[16,1],[w-16,1],[w-1,h/2],[w-16,h-1],[16,h-1],[1,h/2]]);\n else if(type==='cylinder'){path(`M1 10C1 -2 ${w-1} -2 ${w-1} 10V${h-10}C${w-1} ${h+2} 1 ${h+2} 1 ${h-10}Z`);element('path',{d:`M1 10C1 22 ${w-1} 22 ${w-1} 10`,fill:'none'},s);}\n else if(type==='lean_right'||type==='lean_left'){const flip=type==='lean_left';poly(flip?[[1,1],[w-17,1],[w-1,h-1],[17,h-1]]:[[17,1],[w-1,1],[w-17,h-1],[1,h-1]]);}\n else if(type==='trapezoid'||type==='inv_trapezoid')poly(type==='trapezoid'?[[17,1],[w-17,1],[w-1,h-1],[1,h-1]]:[[1,1],[w-1,1],[w-17,h-1],[17,h-1]]);\n else if(type==='block_arrow'||type==='rect_left_inv_arrow'){\n const dirs=new Set(n.directions.flatMap(d=>d==='x'?['left','right']:d==='y'?['up','down']:[d]));if(type==='rect_left_inv_arrow')dirs.add('left');\n const a=Math.min(20,w/5,h/4),l=dirs.has('left')?a:1,r=dirs.has('right')?w-a:w-1,t=dirs.has('up')?a:1,b=dirs.has('down')?h-a:h-1;\n const p=[[l,t]];if(dirs.has('up'))p.push([w/2-a,t],[w/2,1],[w/2+a,t]);p.push([r,t]);if(dirs.has('right'))p.push([r,h/2-a],[w-1,h/2],[r,h/2+a]);p.push([r,b]);if(dirs.has('down'))p.push([w/2+a,b],[w/2,h-1],[w/2-a,b]);p.push([l,b]);if(dirs.has('left'))p.push([l,h/2+a],[1,h/2],[l,h/2-a]);poly(p);\n }else{element('rect',{x:1,y:1,width:w-2,height:h-2,rx:type==='stadium'?h/2:type==='square'||type==='na'||type==='subroutine'?10:14,class:'block-body'},s);if(type==='subroutine')for(const x of [9,w-9])element('path',{d:`M${x} 1V${h-1}`,fill:'none'},s);}\n }\n function rounded(points){let d=`M${points[0].join(' ')}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],p=points[i],b=points[i+1],la=Math.hypot(p[0]-a[0],p[1]-a[1]),lb=Math.hypot(b[0]-p[0],b[1]-p[1]),r=Math.min(7,la/2,lb/2);d+=`L${p[0]+(a[0]-p[0])*r/la} ${p[1]+(a[1]-p[1])*r/la}Q${p.join(' ')} ${p[0]+(b[0]-p[0])*r/lb} ${p[1]+(b[1]-p[1])*r/lb}`;}return d+`L${points.at(-1).join(' ')}`;}\n function heading(id){let b=layout.boxes.get(id),names=[];while(b&&b.id!=='root'){if(b.name)names.unshift(b.name);b=layout.boxes.get(b.parent);}return names.join(' › ');}\n function detail(id,edge){const n=layout.boxes.get(id);const members=[];const collect=b=>{for(const c of b.children||[])if(c.type==='group')collect(c);else if(c.type!=='space')members.push(c.name);};if(n?.type==='group')collect(n);return edge?{name:edge.label||'连接关系',detail:heading(edge.from)+' → '+heading(edge.to)}:{name:n.name,detail:[n.description||heading(n.parent),members.length?'成员:'+members.join('、'):'',...data.edges.filter(e=>e.from===id||e.to===id).map(e=>(e.from===id?'流向:'+heading(e.to):'来自:'+heading(e.from))+(e.label?' · '+e.label:''))].filter(Boolean).join('; ')};}\n function clear(){epoch++;selected=null;tip.hidden=true;for(const el of buttons.values()){el.style.opacity='';delete el.dataset.active;el.removeAttribute('aria-describedby');}for(const p of paths){p.el.style.opacity='';p.el.style.stroke='';}for(const l of labels){l.el.style.opacity='';l.el.removeAttribute('aria-describedby');}for(const el of groupButtons.values())el.removeAttribute('aria-describedby');}\n function choose(id,edge,ensure=false){\n const pending=++epoch;selected={id,edge};if(ensure&&!controller.expanded){const trigger=edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id);if(trigger){const a=trigger.getBoundingClientRect(),v=scroll.getBoundingClientRect();if(a.left<v.left+8)scroll.scrollLeft-=v.left+8-a.left;else if(a.right>v.right-8)scroll.scrollLeft+=a.right-v.right+8;}}\n requestAnimationFrame(()=>requestAnimationFrame(()=>{if(pending!==epoch)return;const linked=new Set(edge?[edge.from,edge.to]:[id]);const descendants=(n)=>{linked.add(n.id);for(const c of n.children||[])descendants(c);};if(!edge&&layout.boxes.get(id)?.type==='group')descendants(layout.boxes.get(id));for(const e of data.edges)if(!edge&&(e.from===id||e.to===id)){linked.add(e.from);linked.add(e.to);}for(const [key,el]of buttons){el.style.opacity=linked.has(key)?'1':'.3';el.dataset.active=String(linked.has(key));}for(const p of paths){const on=edge?p.edge.id===edge.id:linked.has(p.edge.from)&&linked.has(p.edge.to);p.el.style.opacity=on?'1':'.2';p.el.style.stroke=on?'var(--viz-series-1)':'';}if(edge||data.groups.some(g=>g.id===id)||layout.boxes.get(id)?.description)showTip(detail(id,edge),edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id));}));\n }\n function showTip(d,anchor){}\n const stable=createMermaidHover(root,clear);function bind(el,id,edge){stable(el,ensure=>choose(id,edge,ensure));}\n\n function labelLocation(edge,w,h,occupied){\n const candidates=[];for(let i=1;i<edge.points.length;i++){const a=edge.points[i-1],b=edge.points[i],length=Math.hypot(b[0]-a[0],b[1]-a[1]);for(const f of [.5,.25,.75])candidates.push({x:a[0]+(b[0]-a[0])*f,y:a[1]+(b[1]-a[1])*f,length});}\n const rects=[...layout.boxes.values()].filter(b=>!['group','space'].includes(b.type)).map(b=>({x:b.x-4,y:b.y-4,w:b.w+8,h:b.h+8})).concat(occupied);\n const overlap=p=>rects.reduce((s,r)=>s+Math.max(0,Math.min(p.x+w/2,r.x+r.w)-Math.max(p.x-w/2,r.x))*Math.max(0,Math.min(p.y+h/2,r.y+r.h)-Math.max(p.y-h/2,r.y)),0);\n candidates.sort((a,b)=>overlap(a)-overlap(b)||b.length-a.length);return candidates[0];\n }\n function finishMotion(){motionEpoch++;for(const t of headTimers)clearTimeout(t);headTimers=[];for(const a of animations)a.cancel();animations=[];for(const p of paths)p.el.style.removeProperty('marker-end');root.dataset.motion='complete';}\n function replay(){finishMotion();if(reduced.matches||controller.expanded)return;root.dataset.motion='playing';const playEpoch=motionEpoch;for(const [i,el]of [...buttons.values()].entries())animations.push(animateMermaidEntry(el,[{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:240,delay:i*30,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));for(const [i,p]of paths.entries()){const length=p.el.getTotalLength(),delay=120+i*35;if(!p.edge.dotted)animations.push(animateMermaidEntry(p.el,[{strokeDasharray:length,strokeDashoffset:length},{strokeDasharray:length,strokeDashoffset:0}],{duration:650,delay,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));p.el.style.setProperty('marker-end','none');headTimers.push(setTimeout(()=>{if(playEpoch===motionEpoch)p.el.style.removeProperty('marker-end');},delay+650));}for(const l of labels)animations.push(animateMermaidEntry(l.el,[{opacity:0},{opacity:1}],{duration:240,delay:180,fill:'backwards'}));Promise.allSettled(animations.map(a=>a.finished)).then(()=>{if(playEpoch===motionEpoch&&root.dataset.motion==='playing')finishMotion();});}\n function draw(){\n clear();finishMotion();buttons=new Map();groupButtons=new Map();labels=[];paths=[];scene.replaceChildren(svg);svg.replaceChildren();\n layout=layoutMermaidBlocks(data,measure,{cell:160,gap:24,padding:24});routes=routeMermaidBlocks(layout,data.edges);scene.style.width=layout.width+'px';scene.style.minWidth=layout.width+'px';scene.style.height=layout.height+'px';svg.setAttribute('viewBox',`0 0 ${layout.width} ${layout.height}`);svg.style.height=layout.height+'px';\n for(const n of [...layout.boxes.values()].filter(n=>n.type==='group'&&n.id!=='root').sort((a,b)=>b.w*b.h-a.w*a.h)){\n const group=document.createElement('div');group.className='block-group';group.dataset.group=n.id;Object.assign(group.style,{left:n.x+'px',top:n.y+'px',width:n.w+'px',height:n.h+'px'});group.style.background=mermaidContainmentSurface(data,n.id);scene.append(group);if(n.name){const label=document.createElement('button');label.type='button';label.className='block-group-label cursor-interaction';label.textContent=n.name;group.append(label);groupButtons.set(n.id,label);bind(label,n.id);}\n }\n const defs=element('defs',{},svg);for(const marker of ['point','circle','cross']){const el=element('marker',{id:root.id+'-block-'+marker,viewBox:'0 0 6 6',refX:5,refY:3,markerWidth:6,markerHeight:6,orient:'auto-start-reverse',markerUnits:'userSpaceOnUse'},defs);if(marker==='point')element('path',{d:'M0 .8L5 3L0 5.2Z',fill:'context-stroke'},el);else if(marker==='circle')element('circle',{cx:3,cy:3,r:2,fill:'var(--background)',stroke:'context-stroke','stroke-width':1},el);else element('path',{d:'M1 1L5 5M5 1L1 5',fill:'none',stroke:'context-stroke','stroke-width':1},el);}\n for(const edge of routes){if(edge.invisible)continue;const el=element('path',{d:rounded(edge.points),class:'wire block-wire','data-edge':edge.id,'stroke-width':edge.thick?2:1},svg);if(edge.dotted)el.setAttribute('stroke-dasharray','4 4');for(const [side,marker]of [['start',edge.startMarker],['end',edge.endMarker]])if(marker)el.setAttribute('marker-'+side,`url(#${root.id}-block-${marker})`);el.style.strokeWidth=edge.thick?'2':'1';paths.push({el,edge});}\n for(const n of data.nodes){const box=layout.boxes.get(n.id),button=document.createElement('button');button.type='button';button.className='node block-node cursor-interaction';button.dataset.node=n.id;button.dataset.shape=n.type;button.setAttribute('aria-label',heading(n.id));Object.assign(button.style,{left:box.x+'px',top:box.y+'px',width:box.w+'px',height:box.h+'px'});shape(box,button);const name=document.createElement('span');name.className='name';name.textContent=n.name;name.style.width=Math.max(24,box.w*inset(n)-12)+'px';button.append(name);scene.append(button);buttons.set(n.id,button);bind(button,n.id);}\n const occupied=[];for(const edge of routes){if(edge.invisible)continue;const el=document.createElement('button');el.type='button';el.className='edge-label block-edge-label cursor-interaction';el.textContent=edge.label;el.dataset.empty=String(!edge.label);el.setAttribute('aria-label',heading(edge.from)+' → '+heading(edge.to)+(edge.label?' · '+edge.label:''));el.dataset.edge=edge.id;scene.append(el);const w=el.offsetWidth,h=el.offsetHeight,p=labelLocation(edge,w,h,occupied);el.style.left=p.x+'px';el.style.top=p.y+'px';occupied.push({x:p.x-w/2-4,y:p.y-h/2-4,w:w+8,h:h+8});labels.push({el,edge});bind(el,edge.from,edge);}\n controller.refresh();if(seen&&!controller.expanded&&!reduced.matches)root.dataset.motion='complete';\n }\n root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});scroll.addEventListener('scroll',()=>{if(selected){const {id,edge}=selected;choose(id,edge);}});root.addEventListener('qv:display-mode',()=>{draw();if(!controller.expanded&&controller.savedInlinePosition)scroll.scrollLeft=controller.savedInlinePosition.left;controller.refresh();});reduced.addEventListener('change',finishMotion);\n let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(root.clientWidth!==width){width=root.clientWidth;draw();}})).observe(root);\n new IntersectionObserver(entries=>{const e=entries[0];if(e.intersectionRatio===0){exited=true;finishMotion();}else if(e.intersectionRatio>=.2&&exited){exited=false;seen=true;replay();}},{threshold:[0,.2]}).observe(root);draw();\n}\n\n// 从原解析器记录声明顺序;不建立另一套 C4 语法或关系图。\nasync function readMermaidC4Model(mermaid,source,descriptions={}) {\n const seed=await mermaid.mermaidAPI.getDiagramFromText('C4Context\\nPerson(__qv_seed__, \"seed\")'),db=seed.db;\n const methods=['addPersonOrSystem','addContainer','addComponent','addPersonOrSystemBoundary','addContainerBoundary','addDeploymentNode','updateElStyle','updateRelStyle'];\n if(methods.some(key=>typeof db[key]!=='function')||!db.getC4ShapeArray||!db.getBoundaries||!db.getRels)return {reason:'C4 数据接口不兼容'};\n const originals=new Map(),order=new Map();let customStyle=false,optionalFeatures=false,sequence=0;\n try {\n for(const key of methods){\n const original=db[key];originals.set(key,original);\n db[key]=function(...args){\n if(key==='updateElStyle'||key==='updateRelStyle')customStyle=true;\n else {const boundary=key==='addPersonOrSystemBoundary'||key==='addContainerBoundary',id=args[boundary?0:1];if(!order.has(id))order.set(id,sequence++);const optionalStart=boundary?3:key==='addPersonOrSystem'?4:5;if(args.slice(optionalStart).some(value=>value&&typeof value!=='object'||value&&typeof value==='object'&&Object.values(value).some(Boolean)))optionalFeatures=true;}\n return original.apply(this,args);\n };\n }\n await mermaid.mermaidAPI.getDiagramFromText(source);\n } finally {for(const [key,original]of originals)db[key]=original;}\n if(customStyle)return {reason:'C4 自定义节点或关系样式保留原生语义'};\n if(optionalFeatures)return {reason:'C4 图标、标签或链接保留原生语义'};\n const text=value=>String(value?.text??value??''),types=new Set(['person','external_person',...['system','container','component'].flatMap(base=>[base,base+'_db',base+'_queue','external_'+base,'external_'+base+'_db','external_'+base+'_queue'])]);\n const family=db.getC4Type(),shapeInRow=db.getC4ShapeInRow(),boundaryInRow=db.getC4BoundaryInRow();\n if(!['C4Context','C4Container','C4Component','C4Dynamic','C4Deployment'].includes(family))return {reason:'C4 图表类型保留原生语义'};\n if(!Number.isInteger(shapeInRow)||shapeInRow<1||!Number.isInteger(boundaryInRow)||boundaryInRow<1)return {reason:'C4 行布局保留原生语义'};\n const rawNodes=db.getC4ShapeArray(),rawGroups=db.getBoundaries().filter(n=>n.alias!=='global'),rawEdges=db.getRels();\n if([...rawNodes,...rawGroups,...rawEdges].some(n=>n.sprite||n.tags||n.link))return {reason:'C4 图标、标签或链接保留原生语义'};\n if([...rawNodes,...rawGroups,...rawEdges].some(n=>['label','techn','descr'].some(key=>hasRichMermaidLabel(text(n[key])))))return {reason:'C4 富文本保留原生语义'};\n const nodes=rawNodes.map(n=>({id:n.alias,name:text(n.label),c4Type:text(n.typeC4Shape),technology:text(n.techn),description:text(n.descr),note:descriptions[n.alias]||'',parent:n.parentBoundary==='global'?null:n.parentBoundary,order:order.get(n.alias)}));\n if(nodes.some(n=>!types.has(n.c4Type)))return {reason:'C4 特殊节点类型保留原生语义'};\n const groups=rawGroups.map(n=>({id:n.alias,name:text(n.label),c4Type:text(n.type),nodeType:n.nodeType||'',description:text(n.descr),note:descriptions[n.alias]||'',parent:n.parentBoundary==='global'?null:n.parentBoundary,order:order.get(n.alias)}));\n const ids=new Set([...nodes,...groups].map(n=>n.id)),groupIds=new Set(groups.map(n=>n.id));\n if(ids.size!==nodes.length+groups.length||[...nodes,...groups].some(n=>n.parent&&!groupIds.has(n.parent)))return {reason:'C4 边界结构保留原生语义'};\n const relationTypes=new Set(['rel','birel','rel_b','rel_u','rel_d','rel_l','rel_r']);\n const nodeIds=new Set(nodes.map(n=>n.id));\n if(rawEdges.some(e=>!relationTypes.has(e.type)||!nodeIds.has(e.from)||!nodeIds.has(e.to)))return {reason:'C4 特殊关系端点保留原生语义'};\n const edges=rawEdges.map((e,i)=>({id:'relation-'+(i+1),from:e.type==='rel_b'?e.to:e.from,to:e.type==='rel_b'?e.from:e.to,sourceFrom:e.from,sourceTo:e.to,label:text(e.label),technology:text(e.techn),description:text(e.descr),relationType:e.type,sequence:family==='C4Dynamic'?i+1:null,startMarker:e.type==='birel'?'point':null,endMarker:'point'}));\n const unknown=Object.keys(descriptions).filter(id=>!ids.has(id));if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n const tree={id:'global',name:'',type:'group',children:[]},byId=new Map([['global',tree],...groups.map(n=>[n.id,{...n,type:'group',children:[]}])]);\n for(const n of [...groups,...nodes].sort((a,b)=>a.order-b.order))byId.get(n.parent||'global').children.push(byId.get(n.id)||{...n,type:'node'});\n const visiting=new Set(),visited=new Set();function check(n){if(visiting.has(n.id))return false;if(visited.has(n.id))return true;visiting.add(n.id);for(const child of n.children||[])if(!check(child))return false;visiting.delete(n.id);visited.add(n.id);return true;}\n if(!check(tree)||visited.size!==ids.size+1)return {reason:'C4 循环边界保留原生语义'};\n return {data:{kind:'c4',family,title:db.getTitle(),nodes,groups,edges,tree,shapeInRow,boundaryInRow,accessibility:{title:db.getAccTitle?.()||'',description:db.getAccDescription?.()||''}}};\n}\n\n// 边界内按原生节点/边界声明顺序换行;外部参与者形成独立阅读列,不新建语义分组。\nfunction layoutMermaidC4(data,measure,{cell=200,gap=40,padding=32}={}) {\n const boxes=new Map();\n function rows(children,limit){const result=[];let row=[],width=0,shapes=0,boundaries=0;for(const child of children){const group=child.type==='group';if(row.length&&(width+gap+child.w>limit||!group&&shapes>=data.shapeInRow||group&&boundaries>=data.boundaryInRow)){result.push(row);row=[];width=0;shapes=0;boundaries=0;}width+=child.w+(row.length?gap:0);row.push(child);if(group)boundaries++;else shapes++;}if(row.length)result.push(row);return result;}\n const rowWidth=row=>row.reduce((s,c)=>s+c.w,0)+gap*Math.max(0,row.length-1),rowsHeight=rows=>rows.reduce((s,row)=>s+Math.max(...row.map(c=>c.h)),0)+gap*Math.max(0,rows.length-1);\n function pack(n){\n if(n.type!=='group')return {...n,w:cell,h:measure(n,cell),captionHeight:0};\n const children=n.children.map(pack),leaves=children.filter(c=>c.type!=='group'),groups=children.filter(c=>c.type==='group');\n const captionHeight=n.id==='global'?0:measure({...n,caption:true},cell*2+gap)+12;\n if(n.id==='global'&&leaves.length&&groups.length){const mainRows=rows(groups,1280),sideHeight=leaves.reduce((s,c)=>s+c.h,0)+gap*Math.max(0,leaves.length-1),mainWidth=Math.max(...mainRows.map(rowWidth));return {...n,children,sideNodes:leaves,rows:mainRows,w:cell+gap+mainWidth+padding*2,h:Math.max(sideHeight,rowsHeight(mainRows))+padding*2,captionHeight};}\n const limit=String(n.c4Type).toLowerCase()==='container'?cell*2+gap:1280;\n const packedRows=rows([...leaves,...groups],limit);\n return {...n,children,rows:packedRows,w:Math.max(cell,...packedRows.map(rowWidth))+padding*2,h:padding*2+captionHeight+rowsHeight(packedRows),captionHeight};\n }\n function place(n,x,y){\n boxes.set(n.id,{...n,x,y});let top=y+padding+n.captionHeight,leftInset=x+padding;\n if(n.sideNodes){let sideY=top;for(const child of n.sideNodes){place(child,leftInset,sideY);sideY+=child.h+gap;}leftInset+=cell+gap;}\n for(const row of n.rows||[]){let left=leftInset;const height=Math.max(...row.map(c=>c.h));for(const child of row){place(child,left,top);left+=child.w+gap;}top+=height+gap;}\n }\n const tree=pack(data.tree);place(tree,24,24);return {boxes,width:tree.w+48,height:tree.h+48};\n}\n\nfunction renderMermaidC4(root,data){\n const scene=root.querySelector('.scene'),scroll=root.querySelector('.diagram-scroll'),tip=root.querySelector('.node-tip'),svg=root.querySelector('.wires'),reduced=matchMedia('(prefers-reduced-motion:reduce)'),ns='http://www.w3.org/2000/svg';\n root.dataset.state='enhanced';root.dataset.renderer='qv-c4';root.querySelector('.edge-key').hidden=true;scroll.setAttribute('aria-label','C4 架构图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} button.node.c4-node{font-size:12px;padding:20px;line-height:18px;background:var(--background);border:1px solid var(--border);align-items:flex-start;justify-content:flex-start;flex-direction:column;transition:opacity 140ms ease}\n #${root.id} button.node.c4-node .name{font-size:14px;display:block;white-space:normal;text-align:left;overflow-wrap:anywhere;font-weight:600;line-height:20px;margin-top:8px}\n #${root.id} .c4-type{display:flex;align-items:center;gap:6px;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));font-size:12px;line-height:18px}\n #${root.id} .c4-type svg{width:14px;height:14px;flex:none}\n #${root.id} .c4-info{display:flex;flex-direction:column;gap:12px;width:100%;box-sizing:border-box;padding:12px;margin-top:16px;border-radius:8px;background:light-dark(#f5f5f5,#25272b)}\n #${root.id} .c4-field{display:flex;flex-direction:column;gap:4px;width:100%;text-align:left}\n #${root.id} .c4-field-label{font-size:11px;line-height:16px;color:var(--muted-foreground)}\n #${root.id} .c4-technology{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:18px;overflow-wrap:anywhere;color:light-dark(#187f81,#6ec9c9)}\n #${root.id} .c4-description{font-size:12px;line-height:18px;overflow-wrap:anywhere;color:var(--foreground)}\n #${root.id} .c4-group{position:absolute;border:1px solid var(--border);border-radius:12px;box-sizing:border-box;pointer-events:none}\n #${root.id} .c4-group-label{max-width:calc(100% - 24px);white-space:normal;overflow-wrap:anywhere;position:absolute;left:12px;top:10px;font:inherit;font-size:12px;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));border:0;padding:2px 4px;background:transparent;pointer-events:auto;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .c4-wire{pointer-events:stroke;stroke:light-dark(#727a82,#a3aab3);stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .wires{z-index:1} #${root.id} .c4-group{z-index:0} #${root.id} button.node{z-index:2} #${root.id} .edge-label{z-index:3}\n #${root.id} .c4-edge-label[data-empty=true]{width:32px;height:24px;padding:0;background:transparent}\n #${root.id} .c4-edge-label{font:inherit;font-size:12px;line-height:18px;border:0;color:light-dark(color-mix(in srgb,var(--foreground) 60%,var(--background)),color-mix(in srgb,var(--foreground) 68%,var(--background)));padding:2px 5px;background:transparent;cursor:var(--cursor-interaction,pointer);pointer-events:auto;white-space:normal;text-align:center;max-width:200px}\n #${root.id} .node-tip{padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);box-shadow:none;border:1px solid var(--border);line-height:18px;max-width:260px;overflow-wrap:anywhere}\n #${root.id} .c4-tip-title{font-size:14px;line-height:20px;font-weight:600;margin-bottom:12px}\n #${root.id} .c4-tip-section{margin-top:12px;display:flex;flex-direction:column;gap:6px}\n #${root.id} .c4-tip-row{display:grid;grid-template-columns:48px minmax(0,1fr);gap:8px}\n #${root.id} .c4-tip-value{color:var(--muted-foreground)}\n #${root.id} button.c4-node[data-active=true]{border-color:var(--viz-series-1);background:var(--background)}\n @media(prefers-reduced-motion:reduce){#${root.id} .c4-node{transition:none}}\n `;root.append(style);\n const controller=createMermaidViewport(root),canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');ctx.font='500 12px '+getComputedStyle(root).fontFamily;\n let layout,routes,labelMetrics=new Map(),buttons=new Map(),groupButtons=new Map(),paths=[],labels=[],selected=null,epoch=0,animations=[],headTimers=[],motionEpoch=0,seen=false,exited=true;\n function element(name,attrs,parent){const el=document.createElementNS(ns,name);for(const [k,v]of Object.entries(attrs))el.setAttribute(k,String(v));parent.append(el);return el;}\n function textHeight(name,width,fontSize=12,lineHeight=18){const previous=ctx.font;ctx.font='500 '+fontSize+'px '+getComputedStyle(root).fontFamily;let count=1,used=0;for(const c of name){if(c==='\\n'){count++;used=0;continue;}const w=ctx.measureText(c).width;if(used+w>width&&used){count++;used=0;}used+=w;}ctx.font=previous;return count*lineHeight;}\n const typeName=n=>{if(n.type==='group')return n.nodeType?'部署节点'+(n.c4Type?' · '+n.c4Type:''):({enterprise:'企业边界',container:'容器边界',system:'系统边界'}[String(n.c4Type).toLowerCase()]||'边界 · '+n.c4Type);const t=n.c4Type||'',base=t.replace('external_','').replace(/_db|_queue/g,'');return (t.startsWith('external_')?'外部 · ':'')+({person:'角色',system:'系统',container:'容器',component:'组件'}[base]||'部署节点')+(t.endsWith('_db')?' · 数据库':t.endsWith('_queue')?' · 队列':'');};\n function measure(n,w){if(n.caption)return textHeight(n.name+' · '+typeName(n),w-24)+32;const probe=document.createElement('button');probe.className='node c4-node';probe.style.cssText=`width:${w}px;height:auto;visibility:hidden`;contents(n,probe);scene.append(probe);const height=probe.offsetHeight;probe.remove();return height;}\n function contents(n,button){shape(n,button);const name=document.createElement('span');name.className='name';name.textContent=n.name;button.append(name);const info=document.createElement('span');info.className='c4-info';for(const [key,caption,cls]of [['technology','技术栈','c4-technology'],['description','职责','c4-description']])if(n[key]){const field=document.createElement('span');field.className='c4-field';const label=document.createElement('span');label.className='c4-field-label';label.textContent=caption;const span=document.createElement('span');span.className=cls;span.textContent=n[key];field.append(label,span);info.append(field);}if(info.childElementCount)button.append(info);}\n function shape(n,parent){const type=document.createElement('span');type.className='c4-type';const icon=document.createElement('i');icon.dataset.lucide=n.c4Type.includes('_db')?'database':n.c4Type.includes('_queue')?'list-tree':n.c4Type.includes('person')?'user-round':n.c4Type.includes('component')?'component':n.c4Type.includes('container')?'box':'server';icon.setAttribute('aria-hidden','true');type.append(icon,document.createTextNode(typeName(n)));parent.append(type);}\n function rounded(points){let d=`M${points[0].join(' ')}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],p=points[i],b=points[i+1],la=Math.hypot(p[0]-a[0],p[1]-a[1]),lb=Math.hypot(b[0]-p[0],b[1]-p[1]),r=Math.min(7,la/2,lb/2);d+=`L${p[0]+(a[0]-p[0])*r/la} ${p[1]+(a[1]-p[1])*r/la}Q${p.join(' ')} ${p[0]+(b[0]-p[0])*r/lb} ${p[1]+(b[1]-p[1])*r/lb}`;}return d+`L${points.at(-1).join(' ')}`;}\n function heading(id){let b=layout.boxes.get(id),names=[];while(b&&b.id!=='global'){if(b.name)names.unshift(b.name);b=layout.boxes.get(b.parent);}return names.join(' › ');}\n function detail(id,edge){\n const n=layout.boxes.get(id),rows=[],connections=[];\n if(edge){rows.push(['来源',heading(edge.from)],['目标',heading(edge.to)]);if(edge.technology)rows.push(['协议',edge.technology]);if(edge.description)rows.push(['说明',edge.description]);return {name:(edge.sequence?edge.sequence+': ':'')+(edge.label||'连接关系'),rows,connections};}\n rows.push(['类型',typeName(n)]);for(const [label,key]of [['技术','technology'],['说明','description'],['备注','note']])if(n[key])rows.push([label,n[key]]);if(heading(n.parent))rows.push(['所属',heading(n.parent)]);\n for(const e of data.edges.filter(e=>e.from===id||e.to===id))connections.push([(e.from===id?'流向':'来自'),heading(e.from===id?e.to:e.from)+(e.label?'\\n'+e.label:'')+(e.technology?' · '+e.technology:'')]);\n if(n.type==='group'){const collect=b=>{for(const c of b.children||[])if(c.type==='group')collect(c);else if(c.type!=='space')connections.push(['成员',c.name]);};collect(n);}\n return {name:n.name,rows,connections};\n }\n function clear(){epoch++;selected=null;tip.hidden=true;for(const el of buttons.values()){el.style.opacity='';delete el.dataset.active;el.removeAttribute('aria-describedby');}for(const p of paths){p.el.style.opacity='';p.el.style.stroke='';}for(const l of labels){l.el.style.opacity='';l.el.removeAttribute('aria-describedby');}for(const el of groupButtons.values())el.removeAttribute('aria-describedby');}\n function choose(id,edge,ensure=false){\n const pending=++epoch;selected={id,edge};if(ensure&&!controller.expanded){const trigger=edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id);if(trigger){const a=trigger.getBoundingClientRect(),v=scroll.getBoundingClientRect();if(a.left<v.left+8)scroll.scrollLeft-=v.left+8-a.left;else if(a.right>v.right-8)scroll.scrollLeft+=a.right-v.right+8;}}\n requestAnimationFrame(()=>requestAnimationFrame(()=>{if(pending!==epoch)return;const linked=new Set(edge?[edge.from,edge.to]:[id]);const descendants=(n)=>{linked.add(n.id);for(const c of n.children||[])descendants(c);};if(!edge&&layout.boxes.get(id)?.type==='group')descendants(layout.boxes.get(id));for(const e of data.edges)if(!edge&&(e.from===id||e.to===id)){linked.add(e.from);linked.add(e.to);}for(const [key,el]of buttons){el.style.opacity=linked.has(key)?'1':'.3';el.dataset.active=String(linked.has(key));}for(const p of paths){const on=edge?p.edge.id===edge.id:linked.has(p.edge.from)&&linked.has(p.edge.to);p.el.style.opacity=on?'1':'.2';p.el.style.stroke=on?'var(--viz-series-1)':'';}if(edge||data.groups.some(g=>g.id===id)||layout.boxes.get(id)?.description)showTip(detail(id,edge),edge?labels.find(l=>l.edge.id===edge.id)?.el:buttons.get(id)||groupButtons.get(id));}));\n }\n function showTip(d,anchor){}\n const stable=createMermaidHover(root,clear);function bind(el,id,edge){stable(el,ensure=>choose(id,edge,ensure));}\n\n const relationText=edge=>(edge.sequence?edge.sequence+': ':'')+edge.label+(edge.technology?' · '+edge.technology:'');\n const relationSize=edge=>labelMetrics.get(edge.id);\n function labelLocation(edge,w,h,occupied=[]){\n const candidates=[];for(let i=1;i<edge.points.length;i++){const a=edge.points[i-1],b=edge.points[i],length=Math.hypot(b[0]-a[0],b[1]-a[1]);for(const f of [.5,.25,.75])for(const offset of [-(a[0]===b[0]?w:h)/2-8,(a[0]===b[0]?w:h)/2+8])candidates.push({x:a[0]+(b[0]-a[0])*f+(a[0]===b[0]?offset:0),y:a[1]+(b[1]-a[1])*f+(a[1]===b[1]?offset:0),length,offset});}\n const rects=[...layout.boxes.values()].filter(b=>!['group','space'].includes(b.type)).map(b=>({x:b.x-4,y:b.y-4,w:b.w+8,h:b.h+8})).concat([...groupButtons].map(([id,el])=>{const b=layout.boxes.get(id);return {x:b.x+el.offsetLeft-2,y:b.y+el.offsetTop-2,w:el.offsetWidth+4,h:el.offsetHeight+4};}),occupied);\n const overlap=p=>p.x-w/2<8||p.x+w/2>layout.width-8||p.y-h/2<8||p.y+h/2>layout.height-8||rects.some(r=>Math.min(p.x+w/2,r.x+r.w)>Math.max(p.x-w/2,r.x)&&Math.min(p.y+h/2,r.y+r.h)>Math.max(p.y-h/2,r.y));\n return candidates.filter(p=>!overlap(p)).sort((a,b)=>Math.abs(a.offset)-Math.abs(b.offset)||b.length-a.length)[0];\n }\n function finishMotion(){motionEpoch++;for(const t of headTimers)clearTimeout(t);headTimers=[];for(const a of animations)a.cancel();animations=[];for(const p of paths)for(const side of ['start','end'])p.el.style.removeProperty('marker-'+side);root.dataset.motion='complete';}\n function replay(){finishMotion();if(reduced.matches||controller.expanded)return;root.dataset.motion='playing';const playEpoch=motionEpoch;for(const [i,el]of [...buttons.values()].entries())animations.push(animateMermaidEntry(el,[{opacity:0,transform:'translateY(4px)'},{opacity:1,transform:'translateY(0)'}],{duration:240,delay:i*30,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));for(const [i,p]of paths.entries()){const length=p.el.getTotalLength(),delay=120+i*35;if(!p.edge.dotted)animations.push(animateMermaidEntry(p.el,[{strokeDasharray:length,strokeDashoffset:length},{strokeDasharray:length,strokeDashoffset:0}],{duration:650,delay,easing:'cubic-bezier(.22,.7,.2,1)',fill:'backwards'}));for(const side of ['start','end'])p.el.style.setProperty('marker-'+side,'none');headTimers.push(setTimeout(()=>{if(playEpoch===motionEpoch)for(const side of ['start','end'])p.el.style.removeProperty('marker-'+side);},delay+650));}for(const l of labels)animations.push(animateMermaidEntry(l.el,[{opacity:0},{opacity:1}],{duration:240,delay:180,fill:'backwards'}));Promise.allSettled(animations.map(a=>a.finished)).then(()=>{if(playEpoch===motionEpoch&&root.dataset.motion==='playing')finishMotion();});}\n function draw(){\n clear();finishMotion();buttons=new Map();groupButtons=new Map();labels=[];paths=[];scene.replaceChildren(svg);svg.replaceChildren();\n labelMetrics=new Map();for(const e of data.edges){const el=document.createElement('button');el.className='edge-label c4-edge-label';el.textContent=relationText(e);el.dataset.empty=String(!el.textContent);const w=el.textContent?Math.min(180,Math.ceil(ctx.measureText(el.textContent).width)+12):32;el.style.width=w+'px';el.style.boxSizing='border-box';el.style.visibility='hidden';scene.append(el);labelMetrics.set(e.id,{w:el.offsetWidth,h:el.offsetHeight});el.remove();}\n layout=layoutMermaidC4(data,measure,{cell:280,gap:72,padding:40});scene.style.width=layout.width+'px';scene.style.minWidth=layout.width+'px';scene.style.height=layout.height+'px';svg.setAttribute('viewBox',`0 0 ${layout.width} ${layout.height}`);svg.style.height=layout.height+'px';\n for(const n of [...layout.boxes.values()].filter(n=>n.type==='group'&&n.id!=='global').sort((a,b)=>b.w*b.h-a.w*a.h)){\n const group=document.createElement('div');group.className='c4-group';group.dataset.group=n.id;Object.assign(group.style,{left:n.x+'px',top:n.y+'px',width:n.w+'px',height:n.h+'px'});group.style.background=mermaidContainmentSurface(data,n.id);scene.append(group);if(n.name){const label=document.createElement('button');label.type='button';label.className='c4-group-label cursor-interaction';label.textContent=n.name+' · '+typeName(n);group.append(label);groupButtons.set(n.id,label);bind(label,n.id);}\n }\n for(const n of data.nodes){const box=layout.boxes.get(n.id),button=document.createElement('button');button.type='button';button.className='node c4-node cursor-interaction';button.dataset.node=n.id;button.dataset.shape=n.c4Type;button.setAttribute('aria-label',heading(n.id));Object.assign(button.style,{left:box.x+'px',top:box.y+'px',width:box.w+'px',height:box.h+'px'});contents(box,button);scene.append(button);buttons.set(n.id,button);bind(button,n.id);}\n const reserved=[];routes=routeMermaidBlocks(layout,data.edges,{scoreRoute:e=>{const {w,h}=relationSize(e);return labelLocation(e,w,h,reserved)?0:Infinity;},onRoute:e=>{const {w,h}=relationSize(e),p=labelLocation(e,w,h,reserved);if(!p)throw Error('C4 关系标签没有可用位置:'+e.from+' → '+e.to);e.labelPosition=p;reserved.push({x:p.x-w/2-4,y:p.y-h/2-4,w:w+8,h:h+8});}});\n const defs=element('defs',{},svg);for(const marker of ['point']){const el=element('marker',{id:root.id+'-c4-'+marker,viewBox:'0 0 6 6',refX:5,refY:3,markerWidth:6,markerHeight:6,orient:'auto-start-reverse',markerUnits:'userSpaceOnUse'},defs);if(marker==='point')element('path',{d:'M0 .8L5 3L0 5.2Z',fill:'context-stroke'},el);else if(marker==='circle')element('circle',{cx:3,cy:3,r:2,fill:'var(--background)',stroke:'context-stroke','stroke-width':1},el);else element('path',{d:'M1 1L5 5M5 1L1 5',fill:'none',stroke:'context-stroke','stroke-width':1},el);}\n for(const edge of routes){if(edge.invisible)continue;const el=element('path',{d:rounded(edge.points),class:'wire c4-wire','data-edge':edge.id,'stroke-width':edge.thick?2:1},svg);if(edge.dotted)el.setAttribute('stroke-dasharray','4 4');for(const [side,marker]of [['start',edge.startMarker],['end',edge.endMarker]])if(marker)el.setAttribute('marker-'+side,`url(#${root.id}-c4-${marker})`);el.style.strokeWidth=edge.thick?'2':'1';paths.push({el,edge});}\n const occupied=[];for(const edge of routes){if(edge.invisible)continue;const el=document.createElement('button');el.type='button';el.className='edge-label c4-edge-label cursor-interaction';el.textContent=relationText(edge);el.dataset.empty=String(!el.textContent);el.setAttribute('aria-label',heading(edge.from)+' → '+heading(edge.to)+(edge.label?' · '+edge.label:''));el.dataset.edge=edge.id;scene.append(el);const {w,h}=relationSize(edge);el.style.width=w+'px';el.style.boxSizing='border-box';const p=edge.labelPosition;if(!p)throw Error('C4 关系标签没有可用位置:'+e.from+' → '+e.to);el.style.left=p.x+'px';el.style.top=p.y+'px';occupied.push({x:p.x-w/2-4,y:p.y-h/2-4,w:w+8,h:h+8});labels.push({el,edge});bind(el,edge.from,edge);}\n window.lucide?.createIcons({attrs:{width:14,height:14}});controller.refresh();if(seen&&!controller.expanded&&!reduced.matches)root.dataset.motion='complete';\n }\n root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});scroll.addEventListener('scroll',()=>{if(selected){const {id,edge}=selected;choose(id,edge);}});root.addEventListener('qv:display-mode',()=>{draw();if(!controller.expanded&&controller.savedInlinePosition)scroll.scrollLeft=controller.savedInlinePosition.left;controller.refresh();});reduced.addEventListener('change',finishMotion);\n let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(root.clientWidth!==width){width=root.clientWidth;draw();}})).observe(root);\n new IntersectionObserver(entries=>{const e=entries[0];if(e.intersectionRatio===0){exited=true;finishMotion();}else if(e.intersectionRatio>=.2&&exited){exited=false;seen=true;replay();}},{threshold:[0,.2]}).observe(root);draw();\n}\n\n// 固定版本 DB 是唯一数据来源;不另写 CSV 解析器。\nasync function readMermaidSankeyModel(db,descriptions={}) {\n if(!db.getGraph||!db.getConfig)return {reason:'桑基数据接口不兼容'};\n const graph=structuredClone(db.getGraph()),options=structuredClone(db.getConfig());\n if(!graph.links.length||graph.links.some(l=>!Number.isFinite(l.value)||l.value<=0))return {reason:'桑基非正值保留原生语义'};\n if(graph.nodes.some(n=>hasRichMermaidLabel(n.id)))return {reason:'桑基富文本保留原生语义'};\n if(options.linkColor&&!['gradient','source','target'].includes(options.linkColor))return {reason:'桑基自定义带色保留原生语义'};\n const ids=new Set(graph.nodes.map(n=>n.id)),unknown=Object.keys(descriptions).filter(k=>!ids.has(k));\n if(unknown.length)throw Error('未知说明 ID:'+unknown.join('、'));\n const children=new Map(graph.nodes.map(n=>[n.id,[]])),indegree=new Map(graph.nodes.map(n=>[n.id,0]));\n for(const l of graph.links){children.get(l.source).push(l.target);indegree.set(l.target,indegree.get(l.target)+1);}\n const queue=graph.nodes.filter(n=>!indegree.get(n.id)).map(n=>n.id);let count=0;\n while(queue.length){const id=queue.shift();count++;for(const to of children.get(id)){indegree.set(to,indegree.get(to)-1);if(!indegree.get(to))queue.push(to);}}\n if(count!==graph.nodes.length)return {reason:'桑基回路保留原生语义'};\n return {data:{kind:'sankey',nodes:graph.nodes.map(n=>({...n,name:n.id,description:descriptions[n.id]||''})),links:graph.links.map((l,i)=>({...l,id:'flow-'+(i+1)})),alignment:options.nodeAlignment||'justify',linkColor:options.linkColor||'gradient',showValues:options.showValues,prefix:options.prefix||'',suffix:options.suffix||'',title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidSankey(root,data) {\n const d3=window.__mcxQVRuntime.sankey;\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');\n root.dataset.state='enhanced';root.dataset.renderer='qv-sankey';root.classList.add('qv-sankey');root.querySelector('.edge-key').hidden=true;svg.removeAttribute('aria-hidden');viewport.setAttribute('aria-label','桑基图,可横向滚动');\n const title=root.querySelector('h3');if(!title.textContent)title.textContent=data.title;if(title.textContent.trim())title.hidden=false;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .wires{pointer-events:auto}\n #${root.id} .sankey-label>span:first-child{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} #${root.id} .sankey-label{position:absolute;display:flex;flex-direction:column;gap:4px;width:152px;padding:8px;border:0;background:transparent;color:var(--foreground);font:inherit;font-size:14px;line-height:20px;text-align:left;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .sankey-label .value{font-size:12px;line-height:18px;color:var(--muted-foreground);font-variant-numeric:tabular-nums}\n #${root.id} .sankey-detail{position:absolute;z-index:10;width:200px;max-width:calc(100% - 8px);box-sizing:border-box;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:18px;border:1px solid var(--border);overflow-wrap:anywhere;box-shadow:0 3px 12px #0002;pointer-events:none}\n #${root.id} .sankey-detail-title{font-size:14px;line-height:20px;font-weight:600;margin-bottom:12px}\n #${root.id} .sankey-detail-rows{display:grid;gap:6px}\n #${root.id} .sankey-detail-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start}\n #${root.id} .sankey-detail-description{margin-top:12px;color:var(--muted-foreground)}\n #${root.id} .sankey-detail-row>span:last-child{font-variant-numeric:tabular-nums;white-space:nowrap;color:light-dark(color-mix(in srgb,var(--popover-foreground) 64%,var(--popover)),color-mix(in srgb,var(--popover-foreground) 68%,var(--popover)))}\n #${root.id} .sankey-flow-list{position:absolute;inset:0;pointer-events:none}\n #${root.id} .sankey-flow-key{position:absolute;min-width:24px;height:24px;border:0;background:transparent;padding:2px 4px;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;opacity:0;pointer-events:none}\n #${root.id} .sankey-flow-key:focus-visible{opacity:1;background:var(--background)}\n #${root.id} .sankey-scroll-cue{margin:8px 0 0;text-align:right;font-size:12px;line-height:18px;color:var(--muted-foreground)}\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',add=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},color=i=>`var(--viz-series-${i%6+1})`,fmt=v=>data.prefix+new Intl.NumberFormat(undefined,{maximumFractionDigits:20}).format(v)+data.suffix;\n const tip=document.createElement('div');tip.className='sankey-detail';tip.setAttribute('role','tooltip');tip.id=root.id+'-sankey-detail';tip.hidden=true;root.append(tip);\n const cue=document.createElement('div');cue.className='sankey-scroll-cue';cue.textContent='横向滚动查看其余阶段 →';root.insertBefore(cue,tip);\n const list=document.createElement('div');list.className='sankey-flow-list';list.setAttribute('aria-label','源数据流量');scene.append(list);\n let graph,labels=[],bands=[],bars=[],keys=[],frame=0,pending=null,epoch=0,clip,mask;const reduced=matchMedia('(prefers-reduced-motion:reduce)'),view=createMermaidViewport(root);\n function clear(){epoch++;tip.hidden=true;root.style.paddingBottom='';tip.style.position='absolute';tip.style.marginTop='0';[...labels,...keys].forEach(b=>b.removeAttribute('aria-describedby'));bands.forEach(p=>p.style.opacity='1');bars.forEach(p=>p.style.opacity='1');}\n function emphasize(kind,index){const node=graph.nodes[index],link=graph.links[index];bands.forEach((p,i)=>p.style.opacity=kind==='link'?(i===index?'1':'.12'):(graph.links[i].source===node||graph.links[i].target===node?'1':'.12'));bars.forEach((p,i)=>p.style.opacity=kind==='node'?(i===index||graph.links.some(l=>(l.source===node&&l.target.index===i)||(l.target===node&&l.source.index===i))?'1':'.24'):(i===link.source.index||i===link.target.index?'1':'.24'));}\n function show(kind,index,event,anchor){clear();emphasize(kind,index);const n=graph.nodes[index],l=graph.links[index],rows=kind==='link'?[['流量',fmt(l.value)],['占来源流出',(l.value/l.source.sourceLinks.reduce((s,l)=>s+l.value,0)*100).toFixed(2)+'%'],['占目标流入',(l.value/l.target.targetLinks.reduce((s,l)=>s+l.value,0)*100).toFixed(2)+'%']]:[['流入',fmt(n.targetLinks.reduce((s,l)=>s+l.value,0))],['流出',fmt(n.sourceLinks.reduce((s,l)=>s+l.value,0))]];const heading=document.createElement('div');heading.className='sankey-detail-title';heading.textContent=kind==='link'?l.source.name+' → '+l.target.name:n.name;tip.replaceChildren(heading);const body=document.createElement('div');body.className='sankey-detail-rows';tip.append(body);for(const [a,v] of rows){const row=document.createElement('div');row.className='sankey-detail-row';const text=document.createElement('span'),value=document.createElement('span');text.textContent=a;value.textContent=v;row.append(text,value);body.append(row);}if(kind==='node'&&n.description){const text=document.createElement('div');text.className='sankey-detail-description';text.textContent=n.description;tip.append(text);}const button=kind==='node'?labels[index]:keys[index];button?.setAttribute('aria-describedby',tip.id);tip.hidden=false;\n placeTip(kind,index,anchor||button,event);}\n\n // 按实际曲线与带宽避让,而不是用整条曲线的包围盒近似。\n function placeTip(kind,index,anchor,event){\n const rr=root.getBoundingClientRect(),ar=anchor.getBoundingClientRect(),sr=svg.getBoundingClientRect(),scale=sr.width/svg.viewBox.baseVal.width;\n tip.style.width=Math.min(200,root.clientWidth-16)+'px';const w=tip.offsetWidth,h=tip.offsetHeight,pointer=event?.type?.startsWith('pointer'),x=(pointer?event.clientX:ar.left)-rr.left,y=(pointer?event.clientY:ar.top)-rr.top,aw=pointer?0:ar.width,ah=pointer?0:ar.height;\n const segments=[];let radius=0;\n bands.forEach((path,i)=>{const link=graph.links[i],selected=kind==='link'?i===index:link.source===graph.nodes[index]||link.target===graph.nodes[index];if(!selected)return;const r=link.width*scale/2+2;radius=Math.max(radius,r);if(!path.flowSamples){const length=path.getTotalLength(),steps=Math.max(2,Math.ceil(length/4));path.flowSamples=Array.from({length:steps+1},(_,j)=>path.getPointAtLength(length*j/steps));}let previous;for(const p of path.flowSamples){const point={x:sr.left-rr.left+p.x*scale,y:sr.top-rr.top+p.y*scale};if(previous)segments.push({a:previous,b:point,r});previous=point;}});\n const crosses=(a,b,box)=>{let lo=0,hi=1;for(const [start,delta,min,max]of [[a.x,b.x-a.x,box.x,box.x+box.w],[a.y,b.y-a.y,box.y,box.y+box.h]]){if(Math.abs(delta)<1e-9){if(start<min||start>max)return false;}else{const p=(min-start)/delta,q=(max-start)/delta;lo=Math.max(lo,Math.min(p,q));hi=Math.min(hi,Math.max(p,q));if(lo>hi)return false;}}return true;};\n const candidates=[],add=(left,top)=>{const c={x:Math.max(4,Math.min(root.clientWidth-w-4,left)),y:Math.max(4,Math.min(root.clientHeight-h-4,top))};c.crossings=segments.reduce((sum,s)=>sum+(crosses(s.a,s.b,{x:c.x-s.r,y:c.y-s.r,w:w+s.r*2,h:h+s.r*2})?Math.hypot(s.b.x-s.a.x,s.b.y-s.a.y):0),0);c.distance=Math.hypot(Math.max(x-c.x-w,c.x-x-aw,0),Math.max(y-c.y-h,c.y-y-ah,0));candidates.push(c);};\n for(const gap of [12,20,28,Math.ceil(radius+12)])for(const [left,top]of [[x+aw+gap,y+(ah-h)/2],[x-w-gap,y+(ah-h)/2],[x+(aw-w)/2,y-h-gap],[x+(aw-w)/2,y+ah+gap],[x+aw+gap,y-h-gap],[x-w-gap,y-h-gap],[x+aw+gap,y+ah+gap],[x-w-gap,y+ah+gap]])add(left,top);\n // 图的上下留白也是浮层候选,不改变图表高度或追加详情面板。\n add(x+(aw-w)/2,4);add(x+(aw-w)/2,root.clientHeight-h-4);\n candidates.sort((a,b)=>a.crossings-b.crossings||a.distance-b.distance);const best=candidates[0];tip.style.left=best.x+'px';tip.style.top=best.y+'px';tip.dataset.flowOverlap=String(best.crossings);tip.dataset.placement='flow-clear';\n }\n\n function focus(kind,index){pending={kind,index};const b=kind==='node'?labels[index]:keys[index],br=b.getBoundingClientRect(),vr=viewport.getBoundingClientRect();if(!view.expanded){if(br.left<vr.left+8)viewport.scrollLeft-=vr.left+8-br.left;else if(br.right>vr.right-8)viewport.scrollLeft+=br.right-vr.right+8;}const ticket=++epoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(ticket===epoch){pending=null;show(kind,index,null,b);}}));}\n function cancel(){cancelAnimationFrame(frame);frame=0;if(mask)mask.setAttribute('width',scene.offsetWidth);}\n function draw(){cancel();clear();svg.replaceChildren();labels.forEach(b=>b.remove());labels=[];list.replaceChildren();keys=[];bands=[];bars=[];\n const measure=document.createElement('button');measure.className='sankey-label';measure.style.visibility='hidden';scene.append(measure);let labelHeight=0;for(const n of data.nodes){measure.replaceChildren(Object.assign(document.createElement('span'),{textContent:n.name}));if(data.showValues){const v=document.createElement('span');v.className='value';v.textContent=fmt(data.links.filter(l=>l.source===n.id||l.target===n.id).reduce((a,l)=>a+l.value,0));measure.append(v);}labelHeight=Math.max(labelHeight,measure.offsetHeight);}measure.remove();\n const depth=new Map(data.nodes.map(n=>[n.id,0]));for(let k=0;k<data.nodes.length;k++)for(const l of data.links)depth.set(l.target,Math.max(depth.get(l.target),depth.get(l.source)+1));const columns=Math.max(...depth.values())+1,width=Math.max(viewport.clientWidth,view.expanded?(columns===2?560:736+Math.max(0,columns-3)*220):320+(columns-1)*136),count=Math.max(...Array.from({length:columns},(_,i)=>data.nodes.filter(n=>depth.get(n.id)===i).length)),height=Math.max(320,Math.max(72,labelHeight+20)+labelHeight/2+20+count*Math.max(24,labelHeight+14)+Math.max(160,count*16)),padding=Math.max(24,labelHeight+14),top=Math.max(72,labelHeight+20),bottom=labelHeight/2+20;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';svg.setAttribute('role','img');svg.setAttribute('aria-label','桑基图:'+data.links.map(l=>l.source+' → '+l.target+' '+fmt(l.value)).join(';'));\n const align={left:d3.sankeyLeft,right:d3.sankeyRight,center:d3.sankeyCenter,justify:d3.sankeyJustify};graph=d3.sankey().nodeId(n=>n.id).nodeWidth(10).nodePadding(padding).nodeAlign(align[data.alignment]||d3.sankeyJustify).nodeSort(null).extent([[160,top],[width-160,height-bottom]])({nodes:data.nodes.map(n=>({...n})),links:data.links.map(l=>({...l}))});\n const defs=add('defs',{}),id=root.id+'-sankey-mask';clip=add('clipPath',{id},defs);mask=add('rect',{x:0,y:0,width,height},clip);const layer=add('g',{'clip-path':`url(#${id})`});\n graph.links.forEach((l,i)=>{let stroke=color(l.source.index);if(data.linkColor==='target')stroke=color(l.target.index);else if(data.linkColor==='gradient'){const g=add('linearGradient',{id:root.id+'-flow-'+i,gradientUnits:'userSpaceOnUse',x1:l.source.x1,x2:l.target.x0,y1:0,y2:0},defs);add('stop',{offset:0,'stop-color':color(l.source.index)},g);add('stop',{offset:1,'stop-color':color(l.target.index)},g);stroke=`url(#${root.id}-flow-${i})`;}const path=add('path',{d:d3.sankeyLinkHorizontal()(l),fill:'none',stroke,'stroke-opacity':'.22','stroke-width':l.width,'data-flow':l.id,'data-value':l.value},layer);bands.push(path);const hit=add('path',{d:path.getAttribute('d'),fill:'none',stroke:'transparent','stroke-width':Math.max(12,l.width),'data-flow-hit':l.id},layer);hit.onpointerenter=hit.onpointermove=hit.onclick=e=>{if(!frame)show('link',i,e,hit);};hit.onpointerleave=clear;const key=document.createElement('button');key.type='button';key.className='sankey-flow-key';key.textContent=fmt(l.value);key.setAttribute('aria-label',l.source.name+' → '+l.target.name+','+fmt(l.value));const mid=path.getPointAtLength(path.getTotalLength()/2);key.style.left=(mid.x-12)+'px';key.style.top=(mid.y-12)+'px';list.append(key);keys.push(key);key.onpointerenter=key.onclick=e=>show('link',i,e);key.onfocus=()=>focus('link',i);key.onpointerleave=clear;key.onblur=()=>{pending=null;clear();};});\n graph.nodes.forEach((n,i)=>{const bar=add('rect',{x:n.x0,y:n.y0,width:n.x1-n.x0,height:n.y1-n.y0,fill:color(i),'fill-opacity':'.7','data-sankey-node':n.id},layer);bars.push(bar);bar.onpointerenter=bar.onclick=e=>show('node',i,e,bar);bar.onpointerleave=clear;const b=document.createElement('button');b.type='button';b.className='sankey-label';b.dataset.sankeyLabel=n.id;const name=document.createElement('span');name.textContent=n.name;b.title=n.name;b.append(name);if(data.showValues){const value=document.createElement('span');value.className='value';value.textContent=fmt(n.value);b.append(value);}b.setAttribute('aria-label',n.name+',流入 '+fmt(n.targetLinks.reduce((s,l)=>s+l.value,0))+',流出 '+fmt(n.sourceLinks.reduce((s,l)=>s+l.value,0)));scene.append(b);if(n.layer===0){b.style.left=(n.x0-160)+'px';b.style.top=((n.y0+n.y1)/2-b.offsetHeight/2)+'px';b.style.textAlign='right';}else if(n.layer===columns-1){b.style.left=(n.x1+8)+'px';b.style.top=((n.y0+n.y1)/2-b.offsetHeight/2)+'px';}else{b.style.left=(n.x0-71)+'px';b.style.top=(n.y0-b.offsetHeight-10)+'px';b.style.textAlign='center';}labels.push(b);b.onpointerenter=b.onclick=e=>show('node',i,e);b.onfocus=()=>focus('node',i);b.onpointerleave=clear;b.onblur=()=>{pending=null;clear();};});scene.append(list);cue.hidden=view.expanded?viewport.clientWidth>=560:width<=viewport.clientWidth+1;cue.textContent=view.expanded?'整体概览 · 选择节点或流带查看明细;退出放大后可横向阅读完整文字':'横向滚动查看其余阶段 →';view.refresh();}\n function replay(){cancel();if(reduced.matches||view.expanded)return;mask.setAttribute('width',0);const start=performance.now();function step(now){const t=Math.min(1,(now-start)/1000);mask.setAttribute('width',scene.offsetWidth*mermaidEntryProgress(t));frame=t<1?requestAnimationFrame(step):0;}frame=requestAnimationFrame(step);}\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(width!==root.clientWidth){width=root.clientWidth;draw();}})).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting){armed=true;cancel();clear();}else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else if(armed){const r=root.getBoundingClientRect();if(r.bottom>0&&r.top<innerHeight){armed=false;replay();}}});reduced.addEventListener('change',cancel);viewport.addEventListener('scroll',()=>{if(pending){const p=pending;focus(p.kind,p.index);}else clear();},{passive:true});document.addEventListener('keydown',e=>{if(e.key==='Escape'){pending=null;clear();}});window.addEventListener('blur',clear);\n}\n\n// 解析层保留维度顺序、曲线名称、量表与网格选项。\nasync function readMermaidRadarModel(db,descriptions={}) {\n if(!db.getAxes||!db.getCurves||!db.getOptions)return {reason:'Radar 数据接口不兼容'};\n const axes=structuredClone(db.getAxes()),curves=structuredClone(db.getCurves()),options=structuredClone(db.getOptions());\n if(axes.length<3||!curves.length||curves.some(c=>c.entries.length!==axes.length||c.entries.some(v=>!Number.isFinite(v))))return {reason:'雷达维度或数值保留原生语义'};\n if([...axes,...curves].some(d=>hasRichMermaidLabel(d.label))||Object.keys(descriptions).length)return {reason:'雷达富文本与额外说明保留原生语义'};\n const min=options.min??0,max=options.max??Math.max(...curves.flatMap(c=>c.entries));\n if(!Number.isFinite(min)||!Number.isFinite(max)||max<=min||curves.some(c=>c.entries.some(v=>v<min||v>max)))return {reason:'雷达量表边界保留原生语义'};\n return {data:{kind:'radar',axes:axes.map(d=>({id:d.name,name:d.label})),series:curves.map(d=>({id:d.name,name:d.label,values:d.entries})),min,max,ticks:options.ticks,graticule:options.graticule,showLegend:options.showLegend,title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidRadar(root,data) {\n if(data.min===0&&data.graticule==='polygon'&&data.ticks===5&&data.showLegend&&data.axes.length<=8&&data.series.length<=4&&data.axes.every(a=>a.name.length<=12)){\n root.dataset.renderer='qv-radar';root.dataset.preset='radar-chart';\n await renderMermaidRadarPreset(root,{title:root.querySelector('h3').textContent||data.title,dimensions:data.axes.map(a=>a.name),series:data.series.map(s=>({name:s.name,values:s.values})),maxValue:data.max,valuePrefix:'',valueSuffix:'',decimals:2},data.series.map(s=>s.name+':'+s.values.join(',')).join(';'));root.dataset.state='enhanced';return;\n }\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');root.dataset.state='enhanced';root.dataset.renderer='qv-radar';root.classList.add('qv-radar');viewport.setAttribute('aria-label','雷达图,可横向滚动');root.querySelector('.edge-key').hidden=true;svg.removeAttribute('aria-hidden');\n const title=root.querySelector('h3');if(!title.textContent)title.textContent=data.title;if(title.textContent.trim())title.hidden=false;\n const style=document.createElement('style');style.textContent=`\n #${root.id} .wires{pointer-events:auto}\n #${root.id} .radar-axis-label{position:absolute;border:0;background:transparent;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;text-align:center;padding:8px;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .radar-legend{display:flex;justify-content:center;flex-wrap:wrap;gap:8px 22px;margin-top:20px}\n #${root.id} .radar-series-key{display:flex;align-items:center;gap:7px;border:0;background:transparent;color:var(--foreground);font:inherit;font-size:12px;line-height:20px;padding:4px 0;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .radar-swatch{width:10px;height:10px;background:currentColor}\n #${root.id} .radar-detail{position:absolute;z-index:10;display:grid;gap:12px;width:188px;box-sizing:border-box;max-width:min(260px,calc(100% - 16px));padding:12px 14px;border:1px solid var(--border);border-radius:12px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:18px;box-shadow:0 6px 18px color-mix(in srgb,var(--foreground) 10%,transparent);pointer-events:none}\n #${root.id} .radar-detail[hidden]{display:none}\n #${root.id} .radar-detail-title{display:flex;align-items:center;gap:8px;font-size:14px;line-height:20px;font-weight:600;overflow-wrap:anywhere}\n #${root.id} .radar-detail-rows{display:grid;gap:4px}\n #${root.id} .radar-detail-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:6px}\n #${root.id} .radar-detail[data-mode=\"series\"] .radar-detail-row{grid-template-columns:minmax(0,1fr) auto}\n #${root.id} .radar-detail-name{color:var(--popover-foreground);overflow-wrap:anywhere}\n #${root.id} .radar-detail[data-mode=\"series\"] .radar-detail-name{color:var(--popover-foreground);font-weight:500}\n #${root.id} .radar-detail-value{color:light-dark(color-mix(in srgb,var(--popover-foreground) 64%,var(--popover)),color-mix(in srgb,var(--popover-foreground) 68%,var(--popover)));font-variant-numeric:tabular-nums;white-space:nowrap}\n #${root.id} .radar-detail .radar-swatch{flex-shrink:0}\n\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',add=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},color=i=>`var(--viz-series-${i%6+1})`,fmt=v=>new Intl.NumberFormat(undefined,{maximumFractionDigits:2}).format(v);\n const legend=document.createElement('div');legend.className='radar-legend';legend.hidden=!data.showLegend;root.append(legend);const tip=document.createElement('div');tip.className='radar-detail';tip.setAttribute('role','tooltip');tip.id=root.id+'-radar-detail';tip.hidden=true;root.append(tip);\n let groups=[],geometry,frame=0,initialized=false,pendingFocus=null;const reduced=matchMedia('(prefers-reduced-motion:reduce)'),view=createMermaidViewport(root);\n const keys=data.series.map((s,i)=>{const b=document.createElement('button');b.type='button';b.className='radar-series-key';const sw=document.createElement('span');sw.className='radar-swatch';sw.style.color=color(i);sw.style.borderRadius=i%3===0?'50%':'0';if(i%3===2)sw.style.transform='rotate(45deg) scale(.8)';sw.setAttribute('aria-hidden','true');b.append(sw,document.createTextNode(s.name));legend.append(b);b.onpointerenter=b.onclick=b.onfocus=()=>{clear();highlight(i);};b.onpointerleave=b.onblur=clear;return b;});\n let labels=[],detailEpoch=0;\n function openAxis(i,event){pendingFocus=i;const b=labels[i],r=b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(!view.expanded){if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;}const epoch=++detailEpoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(epoch===detailEpoch){pendingFocus=null;show('axis',i,event?.type==='focus'?null:event);}}));}\n function clear(){++detailEpoch;tip.hidden=true;tip.style.position='absolute';tip.style.marginTop='0';[...keys,...labels].forEach(b=>b.removeAttribute('aria-describedby'));groups.forEach(g=>{g.layer.style.opacity='1';g.shape.style.strokeOpacity='';g.shape.style.fillOpacity='';g.markers.forEach(m=>m.style.opacity='');});}\n function highlight(index){groups.forEach((g,j)=>{g.shape.style.strokeOpacity=j===index?'1':'.1';g.shape.style.fillOpacity=j===index?'.24':'.03';g.markers.forEach(m=>m.style.opacity=j===index?'1':'.1');});}\n function seriesSwatch(index){return keys[index].querySelector('.radar-swatch').cloneNode(true);}\n function show(kind,index,event){clear();const objectMode=kind==='series',rows=objectMode?data.axes.map((a,j)=>({name:a.name,value:data.series[index].values[j]})):data.series.map((s,j)=>({name:s.name,value:s.values[index],series:j}));tip.dataset.mode=kind;const header=document.createElement('div');header.className='radar-detail-title';header.textContent=objectMode?data.series[index].name:data.axes[index].name;if(objectMode)header.prepend(seriesSwatch(index));tip.replaceChildren(header);const list=document.createElement('div');list.className='radar-detail-rows';for(const d of rows){const row=document.createElement('div');row.className='radar-detail-row';const name=document.createElement('span');name.className='radar-detail-name';name.textContent=d.name;const value=document.createElement('span');value.className='radar-detail-value';value.textContent=fmt(d.value);if(!objectMode)row.append(seriesSwatch(d.series));row.append(name,value);list.append(row);}tip.append(list);if(objectMode)highlight(index);const b=(objectMode?keys:labels)[index];b?.setAttribute('aria-describedby',tip.id);tip.hidden=false;placeRadarDetail(objectMode?index:null,b,event);}\n\n\n function placeRadarDetail(index,anchor,event){const rr=root.getBoundingClientRect(),sr=svg.getBoundingClientRect(),sc=scene.getBoundingClientRect(),scale=sr.width/svg.viewBox.baseVal.width;tip.style.position='absolute';tip.style.marginTop='0';tip.style.boxSizing='border-box';tip.style.width=Math.min(188,root.clientWidth-16)+'px';const w=tip.offsetWidth,h=tip.offsetHeight,map=p=>({x:sr.left-rr.left+p[0]*scale,y:sr.top-rr.top+p[1]*scale}),polys=groups.map(g=>g.points.map(map));const ar=anchor?.getBoundingClientRect(),pointer=event?.type?.startsWith('pointer'),px=pointer?event.clientX-rr.left:ar?ar.left-rr.left+ar.width/2:rr.width/2,py=pointer?event.clientY-rr.top:ar?ar.top-rr.top+ar.height/2:rr.height/2;const raw=[];for(const g of [16,32,56])for(const f of [0,.5,1])raw.push([px+g,py-h*f],[px-g-w,py-h*f],[px-w*f,py-g-h],[px-w*f,py+g]);const l=sc.left-rr.left+8,t=sc.top-rr.top+8,r=sc.right-rr.left-w-8,b=sc.bottom-rr.top-h-8;raw.push([l,t],[r,t],[l,b],[r,b]);const clip=(poly,c)=>{let pts=poly;for(const [axis,edge,sign] of [['x',c.x,1],['x',c.x+w,-1],['y',c.y,1],['y',c.y+h,-1]]){const out=[];for(let i=0;i<pts.length;i++){const a=pts[i],q=pts[(i+1)%pts.length],ai=sign*(a[axis]-edge)>=0,bi=sign*(q[axis]-edge)>=0;if(ai)out.push(a);if(ai!==bi){const k=(edge-a[axis])/(q[axis]-a[axis]);out.push({x:a.x+k*(q.x-a.x),y:a.y+k*(q.y-a.y)});}}pts=out;if(!pts.length)return 0;}return Math.abs(pts.reduce((s,p,i)=>{const q=pts[(i+1)%pts.length];return s+p.x*q.y-q.x*p.y;},0))/2;};const boxes=[...labels,...keys].map(n=>{const o=n.getBoundingClientRect();return{x:o.left-rr.left,y:o.top-rr.top,w:o.width,h:o.height};}),cover=(c,o)=>Math.max(0,Math.min(c.x+w,o.x+o.w)-Math.max(c.x,o.x))*Math.max(0,Math.min(c.y+h,o.y+o.h)-Math.max(c.y,o.y));const cands=raw.map(([x,y],order)=>{const c={x:Math.max(4,Math.min(root.clientWidth-w-4,x)),y:Math.max(4,Math.min(root.clientHeight-h-4,y)),order};c.pointer=px>c.x-4&&px<c.x+w+4&&py>c.y-4&&py<c.y+h+4?1:0;c.selected=index===null?0:clip(polys[index],c);c.others=polys.reduce((s,p,i)=>s+(i===index?0:clip(p,c)),0)+boxes.reduce((s,o)=>s+cover(c,o),0);c.distance=Math.hypot(Math.max(c.x-px,0,px-c.x-w),Math.max(c.y-py,0,py-c.y-h));return c;});const nearby=nearbyRadarDetails(cands);nearby.sort((a,b)=>a.pointer-b.pointer||Math.round(a.selected/200)-Math.round(b.selected/200)||Math.round(a.others/400)-Math.round(b.others/400)||a.distance-b.distance||a.order-b.order);tip.style.left=nearby[0].x+'px';tip.style.top=nearby[0].y+'px';tip.dataset.placement='radar-clear';}\nfunction cancel(){cancelAnimationFrame(frame);frame=0;groups.forEach(g=>{g.shape.setAttribute('points',g.points.map(p=>p.join(',')).join(' '));g.markers.forEach((m,j)=>m.setAttribute('transform',`translate(${g.points[j][0]} ${g.points[j][1]})`));});}\n function draw(){cancel();clear();svg.replaceChildren();labels.forEach(b=>b.remove());labels=[];const dense=data.axes.length>6||data.axes.some(a=>a.name.length>8),width=Math.max(viewport.clientWidth,dense?640:560),height=dense?480:450,cx=width/2,cy=dense?240:220,radius=dense?150:142;geometry={cx,cy,radius};scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';svg.setAttribute('role','img');svg.setAttribute('aria-label',`${data.title},量表${data.min}至${data.max}。`+data.series.map(s=>s.name+':'+s.values.map((v,j)=>data.axes[j].name+' '+v).join(',')).join(';'));\n const point=(i,r)=>{const a=-Math.PI/2+i*Math.PI*2/data.axes.length;return[cx+Math.cos(a)*r,cy+Math.sin(a)*r];},outline=r=>data.axes.map((a,i)=>point(i,r).join(',')).join(' ');\n for(let k=1;k<=data.ticks;k++){const r=radius*k/data.ticks;add(data.graticule==='circle'?'circle':'polygon',data.graticule==='circle'?{cx,cy,r,fill:'none',stroke:'var(--border)'}:{points:outline(r),fill:'none',stroke:'var(--border)'});}\n data.axes.forEach((a,i)=>{const p=point(i,radius);add('line',{x1:cx,y1:cy,x2:p[0],y2:p[1],stroke:'var(--border)'});const angle=-Math.PI/2+i*Math.PI*2/data.axes.length,cos=Math.cos(angle),sin=Math.sin(angle),lp=point(i,radius+14),side=cos>.25?'right':cos<-.25?'left':sin<0?'top':'bottom';const b=document.createElement('button');b.className='radar-axis-label';b.type='button';b.title=a.name;b.dataset.side=side;const text=document.createElement('span');text.textContent=a.name;text.style.cssText='display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden';b.append(text);const labelWidth=side==='right'?Math.max(72,Math.min(168,width-lp[0]-4)):side==='left'?Math.max(72,Math.min(168,lp[0]-4)):dense?140:128;b.style.width=labelWidth+'px';b.style.textAlign=side==='right'?'left':side==='left'?'right':'center';scene.append(b);const h=b.offsetHeight;b.style.left=(side==='right'?lp[0]-8:side==='left'?lp[0]-labelWidth+8:lp[0]-labelWidth/2)+'px';b.style.top=(side==='top'?lp[1]-h+8:side==='bottom'?lp[1]-8:lp[1]-h/2)+'px';b.onpointerenter=b.onclick=e=>show('axis',i,e);b.onfocus=e=>openAxis(i,e);b.onpointerleave=clear;b.onblur=()=>{pendingFocus=null;clear();};labels.push(b);});for(const side of ['left','right']){const group=labels.filter(b=>b.dataset.side===side).sort((a,b)=>a.offsetTop-b.offsetTop);let bottom=-Infinity;for(const b of group){const top=Math.max(b.offsetTop,bottom+4);b.style.top=top+'px';bottom=top+b.offsetHeight;}const overflow=bottom-(height-4);if(overflow>0)for(const b of group)b.style.top=(b.offsetTop-overflow)+'px';}\n groups=data.series.map((s,i)=>{const layer=add('g',{'data-series':s.id}),points=s.values.map((v,j)=>point(j,(v-data.min)/(data.max-data.min)*radius)),shape=add('polygon',{points:points.map(p=>p.join(',')).join(' '),fill:color(i),'fill-opacity':'.14',stroke:color(i),'stroke-width':1.7,'stroke-linejoin':'round'},layer);if(i%3)shape.setAttribute('stroke-dasharray',i%3===1?'5 3':'2 3');shape.onpointerenter=shape.onpointermove=e=>show('series',i,e);shape.onpointerleave=clear;const markers=points.map((p,j)=>{const m=add(i%3===0?'circle':i%3===1?'rect':'path',i%3===0?{cx:0,cy:0,r:3,fill:color(i)}:i%3===1?{x:-3,y:-3,width:6,height:6,fill:color(i)}:{d:'M0 -4L4 0L0 4L-4 0Z',fill:color(i)},layer);m.setAttribute('transform',`translate(${p[0]} ${p[1]})`);m.onpointerenter=m.onpointermove=e=>show('series',i,e);m.onpointerleave=clear;return m;});return{layer,shape,points,markers};});geometry.axes=data.axes.map((a,i)=>point(i,radius));view.refresh();if(!initialized&&!view.expanded)viewport.scrollLeft=Math.max(0,(width-viewport.clientWidth)/2);initialized=true;}\n function replay(){cancel();if(reduced.matches||view.expanded)return;const {cx,cy}=geometry,start=performance.now();function step(now){const t=Math.min(1,(now-start)/900),k=mermaidEntryProgress(t);groups.forEach(g=>{const points=g.points.map(p=>[cx+(p[0]-cx)*k,cy+(p[1]-cy)*k]);g.shape.setAttribute('points',points.map(p=>p.join(',')).join(' '));g.markers.forEach((m,j)=>m.setAttribute('transform',`translate(${points[j][0]} ${points[j][1]})`));});frame=t<1?requestAnimationFrame(step):0;}frame=requestAnimationFrame(step);}\n\n const segmentDistance=(p,a,b)=>{const dx=b[0]-a[0],dy=b[1]-a[1],t=Math.max(0,Math.min(1,((p[0]-a[0])*dx+(p[1]-a[1])*dy)/(dx*dx+dy*dy||1)));return Math.hypot(p[0]-a[0]-t*dx,p[1]-a[1]-t*dy);};\n function inspect(event){if(frame||!geometry)return;const r=svg.getBoundingClientRect(),scale=svg.viewBox.baseVal.width/r.width,p=[(event.clientX-r.left)*scale,(event.clientY-r.top)*scale],center=[geometry.cx,geometry.cy];const points=groups.flatMap(g=>g.points.map((q,j)=>({index:groups.indexOf(g),distance:Math.hypot(p[0]-q[0],p[1]-q[1])}))).sort((a,b)=>a.distance-b.distance);if(points[0]?.distance<=12*scale){show('series',points[0].index,event);return;}if(Math.hypot(p[0]-center[0],p[1]-center[1])<14*scale){clear();return;}const axes=geometry.axes.map((q,i)=>({index:i,distance:segmentDistance(p,center,q)})).sort((a,b)=>a.distance-b.distance);if(axes[0]?.distance<=8*scale)show('axis',axes[0].index,event);}\n svg.addEventListener('pointermove',inspect);svg.addEventListener('click',inspect);svg.addEventListener('pointerleave',clear);\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let width=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(width!==root.clientWidth){width=root.clientWidth;draw();}})).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting){armed=true;cancel();clear();}else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else if(armed){const r=root.getBoundingClientRect();if(r.bottom>0&&r.top<innerHeight){armed=false;replay();}}});reduced.addEventListener('change',cancel);viewport.addEventListener('scroll',()=>{if(pendingFocus!==null){const i=pendingFocus;openAxis(i,null);}else clear();},{passive:true});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!tip.hidden){pendingFocus=null;clear();}});window.addEventListener('blur',clear);\n}\n\n// 使用真实坐标值与解析后域,不从源码猜测点序列。\nasync function readMermaidXYModel(db,descriptions={}) {\n if(!db.getXYChartData||!db.getChartConfig)return {reason:'XY 数据接口不兼容'};\n const raw=structuredClone(db.getXYChartData()),config=db.getChartConfig(),counts={bar:0,line:0};\n const plots=raw.plots.map((p,i)=>({id:'series-'+(i+1),type:p.type,name:(p.type==='bar'?'柱形':'折线')+(++counts[p.type]),points:p.data.map(([x,y],index)=>({x,y,index}))}));\n if(!plots.length||plots.some(p=>!['bar','line'].includes(p.type)||p.points.some(d=>!Number.isFinite(d.y))))return {reason:'XY 系列数据保留原生语义'};\n if(hasRichMermaidLabel(raw.xAxis.title)||hasRichMermaidLabel(raw.yAxis.title)||raw.xAxis.categories?.some(hasRichMermaidLabel))return {reason:'富文本坐标轴保留原生语义'};\n if(Object.keys(descriptions).length)return {reason:'XY 额外说明保留原生语义'};\n return {data:{kind:'xy',plots,xAxis:raw.xAxis,yAxis:raw.yAxis,horizontal:config.chartOrientation==='horizontal',title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidXY(root,data) {\n const d3=window.__mcxQVRuntime.d3,scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';svg.removeAttribute('aria-hidden');root.classList.add('qv-xy');root.dataset.state='enhanced';root.dataset.renderer='qv-xy';root.querySelector('.edge-key').hidden=true;\n const style=document.createElement('style');style.textContent=`#${root.id} .wires{pointer-events:auto}#${root.id} .xy-label{position:absolute;border:0;background:transparent;padding:4px;color:var(--foreground);font:inherit;font-size:12px;line-height:18px;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}#${root.id} .xy-tip{position:absolute;padding:12px 14px;border-radius:12px;background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);box-sizing:border-box;font-size:12px;line-height:18px;pointer-events:none;z-index:10;box-shadow:0 3px 12px #0002}#${root.id} .xy-tip-title{font-size:14px;line-height:20px;font-weight:600;overflow-wrap:anywhere;padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--border)}#${root.id} .xy-tip-row{display:grid;grid-template-columns:8px minmax(0,1fr) auto;gap:8px;align-items:center;margin-top:6px}#${root.id} .xy-tip-name{color:var(--popover-foreground);overflow-wrap:anywhere}#${root.id} .xy-tip-value{color:var(--muted-foreground);font-variant-numeric:tabular-nums;font-weight:500}#${root.id} .xy-key{display:flex;flex-wrap:wrap;gap:18px;margin-top:20px;justify-content:center;font-size:12px}#${root.id} .xy-key span{display:flex;gap:6px;align-items:center}`;root.append(style);\n const el=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;},text=(value,attrs)=>{const n=el('text',{fill:'var(--foreground)','font-size':12,...attrs});n.textContent=value;return n;},color=i=>`var(--viz-series-${i%6+1})`;\n const reference=document.createElement('div');reference.style.cssText='position:absolute;left:0;width:64px;background:var(--background);pointer-events:none;z-index:2;font-size:12px';root.append(reference);const caption=document.createElement('div');caption.className='axis-title';caption.dataset.axis='x';caption.style.cssText='text-align:center;font-size:12px;margin-top:12px;color:var(--foreground)';caption.textContent=data.horizontal?data.yAxis.title:data.xAxis.title;root.append(caption);\n const tip=document.createElement('div');tip.className='xy-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);const view=createMermaidViewport(root),reduced=matchMedia('(prefers-reduced-motion:reduce)');let buttons=[],motions=[],animations=[],state;\n const categories=data.xAxis.type==='band'?data.xAxis.categories:[...new Set(data.plots.flatMap(p=>p.points.map(d=>d.x)))];\n function clear(){tip.hidden=true;root.querySelectorAll('[data-guide]').forEach(n=>n.remove());root.querySelectorAll('[data-category]').forEach(n=>n.style.opacity='');}\n // 以当前分类的真实柱子和点为避让对象;侧向放置并缩窄浮层,禁止盖住选中标记。\n function placeTip(index,event){\n const rr=root.getBoundingClientRect(),vr=viewport.getBoundingClientRect(),marks=[...svg.querySelectorAll(`[data-category=\"${index}\"]`)].map(n=>n.getBoundingClientRect()).filter(b=>b.right>=vr.left&&b.left<=vr.right&&b.bottom>=vr.top&&b.top<=vr.bottom);\n const fallback=buttons[index].getBoundingClientRect(),left=Math.max(vr.left,marks.length?Math.min(...marks.map(b=>b.left)):fallback.left)-rr.left,right=Math.min(vr.right,marks.length?Math.max(...marks.map(b=>b.right)):fallback.right)-rr.left,top=Math.max(vr.top,marks.length?Math.min(...marks.map(b=>b.top)):fallback.top)-rr.top,bottom=Math.min(vr.bottom,marks.length?Math.max(...marks.map(b=>b.bottom)):fallback.bottom)-rr.top;\n const gap=12,spaces={left:Math.max(0,left-gap-4),right:Math.max(0,root.clientWidth-right-gap-4)},side=spaces.right>=Math.min(240,spaces.left)?'right':'left',width=Math.min(240,spaces[side]);\n tip.style.width=width+'px';tip.style.maxWidth=width+'px';tip.style.overflowWrap='anywhere';\n const pointer=event?.type?.startsWith('pointer'),y=pointer?Math.max(top,Math.min(bottom,event.clientY-rr.top)):(top+bottom)/2,h=tip.offsetHeight;\n tip.style.left=(side==='right'?right+gap:left-gap-width)+'px';tip.style.top=Math.max(4,Math.min(root.clientHeight-h-4,y-h/2))+'px';tip.dataset.placement=side;\n }\n function show(index,event){clear();const value=categories[index];root.querySelectorAll('[data-category]').forEach(n=>n.style.opacity=+n.dataset.category===index?'1':'.18');tip.replaceChildren();const name=document.createElement('div');name.className='xy-tip-title';name.textContent=value;tip.append(name);for(const [j,p]of data.plots.entries()){const d=data.xAxis.type==='band'?p.points[index]:p.points.find(d=>d.x===value);if(!d)continue;const row=document.createElement('div');row.className='xy-tip-row';const swatch=document.createElement('span');swatch.style.cssText=`width:8px;height:${p.type==='line'?2:8}px;border-radius:2px;background:${color(j)}`;swatch.setAttribute('aria-hidden','true');const label=document.createElement('span');label.className='xy-tip-name';label.textContent=p.name;const number=document.createElement('span');number.className='xy-tip-value';number.textContent=d.y;row.append(swatch,label,number);tip.append(row);}tip.hidden=false;placeTip(index,event);if(state.horizontal)el('line',{x1:state.left,x2:state.right,y1:state.position(value,index),y2:state.position(value,index),stroke:'var(--border)','data-guide':true});else el('line',{x1:state.position(value,index),x2:state.position(value,index),y1:state.top,y2:state.bottom,stroke:'var(--border)','data-guide':true});}\n\n function cancel(){animations.forEach(a=>a.cancel());animations=[];}\n function draw(){cancel();clear();svg.replaceChildren();buttons.forEach(n=>n.remove());buttons=[];motions=[];const minSlot=data.horizontal?Math.min(120,Math.max(56,...categories.map(v=>String(v).length*12+8))):16,width=Math.max(viewport.clientWidth,data.horizontal?480:84+categories.length*minSlot),horizontal=data.horizontal,left=horizontal?138:64,right=width-20,top=32,bottom=horizontal?top+Math.max(260,categories.length*Math.max(40,...categories.map(v=>Math.ceil(String(v).length/9)*18+12))):292,plotWidth=right-left;\n const axis=data.yAxis,values=data.plots.flatMap(p=>p.points.map(d=>d.y));let min=axis.min,max=axis.max;if(!Number.isFinite(min)||!Number.isFinite(max)){min=Math.min(0,...values);max=Math.max(0,...values);}if(min===max){min-=1;max+=1;}\n const categoryBand=(horizontal?(bottom-top):plotWidth)/Math.max(1,categories.length);const x=data.xAxis.type==='band'?d3.scalePoint(categories.map((_,i)=>i),horizontal?[top+categoryBand/2,bottom-categoryBand/2]:[left+categoryBand/2,right-categoryBand/2]):d3.scaleLinear([data.xAxis.min,data.xAxis.max],horizontal?[top+16,bottom-16]:[left+16,right-16]);const y=d3.scaleLinear([min,max],horizontal?[left,right]:[bottom,top]);const position=(value,index)=>x(data.xAxis.type==='band'?index:value);state={horizontal,left,right,top,bottom,x,y,position};reference.hidden=horizontal||view.expanded;reference.replaceChildren();reference.style.top=viewport.offsetTop+'px';reference.style.height=(bottom+8)+'px';const unit=document.createElement('span');unit.textContent=data.yAxis.title;unit.title=data.yAxis.title;unit.style.cssText='position:absolute;top:0;left:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:'+Math.max(60,viewport.clientWidth-8)+'px';reference.append(unit);for(const v of y.ticks(width<400?4:6)){const label=document.createElement('span');label.textContent=v;label.style.cssText='position:absolute;right:12px;color:var(--muted-foreground);top:'+(y(v)-8)+'px';reference.append(label);}\n svg.setAttribute('viewBox',`0 0 ${width} 380`);svg.style.width=width+'px';svg.style.height='380px';svg.setAttribute('role','img');svg.setAttribute('aria-label',data.plots.map(p=>p.name+':'+p.points.map(d=>d.x+' '+d.y).join(',')).join(';'));scene.style.width=scene.style.minWidth=width+'px';scene.style.height='400px';\n for(const v of y.ticks(width<400?4:6)){if(horizontal){el('line',{x1:y(v),x2:y(v),y1:top,y2:bottom,stroke:'var(--border)'});text(v,{x:y(v),y:bottom+22,'text-anchor':'middle',fill:'var(--muted-foreground)'});}else{el('line',{x1:left,x2:right,y1:y(v),y2:y(v),stroke:'var(--border)'});text(v,{x:left-12,y:y(v)+4,'text-anchor':'end',fill:'var(--muted-foreground)'});}}\n const axisTitle=text(horizontal?data.yAxis.title:data.xAxis.title,{x:(left+right)/2,y:370,'text-anchor':'middle','data-axis':'x',class:'axis-title'});text(horizontal?data.xAxis.title:data.yAxis.title,{x:horizontal?8:left,y:18,'data-axis':'y',class:'axis-title',visibility:(!horizontal&&!view.expanded)?'hidden':'visible'});\n const bars=data.plots.filter(p=>p.type==='bar'),band=(horizontal?(bottom-top):plotWidth)/Math.max(1,categories.length),barWidth=Math.min(24,band*.6/Math.max(1,bars.length));let barIndex=0;\n data.plots.forEach((p,j)=>{if(p.type==='bar'){const offset=(barIndex++-(bars.length-1)/2)*barWidth;for(const d of p.points){const a=position(d.x,d.index)+offset,b=y(d.y),zero=y(Math.max(min,Math.min(max,0)));const n=el('rect',{x:horizontal?Math.min(b,zero):a-barWidth/2,y:horizontal?a-barWidth/2:Math.min(b,zero),width:horizontal?Math.abs(b-zero):barWidth,height:horizontal?barWidth:Math.abs(b-zero),fill:color(j),'data-mark':p.id,'data-category':data.xAxis.type==='band'?d.index:categories.indexOf(d.x)});n.style.transformOrigin=horizontal?`${zero}px 0px`:`0px ${zero}px`;motions.push({n,bar:true,zero,horizontal});}}else{const points=p.points.map(d=>horizontal?[y(d.y),position(d.x,d.index)]:[position(d.x,d.index),y(d.y)]),path=el('path',{d:d3.line()(points),fill:'none',stroke:color(j),'stroke-width':1.5,'data-mark':p.id});motions.push({n:path,line:true});for(const [i,[a,b]]of points.entries())el('circle',{cx:a,cy:b,r:3,fill:color(j),'data-category':data.xAxis.type==='band'?p.points[i].index:categories.indexOf(p.points[i].x)});}});\n const metrics=document.createElement('canvas').getContext('2d');metrics.font='12px '+getComputedStyle(root).fontFamily;const labelWidth=Math.min(160,Math.max(24,...categories.map(v=>Math.ceil(metrics.measureText(String(v)).width))))+8,every=horizontal?1:Math.max(1,Math.ceil((labelWidth+8)/band));categories.forEach((value,i)=>{const b=document.createElement('button');b.type='button';b.className='xy-label';b.textContent=value;b.title=String(value);const w=horizontal?left-14:Math.min(labelWidth,every*band-4);b.style.left=(horizontal?0:Math.max(4,Math.min(width-w-4,position(value,i)-w/2)))+'px';b.style.top=(horizontal?position(value,i)-18:bottom+12)+'px';b.style.width=w+'px';if(!horizontal){b.style.whiteSpace='nowrap';b.style.overflow='hidden';b.style.textOverflow='ellipsis';if(i%every)b.style.cssText+=';height:1px;padding:0;clip-path:inset(50%)';}b.onpointerenter=b.onclick=()=>show(i);b.onfocus=()=>{closed=false;focused=i;const v=viewport.getBoundingClientRect(),r=b.getBoundingClientRect(),margin=data.horizontal?12:68;if(r.left<v.left+margin)viewport.scrollLeft-=v.left+margin-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;show(i);restore();};b.onpointerleave=clear;b.onblur=()=>{focused=null;++epoch;clear();};scene.append(b);buttons.push(b);});\n const labelBottom=Math.max(bottom+24,...buttons.map(b=>b.offsetTop+b.offsetHeight));const figureHeight=labelBottom+16;axisTitle.setAttribute('y',labelBottom+32);axisTitle.setAttribute('visibility','hidden');svg.setAttribute('viewBox',`0 0 ${width} ${figureHeight}`);svg.style.height=figureHeight+'px';scene.style.height=figureHeight+'px';\n const hit=el('rect',{x:left,y:top,width:Math.max(1,plotWidth),height:bottom-top,fill:'transparent','data-chart-hit':true,'data-chart-hover-overlay':'cross-series'});hit.onpointermove=e=>{const r=svg.getBoundingClientRect(),coordinate=(horizontal?e.clientY-r.top:e.clientX-r.left)/view.scale;let best=0;categories.forEach((v,i)=>{if(Math.abs(position(v,i)-coordinate)<Math.abs(position(categories[best],best)-coordinate))best=i;});show(best,e);};hit.onpointerleave=clear;\n const key=root.querySelector('.xy-key')||document.createElement('div');key.className='xy-key';key.replaceChildren();key.hidden=data.plots.length<2;data.plots.forEach((p,i)=>{const item=document.createElement('span'),sw=document.createElement('span');sw.style.width='12px';sw.style.height=p.type==='line'?'2px':'8px';sw.style.background=color(i);item.append(sw,document.createTextNode(p.name));key.append(item);});if(!key.parentElement)root.append(key);view.refresh();}\n function replay(){cancel();if(reduced.matches||view.expanded)return;motions.forEach(m=>{if(m.line){const length=m.n.getTotalLength();animations.push(animateMermaidEntry(m.n,[{strokeDasharray:length,strokeDashoffset:length},{strokeDasharray:length,strokeDashoffset:0}],{duration:1200}));}else animations.push(animateMermaidEntry(m.n,[{transform:m.horizontal?'scaleX(0)':'scaleY(0)'},{transform:m.horizontal?'scaleX(1)':'scaleY(1)'}],{duration:650}));});}\n draw();root.addEventListener('qv:display-mode',()=>{draw();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});let last=root.clientWidth;new ResizeObserver(()=>requestAnimationFrame(()=>{if(root.clientWidth!==last){last=root.clientWidth;draw();}})).observe(root);let armed=true;new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);let focused=null,closed=false,epoch=0;function restore(){const id=++epoch;requestAnimationFrame(()=>requestAnimationFrame(()=>{if(id===epoch&&!closed&&focused!==null&&document.activeElement===buttons[focused])show(focused);}));}document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;cancel();clear();}else{const r=root.getBoundingClientRect(),visibleHeight=Math.max(0,Math.min(r.bottom,innerHeight)-Math.max(r.top,0));if(armed&&visibleHeight>=r.height*.2){armed=false;replay();}}});reduced.addEventListener('change',cancel);root.addEventListener('keydown',e=>{if(e.key==='Escape'){closed=true;++epoch;clear();}});viewport.addEventListener('scroll',()=>{clear();restore();},{passive:true});root.addEventListener('qv:display-mode',()=>{focused=null;++epoch;clear();});\n}\n\n// 固定版本返回 Map;保留标签顺序,不重排或合并小项。\nasync function readMermaidPieModel(db,descriptions={}) {\n if(!db.getSections||!db.getShowData)return {reason:'Pie 数据接口不兼容'};\n const items=[...db.getSections()].map(([name,value],i)=>({id:'slice-'+(i+1),name,value,description:descriptions[name]||''}));\n const total=items.reduce((s,d)=>s+d.value,0);\n if(!items.length||!Number.isFinite(total)||total<=0||items.some(d=>!Number.isFinite(d.value)||d.value<0))return {reason:'饼图需要有效的非负数与正总量'};\n if(items.some(d=>hasRichMermaidLabel(d.name)))return {reason:'富文本饼图保留原生语义'};\n const unknown=Object.keys(descriptions).filter(name=>!items.some(d=>d.name===name));if(unknown.length)throw Error('说明引用了未知分类:'+unknown.join('、'));\n return {data:{kind:'pie',items,total,showData:db.getShowData(),title:db.getDiagramTitle?.()||''}};\n}\n\nasync function renderMermaidDonutPreset(presetRoot,config,summary){\n const d3=window.__mcxQVRuntime.d3;\n class PresetResizeObserver{constructor(callback){let frame=0;this.observer=new ResizeObserver(()=>{if(!frame)frame=requestAnimationFrame(()=>{frame=0;callback();});});}observe(target){this.observer.observe(target);}}\n const escape=s=>String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\"','"');\n presetRoot.innerHTML=\"\\n\\u003cstyle\\u003e\\n#@@ID@@{position:relative;padding-top:32px;width:100%;color:var(--foreground);--ring-enter:650}\\n#@@ID@@ h3{margin:0 90px 32px 0;font-weight:500}\\n#@@ID@@ .plot{position:relative}\\n#@@ID@@ .direct-label{position:absolute;transform:translate(-50%,-50%);background:transparent;border:0;padding:5px;color:var(--foreground);font:inherit;font-size:14px;text-align:center;white-space:nowrap;cursor:var(--cursor-interaction,pointer)}\\n#@@ID@@ .direct-label span{display:block;margin-top:4px;color:var(--muted-foreground);font-size:13px;font-variant-numeric:tabular-nums}\\n#@@ID@@ .legend[hidden]{display:none}\\n#@@ID@@ .chart{width:100%;display:block}\\n#@@ID@@ .center{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;pointer-events:none;width:140px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px}\\n#@@ID@@ .center-value{line-height:1;white-space:nowrap;font-size:calc(var(--font-size-base,14px)*2.3);font-weight:500;letter-spacing:-.04em;font-variant-numeric:tabular-nums}\\n#@@ID@@ .center-label{line-height:1.4;font-size:12px;color:var(--muted-foreground)}\\n#@@ID@@ .legend{display:flex;flex-wrap:wrap;justify-content:center;gap:8px 22px;margin-top:20px}\\n#@@ID@@ .legend button{font:inherit;font-size:12px;color:var(--foreground);background:transparent;border:0;border-radius:0;display:inline-flex;align-items:center;gap:7px;padding:4px 0;cursor:var(--cursor-interaction,pointer)}\\n#@@ID@@ .swatch{width:9px;height:9px;border-radius:2px;display:inline-block;flex-shrink:0}\\n#@@ID@@ .percent{color:var(--muted-foreground);font-variant-numeric:tabular-nums}\\n#@@ID@@ .slice{transition:opacity 160ms ease;cursor:var(--cursor-interaction,pointer)}\\n#@@ID@@ .bubble{position:absolute;z-index:2;width:176px;max-width:calc(100% - 16px);padding:12px 14px;box-sizing:border-box;background:var(--popover);color:var(--popover-foreground);border:1px solid var(--border);border-radius:12px;box-shadow:0 4px 16px color-mix(in srgb,var(--foreground) 10%,transparent);pointer-events:none;font-size:12px}\\n#@@ID@@ .bubble[hidden]{display:none}\\n#@@ID@@ .bubble-title{font-weight:500;margin-bottom:10px}\\n#@@ID@@ .bubble-row{display:flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums}\\n#@@ID@@ .bubble-percent{margin-left:auto;color:var(--muted-foreground)}\\n@media(prefers-reduced-motion:reduce){#@@ID@@ .slice{transition:none}}\\n@media(pointer:coarse){#@@ID@@ .legend button{min-height:44px}}\\n\\u003c/style\\u003e\\n\\u003ch3 id=\\\"@@ID@@-title\\\"\\u003e@@TITLE@@\\u003c/h3\\u003e\\n\\u003cdiv class=\\\"plot\\\"\\u003e\\u003csvg class=\\\"chart\\\" role=\\\"img\\\" aria-label=\\\"@@SUMMARY@@\\\"\\u003e\\u003c/svg\\u003e\\u003cdiv class=\\\"center\\\" aria-hidden=\\\"true\\\"\\u003e\\u003cspan class=\\\"center-value\\\"\\u003e\\u003c/span\\u003e\\u003cspan class=\\\"center-label\\\"\\u003e\\u003c/span\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\n\\u003cdiv class=\\\"legend\\\" aria-label=\\\"来源及占比\\\"\\u003e\\u003c/div\\u003e\\n\\u003cdiv class=\\\"bubble\\\" role=\\\"tooltip\\\" hidden\\u003e\\u003cdiv class=\\\"bubble-title\\\"\\u003e\\u003c/div\\u003e\\u003cdiv class=\\\"bubble-row\\\"\\u003e\\u003cspan class=\\\"swatch\\\"\\u003e\\u003c/span\\u003e\\u003cspan class=\\\"bubble-value\\\"\\u003e\\u003c/span\\u003e\\u003cspan class=\\\"bubble-percent\\\"\\u003e\\u003c/span\\u003e\\u003c/div\\u003e\\u003cdiv class=\\\"description text-small\\\" style=\\\"margin-top:8px\\\" hidden\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\n\\n\\n\".replaceAll('@@ID@@',presetRoot.id).replaceAll('@@TITLE@@',escape(config.title)).replaceAll('@@SUMMARY@@',escape(summary));\n \n(()=>{\nconst root=presetRoot,plot=root.querySelector('.plot'),svg=d3.select(root.querySelector('.chart')),legend=root.querySelector('.legend'),bubble=root.querySelector('.bubble');\n\nconst numberFormat=new Intl.NumberFormat(undefined,{minimumFractionDigits:config.decimals,maximumFractionDigits:config.decimals});const formatValue=v=>config.valuePrefix+(config.exactValues?String(v):numberFormat.format(v))+config.valueSuffix;\nconst data=config.items;\nconst total=d3.sum(data,d=>d.value),color=i=>`var(--viz-series-${i%6+1})`,pct=i=>`${Number((data[i].value/total*100).toFixed(1))}%`;\nconst reduced=window.matchMedia('(prefers-reduced-motion: reduce)');let layout=null,entered=false,lastWidth=0;\nlet direct=false;const eligibleDirect=data.length<=4&&d3.min(data,d=>d.value/total)>=.08&&data.every(d=>d.label.length<=12);\nfunction clear(){svg.selectAll('.slice').attr('opacity',1);root.querySelectorAll('.legend button,.direct-label').forEach(b=>b.style.opacity=1);root.querySelector('.center-value').textContent=config.valuePrefix+numberFormat.format(total);root.querySelector('.center-label').textContent='总计'+(config.valueSuffix.trim()?' · '+config.valueSuffix.trim():'');bubble.hidden=true;}\nfunction show(i){if(!layout)return;svg.selectAll('.slice').attr('opacity',d=>d.index===i?1:.25);root.querySelectorAll('.legend button,.direct-label').forEach((b,j)=>b.style.opacity=j===i?1:.55);root.querySelector('.center-value').textContent=pct(i);root.querySelector('.center-label').textContent=data[i].label;\nif(config.showTooltip===false)return;\nroot.querySelector('.bubble-title').textContent=data[i].label;const description=root.querySelector('.description');description.textContent=data[i].description||'';description.hidden=!data[i].description;root.querySelector('.bubble-value').textContent=formatValue(data[i].value);root.querySelector('.bubble-percent').textContent=pct(i);root.querySelector('.bubble .swatch').style.background=color(i);bubble.hidden=false;\nconst d=layout.arcs[i],angle=(d.startAngle+d.endAngle)/2,px=layout.cx+Math.sin(angle)*layout.radius,py=layout.cy-Math.cos(angle)*layout.radius;\nconst rb=root.getBoundingClientRect(),pb=plot.getBoundingClientRect(),bw=bubble.offsetWidth,bh=bubble.offsetHeight;\nbubble.style.left=Math.max(8,Math.min(rb.width-bw-8,px>=layout.cx?px+14:px-bw-14))+'px';bubble.style.top=Math.max(pb.top-rb.top+8,Math.min(pb.top-rb.top+py-bh/2,pb.bottom-rb.top-bh-8))+'px';}\ndata.forEach((d,i)=>{const b=document.createElement('button');b.type='button';b.innerHTML=`<span class=\"swatch\" style=\"background:${color(i)}\" aria-hidden=\"true\"></span><span class=\"name\"></span><span class=\"percent\"></span>`;b.querySelector('.name').textContent=d.label;b.querySelector('.percent').textContent=pct(i);b.addEventListener('pointerenter',()=>show(i));b.addEventListener('pointerleave',clear);b.addEventListener('focus',()=>show(i));b.addEventListener('blur',clear);legend.append(b);});\nfunction draw(animate=false){const width=Math.round(plot.getBoundingClientRect().width);if(width<1)return;lastWidth=width;direct=eligibleDirect&&width>=420;const height=direct?340:280,cx=width/2,cy=height/2,radius=direct?Math.min(104,width/2-50):Math.min(118,width/2-16),inner=radius*.75;\n// 间隔不参与角度分配;随最小正值缩小,避免零值和极小值撑大缺口。\nconst arcs=d3.pie().sort(null).value(d=>d.value)(data),positiveArcs=arcs.filter(d=>d.data.value>0),smallestAngle=d3.min(positiveArcs,d=>d.endAngle-d.startAngle),gap=positiveArcs.length>1?Math.min(.025,smallestAngle*.2):0;\n// 保留接近整圆时的微小端点差,避免三位小数将两端舍入为同一点。\nconst arc=d3.arc().digits(9).innerRadius(inner).outerRadius(radius).padAngle(gap).cornerRadius(Math.min(3,radius*smallestAngle*.2)),enter=parseFloat(getComputedStyle(root).getPropertyValue('--ring-enter'));layout={width,cx,cy,radius,arcs};\nsvg.selectAll('*').interrupt();svg.selectAll('*').remove();svg.attr('viewBox',`0 0 ${width} ${height}`);const g=svg.append('g').attr('transform',`translate(${cx},${cy})`);\nconst slices=g.selectAll('.slice').data(positiveArcs).join('path').attr('class','slice').attr('fill',d=>color(d.index)).attr('d',arc).on('pointerenter pointermove',(_,d)=>show(d.index)).on('pointerleave pointercancel',clear).on('pointerup',e=>{if(e.pointerType==='touch')clear();});\nplot.querySelectorAll('.direct-label').forEach(e=>{e.getAnimations().forEach(a=>a.cancel());e.remove();});legend.hidden=direct;\nif(direct){arcs.forEach((d,i)=>{const a=(d.startAngle+d.endAngle)/2,side=Math.sin(a)>0?1:-1,narrow=width<460;\nconst peers=arcs.filter(q=>(Math.sin((q.startAngle+q.endAngle)/2)>0?1:-1)===side).sort((p,q)=>Math.cos((q.startAngle+q.endAngle)/2)-Math.cos((p.startAngle+p.endAngle)/2));const rank=peers.indexOf(d);\nconst x=narrow?cx+(i<2?1:-1)*width*.28:cx+Math.sin(a)*(radius+64),y=narrow?(i===0||i===3?32:height-32):cy-Math.cos(a)*(radius+48);\nconst b=document.createElement('button');b.type='button';b.className='direct-label';b.style.left=x+'px';b.style.top=y+'px';b.append(document.createTextNode(data[i].label));const percent=document.createElement('span');percent.textContent=(config.showData?data[i].value+' · ':'')+pct(i);b.append(percent);b.onpointerenter=()=>show(i);b.onpointerleave=clear;b.onfocus=()=>show(i);b.onblur=clear;plot.append(b);\nif(animate&&!reduced.matches){\nconst delay=enter*(.2+i*.05);\nanimateMermaidEntry(b,[{opacity:0},{opacity:1}],{delay,duration:enter*.4,easing:'ease',fill:'backwards'});\n}\nconst px=cx+Math.sin(a)*(radius+7),py=cy-Math.cos(a)*(radius+7);\n});}\nif(animate&&!reduced.matches){\n// 细分遮罩只控制显现节奏,不改变实际数据分段。\nconst id=root.id+'-reveal',mask=svg.append('defs').append('mask').attr('id',id).attr('maskUnits','userSpaceOnUse').attr('x',-radius-2).attr('y',-radius-2).attr('width',radius*2+4).attr('height',radius*2+4);\nconst segments=d3.range(120),unit=Math.PI*2/120;\nconst reveal=d3.arc().innerRadius(inner-2);\nconst cells=mask.selectAll('path').data(segments).join('path').attr('fill','white').attr('d',i=>reveal.outerRadius(inner-2)({startAngle:i*unit-.002,endAngle:(i+1)*unit+.002}));\ng.attr('mask',`url(#${id})`);\ncells.transition().delay(i=>i/(segments.length-1)*enter*.55).duration(enter*.45).ease(mermaidEntryProgress).attrTween('d',i=>t=>reveal.outerRadius(inner-2+(radius-inner+4)*t)({startAngle:i*unit-.002,endAngle:(i+1)*unit+.002}));\ng.transition().delay(enter).on('end',()=>{g.attr('mask',null);mask.remove();});\n}clear();}\nroot.addEventListener('pointerleave',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});window.addEventListener('blur',clear);\ndraw();if(!reduced.matches)svg.attr('visibility','hidden');let inView=false,armed=true;\nfunction replayIfReady(){if(inView&&armed&&!document.hidden){armed=false;entered=true;svg.attr('visibility','visible');draw(true);}}\nconst observer=new IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){inView=false;armed=true;}else if(e.intersectionRatio>=.2){inView=true;replayIfReady();}},{threshold:[0,.2]});\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else replayIfReady();});\nwindow.addEventListener('pageshow',e=>{if(e.persisted){armed=true;replayIfReady();}});observer.observe(root);\nnew PresetResizeObserver(()=>{if(Math.round(plot.getBoundingClientRect().width)!==lastWidth)draw();}).observe(plot);\nroot.dispatchEvent(new Event('qv:ready',{bubbles:true}));\n})();\n\n}\nasync function renderMermaidRadarPreset(presetRoot,config,summary){\n \n class PresetResizeObserver{constructor(callback){let frame=0;this.observer=new ResizeObserver(()=>{if(!frame)frame=requestAnimationFrame(()=>{frame=0;callback();});});}observe(target){this.observer.observe(target);}}\n const escape=s=>String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\"','"');\n presetRoot.innerHTML=\"\\n \\u003cstyle\\u003e\\n #@@ID@@ {\\n position: relative;\\n display: grid;\\n gap: 0;\\n padding-top:32px;\\n width: 100%;\\n color: var(--foreground);\\n }\\n\\n #@@ID@@ .quick-radar-title {\\n margin: 0 90px 32px 0;\\n color: var(--foreground);\\n font-weight: 500;\\n }\\n\\n #@@ID@@ .quick-radar-legend {\\n display: flex;\\n flex-wrap: wrap;\\n gap: 8px 22px;\\n justify-content:center;\\n margin-top:20px;\\n }\\n\\n #@@ID@@ .quick-radar-legend[hidden] {\\n display: none;\\n }\\n\\n #@@ID@@ .quick-radar-legend-button {\\n display: inline-flex;\\n align-items: center;\\n gap: 6px;\\n margin: 0;\\n padding: 2px 0;\\n color: var(--foreground);\\n font: inherit;\\n background: transparent;\\n border: 0;\\n cursor: var(--cursor-interaction, pointer);\\n }\\n\\n #@@ID@@ .quick-radar-legend-button.is-dimmed {\\n color: var(--muted-foreground);\\n opacity: 0.32;\\n }\\n\\n #@@ID@@ .quick-radar-swatch {\\n width: 9px;\\n height: 9px;\\n flex: 0 0 auto;\\n background: currentColor;\\n }\\n\\n #@@ID@@ .quick-radar-swatch[data-shape=\\\"circle\\\"] {\\n border-radius: 50%;\\n }\\n\\n #@@ID@@ .quick-radar-swatch[data-shape=\\\"diamond\\\"] {\\n transform: rotate(45deg) scale(0.78);\\n }\\n\\n #@@ID@@ .quick-radar-swatch[data-shape=\\\"triangle\\\"] {\\n clip-path: polygon(50% 0, 100% 100%, 0 100%);\\n }\\n\\n #@@ID@@ .quick-radar-plot {\\n position: relative;\\n min-width: 0;\\n }\\n\\n #@@ID@@ .quick-radar-svg {\\n display: block;\\n width: 100%;\\n height: auto;\\n overflow: visible;\\n }\\n\\n #@@ID@@ .quick-radar-grid,\\n #@@ID@@ .quick-radar-axis {\\n fill: transparent;\\n stroke: var(--border);\\n vector-effect: non-scaling-stroke;\\n }\\n\\n #@@ID@@ .quick-radar-grid {\\n stroke-opacity: 0.72;\\n }\\n\\n #@@ID@@ .quick-radar-axis {\\n stroke-opacity: 0.58;\\n }\\n\\n #@@ID@@ .quick-radar-axis-label,\\n #@@ID@@ .quick-radar-scale-label {\\n fill: var(--foreground);\\n font-size: 12px;\\n font-weight: 400;\\n }\\n\\n #@@ID@@ .quick-radar-scale-label {\\n fill: var(--muted-foreground);\\n }\\n\\n #@@ID@@ .quick-radar-series-polygon,\\n #@@ID@@ .quick-radar-marker {\\n transition: fill-opacity 160ms ease, stroke-opacity 160ms ease, opacity 160ms ease, stroke-width 160ms ease;\\n vector-effect: non-scaling-stroke;\\n }\\n\\n #@@ID@@ .quick-radar-series-polygon {\\n fill: currentColor;\\n stroke: currentColor;\\n stroke-linejoin: round;\\n }\\n\\n #@@ID@@ .quick-radar-marker {\\n fill: currentColor;\\n stroke: var(--background);\\n stroke-width: 1.5;\\n }\\n\\n #@@ID@@ .quick-radar-hit {\\n fill: transparent;\\n cursor: crosshair;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip {\\n position: absolute;\\n z-index: 2;\\n display: grid;\\n gap: 12px;\\n width:188px;\\n box-sizing:border-box;\\n max-width: min(260px, calc(100% - 16px));\\n padding: 12px 14px;\\n color: var(--popover-foreground);\\n background: var(--popover);\\n border: 1px solid var(--border);\\n border-radius: 12px;\\n box-shadow: 0 6px 18px color-mix(in srgb, var(--foreground) 10%, transparent);\\n pointer-events: none;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip[hidden] {\\n display: none;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip[data-mode=\\\"object\\\"] .quick-radar-tooltip-row {grid-template-columns:minmax(0,1fr) auto;}\\n\\n #@@ID@@ .quick-radar-tooltip-dimension {\\n display:flex;\\n align-items:center;\\n gap:8px;\\n color: var(--popover-foreground);\\n font-size:14px;\\n line-height:20px;\\n font-weight:600;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-rows {\\n display: grid;\\n gap: 4px;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-row {\\n display: grid;\\n grid-template-columns: auto minmax(0, 1fr) auto;\\n align-items: center;\\n gap: 6px;\\n font-size: 12px;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-row.is-active .quick-radar-tooltip-name {\\n color: var(--popover-foreground);\\n font-weight: 500;\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-name {\\n color: var(--popover-foreground);\\n }\\n\\n #@@ID@@ .quick-radar-tooltip-value {\\n color: light-dark(color-mix(in srgb,var(--popover-foreground) 64%,var(--popover)),color-mix(in srgb,var(--popover-foreground) 68%,var(--popover)));\\n font-variant-numeric: tabular-nums;\\n }\\n\\n @media (prefers-reduced-motion: reduce) {\\n #@@ID@@ .quick-radar-series-polygon,\\n #@@ID@@ .quick-radar-marker {\\n transition: none;\\n }\\n }\\n \\u003c/style\\u003e\\n\\n \\u003ch3 class=\\\"quick-radar-title\\\" id=\\\"@@ID@@-title\\\"\\u003e@@TITLE@@\\u003c/h3\\u003e\\n \\u003cp class=\\\"sr-only\\\" id=\\\"@@ID@@-summary\\\"\\u003e@@SUMMARY@@\\u003c/p\\u003e\\n\\n \\u003cdiv class=\\\"quick-radar-plot\\\" id=\\\"@@ID@@-plot\\\"\\u003e\\n \\u003csvg class=\\\"quick-radar-svg\\\" id=\\\"@@ID@@-svg\\\" role=\\\"img\\\" aria-label=\\\"@@SUMMARY@@\\\"\\u003e\\n \\u003cdesc id=\\\"@@ID@@-svg-desc\\\"\\u003e@@SUMMARY@@\\u003c/desc\\u003e\\n \\u003c/svg\\u003e\\n \\u003c/div\\u003e\\n\\n \\u003cdiv class=\\\"quick-radar-legend\\\" id=\\\"@@ID@@-legend\\\" aria-label=\\\"数据系列\\\"\\u003e\\u003c/div\\u003e\\n\\n \\u003cdiv class=\\\"quick-radar-tooltip\\\" id=\\\"@@ID@@-tooltip\\\" role=\\\"tooltip\\\" hidden\\u003e\\n \\u003cspan class=\\\"quick-radar-tooltip-dimension\\\"\\u003e\\u003c/span\\u003e\\n \\u003cdiv class=\\\"quick-radar-tooltip-rows\\\"\\u003e\\u003c/div\\u003e\\n \\u003c/div\\u003e\\n\\n \\n\".replaceAll('@@ID@@',presetRoot.id).replaceAll('@@TITLE@@',escape(config.title)).replaceAll('@@SUMMARY@@',escape(summary));\n \n (() => {\n const SVG_NS = 'http://www.w3.org/2000/svg';\n const root = presetRoot;\n const plot = root.querySelector(`#${presetRoot.id}-plot`);\n const svg = root.querySelector(`#${presetRoot.id}-svg`);\n const legend = root.querySelector(`#${presetRoot.id}-legend`);\n const tooltip = root.querySelector(`#${presetRoot.id}-tooltip`);\n const tooltipDimension = tooltip.querySelector('.quick-radar-tooltip-dimension');\n const tooltipRows = tooltip.querySelector('.quick-radar-tooltip-rows');\n \n const shapes = ['circle', 'square', 'diamond', 'triangle'];\n const numberFormat = new Intl.NumberFormat(undefined, {\n minimumFractionDigits: config.decimals,\n maximumFractionDigits: config.decimals,\n });\n const valueText = (value) => `${config.valuePrefix}${numberFormat.format(value)}${config.valueSuffix}`;\n const colorFor = (index) => `var(--viz-series-${index + 1})`;\n let focusedIndex = null;\n let hoveredIndex = null;\n let pointLocations = [];\n let seriesGroups = [];\n let geometry=null,motionFrame=0;\n\n const createSvg = (name, attributes = {}) => {\n const element = document.createElementNS(SVG_NS, name);\n Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, value));\n return element;\n };\n\n const activeIndex = () => hoveredIndex ?? focusedIndex;\n\n const updateLegend = () => {\n const active = activeIndex();\n legend.querySelectorAll('button').forEach((button) => {\n const index = Number(button.dataset.seriesIndex);\n\n button.classList.toggle('is-dimmed', active !== null && active !== index);\n });\n };\n\n const updateSeries = () => {\n const active = activeIndex();\n seriesGroups.forEach((entry, index) => {\n const isActive = active === index;\n const isDimmed = active !== null && !isActive;\n entry.polygon.setAttribute('fill-opacity', isActive ? '0.24' : isDimmed ? '0.03' : '0.14');\n entry.polygon.setAttribute('stroke-opacity', isDimmed ? '0.1' : '1');\n entry.polygon.setAttribute('stroke-width', isActive ? '2.2' : '1.7');\n entry.markers.forEach((marker) => {\n marker.setAttribute('opacity', isDimmed ? '0.1' : isActive ? '1' : '0.78');\n marker.setAttribute('stroke-width', isActive ? '2' : '1.5');\n });\n });\n if (active !== null && seriesGroups[active]) {\n seriesGroups[active].layer.append(seriesGroups[active].group);\n }\n updateLegend();\n };\n\n const setHover = (index) => {\n if (hoveredIndex === index) return;\n hoveredIndex = index;\n updateSeries();\n };\n\n const clearHover = () => {\n if (hoveredIndex === null) return;\n hoveredIndex = null;\n updateSeries();\n };\n\n if (config.series.length <= 1) {\n legend.hidden = true;\n } else {\n config.series.forEach((series, index) => {\n const button = document.createElement('button');\n button.className = 'quick-radar-legend-button';\n button.type = 'button';\n button.dataset.seriesIndex = String(index);\n\n\n\n const swatch = document.createElement('span');\n swatch.className = 'quick-radar-swatch';\n swatch.dataset.shape = shapes[index];\n swatch.style.color = colorFor(index);\n swatch.setAttribute('aria-hidden', 'true');\n const label = document.createElement('span');\n label.textContent = series.name;\n button.append(swatch, label);\n\n button.addEventListener('mouseenter', () => setHover(index));\n button.addEventListener('mouseleave', clearHover);\n button.addEventListener('focus', () => setHover(index));\n button.addEventListener('blur', clearHover);\n\n legend.append(button);\n });\n }\n\n const markerPath = (shape, radius) => {\n if (shape === 'circle') {\n return `M ${-radius},0 A ${radius},${radius} 0 1,0 ${radius},0 A ${radius},${radius} 0 1,0 ${-radius},0`;\n }\n if (shape === 'diamond') {\n return `M 0,${-radius * 1.25} L ${radius},0 L 0,${radius * 1.25} L ${-radius},0 Z`;\n }\n if (shape === 'triangle') {\n return `M 0,${-radius * 1.2} L ${radius * 1.05},${radius} L ${-radius * 1.05},${radius} Z`;\n }\n return `M ${-radius},${-radius} L ${radius},${-radius} L ${radius},${radius} L ${-radius},${radius} Z`;\n };\n\n const splitLabel = (label, narrow) => {\n const characters = Array.from(label);\n const chunkSize = narrow ? 4 : 9;\n const lines = [];\n for (let index = 0; index < characters.length; index += chunkSize) {\n lines.push(characters.slice(index, index + chunkSize).join(''));\n }\n return lines;\n };\n\n const polygonPoints = (cx, cy, radius, ratio = 1) => config.dimensions.map((_, index) => {\n const angle = -Math.PI / 2 + index * Math.PI * 2 / config.dimensions.length;\n return [cx + Math.cos(angle) * radius * ratio, cy + Math.sin(angle) * radius * ratio];\n });\n\n const pointsAttribute = (points) => points.map(([x, y]) => `${x},${y}`).join(' ');\n\n const showTooltip = (event, dimensionIndex, nearestSeriesIndex) => {\n const objectMode=dimensionIndex===null;\n tooltip.dataset.mode=objectMode?'object':'dimension';\n tooltipDimension.textContent = objectMode?config.series[nearestSeriesIndex].name:config.dimensions[dimensionIndex];\n if(objectMode){const swatch=document.createElement('span');swatch.className='quick-radar-swatch';swatch.dataset.shape=shapes[nearestSeriesIndex];swatch.style.color=colorFor(nearestSeriesIndex);swatch.setAttribute('aria-hidden','true');tooltipDimension.prepend(swatch);}\n tooltipRows.replaceChildren();\n const rows=objectMode?config.dimensions.map((name,i)=>({name,score:config.series[nearestSeriesIndex].values[i],color:nearestSeriesIndex})):config.series.map((series,i)=>({name:series.name,score:series.values[dimensionIndex],color:i}));\n rows.forEach((series, index) => {\n const row = document.createElement('div');\n row.className = 'quick-radar-tooltip-row';\n row.classList.toggle('is-active', objectMode);\n const swatch = document.createElement('span');\n swatch.className = 'quick-radar-swatch';\n swatch.dataset.shape = shapes[series.color];\n swatch.style.color = colorFor(series.color);\n swatch.setAttribute('aria-hidden', 'true');\n const name = document.createElement('span');\n name.className = 'quick-radar-tooltip-name';\n name.textContent = series.name;\n const value = document.createElement('span');\n value.className = 'quick-radar-tooltip-value';\n value.textContent = valueText(series.score);\n if(objectMode)row.append(name,value);else row.append(swatch,name,value);\n tooltipRows.append(row);\n });\n tooltip.hidden = false;\n\n const rootBounds = root.getBoundingClientRect();\n const plotBounds = plot.getBoundingClientRect();\n const tooltipBounds = tooltip.getBoundingClientRect();\n const bw=tooltipBounds.width,bh=tooltipBounds.height,pad=8,gap=16;\n const ox=plotBounds.left-rootBounds.left,oy=plotBounds.top-rootBounds.top;\n const sx=plotBounds.width/svg.viewBox.baseVal.width,sy=plotBounds.height/svg.viewBox.baseVal.height;\n const polygons=seriesGroups.map(e=>e.points.map(p=>({x:ox+p.x*sx,y:oy+p.y*sy})));\n const minY=root.querySelector('h3').getBoundingClientRect().bottom-rootBounds.top+pad,maxY=Math.max(minY,rootBounds.height-bh-pad),maxX=Math.max(pad,rootBounds.width-bw-pad);\n const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));\n const px=event.clientX-rootBounds.left,py=event.clientY-rootBounds.top;\n // 所有候选都紧邻光标,边界约束后再比较附近的遮挡。\n const positions=[];\n for(const offset of [16,28,40]){\n for(const fraction of [0,.25,.5,.75,1])positions.push([px+offset,py-bh*fraction],[px-offset-bw,py-bh*fraction],[px-bw*fraction,py-offset-bh],[px-bw*fraction,py+offset]);\n positions.push([px+offset,py+offset],[px+offset,py-offset-bh],[px-offset-bw,py+offset],[px-offset-bw,py-offset-bh]);\n }\n const candidates=positions.map(([x,y],i)=>({x:clamp(x,pad,maxX),y:clamp(y,minY,maxY),order:i}));\n const overlap=(poly,box)=>{\n let pts=poly;\n for(const [axis,edge,sign] of [['x',box.x,1],['x',box.x+bw,-1],['y',box.y,1],['y',box.y+bh,-1]]){\n const out=[];for(let i=0;i<pts.length;i++){const a=pts[i],b=pts[(i+1)%pts.length],ai=sign*(a[axis]-edge)>=0,bi=sign*(b[axis]-edge)>=0;if(ai)out.push(a);if(ai!==bi){const t=(edge-a[axis])/(b[axis]-a[axis]);out.push({x:a.x+t*(b.x-a.x),y:a.y+t*(b.y-a.y)});}}pts=out;if(!pts.length)return 0;\n }\n return Math.abs(pts.reduce((sum,p,i)=>{const q=pts[(i+1)%pts.length];return sum+p.x*q.y-q.x*p.y;},0))/2;\n };\n const labels=Array.from(svg.querySelectorAll('.quick-radar-axis-label')).map(e=>{const r=e.getBoundingClientRect();return {x:r.left-rootBounds.left,y:r.top-rootBounds.top,w:r.width,h:r.height};});\n candidates.forEach(c=>{\n c.distance=Math.hypot(Math.max(c.x-px,0,px-c.x-bw),Math.max(c.y-py,0,py-c.y-bh));\n c.coversPointer=c.distance<gap/2?1:0;\n c.area=polygons.reduce((sum,p)=>sum+overlap(p,c),0);\n c.labels=labels.reduce((sum,r)=>sum+Math.max(0,Math.min(c.x+bw,r.x+r.w)-Math.max(c.x,r.x))*Math.max(0,Math.min(c.y+bh,r.y+r.h)-Math.max(c.y,r.y)),0);\n });\n candidates.splice(0,candidates.length,...nearbyRadarDetails(candidates));candidates.sort((a,b)=>a.coversPointer-b.coversPointer||Math.round(a.area/200)-Math.round(b.area/200)||Math.round(a.labels/400)-Math.round(b.labels/400)||a.distance-b.distance||a.order-b.order);\n const origin=tooltip.offsetParent.getBoundingClientRect();tooltip.style.left=(rootBounds.left+candidates[0].x-origin.left)+'px';tooltip.style.top=(rootBounds.top+candidates[0].y-origin.top)+'px';\n };\n\n const hideTooltip = () => {\n tooltip.hidden = true;\n clearHover();updateSeries();\n svg.querySelectorAll('.quick-radar-axis').forEach(e=>{e.style.stroke='var(--border)';e.style.strokeOpacity='';e.setAttribute('stroke-opacity','.58');});\n };\n\n const draw = (animate=false) => {\n cancelAnimationFrame(motionFrame);motionFrame=0;hideTooltip();\n const width = Math.max(1, Math.round(plot.getBoundingClientRect().width));\n if (width <= 1) return;\n const narrow = width < 480;\n const denseNarrow = narrow && (\n config.dimensions.length > 6\n || config.dimensions.some((dimension) => Array.from(dimension).length > 4)\n );\n const height = narrow ? 330 : 380;\n const cx = width / 2;\n const cy = height / 2;\n const radius = Math.min(140, (width-132)/2, height * 0.36);\n svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n geometry={cx,cy,radius,axes:polygonPoints(cx,cy,radius)};\n seriesGroups.forEach(e=>e.group.getAnimations().forEach(a=>a.cancel()));\n plot.querySelectorAll('.dimension-key').forEach(e=>e.remove());\n svg.replaceChildren();\n\n const description = createSvg('desc', { id: `${presetRoot.id}-svg-desc` });\n description.textContent = config.summary;\n svg.append(description);\n\n const gridLayer = createSvg('g');\n [0.25, 0.5, 0.75, 1].forEach((ratio) => {\n gridLayer.append(createSvg('polygon', {\n class: 'quick-radar-grid',\n points: pointsAttribute(polygonPoints(cx, cy, radius, ratio)),\n }));\n const scaleLabel = createSvg('text', {\n class: 'quick-radar-scale-label',\n x: String(cx + 5),\n y: String(cy - radius * ratio + 13),\n });\n scaleLabel.textContent = numberFormat.format(config.maxValue * ratio);\n\n });\n\n polygonPoints(cx, cy, radius).forEach(([x, y]) => {\n gridLayer.append(createSvg('line', {\n class: 'quick-radar-axis',\n x1: String(cx),\n y1: String(cy),\n x2: String(x),\n y2: String(y),\n }));\n });\n svg.append(gridLayer);\n\n const seriesLayer = createSvg('g');\n seriesGroups = [];\n pointLocations = [];\n config.series.forEach((series, seriesIndex) => {\n const group = createSvg('g', { style: `color:${colorFor(seriesIndex)}` });\n const points = series.values.map((value, dimensionIndex) => {\n const angle = -Math.PI / 2 + dimensionIndex * Math.PI * 2 / config.dimensions.length;\n const distance = radius * value / config.maxValue;\n return {\n x: cx + Math.cos(angle) * distance,\n y: cy + Math.sin(angle) * distance,\n seriesIndex,\n dimensionIndex,\n };\n });\n const polygon = createSvg('polygon', {\n class: 'quick-radar-series-polygon',\n points: pointsAttribute(points.map(({ x, y }) => [x, y])),\n });\n group.append(polygon);\n const markers = points.map((point) => {\n const marker = createSvg('path', {\n class: 'quick-radar-marker',\n d: markerPath(shapes[seriesIndex], 2.8),\n transform: `translate(${point.x} ${point.y})`,\n });\n group.append(marker);\n pointLocations.push(point);\n return marker;\n });\n seriesLayer.append(group);\n seriesGroups.push({ group, polygon, markers, points, layer: seriesLayer });\n });\n svg.append(seriesLayer);\n\n const labelLayer = createSvg('g');\n config.dimensions.forEach((dimension, index) => {\n const angle = -Math.PI / 2 + index * Math.PI * 2 / config.dimensions.length;\n const directionX = Math.cos(angle);\n const directionY = Math.sin(angle);\n const labelDistance = radius + (denseNarrow ? 12 : narrow ? 18 : 24);\n const x = Math.min(width - 4, Math.max(4, cx + directionX * labelDistance));\n const y = cy + directionY * labelDistance;\n const text = createSvg('text', {\n class: 'quick-radar-axis-label',\n x: String(x),\n y: String(y),\n 'text-anchor': directionX > 0.28 ? 'start' : directionX < -0.28 ? 'end' : 'middle',\n });\n const lines = splitLabel(dimension, narrow);\n const firstLineOffset = directionY > 0.45\n ? 12\n : directionY < -0.45\n ? -5 - (lines.length - 1) * 14\n : 4 - (lines.length - 1) * 7;\n lines.forEach((line, lineIndex) => {\n const tspan = createSvg('tspan', {\n x: String(x),\n dy: lineIndex === 0 ? String(firstLineOffset) : '14',\n });\n tspan.textContent = line;\n text.append(tspan);\n });\n labelLayer.append(text);\n const key=document.createElement('button');key.type='button';key.className='dimension-key';key.setAttribute('aria-label',`查看${dimension},比较全部方案`);key.style.cssText=`position:absolute;left:${x}px;top:${y+firstLineOffset-6}px;transform:translate(${directionX>.28?'0':directionX<-.28?'-100%':'-50%'},-50%);width:${Math.max(48,Math.min(108,dimension.length*12))}px;height:32px;background:transparent;border:0;padding:0;`;key.onfocus=()=>{const r=root.getBoundingClientRect(),p=plot.getBoundingClientRect();showTooltip({clientX:r.left+x,clientY:p.top+y},index,null);};key.onpointerenter=e=>showDimension(e,index);key.onpointerleave=hideTooltip;key.onblur=hideTooltip;plot.append(key);\n });\n svg.append(labelLayer);\n\n const hit = createSvg('rect', {\n class: 'quick-radar-hit',\n x: '0',\n y: '0',\n width: String(width),\n height: String(height),\n 'data-chart-hit': '',\n });\n svg.append(hit);\n updateSeries();\n if(animate){\n // 顶点按数值向外展开,线宽与标记尺寸保持不变,避免整组缩放。\n const paint=t=>{const k=mermaidEntryProgress(t);seriesGroups.forEach(entry=>{const points=entry.points.map(p=>({x:cx+(p.x-cx)*k,y:cy+(p.y-cy)*k}));entry.polygon.setAttribute('points',pointsAttribute(points.map(p=>[p.x,p.y])));entry.markers.forEach((m,i)=>{m.setAttribute('transform',`translate(${points[i].x} ${points[i].y})`);m.style.opacity=String(Math.min(1,t*5));});});};\n paint(0);const start=performance.now();const tick=now=>{const t=Math.min(1,(now-start)/900);paint(t);if(t<1)motionFrame=requestAnimationFrame(tick);else{motionFrame=0;seriesGroups.forEach(e=>e.markers.forEach(m=>m.style.opacity=''));}};motionFrame=requestAnimationFrame(tick);\n }\n };\n\n const segmentDistance=(p,a,b)=>{const dx=b.x-a.x,dy=b.y-a.y,t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(dx*dx+dy*dy||1)));return Math.hypot(p.x-a.x-t*dx,p.y-a.y-t*dy);};\n const contains=(p,points)=>{let inside=false;for(let i=0,j=points.length-1;i<points.length;j=i++){const a=points[i],b=points[j];if((a.y>p.y)!==(b.y>p.y)&&p.x<(b.x-a.x)*(p.y-a.y)/(b.y-a.y)+a.x)inside=!inside;}return inside;};\n function showDimension(event,index){hideTooltip();svg.querySelectorAll('.quick-radar-axis').forEach((e,i)=>{if(i===index){e.style.stroke='var(--foreground)';e.style.strokeOpacity='.4';}});seriesGroups.forEach(e=>e.markers.forEach((m,i)=>m.setAttribute('opacity',i===index?'1':'.2')));showTooltip(event,index,null);}\n function showObject(event,index){hideTooltip();setHover(index);showTooltip(event,null,index);}\n svg.addEventListener('pointermove',event=>{\n if(!geometry||motionFrame){hideTooltip();return;}\n const r=svg.getBoundingClientRect(),scale=svg.viewBox.baseVal.width/r.width,p={x:(event.clientX-r.left)*scale,y:(event.clientY-r.top)*scale};\n const nearest=pointLocations.reduce((best,q)=>{const distance=Math.hypot(p.x-q.x,p.y-q.y);return !best||distance<best.distance?{...q,distance}:best;},null);\n if(nearest&&nearest.distance<=9*scale){showObject(event,nearest.seriesIndex);return;}\n const center={x:geometry.cx,y:geometry.cy};if(Math.hypot(p.x-center.x,p.y-center.y)<12){hideTooltip();return;}\n const axes=geometry.axes.map(([x,y],i)=>({i,distance:segmentDistance(p,center,{x,y})})).sort((a,b)=>a.distance-b.distance);\n if(axes[0].distance<=6*scale){showDimension(event,axes[0].i);return;}\n const edges=seriesGroups.map((e,i)=>({i,inside:contains(p,e.points),distance:Math.min(...e.points.map((q,j)=>segmentDistance(p,q,e.points[(j+1)%e.points.length])))})).sort((a,b)=>a.distance-b.distance||a.i-b.i);\n const target=edges.find(e=>e.distance<=8*scale)||edges.find(e=>e.inside);\n if(target)showObject(event,target.i);else hideTooltip();\n });\n svg.addEventListener('pointerleave', hideTooltip);\n\n const reduced=matchMedia('(prefers-reduced-motion: reduce)');let visible=false,armed=true,lastWidth=0;\n function replay(){if(visible&&armed&&!document.hidden){armed=false;draw(!reduced.matches);}}\n const observer=new IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){visible=false;armed=true;}else if(e.intersectionRatio>=.2){visible=true;replay();}},{threshold:[0,.2]});\n document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else replay();});\n root.addEventListener('pointerleave',hideTooltip);window.addEventListener('blur',hideTooltip);\n const resizeObserver = new PresetResizeObserver(()=>{const w=Math.round(plot.getBoundingClientRect().width);if(w!==lastWidth){lastWidth=w;draw();}});\n resizeObserver.observe(plot);draw();observer.observe(root);\n root.dispatchEvent(new Event('qv:ready',{bubbles:true}));\n})();\n \n}\nasync function renderMermaidPie(root,data) {\n root.dataset.renderer='qv-pie';root.classList.add('qv-pie');\n const config={title:root.querySelector('h3').textContent||data.title,items:data.items.map(d=>({label:d.name,value:d.value,description:d.description})),decimals:data.items.every(d=>Number.isInteger(d.value))?0:2,showData:data.showData,exactValues:true,showTooltip:false,valuePrefix:'',valueSuffix:''};\n await renderMermaidDonutPreset(root,config,data.items.map(d=>`${d.name}:${d.value}`).join(';'));root.dataset.state='enhanced';\n const style=document.createElement('style');style.textContent=`#${root.id} .legend button{max-width:100%;min-width:0;text-align:left}#${root.id} .legend .name{min-width:0;overflow-wrap:anywhere}#${root.id} .legend .percent{flex:none}#${root.id} .center-value{line-height:1.2}#${root.id} .center-label{max-width:140px;overflow-wrap:anywhere}`;root.append(style);\n const number=root.querySelector('.center-value'),ctx=document.createElement('canvas').getContext('2d');ctx.font='500 32px '+getComputedStyle(root).fontFamily;const fit=()=>number.style.fontSize=Math.min(32,32*136/Math.max(1,ctx.measureText(number.textContent).width))+'px';fit();new MutationObserver(fit).observe(number,{childList:true,characterData:true,subtree:true});\n if(data.showData)root.querySelectorAll('.legend .percent').forEach((n,i)=>n.textContent=data.items[i].value+' · '+n.textContent);\n}\n\n// 使用固定版本的真实列与节点数据;不从源码重新编写看板。\nasync function readMermaidKanbanModel(db,descriptions={}) {\n if(!db.getData||!db.getSections)return {reason:'Kanban 数据接口不兼容'};\n const raw=db.getData(),nodes=raw.nodes;\n if(raw.config?.kanban?.ticketBaseUrl)return {reason:'票据外部链接保留原生行为'};\n if(nodes.some(n=>n.icon||hasRichMermaidLabel(n.label)))return {reason:'图标或富文本看板保留原生语义'};\n const columns=nodes.filter(n=>n.isGroup).map(n=>({id:n.id,name:n.label,tasks:nodes.filter(t=>t.parentId===n.id).map(t=>({id:t.id,name:t.label,assigned:t.assigned||'',ticket:t.ticket||'',priority:t.priority||'',description:descriptions[t.id]||''}))}));\n if(!columns.length||nodes.some(n=>!n.isGroup&&!columns.some(c=>c.id===n.parentId)))return {reason:'看板层级保留原生语义'};\n const tasks=columns.flatMap(c=>c.tasks),unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw Error('说明引用了未知卡片:'+unknown.join('、'));\n return {data:{kind:'kanban',columns}};\n}\n\nfunction renderMermaidKanban(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll');scene.querySelector('svg').setAttribute('hidden','');root.querySelector('.edge-key').hidden=true;\n if(!window.lucide)throw Error('看板的 Lucide 图标尚未就绪');\n root.classList.add('qv-kanban');root.dataset.state='enhanced';root.dataset.renderer='qv-kanban';viewport.setAttribute('aria-label','看板:按源码阶段排列的任务,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id} .kanban-column{position:absolute;top:0;box-sizing:border-box;padding:12px;border-radius:12px;background:color-mix(in srgb,var(--kanban-tone) 6%,var(--background))}\n #${root.id} .kanban-heading{display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:13px;line-height:20px;font-weight:500}\n #${root.id} .kanban-category{display:flex;align-items:center;gap:6px;min-width:0;max-width:calc(100% - 32px);padding:3px 9px;border-radius:999px;background:color-mix(in srgb,var(--kanban-tone) 15%,var(--background));color:color-mix(in srgb,var(--kanban-tone) 55%,var(--foreground))}\n #${root.id} .kanban-category-dot{width:7px;height:7px;border-radius:50%;background:var(--kanban-tone);flex:none}\n #${root.id} .kanban-category-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n #${root.id} .kanban-heading .count{color:color-mix(in srgb,var(--kanban-tone) 55%,var(--foreground));font-size:12px;font-weight:400;flex:none;font-variant-numeric:tabular-nums}\n #${root.id} .kanban-task{display:block;width:100%;box-sizing:border-box;margin:10px 0 0;padding:14px;text-align:left;border:1px solid transparent;border-radius:9px;background:var(--background);color:var(--foreground);font:inherit;cursor:var(--cursor-interaction,pointer);box-shadow:none}\n #${root.id} .kanban-task[data-active=true],#${root.id} .kanban-task:focus-visible{border-color:var(--viz-series-1);outline:none}\n #${root.id} .kanban-name{display:block;font-size:16px;line-height:24px;font-weight:500;overflow-wrap:anywhere}\n #${root.id} .kanban-description,#${root.id} .kanban-ticket{display:block;margin-top:10px;font-size:12px;line-height:18px;color:color-mix(in srgb,var(--foreground) 64%,var(--background));overflow-wrap:anywhere}\n #${root.id} .kanban-meta{display:flex;align-items:center;gap:10px 12px;flex-wrap:wrap;font-size:12px;line-height:18px;color:color-mix(in srgb,var(--foreground) 76%,var(--background));margin-top:12px}\n #${root.id} .kanban-assignee{display:flex;align-items:center;gap:6px;min-width:0;max-width:100%;overflow-wrap:anywhere}\n #${root.id} .kanban-assignee svg{--icon-size:14px;width:14px;height:14px;flex:none;stroke-width:1.6}\n #${root.id} .kanban-assignee-name{min-width:0;overflow-wrap:anywhere}\n #${root.id} .kanban-priority{display:flex;align-items:center;gap:6px;flex:none;max-width:100%;overflow-wrap:anywhere}\n #${root.id} .kanban-priority-meter{display:flex;gap:2px;flex:none}\n #${root.id} .kanban-priority-segment{width:6px;height:10px;border-radius:2px;background:color-mix(in srgb,var(--foreground) 12%,var(--background))}\n #${root.id} .kanban-priority-segment[data-filled=true]{background:var(--viz-series-1)}\n `;root.append(style);\n const make=(tag,className,text)=>{const n=document.createElement(tag);n.className=className;if(text!==undefined)n.textContent=text;return n;};\n const tones=[3,6,2,1,4,5],levels={'Very Low':1,'Low':2,'High':3,'Very High':4},objects=[];\n const columns=data.columns.map((c,column)=>{\n const box=make('div','kanban-column');box.dataset.column=c.id;box.setAttribute('role','group');box.setAttribute('aria-label',c.name);box.style.setProperty('--kanban-tone',`var(--viz-series-${tones[column%tones.length]})`);\n const header=make('div','kanban-heading'),category=make('span','kanban-category'),letters=Array.from(c.name);\n // 展示限制不改源码类别或列归属,完整名称保留在可访问标签。\n category.setAttribute('aria-label',c.name);category.append(make('span','kanban-category-dot'),make('span','kanban-category-name',letters.length>12?letters.slice(0,11).join('')+'…':c.name));header.append(category,make('span','count',String(c.tasks.length)));box.append(header);scene.append(box);\n c.tasks.forEach((t,i)=>{\n const b=make('button','kanban-task');b.type='button';b.dataset.task=t.id;b.append(make('span','kanban-name',t.name));\n if(t.description)b.append(make('span','kanban-description',t.description));\n if(t.ticket)b.append(make('span','kanban-ticket',t.ticket));\n const meta=make('span','kanban-meta');\n if(t.assigned){const person=make('span','kanban-assignee'),icon=window.lucide.createElement(window.lucide.icons.UserRound);icon.classList.add('lucide');icon.setAttribute('aria-hidden','true');person.append(icon,make('span','kanban-assignee-name',t.assigned));meta.append(person);}\n if(t.priority){const priority=make('span','kanban-priority'),level=levels[t.priority];priority.dataset.priority=t.priority;priority.setAttribute('aria-label','优先级:'+t.priority);if(level){const meter=make('span','kanban-priority-meter');meter.setAttribute('aria-hidden','true');for(let j=0;j<4;j++){const segment=make('span','kanban-priority-segment');segment.dataset.filled=String(j<level);meter.append(segment);}priority.append(meter);}priority.append(make('span','kanban-priority-name',t.priority));meta.append(priority);}\n if(meta.childNodes.length)b.append(meta);\n b.setAttribute('aria-label',[c.name,t.name,t.description,t.ticket,t.assigned&&'负责人:'+t.assigned,t.priority&&'优先级:'+t.priority].filter(Boolean).join(' · '));box.append(b);objects.push({b,t,i,column});\n });return {box,header,c};\n });\n const view=createMermaidViewport(root);\n function layout(){const width=view.expanded?248:272,gap=16;columns.forEach((c,i)=>{c.box.style.width=width+'px';c.box.style.left=12+i*(width+gap)+'px';c.header.style.height='auto';});const headerHeight=Math.max(...columns.map(c=>c.header.offsetHeight));columns.forEach(c=>c.header.style.height=headerHeight+'px');scene.style.width=scene.style.minWidth=24+columns.length*(width+gap)-gap+'px';scene.style.height=Math.max(...columns.map(c=>c.box.offsetHeight))+12+'px';view.refresh();}\n function clear(){objects.forEach(o=>o.b.removeAttribute('data-active'));}\n function show(o){clear();o.b.dataset.active='true';}\n objects.forEach(o=>{o.b.onpointerenter=o.b.onclick=()=>show(o);o.b.onpointerleave=()=>{if(!o.b.matches(':focus-visible'))o.b.removeAttribute('data-active');};o.b.onfocus=()=>{const v=viewport.getBoundingClientRect(),r=o.b.getBoundingClientRect();if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;show(o);};o.b.onblur=()=>o.b.removeAttribute('data-active');});viewport.addEventListener('scroll',clear,{passive:true});root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n layout();root.addEventListener('qv:display-mode',()=>{clear();layout();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let armed=true,running=[];\n function cancel(){running.forEach(a=>a.cancel());running=[];}\n function replay(){cancel();if(reduced.matches||view.expanded)return;columns.forEach((c,i)=>running.push(animateMermaidEntry(c.header,[{opacity:0,transform:'translateY(8px)'},{opacity:1,transform:'translateY(0)'}],{duration:300,delay:i*120,fill:'backwards'})));objects.forEach(o=>running.push(animateMermaidEntry(o.b,[{opacity:0,transform:'translateY(8px)'},{opacity:1,transform:'translateY(0)'}],{duration:240,delay:o.column*120+o.i*70+80,fill:'backwards'})));root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});root.addEventListener('qv:display-mode',cancel);reduced.addEventListener('change',cancel);\n let lastWidth=root.clientWidth,frame=0;new ResizeObserver(()=>{if(lastWidth===root.clientWidth||frame)return;frame=requestAnimationFrame(()=>{frame=0;const width=root.clientWidth;if(!root.isConnected||lastWidth===width)return;lastWidth=width;cancel();clear();layout();});}).observe(root);\n}\n\nfunction readMermaidGanttModel(db,descriptions={}){\n if(!db.getTasks)return {reason:'Gantt 数据接口不兼容'};\n if(db.getExcludes?.().length||db.getIncludes?.().length||db.endDatesAreInclusive?.()||Object.keys(db.getLinks?.()||{}).length)return {reason:'Gantt 排除日期、含结束日期或交互链接保留原生语义'};\n const marker=db.getTodayMarker?.();if((marker&&marker!=='off')||/[HhmsXx]/.test(db.getDateFormat?.()||''))return {reason:'Gantt 自定义今日标记或时分秒排期保留原生语义'};\n const tasks=db.getTasks().map(t=>({...t,task:t.task.trim(),startTime:new Date(t.startTime),endTime:new Date(t.endTime),description:descriptions[t.id]||'',dependencies:t.raw?.startTime?.startData?.startsWith('after ')?t.raw.startTime.startData.slice(6).trim().split(/\\s+/):[]}));\n if(!tasks.length||tasks.some(t=>!Number.isFinite(+t.startTime)||!Number.isFinite(+t.endTime)||t.endTime<t.startTime))return {reason:'Gantt 任务日期无法确定'};\n if(tasks.some(t=>hasRichMermaidLabel(t.task)||hasRichMermaidLabel(t.section)||t.classes?.length||t.dependencies.some(id=>!tasks.some(n=>n.id===id))))return {reason:'Gantt 富文本、样式或未匹配依赖保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw Error('说明引用了未知任务:'+unknown.join('、'));\n return {data:{kind:'gantt',tasks,title:db.getDiagramTitle?.()||'',today:marker!=='off',sections:[...new Set(tasks.map(t=>t.section))]}};\n}\n\n// 接入已审视的时间尺、阶段条与里程碑胶囊,不复制模拟数据。\nasync function renderMermaidGantt(root,data){\n const d3=window.__mcxQVRuntime.d3;\n const heading=root.querySelector('h3');if(heading.hidden&&data.title){heading.textContent=data.title;heading.hidden=false;}\n root.dataset.state='enhanced';root.dataset.renderer='qv-gantt';root.querySelector('.edge-key').hidden=true;\n const viewport=root.querySelector('.diagram-scroll'),plot=root.querySelector('.scene');plot.replaceChildren();\n viewport.setAttribute('aria-label','甘特图:任务起止日期与里程碑,可横向滚动');\n const chart=document.createElementNS('http://www.w3.org/2000/svg','svg');chart.classList.add('chart');const hits=document.createElement('div');hits.className='hits';plot.append(chart,hits);const svg=d3.select(chart);\n const bubble=document.createElement('div');bubble.className='bubble';bubble.setAttribute('role','tooltip');bubble.hidden=true;bubble.innerHTML='<div class=\"bubble-head\"><i class=\"swatch\" aria-hidden=\"true\"></i><span></span></div><div class=\"bubble-rows\"></div><div class=\"bubble-dependency\"></div>';root.append(bubble);\n const style=document.createElement('style');style.textContent=`\n#${root.id}{position:relative;width:100%;padding-top:32px;color:var(--foreground);--tip-radius:12px;--title-gap:32px}\n#${root.id} h3{margin:0 90px var(--title-gap) 0;font-weight:500}\n#${root.id} .plot{position:relative}\n#${root.id} .chart .group-label{font-size:12px;font-weight:400;fill:var(--muted-foreground)}\n#${root.id} .chart .month-label{font-size:14px;font-weight:500}\n#${root.id} .dependency{fill:none;stroke:color-mix(in srgb,var(--muted-foreground) 38%,transparent);stroke-width:1;transition:opacity 160ms ease}\n#${root.id} .dependency-arrow{fill:color-mix(in srgb,var(--muted-foreground) 38%,transparent)}\n#${root.id} .milestone-guide{stroke:color-mix(in srgb,var(--muted-foreground) 35%,transparent);stroke-width:1;stroke-dasharray:3 5}\n#${root.id} .chart{display:block;width:100%;overflow:visible}\n#${root.id} .chart text{font:inherit;font-size:12px;fill:var(--foreground);font-variant-numeric:tabular-nums}\n#${root.id} .chart .tick-label{fill:var(--muted-foreground)}\n#${root.id} .task{transition:opacity 160ms ease}\n#${root.id} .swatch{display:inline-block;width:9px;height:9px;border-radius:2px;flex-shrink:0}\n#${root.id} .bubble{position:absolute;z-index:3;pointer-events:none;width:206px;max-width:calc(100% - 16px);box-sizing:border-box;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);border:1px solid color-mix(in srgb,var(--border) 70%,transparent);border-radius:var(--tip-radius);box-shadow:0 4px 16px color-mix(in srgb,var(--foreground) 10%,transparent)}\n#${root.id} .bubble[hidden]{display:none}\n#${root.id} .bubble-head{display:flex;gap:8px;align-items:center;margin-bottom:10px;font-weight:500}\n#${root.id} .bubble-row{display:flex;justify-content:space-between;gap:14px;margin-top:7px;font-size:12px}\n#${root.id} .bubble-row span{color:var(--muted-foreground)}\n#${root.id} .bubble-row strong{font-weight:400;font-variant-numeric:tabular-nums;text-align:right}\n#${root.id} .bubble-dependency{border-top:1px solid var(--border);margin-top:10px;padding-top:10px;font-size:12px}\n#${root.id} .bubble-dependency[hidden]{display:none}\n#${root.id} .hits{position:absolute;inset:0;pointer-events:none}\n#${root.id} .task-hit{position:absolute;pointer-events:auto;opacity:0;margin:0;cursor:var(--cursor-interaction,pointer)}\n#${root.id} .task-hit:focus-visible{opacity:1;background:transparent;outline:2px solid var(--ring);outline-offset:2px}\n@media(prefers-reduced-motion:reduce){#${root.id} .task{transition:none}}\n\n#${root.id} .scene{margin:0;min-width:0} #${root.id} .bubble-head span{overflow-wrap:anywhere} #${root.id} .task-hit{border:0;background:none;min-height:0;padding:0}\n`;root.append(style);\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),measure=document.createElement('canvas').getContext('2d'),view=createMermaidViewport(root,{expandedSizing:'hug'});\n const day=86400000,date=d=>`${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}`,tasks=data.tasks,sections=data.sections;\n let layout=null,lastWidth=0,resizeFrame=0,hover=null,inView=false,armed=true;\n const color=t=>`var(--viz-series-${sections.indexOf(t.section)%6+1})`;\nfunction clear(){hover=null;paint();}\nfunction linesFor(text,width){\n if(measure.measureText(text).width<=width)return [text];\n if(width<measure.measureText('…').width)return [];\n const chars=Array.from(text),lines=[];\n for(let line=0;line<2&&chars.length;line++){\n let value='';while(chars.length&&measure.measureText(value+chars[0]).width<=width)value+=chars.shift();\n if(line===1&&chars.length){while(value&&measure.measureText(value+'…').width>width)value=value.slice(0,-1);value+='…';}\n if(!value){lines.push('…');break;}lines.push(value);\n }\n return lines;\n}\nfunction paint(){if(!layout)return;svg.selectAll('.task').style('opacity',t=>hover!==null?(t.id===hover?1:.45):1);svg.selectAll('.dependency-group').style('opacity',e=>hover===null||e.from.id===hover||e.to.id===hover?1:.16);if(hover===null)bubble.hidden=true;}\nfunction show(t,event){\n hover=t.id;paint();root.querySelector('.bubble-head span').textContent=t.task;root.querySelector('.bubble-head .swatch').style.background=color(t);\n const rows=root.querySelector('.bubble-rows');rows.replaceChildren();\n const values=t.milestone?[['日期',date(t.startTime)],['类型','里程碑']]:[['开始',date(t.startTime)],['结束',date(t.endTime)],['时长',`${Math.round((t.endTime-t.startTime)/day)} 自然日`],['状态',t.done?'已完成':t.active?'进行中':'未开始']];\n if(t.description)values.push(['说明',t.description]);\n if(t.crit)values.push(['标记','关键任务']);\n values.forEach(([label,value])=>{const row=document.createElement('div');row.className='bubble-row';const name=document.createElement('span'),valueNode=document.createElement('strong');name.textContent=label;valueNode.textContent=value;row.append(name,valueNode);rows.append(row);});\n const dependency=root.querySelector('.bubble-dependency');dependency.hidden=!t.dependencies.length;dependency.textContent=t.dependencies.length?'前置任务:'+t.dependencies.map(id=>tasks.find(q=>q.id===id).task).join('、'):'';\n bubble.hidden=false;\n const box=layout.boxes.get(t.id),rb=root.getBoundingClientRect(),pb=plot.getBoundingClientRect(),bw=bubble.offsetWidth,bh=bubble.offsetHeight,hit=[...hits.children].find(n=>n.dataset.task===t.id),hb=hit.getBoundingClientRect();\n const px=event?.clientX===undefined?(hb.left+hb.right)/2-rb.left:event.clientX-rb.left,py=hb.top-rb.top;\n const candidates=[{left:px+16,top:py-bh-14},{left:px-bw-16,top:py-bh-14},{left:px+16,top:hb.bottom-rb.top+12},{left:px-bw-16,top:hb.bottom-rb.top+12}];\n const own={left:hb.left-rb.left,right:hb.right-rb.left,top:hb.top-rb.top,bottom:hb.bottom-rb.top};\n const overlap=(a,b)=>Math.max(0,Math.min(a.right,b.right)-Math.max(a.left,b.left))*Math.max(0,Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top));\n const best=candidates.map(c=>{const left=Math.max(8,Math.min(rb.width-bw-8,c.left)),top=Math.max(0,Math.min(root.offsetHeight-bh-8,c.top));return {left,top,score:overlap({left,right:left+bw,top,bottom:top+bh},own)};}).sort((a,b)=>a.score-b.score)[0];\n bubble.style.left=best.left+'px';bubble.style.top=best.top+'px';\n}\nfunction draw(animate=false){\n const width=Math.max(320,viewport.clientWidth);plot.style.width=plot.style.minWidth=width+'px';if(!width)return;lastWidth=width;clear();\n measure.font='12px '+getComputedStyle(root).fontFamily;\n const narrow=width<500,row=narrow?60:44,left=8,right=14,top=56;\n const x=d3.scaleTime().domain([d3.min(tasks,t=>t.startTime),d3.max(tasks,t=>t.endTime)]).range([left,width-right]);\n const milestoneBoxes=new Map(),tiers=[];for(const t of tasks.filter(t=>t.milestone)){const lines=linesFor(t.task,Math.min(160,width-left-right-16)),bw=Math.min(176,Math.max(32,...lines.map(line=>measure.measureText(line).width))+16),bx=Math.max(left,Math.min(width-right-bw,x(t.startTime)-bw/2));let tier=tiers.findIndex(end=>end+8<=bx);if(tier<0)tier=tiers.length;tiers[tier]=bx+bw;milestoneBoxes.set(t.id,{lines,bw,bx,y:top+4+tier*44});}\n const placed=new Map(),groupRows=[];let cursor=top+Math.max(56,tiers.length*44+12);\n sections.forEach((section,index)=>{const entries=tasks.filter(t=>t.section===section&&!t.milestone);groupRows.push({section,y:cursor,index});cursor+=16;entries.forEach(t=>{const a=x(t.startTime),b=x(t.endTime),lines=linesFor(t.task,Math.max(0,b-a-16)),barHeight=lines.length>1?40:32;placed.set(t.id,{y:cursor,a,b,lines,barHeight,center:cursor+barHeight/2});cursor+=row;});cursor+=16;});\n const base=cursor-4,height=base+14;\n layout={width,height,base,x,boxes:new Map()};svg.selectAll('*').interrupt();svg.selectAll('*').remove();hits.replaceChildren();svg.attr('viewBox',`0 0 ${width} ${height}`).attr('height',height);\n const start=x.domain()[0],end=x.domain()[1],span=Math.round((end-start)/day),ticks=[0];for(let d=7;d<span;d+=7)ticks.push(d);ticks.push(span);\n // 刻度随容器减量,保留实际项目开始与结束日期。\n const tickDates=[...new Set(ticks)].map(n=>new Date(+start+n*day));\n svg.append('text').attr('x',left).attr('y',14).attr('class','month-label').text(`${start.getFullYear()} 年 ${start.getMonth()+1} 月`+(start.getMonth()!==end.getMonth()||start.getFullYear()!==end.getFullYear()?` — ${end.getFullYear()} 年 ${end.getMonth()+1} 月`:''));\n const visibleDates=tickDates;const today=new Date();if(data.today&&today>=start&&today<=end){svg.append('line').attr('class','today-marker').attr('x1',x(today)).attr('x2',x(today)).attr('y1',top).attr('y2',base).attr('stroke','var(--muted-foreground)').attr('stroke-dasharray','3 5');svg.append('text').attr('x',Math.max(left,Math.min(width-right-24,x(today)+4))).attr('y',top+16).text('今天');}\n const days=d3.timeDay.range(start,d3.timeDay.offset(end,1)),majorDays=new Set([...visibleDates.map(Number),...tasks.filter(t=>t.milestone).map(t=>+t.startTime)]);\n svg.append('line').attr('class','ruler-baseline').attr('x1',left).attr('x2',width-right).attr('y1',top).attr('y2',top).attr('stroke','var(--border)').attr('stroke-opacity',.85);\n svg.selectAll('.day-tick').data(days).join('line').attr('class','day-tick').attr('x1',x).attr('x2',x).attr('y1',d=>majorDays.has(+d)?top-10:top-5).attr('y2',top).attr('stroke','var(--muted-foreground)').attr('stroke-opacity',d=>majorDays.has(+d)?.55:.28);\n svg.selectAll('.grid').data(visibleDates.filter(d=>+d!==+start&&+d!==+end)).join('line').attr('class','grid').attr('x1',x).attr('x2',x).attr('y1',top).attr('y2',base).attr('stroke','var(--border)').attr('stroke-opacity',.35);\n svg.selectAll('.date-label').data(visibleDates).join('text').attr('class','date-label').attr('x',x).attr('y',41).attr('text-anchor',d=>+d===+start?'start':+d===+end?'end':'middle').text(d=>String(d.getDate()).padStart(2,'0'));\n groupRows.forEach(g=>{if(g.index)svg.append('line').attr('x1',left).attr('x2',width-right).attr('y1',g.y-16).attr('y2',g.y-16).attr('stroke','var(--border)').attr('stroke-opacity',.45);svg.append('text').attr('class','group-label').attr('x',left).attr('y',g.y-1).text(g.section);});\n const edges=tasks.flatMap(to=>to.dependencies.map(id=>({from:tasks.find(t=>t.id===id),to}))).filter(e=>!e.from.milestone&&!e.to.milestone&&+e.to.startTime>+e.from.endTime);\n const wires=svg.append('g').attr('class','wires');edges.forEach(e=>{const from=placed.get(e.from.id),to=placed.get(e.to.id),sx=from.b,sy=from.center,ex=to.a-6,ey=to.center,bend=(ex-sx)*.55,g=wires.append('g').datum(e).attr('class','dependency-group');g.append('path').attr('class','dependency').attr('d',`M${sx},${sy}C${sx+bend},${sy} ${ex-bend},${ey} ${ex},${ey}`);g.append('path').attr('class','dependency-arrow').attr('d',`M${ex},${ey-2.2}L${ex+5},${ey}L${ex},${ey+2.2}Z`);});\n const groups=svg.selectAll('.task').data(tasks).join('g').attr('class','task').attr('data-task',t=>t.id);\n groups.each(function(t,i){\n const g=d3.select(this),a=x(t.startTime),b=x(t.endTime),label=t.task,labelWidth=measure.measureText(label).width;\n let box;\n if(t.milestone){\n const m=milestoneBoxes.get(t.id),bubbleWidth=m.bw,bubbleX=m.bx,bubbleY=m.y,bubbleHeight=m.lines.length>1?40:24;\n g.append('line').attr('class','milestone-guide').attr('x1',a).attr('x2',a).attr('y1',top+2).attr('y2',base);\n g.append('rect').attr('class','milestone-label-bubble').attr('x',bubbleX).attr('y',bubbleY).attr('width',bubbleWidth).attr('height',bubbleHeight).attr('rx',12).attr('fill',color(t)).attr('fill-opacity',.13);\n const caption=g.append('text').attr('class','task-label milestone-label').attr('x',bubbleX+8).attr('y',bubbleY+16);m.lines.forEach((line,i)=>caption.append('tspan').attr('x',bubbleX+8).attr('dy',i?14:0).text(line));\n g.append('path').attr('class','milestone').attr('transform',`translate(${a},${top-5})`).attr('d','M-4,-6H4V-1L0,4L-4,-1Z').attr('fill',color(t));\n box={left:bubbleX-4,right:Math.min(width,Math.max(a+6,bubbleX+bubbleWidth+4)),top:top-12,bottom:bubbleY+bubbleHeight+4};\n }else{\n const p=placed.get(t.id),y=p.y,labelX=a+8;\n const bar=g.append('rect').attr('class','bar').attr('x',a).attr('y',y).attr('height',p.barHeight).attr('rx',3).attr('width',animate&&!reduced.matches?0:b-a).attr('fill',color(t)).attr('fill-opacity',.13);\n g.append('line').attr('class','bar-start').attr('x1',a).attr('x2',a).attr('y1',y+1).attr('y2',y+p.barHeight-1).attr('stroke',color(t)).attr('stroke-width',2);\n const labelNode=g.append('text').attr('class','task-label').attr('x',labelX).attr('y',y+(p.lines.length>1?16:20)).attr('aria-label',t.task);\n p.lines.forEach((line,index)=>labelNode.append('tspan').attr('x',labelX).attr('dy',index?14:0).text(line));\n if(animate&&!reduced.matches){const f=i/Math.max(1,tasks.length-1),weight=f*f*(3-2*f);bar.transition().duration(580+100*weight).delay(0).ease(mermaidEntryProgress).attr('width',b-a);}\n box={left:a-4,right:Math.min(width-right,b+4),top:p.y-5,bottom:y+p.barHeight+5};\n }\n layout.boxes.set(t.id,box);\n const hit=document.createElement('button');hit.type='button';hit.className='task-hit';hit.dataset.task=t.id;hit.setAttribute('aria-label',`${t.task},${date(t.startTime)}至${date(t.endTime)},${t.milestone?'里程碑':Math.round((t.endTime-t.startTime)/day)+'自然日'}`);Object.assign(hit.style,{left:box.left+'px',top:box.top+'px',width:Math.max(32,box.right-box.left)+'px',height:box.bottom-box.top+'px'});hits.append(hit);\n hit.addEventListener('pointerenter',e=>show(t,e));hit.addEventListener('pointermove',e=>show(t,e));hit.addEventListener('pointerleave',clear);hit.addEventListener('pointercancel',clear);hit.addEventListener('focus',()=>show(t));hit.addEventListener('blur',clear);hit.addEventListener('pointerup',e=>{if(e.pointerType==='touch')clear();});\n });\n paint();plot.style.height=height+'px';view.refresh();\n}\n\n function replayIfReady(){if(inView&&armed&&!document.hidden&&!view.expanded){armed=false;draw(!reduced.matches);root.dataset.replays=String((+root.dataset.replays||0)+1);}}\n draw();\n new IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){inView=false;armed=true;clear();}else if(e.intersectionRatio>=.2){inView=true;replayIfReady();}},{threshold:[0,.2]}).observe(root);\n new ResizeObserver(()=>{if(viewport.clientWidth===lastWidth||resizeFrame)return;resizeFrame=requestAnimationFrame(()=>{resizeFrame=0;if(Math.max(320,viewport.clientWidth)!==lastWidth)draw();});}).observe(viewport);\n root.addEventListener('qv:display-mode',()=>{svg.selectAll('*').interrupt();clear();draw();});\n reduced.addEventListener('change',()=>{svg.selectAll('*').interrupt();draw();});\n document.addEventListener('visibilitychange',()=>{if(document.hidden){armed=true;clear();}else replayIfReady();});\n root.addEventListener('pointerleave',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});viewport.addEventListener('scroll',clear,{passive:true});view.refresh();\n}\n\nfunction readMermaidMindmapModel(db,descriptions={}){\n if(!db.getMindmap)return {reason:'Mindmap 数据接口不兼容'};\n const tree=db.getMindmap(),nodes=[];if(!tree)return {reason:'Mindmap 缺少根节点'};\n function visit(n,parent=null,depth=0){const id=String(n.id);nodes.push({id,sourceId:n.nodeId,label:String(n.descr||''),parent,depth,children:n.children||[],description:descriptions[n.nodeId]||descriptions[id]||'',icon:n.icon,classes:n.cssClasses});for(const c of n.children||[])visit(c,id,depth+1);}\n visit(tree);\n if(nodes.some(n=>n.icon||n.classes||hasRichMermaidLabel(n.label)))return {reason:'Mindmap 自定义图标、样式或富文本保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!nodes.some(n=>n.id===id||n.sourceId===id));if(unknown.length)throw Error('说明引用了未知主题:'+unknown.join('、'));\n return {data:{kind:'mindmap',nodes}};\n}\n\n// 复用已审视的双侧树布局;全部层级来自 Mermaid DB。\nfunction renderMermaidMindmap(root,data){\n root.dataset.state='enhanced';root.dataset.renderer='qv-mindmap';root.querySelector('.edge-key').hidden=true;\n const scene=root.querySelector('.scene'),svg=root.querySelector('.wires'),viewport=root.querySelector('.diagram-scroll'),tip=root.querySelector('.node-tip');\n viewport.setAttribute('aria-label','思维导图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n#${root.id}{position:relative;padding-top:32px;color:var(--foreground);width:100%}\n#${root.id} h3{font-weight:500;margin:0 90px 32px 0}\n#${root.id} .diagram-scroll{overflow-x:auto;width:100%;padding-bottom:8px}\n#${root.id} .scene{position:relative;margin:auto;height:364px}\n#${root.id} .wires{position:absolute;inset:0;overflow:visible;pointer-events:none}\n#${root.id} .wire{fill:none;stroke:color-mix(in srgb,var(--muted-foreground) 26%,var(--background));stroke-width:1;transition:opacity 140ms}\n#${root.id} button.node{position:absolute;display:flex;align-items:center;justify-content:center;font:inherit;font-size:12px;color:var(--foreground);background:var(--background);border:1px solid var(--border);border-radius:10px;padding:12px 14px;box-sizing:border-box;white-space:normal;overflow-wrap:anywhere;line-height:1.6;transition:opacity 140ms,border-color 140ms;cursor:var(--cursor-interaction,pointer)}\n#${root.id} button.node.root{font-size:14px;font-weight:500;background:color-mix(in srgb,var(--muted) 30%,var(--background))}\n#${root.id} button.node.leaf,#${root.id} button.node.twig{border:0;background:transparent;justify-content:flex-start;text-align:left;padding:6px 12px} #${root.id} button.node.twig{font-weight:500}\n#${root.id} button.node.leaf.left,#${root.id} button.node.twig.left{justify-content:flex-end;text-align:right}\n#${root.id} button.node:hover{border-color:var(--muted-foreground)}\n#${root.id} button.node.dim{opacity:.22}\n#${root.id} .wire.dim{opacity:.055}\n#${root.id} .wire.active{stroke:var(--muted-foreground)}\n#${root.id} .error{color:var(--muted-foreground)}\n@media(prefers-reduced-motion:reduce){#${root.id} button.node,#${root.id} .wire{transition:none}}\n\n#${root.id} button.node{min-width:0;transform:none;text-align:center} #${root.id} .scene{min-width:0;margin:0} #${root.id} button.node-tip{max-width:260px} #${root.id} button.node.branch{border-color:color-mix(in srgb,var(--branch) 45%,var(--background));background:color-mix(in srgb,var(--branch) 10%,var(--background))} #${root.id} button.node.branch:hover{border-color:color-mix(in srgb,var(--branch) 75%,var(--background))} #${root.id} button.node.twig{color:color-mix(in srgb,var(--branch) 40%,var(--foreground))} #${root.id} .wire[data-branch]{stroke:color-mix(in srgb,var(--branch) 50%,var(--background))} #${root.id} .wire[data-depth=\"0\"]{stroke-width:2} #${root.id} .wire[data-depth=\"1\"]{stroke-width:1.5} #${root.id} .wire.active[data-branch]{stroke:color-mix(in srgb,var(--branch) 85%,var(--foreground))}\n`;root.append(style);\n const NS='http://www.w3.org/2000/svg',reduce=matchMedia('(prefers-reduced-motion:reduce)'),view=createMermaidViewport(root),isExpanded=()=>view.expanded;\n const model=data.nodes;let animations=[],visible=false,replayArmed=true,viewGeometry=null,inlineOffset=0;\nfunction highlight(id){const target=model.find(n=>n.id===id);if(target.description){tip.textContent=target.description;tip.hidden=false;tip.style.left='8px';tip.style.top='48px';}else tip.hidden=true;const active=new Set();let n=target;while(n){active.add(n.id);n=model.find(t=>t.id===n.parent);}const descendants=p=>{for(const c of model.filter(t=>t.parent===p)){active.add(c.id);descendants(c.id);}};descendants(id);scene.querySelectorAll('.node').forEach(b=>b.classList.toggle('dim',!active.has(b.dataset.id)));svg.querySelectorAll('.wire').forEach(p=>{const a=active.has(p.dataset.from)&&active.has(p.dataset.to);p.classList.toggle('dim',!a);p.classList.toggle('active',a);});}\nfunction clear(){tip.hidden=true;scene.querySelectorAll('.dim,.active').forEach(n=>n.classList.remove('dim','active'));}\nfunction replay(){if(reduce.matches||isExpanded())return;animations.forEach(a=>a.cancel());animations=[];scene.querySelectorAll('.node').forEach(b=>{animations.push(animateMermaidEntry(b,[{transform:'scale(.08)',opacity:0},{transform:'scale(1)',opacity:1}],{duration:240,delay:+b.dataset.depth*220,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));});svg.querySelectorAll('.wire').forEach(p=>{const length=p.getTotalLength();animations.push(animateMermaidEntry(p,[{strokeDasharray:`${length} ${length}`,strokeDashoffset:length},{strokeDasharray:`${length} ${length}`,strokeDashoffset:0}],{duration:650,delay:+p.dataset.depth*220,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'}));});root.dataset.replays=String(+(root.dataset.replays||0)+1);}\nfunction draw(){\n const viewOffset=viewGeometry?.mode==='inline'?viewport.scrollLeft+viewGeometry.viewportWidth/2-viewGeometry.rootCenter:inlineOffset;\n if(viewGeometry?.mode==='inline')inlineOffset=viewOffset;\n scene.style.transform='';scene.style.marginLeft='';viewport.style.height='';\n animations.forEach(a=>a.cancel());svg.replaceChildren();scene.querySelectorAll('.node').forEach(n=>n.remove());\n const byId=new Map(model.map(n=>[n.id,n])),buttons=new Map(),sizes=new Map(),spans=new Map(),positions=new Map();\n const canvas=document.createElement('canvas'),context=canvas.getContext('2d');context.font='12px '+getComputedStyle(root).fontFamily;\n for(const n of model){\n const leaf=n.children.length===0,b=document.createElement('button');b.type='button';b.className='node '+(n.depth===0?'root':n.depth===1?'branch':leaf?'leaf':'twig');b.textContent=n.label;b.dataset.id=n.id;b.dataset.depth=n.depth;b.setAttribute('aria-label',n.label+(n.description?' · '+n.description:''));if(n.depth>0){let owner=n;while(owner.depth>1)owner=byId.get(owner.parent);const k=model.filter(t=>t.depth===1).indexOf(owner);b.style.setProperty('--branch',`var(--viz-series-${k%6+1})`);}\n const w=n.depth===0?116:n.depth===1?100:Math.min(140,Math.max(76,Math.ceil(context.measureText(n.label).width)+32));\n Object.assign(b.style,{width:w+'px',minHeight:'44px',height:'auto',visibility:'hidden'});scene.append(b);\n const h=Math.max(44,Math.ceil(b.offsetHeight));sizes.set(n.id,{w,h});buttons.set(n.id,b);\n b.addEventListener('mouseenter',()=>highlight(n.id));b.addEventListener('mouseleave',clear);b.addEventListener('focus',()=>highlight(n.id));b.addEventListener('blur',clear);\n }\n function span(id){const n=byId.get(id),h=n.children.length?n.children.reduce((s,c)=>s+span(String(c.id)),0)+(n.children.length-1)*6:sizes.get(id).h;spans.set(id,Math.max(sizes.get(id).h,h));return spans.get(id);}\n const top=model[0],branches=model.filter(n=>n.depth===1),sides={left:[],right:[]},totals={left:0,right:0};\n for(const n of branches){span(n.id);const side=totals.left<=totals.right?'left':'right';sides[side].push(n);totals[side]+=spans.get(n.id)+(sides[side].length>1?30:0);}\n const columns={left:[],right:[]};\n function measure(id,side){const n=byId.get(id);columns[side][n.depth]=Math.max(columns[side][n.depth]||0,sizes.get(id).w);for(const c of n.children)measure(String(c.id),side);}\n for(const side of ['left','right'])for(const n of sides[side])measure(n.id,side);\n const gap=28,margin=20,extent=side=>columns[side].slice(1).reduce((s,w)=>s+w+gap,0),leftExtent=extent('left'),rightExtent=extent('right'),rw=sizes.get(top.id).w;\n // 两侧预留相同画布空间,让根节点可停在可见窗口中心。\n const width=Math.max(viewport.clientWidth,2*Math.max(leftExtent,rightExtent)+rw+margin*2),height=Math.max(300,totals.left,totals.right)+32,cx=(width-rw)/2,cy=height/2;\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);positions.set(top.id,{x:cx,y:cy-sizes.get(top.id).h/2,...sizes.get(top.id)});\n function place(id,side,start){const n=byId.get(id),s=sizes.get(id),center=start+spans.get(id)/2;\n const offset=gap+columns[side].slice(1,n.depth).reduce((sum,w)=>sum+w+gap,0);\n const x=side==='left'?cx-offset-s.w:cx+rw+offset;positions.set(id,{x,y:center-s.h/2,...s,side});\n if(n.children.length){const kidsHeight=n.children.reduce((sum,c)=>sum+spans.get(String(c.id)),0)+(n.children.length-1)*6;let cursor=center-kidsHeight/2;for(const c of n.children){place(String(c.id),side,cursor);cursor+=spans.get(String(c.id))+6;}}\n }\n for(const side of ['left','right']){let cursor=(height-totals[side])/2;for(const n of sides[side]){place(n.id,side,cursor);cursor+=spans.get(n.id)+30;}}\n for(const n of model){const p=positions.get(n.id),b=buttons.get(n.id);b.classList.toggle('left',p.side==='left');Object.assign(b.style,{left:p.x+'px',top:p.y+'px',height:p.h+'px',visibility:''});\n if(n.parent){const a=positions.get(n.parent),left=p.side==='left',sx=left?a.x:a.x+a.w,ex=left?p.x+p.w:p.x,sy=a.y+a.h/2,ey=p.y+p.h/2,mid=(sx+ex)/2;\n const path=document.createElementNS(NS,'path');path.setAttribute('d',`M${sx},${sy} C${mid},${sy} ${mid},${ey} ${ex},${ey}`);path.setAttribute('class','wire');Object.assign(path.dataset,{from:n.parent,to:n.id,depth:n.depth-1,branch:''});path.style.setProperty('--branch',buttons.get(n.id).style.getPropertyValue('--branch'));svg.append(path);}\n }\n view.refresh();\n if(!view.expanded){viewport.scrollLeft=cx+rw/2+viewOffset-viewport.clientWidth/2;}\n viewGeometry={viewportWidth:viewport.clientWidth,rootCenter:cx+rw/2,mode:view.expanded?'fullscreen':'inline'};\n root.dataset.nodes=String(model.length);root.dataset.depth=String(Math.max(...model.map(n=>n.depth))+1);root.dataset.sides=JSON.stringify({left:sides.left.map(n=>n.label),right:sides.right.map(n=>n.label)});\n}\n\n\n draw();let lastWidth=root.clientWidth;\n new ResizeObserver(()=>{if(lastWidth===root.clientWidth)return;lastWidth=root.clientWidth;draw();}).observe(root);\n root.addEventListener('qv:display-mode',()=>{clear();draw();if(!view.expanded&&view.savedInlinePosition)viewport.scrollLeft=view.savedInlinePosition.left;});\n new IntersectionObserver(entries=>{const e=entries[0];if(!e.isIntersecting){visible=false;replayArmed=true;}else if(e.intersectionRatio>=.2){visible=true;if(replayArmed){replay();replayArmed=false;}}},{threshold:[0,.2]}).observe(root);\n document.addEventListener('visibilitychange',()=>{if(!document.hidden&&visible)replay();});\n viewport.addEventListener('scroll',()=>{clear();const focused=document.activeElement;if(scene.contains(focused)&&focused.dataset.id)highlight(focused.dataset.id);},{passive:true});\n root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n reduce.addEventListener('change',()=>{if(reduce.matches)animations.forEach(a=>a.cancel());});view.refresh();\n}\n\n// Journey getTasks 追加共享数组;只读一次,再复制源码的顺序、评分与角色。\nasync function readMermaidJourneyModel(db,descriptions={}) {\n if(!db.getTasks||!db.getActors)return {reason:'Journey 数据接口不兼容'};\n const tasks=db.getTasks().map((t,i)=>({id:'step-'+(i+1),name:t.task.trim(),section:t.section.trim(),score:t.score,actors:[...t.people].filter(Boolean),description:descriptions['step-'+(i+1)]||''}));\n if(!tasks.length||tasks.some(t=>!Number.isFinite(t.score)||t.score<1||t.score>5))return {reason:'体验评分超出1–5范围,保留原生语义'};\n if(tasks.some(t=>[t.name,t.section,...t.actors].some(hasRichMermaidLabel)))return {reason:'此用户旅程包含富文本,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw new Error('说明引用了未知步骤:'+unknown.join('、'));\n return {data:{kind:'journey',tasks,actors:[...db.getActors()].filter(Boolean),title:db.getDiagramTitle?.()||''}};\n}\n\nfunction renderMermaidJourney(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-journey');root.dataset.state='enhanced';root.dataset.renderer='qv-journey';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','用户旅程:步骤、体验曲线与角色参与,可横向滚动');\n if(data.title&&root.querySelector('h3').hidden){root.querySelector('h3').textContent=data.title;root.querySelector('h3').hidden=false;}\n if(!window.lucide)throw Error('用户旅程的 Lucide 图标尚未就绪');\n const frame=document.createElement('div'),reference=document.createElement('div'),axis=document.createElementNS(NS,'svg');frame.className='journey-frame';reference.className='journey-reference';axis.classList.add('journey-reference-axis');reference.append(axis);viewport.before(frame);frame.append(reference,viewport);\n const style=document.createElement('style');style.textContent=`\n #${root.id}{--journey-low:light-dark(#cf6f68,#e79b94);--journey-mid:light-dark(#b49a49,#d8c175);--journey-high:light-dark(#6b9b7a,#96c5a4);--journey-block:color-mix(in srgb,var(--muted) 18%,var(--background));--journey-role-bar:color-mix(in srgb,var(--foreground) 9%,var(--background))}\n #${root.id} .journey-frame{display:flex;align-items:flex-start}\n #${root.id} .journey-frame>.diagram-scroll{flex:1;min-width:0}\n #${root.id} .scene{margin:0}\n #${root.id} .journey-reference{position:relative;width:112px;flex:none;background:var(--background)}\n #${root.id} .journey-reference-axis{position:absolute;inset:0;width:112px;overflow:visible;pointer-events:none}\n #${root.id} .journey-column{position:absolute;top:0;z-index:2;min-width:0;min-height:0;margin:0;padding:0;border:0;border-radius:8px;background:transparent;color:var(--foreground);font:inherit;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .journey-column:focus-visible{outline:1px solid var(--ring);outline-offset:-2px}\n #${root.id} .journey-step{position:absolute;left:8px;right:8px;display:flex;align-items:center;flex-direction:column;gap:10px;font-size:12px;line-height:20px;font-weight:400;text-align:center;white-space:normal;overflow-wrap:anywhere}\n #${root.id} .journey-step-name{display:block;width:100%;text-wrap:balance}\n #${root.id} .journey-step-index{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:1px solid var(--border);border-radius:50%;font-size:11px;font-variant-numeric:tabular-nums;flex:none;color:var(--muted-foreground)}\n #${root.id} .journey-section{position:absolute;top:0;box-sizing:border-box;padding:10px 12px;border-radius:8px;background:linear-gradient(var(--journey-block),var(--journey-block)),var(--background);font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere}\n #${root.id} .journey-section span{position:relative;display:inline-block;overflow-wrap:anywhere}\n #${root.id} .journey-line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}\n #${root.id} .journey-grid{stroke:var(--border);stroke-width:1;fill:none;opacity:.65}\n #${root.id} .journey-axis-label{font-size:12px;fill:var(--muted-foreground)}\n #${root.id} .journey-face{--icon-size:24px;position:absolute;left:50%;display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:var(--background);transform:translate(-50%,-50%);pointer-events:none}\n #${root.id} .journey-face svg{width:24px;height:24px;stroke-width:1.6}\n #${root.id} .journey-actor{position:absolute;left:8px;width:96px;display:flex;align-items:center;gap:8px;box-sizing:border-box;min-height:30px;color:var(--muted-foreground);font-size:12px;line-height:18px;overflow-wrap:anywhere}\n #${root.id} .journey-actor svg{width:16px;height:16px;flex:none;stroke-width:1.6}\n #${root.id} .journey-participation{position:absolute;height:14px;border-radius:5px;background:var(--journey-role-bar);pointer-events:none}\n #${root.id} .journey-column-mask{position:absolute;z-index:3;background:var(--background);opacity:0;pointer-events:none;transition:opacity 140ms ease}\n #${root.id}[data-active-column] .journey-column-mask{opacity:.72}\n #${root.id}[data-active-column] .journey-column-mask[data-selected=true]{opacity:0}\n @media(prefers-reduced-motion:reduce){#${root.id} .journey-column-mask{transition:none}}\n `;root.append(style);\n const icon=name=>{const n=window.lucide.createElement(window.lucide.icons[name]||window.lucide.icons.UserRound);n.setAttribute('aria-hidden','true');n.classList.add('lucide');n.dataset.icon=name;return n;};\n const make=(tag,attrs,parent=svg)=>{const n=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;};\n const defs=make('defs',{}),tone=make('linearGradient',{id:root.id+'-tone',gradientUnits:'userSpaceOnUse',x1:0,x2:0},defs);\n for(const [offset,color]of [['0%','var(--journey-high)'],['50%','var(--journey-mid)'],['100%','var(--journey-low)']])make('stop',{offset,'stop-color':color},tone);\n const fade=make('linearGradient',{id:root.id+'-fade',x1:0,x2:0,y1:0,y2:1},defs);make('stop',{offset:'0%','stop-color':'white','stop-opacity':'.22'},fade);make('stop',{offset:'55%','stop-color':'white','stop-opacity':'.10'},fade);make('stop',{offset:'100%','stop-color':'white','stop-opacity':'0'},fade);\n const mask=make('mask',{id:root.id+'-area-mask',maskContentUnits:'userSpaceOnUse'},defs),maskRect=make('rect',{fill:`url(#${root.id}-fade)`},mask);\n const grid=Array.from({length:5},(_,i)=>({path:make('path',{class:'journey-grid'}),label:make('text',{class:'journey-axis-label','text-anchor':'end'},axis)}));grid.forEach((g,i)=>g.label.textContent=5-i);\n const quantity=make('text',{class:'journey-axis-label'},axis);quantity.textContent='体验曲线';const stepCaption=make('text',{class:'journey-axis-label'},axis);stepCaption.textContent='用户旅程';\n const area=make('path',{class:'journey-area',fill:`url(#${root.id}-tone)`,mask:`url(#${root.id}-area-mask)`}),line=make('path',{class:'journey-line',stroke:`url(#${root.id}-tone)`});\n const objects=[],roles=[],sections=[];\n let start=0,phase=data.tasks[0].section;for(let i=1;i<=data.tasks.length;i++)if(i===data.tasks.length||data.tasks[i].section!==phase){if(phase){const box=document.createElement('div'),caption=document.createElement('span');box.className='journey-section';caption.textContent=phase;box.append(caption);scene.append(box);sections.push({box,caption,start,end:i});}if(i<data.tasks.length){start=i;phase=data.tasks[i].section;}}\n data.tasks.forEach((t,i)=>{const b=document.createElement('button');b.type='button';b.className='journey-column';b.dataset.step=t.id;b.dataset.score=String(t.score);b.setAttribute('aria-label',[`${i+1},${t.name}`,t.section,`体验评分${t.score}/5`,t.actors.length?'参与角色:'+t.actors.join('、'):'未指定角色',t.description].filter(Boolean).join(' · '));const header=document.createElement('span'),number=document.createElement('span'),name=document.createElement('span'),face=document.createElement('span'),columnMask=document.createElement('div');header.className='journey-step';number.className='journey-step-index';number.textContent=String(i+1);name.className='journey-step-name';name.textContent=t.name;header.append(number,name);face.className='journey-face';face.style.color=t.score===3?'var(--journey-mid)':t.score<3?'var(--journey-low)':'var(--journey-high)';face.append(icon(t.score===3?'Meh':t.score<3?'Frown':'Smile'));b.append(header,face);columnMask.className='journey-column-mask';columnMask.dataset.step=t.id;scene.append(b,columnMask);objects.push({t,b,header,face,mask:columnMask});});\n const iconRules=[[/编辑|作者|editor|writer/i,'PencilLine'],[/审核|review|check/i,'BadgeCheck'],[/运营|市场|market/i,'Megaphone'],[/设计|design/i,'Palette'],[/法务|legal/i,'Scale'],[/策略|strategy/i,'Compass']];\n data.actors.forEach(actor=>{const b=document.createElement('div'),name=document.createElement('span');b.className='journey-actor';b.dataset.actor=actor;name.textContent=actor;b.append(icon(iconRules.find(([re])=>re.test(actor))?.[1]||'UserRound'),name);reference.append(b);const runs=[];for(let i=0;i<objects.length;i++)if(objects[i].t.actors.includes(actor)){const last=runs.at(-1);if(last&&last.end===i)last.end=i+1;else runs.push({start:i,end:i+1});}runs.forEach(run=>{run.b=document.createElement('div');run.b.className='journey-participation';run.b.dataset.actor=actor;run.b.dataset.start=String(run.start);run.b.dataset.end=String(run.end);run.b.setAttribute('aria-hidden','true');scene.append(run.b);});roles.push({actor,b,runs});});\n const view=createMermaidViewport(root);let columnWidth=0,left=0,stepTop=0,plotTop=0,plotBottom=0,width=0,height=0,active=null;\n function clear(){active=null;delete root.dataset.activeColumn;objects.forEach(o=>o.mask.removeAttribute('data-selected'));}\n function select(id){if(active===id)return;active=id;root.dataset.activeColumn=id;objects.forEach(o=>o.mask.dataset.selected=String(o.t.id===id));}\n objects.forEach(o=>{o.b.onpointerenter=o.b.onfocus=o.b.onclick=()=>select(o.t.id);o.b.onblur=clear;});root.addEventListener('pointerleave',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n function placeSections(){sections.forEach(s=>s.caption.style.left=(view.expanded?0:Math.max(0,Math.min(s.box.offsetWidth-24-s.caption.offsetWidth,viewport.scrollLeft-s.left+8)))+'px');}\n function layout(){frame.style.width='';\n if(view.expanded)scene.append(reference);else frame.prepend(reference);reference.style.position=view.expanded?'absolute':'relative';reference.style.left=reference.style.top='0';\n columnWidth=view.expanded?120:Math.max(76,Math.min(144,(viewport.clientWidth-24)/objects.length));left=view.expanded?124:12;\n sections.forEach(s=>{s.left=left+s.start*columnWidth;s.box.style.left=s.left+4+'px';s.box.style.width=(s.end-s.start)*columnWidth-8+'px';s.caption.style.maxWidth=Math.max(32,Math.min(s.box.offsetWidth-24,viewport.clientWidth-32))+'px';});\n stepTop=Math.max(56,...sections.map(s=>s.box.offsetHeight+18));objects.forEach((o,i)=>{o.b.style.left=left+i*columnWidth+'px';o.b.style.width=columnWidth+'px';o.header.style.top=stepTop+'px';});\n const headerHeight=Math.max(...objects.map(o=>o.header.offsetHeight));plotTop=stepTop+headerHeight+42;plotBottom=plotTop+160;// 曲线块包含底部表情的15px半径;与人员块外缘保持24px。\n let roleBottom=plotBottom+15+24;\n roles.forEach(r=>{const rowHeight=Math.max(14,r.b.offsetHeight);r.y=roleBottom+rowHeight/2;roleBottom+=rowHeight+12;});\n width=left+objects.length*columnWidth+12;height=roles.length?roleBottom-12+16:plotBottom+31;\n scene.style.width=scene.style.minWidth=width+'px';frame.style.width=(width+(view.expanded?0:reference.offsetWidth))+'px';scene.style.height=height+'px';svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);reference.style.height=height+'px';axis.style.height=height+'px';axis.setAttribute('viewBox',`0 0 112 ${height}`);\n const x=i=>left+(i+.5)*columnWidth,y=score=>plotBottom-(score-1)*40;objects.forEach((o,i)=>{o.x=x(i);o.y=y(o.t.score);o.face.style.top=o.y+'px';o.b.style.height=height+'px';Object.assign(o.mask.style,{left:left+i*columnWidth+'px',top:stepTop-8+'px',width:columnWidth+'px',height:height-stepTop+8+'px'});});\n // 控制点停留在相邻真实评分之间,不产生超出1–5的过冲。\n const path=objects.map((o,i)=>{if(!i)return `M${o.x} ${o.y}`;const a=objects[i-1],mid=(a.x+o.x)/2;return `C${mid} ${a.y} ${mid} ${o.y} ${o.x} ${o.y}`;}).join(' ');line.setAttribute('d',path);area.setAttribute('d',path+`L${objects.at(-1).x} ${plotBottom}L${objects[0].x} ${plotBottom}Z`);tone.setAttribute('y1',plotTop);tone.setAttribute('y2',plotBottom);for(const [k,v]of Object.entries({x:left,y:plotTop,width:width-left,height:plotBottom-plotTop}))maskRect.setAttribute(k,v);\n grid.forEach((g,i)=>{const yy=plotTop+i*40;g.path.setAttribute('d',`M${view.expanded?112:0} ${yy}H${width-12}`);g.label.setAttribute('x',100);g.label.setAttribute('y',yy+4);});quantity.setAttribute('x',8);quantity.setAttribute('y',plotTop-22);stepCaption.setAttribute('x',8);stepCaption.setAttribute('y',22);\n roles.forEach(r=>{const cy=r.y;r.b.style.top=cy-r.b.offsetHeight/2+'px';r.runs.forEach(run=>Object.assign(run.b.style,{left:left+run.start*columnWidth+6+'px',top:cy-7+'px',width:(run.end-run.start)*columnWidth-12+'px'}));});placeSections();view.refresh();\n }\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let running=[],armed=true;\n function cancelMotion(){running.forEach(a=>a.cancel());running=[];line.style.strokeDasharray='';}\n function replay(){cancelMotion();if(reduced.matches||view.expanded)return;const length=line.getTotalLength();line.style.strokeDasharray=`${length} ${length}`;running.push(animateMermaidEntry(line,[{strokeDashoffset:length},{strokeDashoffset:0}],{duration:1000,fill:'backwards'}),animateMermaidEntry(area,[{opacity:0},{opacity:1}],{duration:500,fill:'backwards'}));objects.forEach((o,i)=>running.push(animateMermaidEntry(o.face,[{opacity:0},{opacity:1}],{duration:200,delay:i/Math.max(1,objects.length-1)*1000,fill:'backwards'})));root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n layout();viewport.addEventListener('scroll',()=>{placeSections();clear();if(document.activeElement?.classList.contains('journey-column'))select(document.activeElement.dataset.step);},{passive:true});root.addEventListener('qv:display-mode',()=>{cancelMotion();clear();layout();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});reduced.addEventListener('change',cancelMotion);\n let lastWidth=root.clientWidth,layoutFrame=0;new ResizeObserver(()=>{if(lastWidth===root.clientWidth||layoutFrame)return;layoutFrame=requestAnimationFrame(()=>{layoutFrame=0;const nextWidth=root.clientWidth;if(!root.isConnected||lastWidth===nextWidth)return;lastWidth=nextWidth;cancelMotion();clear();layout();});}).observe(root);\n}\n\n// Timeline DB 的 getTasks 会追加到共享数组;只读取一次并立即复制。\nasync function readMermaidTimelineModel(db,descriptions={}) {\n if(!db.getTasks||!db.getSections)return {reason:'Timeline 数据接口不兼容'};\n const tasks=db.getTasks().map(t=>({id:String(t.id),name:t.task.trim(),section:t.section.trim(),events:[...t.events].map(v=>v.trim()),description:descriptions[String(t.id)]||''}));\n if(tasks.some(t=>[t.name,t.section,...t.events].some(hasRichMermaidLabel)))return {reason:'此时间轴包含富文本,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!tasks.some(t=>t.id===id));if(unknown.length)throw new Error('说明引用了未知时间节点:'+unknown.join('、'));\n return {data:{kind:'timeline',tasks,title:db.getCommonDb?.().getDiagramTitle?.()||''}};\n}\n\n// QV 风格的 Git 分支图:模型读取与绘制。由构建器组装进 QV 模板,不修改 vendor 快照。\n// 视觉沿用 QV:分支色取 --viz-series-*(与背景混合降饱和)、12px 文字、按钮承载交互、共享视口与入场动效。\nfunction readMermaidGitModel(db,descriptions={}){\n if(!db.getCommits||!db.getBranchesAsObjArray)return {reason:'gitGraph 数据接口不兼容'};\n const branches=db.getBranchesAsObjArray().map(b=>b.name);\n // 未写 id 的提交由 Mermaid 生成「序号-7 位哈希」,只展示哈希。\n const commits=[...db.getCommits().values()].sort((a,b)=>a.seq-b.seq).map(c=>({id:String(c.id),message:String(c.message||''),type:c.type,tags:[...(c.tags||[])].map(String),parents:[...(c.parents||[])].map(String),branch:c.branch,auto:/^\\d+-[0-9a-f]{7}$/.test(String(c.id)),description:descriptions[c.id]||''}));\n if(!commits.length)return {reason:'gitGraph 没有提交'};\n if(commits.some(c=>[c.id,c.message,...c.tags].some(hasRichMermaidLabel)))return {reason:'gitGraph 包含富文本,保留原生语义'};\n const unknown=Object.keys(descriptions).filter(id=>!commits.some(c=>c.id===id));if(unknown.length)throw new Error('说明引用了未知提交:'+unknown.join('、'));\n return {data:{kind:'git',branches,commits,direction:db.getDirection?.()||'LR',title:db.getDiagramTitle?.()||''}};\n}\n\nfunction renderMermaidGit(root,data){\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-git');root.dataset.state='enhanced';root.dataset.renderer='qv-git';root.querySelector('.edge-key').hidden=true;\n if(data.title&&root.querySelector('h3').hidden){root.querySelector('h3').textContent=data.title;root.querySelector('h3').hidden=false;}\n const style=document.createElement('style');style.textContent=`\n #${root.id} .git-branch{position:absolute;display:block;box-sizing:border-box;height:24px;padding:0 10px;border:0;border-radius:999px;font:inherit;font-size:12px;line-height:24px;color:color-mix(in srgb,var(--git-tone) 60%,var(--foreground));background:color-mix(in srgb,var(--git-tone) 14%,var(--background));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .git-commit{position:absolute;display:flex;align-items:center;gap:8px;box-sizing:border-box;height:24px;padding:0;border:0;background:transparent;font:inherit;font-size:12px;line-height:18px;color:var(--foreground);white-space:nowrap;overflow:hidden;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .git-commit.lr{justify-content:center}\n #${root.id} .git-commit>span{overflow:hidden;text-overflow:ellipsis;min-width:0}\n #${root.id} .git-commit .git-text{flex:0 1 auto}\n #${root.id} .git-commit .git-hash{color:var(--muted-foreground);font-variant-numeric:tabular-nums}\n #${root.id} .git-commit .git-ref{flex:none;max-width:160px;height:20px;padding:0 8px;border-radius:999px;line-height:20px;color:color-mix(in srgb,var(--git-tone) 60%,var(--foreground));background:color-mix(in srgb,var(--git-tone) 14%,var(--background))}\n #${root.id} .git-tag{flex:none;max-width:200px;height:20px;padding:0 8px;box-sizing:border-box;border:1px solid var(--border);border-radius:6px;line-height:18px;color:var(--muted-foreground);background:var(--background);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n #${root.id} .git-tag.lr{position:absolute}\n #${root.id} .git-edge{fill:none;stroke:color-mix(in srgb,var(--git-tone) 70%,var(--background));stroke-width:2;stroke-linecap:round}\n #${root.id} .git-dot{fill:var(--git-tone);stroke:var(--background);stroke-width:2}\n #${root.id} .git-dot.merge{fill:var(--background);stroke:var(--git-tone);stroke-width:2.5}\n #${root.id} .git-dot.cherry{fill:var(--background);stroke:var(--git-tone);stroke-width:2;stroke-dasharray:3 2}\n #${root.id} .git-mark{fill:none;stroke:var(--git-tone);stroke-width:2;stroke-linecap:round}\n #${root.id} [data-branch]{transition:opacity 140ms}\n #${root.id} [data-dim]{opacity:.22}\n #${root.id} .git-commit:focus-visible,#${root.id} .git-branch:focus-visible{outline:1px solid var(--viz-series-1);outline-offset:2px}\n @media(prefers-reduced-motion:reduce){#${root.id} [data-branch]{transition:none}}\n `;root.append(style);\n const el=(tag,attrs)=>{const n=document.createElementNS(NS,tag);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);svg.append(n);return n;};\n const html=(tag,cls,text)=>{const n=document.createElement(tag);n.className=cls;if(text!==undefined)n.textContent=text;return n;};\n const lane=new Map(data.branches.map((b,i)=>[b,i])),tone=b=>`var(--viz-series-${(lane.get(b)??0)%6+1})`;\n const index=new Map(data.commits.map((c,i)=>[c.id,i])),firstOf=new Map();data.commits.forEach(c=>{if(!firstOf.has(c.branch))firstOf.set(c.branch,c.id);});\n const context=document.createElement('canvas').getContext('2d');context.font='12px '+getComputedStyle(root).fontFamily;const measure=t=>Math.ceil(context.measureText(t).width);\n const label=c=>c.auto?c.id.split('-')[1]:c.id;\n // 横排放得下正文宽度就沿用 Mermaid 默认方向;放不下改为 git log 式纵排(与流程图主轴规则一致)。\n const limit=cfg.readingWidth||root.clientWidth,n=data.commits.length,lanes=data.branches.length;\n const branchCol=Math.min(160,Math.max(...data.branches.map(measure))+20),step=Math.min(140,Math.max(56,...data.commits.map(c=>measure(label(c))+16)));\n const lrStart=24+branchCol+24,lrWidth=lrStart+(n-1)*step+step/2+24;\n const vertical=data.direction!=='LR'||lrWidth>limit;root.dataset.gitLayout=vertical?'rows':'lanes';root.dataset.mcxLayout=vertical?'git-rows':'git-lanes';\n const commits=[],edges=[],refs=[];let width,height,point;\n if(!vertical){\n const tagged=new Set(data.commits.filter(c=>c.tags.length).map(c=>lane.get(c.branch)));let y=36;const laneY=data.branches.map((b,i)=>{if(tagged.has(i))y+=26;const v=y;y+=56;return v;});\n width=lrWidth;height=laneY.at(-1)+48;point=c=>({x:lrStart+index.get(c.id)*step,y:laneY[lane.get(c.branch)]});\n data.branches.forEach((b,i)=>{const r=html('button','git-branch',b);r.type='button';r.title=b;r.dataset.branch=b;r.style.setProperty('--git-tone',tone(b));r.style.left='24px';r.style.top=(laneY[i]-12)+'px';r.style.maxWidth=branchCol+'px';scene.append(r);refs.push(r);});\n }else{\n const rowH=32,top=12,textX=24+lanes*24+12,order=data.direction==='BT'?[...data.commits].reverse():data.commits,row=new Map(order.map((c,i)=>[c.id,i]));\n width=Math.max(textX+240,limit);height=top+n*rowH+12;point=c=>({x:24+12+lane.get(c.branch)*24,y:top+row.get(c.id)*rowH+rowH/2});\n }\n scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.setAttribute('width',width);svg.setAttribute('height',height);svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.style.width=width+'px';svg.style.height=height+'px';\n // 分支分出时在父提交附近转入新泳道;合并时沿来源泳道前进,在合并提交前转入。\n const toXY=(m,c)=>vertical?[c,m]:[m,c],unit=vertical?32:step;\n for(const c of data.commits){const b=point(c),bm=vertical?b.y:b.x,bc=vertical?b.x:b.y;\n for(const [k,pid] of c.parents.entries()){const parent=data.commits[index.get(pid)];if(!parent)continue;const a=point(parent),am=vertical?a.y:a.x,ac=vertical?a.x:a.y,s=Math.sign(bm-am)||1,r=Math.min(Math.abs(bm-am),unit);\n let d,owner;const curve=(m0,c0,m1,c1)=>{const mid=(m0+m1)/2;return `C${toXY(mid,c0)} ${toXY(mid,c1)} ${toXY(m1,c1)}`;};\n if(ac===bc){d=`M${toXY(am,ac)} L${toXY(bm,bc)}`;owner=c.branch;}\n else if(firstOf.get(c.branch)===c.id&&k===0){d=`M${toXY(am,ac)} ${curve(am,ac,am+s*r,bc)} L${toXY(bm,bc)}`;owner=c.branch;}\n else{d=`M${toXY(am,ac)} L${toXY(bm-s*r,ac)} ${curve(bm-s*r,ac,bm,bc)}`;owner=parent.branch;}\n const path=el('path',{d,class:'git-edge','data-branch':owner});path.style.setProperty('--git-tone',tone(owner));edges.push({path,index:index.get(c.id)});}}\n for(const c of data.commits){const p=point(c),i=index.get(c.id),t=tone(c.branch),kind=['normal','reverse','highlight','merge','cherry'][c.type]||'normal';let mark;\n if(kind==='highlight')mark=el('rect',{x:p.x-6,y:p.y-6,width:12,height:12,rx:2,class:'git-dot','data-branch':c.branch});\n else{mark=el('circle',{cx:p.x,cy:p.y,r:kind==='merge'?6:kind==='cherry'?6:5.5,class:'git-dot '+kind,'data-branch':c.branch});}\n mark.style.setProperty('--git-tone',t);const marks=[mark];\n if(kind==='reverse'){const x=el('path',{d:`M${p.x-3} ${p.y-3}l6 6m0 -6l-6 6`,class:'git-mark','data-branch':c.branch});x.style.setProperty('--git-tone','var(--background)');marks.push(x);}\n const b=html('button','git-commit'+(vertical?'':' lr'));b.type='button';b.dataset.branch=c.branch;b.style.setProperty('--git-tone',t);\n b.title=[c.branch,c.auto?c.id.split('-')[1]:c.id,c.message,...c.tags,c.description].filter(Boolean).join(' · ');b.setAttribute('aria-label',b.title);\n const text=html('span','git-text'+(c.auto?' git-hash':''),label(c));const tags=[];\n if(vertical){const textX=24+lanes*24+12;if(firstOf.get(c.branch)===c.id){const ref=html('span','git-ref',c.branch);b.append(ref);}b.append(text);if(c.message&&!c.auto)b.append(html('span','git-hash',c.message));c.tags.forEach(v=>{const g=html('span','git-tag',v);b.append(g);});b.style.left=textX+'px';b.style.top=(p.y-12)+'px';b.style.maxWidth=(width-textX-16)+'px';}\n else{b.append(text);b.style.left=(p.x-(step-8)/2)+'px';b.style.width=(step-8)+'px';b.style.top=(p.y+10)+'px';c.tags.forEach((v,j)=>{const g=html('span','git-tag lr',v);g.title=v;g.dataset.branch=c.branch;g.style.maxWidth=Math.max(step-8,96)+'px';scene.append(g);g.style.left=(p.x-g.offsetWidth/2)+'px';g.style.top=(p.y-34-j*24)+'px';tags.push(g);});}\n scene.append(b);commits.push({c,i,b,marks,tags});}\n const view=createMermaidViewport(root);\n // 悬停或聚焦某提交时只保留其所在分支,其余分支降为背景。\n const all=()=>root.querySelectorAll('[data-branch]');function clear(){all().forEach(n=>n.removeAttribute('data-dim'));}\n function focus(branch){all().forEach(n=>n.toggleAttribute('data-dim',n.dataset.branch!==branch));}\n for(const o of commits){o.b.addEventListener('pointerenter',()=>focus(o.c.branch));o.b.addEventListener('pointerleave',clear);o.b.addEventListener('focus',()=>{focus(o.c.branch);const r=o.b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;});o.b.addEventListener('blur',clear);}\n for(const r of refs){r.addEventListener('pointerenter',()=>focus(r.dataset.branch));r.addEventListener('pointerleave',clear);r.addEventListener('focus',()=>focus(r.dataset.branch));r.addEventListener('blur',clear);}\n viewport.addEventListener('scroll',clear,{passive:true});root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n // 入场:按提交顺序画出连线并点亮提交,总时长约 1.2 秒;减少动态效果时不播放。\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let running=[],armed=true;const gap=Math.min(120,1200/Math.max(1,n-1));\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;\n for(const {path,index:i} of edges){const length=path.getTotalLength();path.style.strokeDasharray=`${length} ${length}`;const a=animateMermaidEntry(path,[{strokeDashoffset:length},{strokeDashoffset:0}],{duration:360,delay:Math.max(0,i-1)*gap,easing:'cubic-bezier(.2,.7,.3,1)',fill:'backwards'});a.onfinish=()=>{path.style.strokeDasharray='';};running.push(a);}\n for(const o of commits){const delay=o.i*gap;o.marks.forEach(m=>running.push(animateMermaidEntry(m,[{opacity:0},{opacity:1}],{duration:200,delay,fill:'backwards'})));[o.b,...o.tags].forEach(n=>running.push(animateMermaidEntry(n,[{opacity:0},{opacity:1}],{duration:200,delay:delay+60,fill:'backwards'})));}\n root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay();}},{threshold:[0,.2]}).observe(root);\n document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay();}});\n root.addEventListener('qv:display-mode',()=>{running.forEach(a=>a.cancel());clear();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\nfunction renderMermaidTimeline(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg');\n root.classList.add('qv-timeline');root.dataset.state='enhanced';root.dataset.renderer='qv-timeline';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','按源码顺序排列的时间轴,可横向滚动');\n if(data.title&&root.querySelector('h3').hidden){root.querySelector('h3').textContent=data.title;root.querySelector('h3').hidden=false;}\n const style=document.createElement('style');style.textContent=`\n #${root.id} .timeline-column{position:absolute;width:216px;top:42px;box-sizing:border-box;padding:12px;border-radius:12px;background:transparent}\n #${root.id} .timeline-period{display:block;width:100%;min-height:38px;border:0;background:none;text-align:left;font:inherit;font-size:13px;font-weight:500;color:var(--foreground);padding:0 0 10px;line-height:20px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .timeline-events{margin-top:38px;padding-left:22px;display:flex;flex-direction:column;gap:8px}\n #${root.id} .timeline-event{display:block;width:100%;box-sizing:border-box;border:0;border-radius:4px;background:transparent;color:var(--foreground);text-align:left;font:inherit;font-size:14px;line-height:22px;min-height:30px;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--timeline-tone) 7%,var(--background));transition:background-color 140ms;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .timeline-event[data-active=true],#${root.id} .timeline-event:focus-visible{color:var(--viz-series-1);outline:none;background:color-mix(in srgb,var(--timeline-tone) 14%,var(--background))}\n #${root.id} .timeline-event:focus-visible{outline:1px solid var(--viz-series-1);outline-offset:4px}\n #${root.id} .timeline-event-point{fill:var(--timeline-tone);stroke:var(--background);stroke-width:3}\n #${root.id} .timeline-section{position:absolute;top:0;font-size:12px;line-height:20px;color:color-mix(in srgb,var(--timeline-tone) 55%,var(--foreground))}\n #${root.id} .timeline-section>span{position:sticky;left:0;display:inline-block;box-sizing:border-box;max-width:100%;padding:4px 10px;border-radius:999px;background:color-mix(in srgb,var(--timeline-tone) 15%,var(--background));overflow-wrap:anywhere}\n #${root.id} .wires{z-index:1}\n #${root.id} .timeline-axis,#${root.id} .timeline-stem{stroke:var(--border);stroke-width:1;fill:none}\n #${root.id} .timeline-point{fill:var(--timeline-tone);stroke:var(--background);stroke-width:2}\n\n `;root.append(style);\n const NS='http://www.w3.org/2000/svg',el=(tag,attrs)=>{const n=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,v);svg.append(n);return n;};\n const tones=[3,6,2,1,4,5],groups=[...new Set(data.tasks.map(t=>t.section))],tone=section=>`var(--viz-series-${tones[groups.indexOf(section)%tones.length]})`;let gap=40,w=216;const left=24,width=left*2+data.tasks.length*(w+gap)-gap,objects=[];scene.style.width=width+'px';scene.style.minWidth=width+'px';\n let lastSection=null,sectionStart=0;data.tasks.forEach((t,i)=>{if(t.section!==lastSection){if(lastSection)renderSection(lastSection,sectionStart,i);lastSection=t.section;sectionStart=i;}if(i===data.tasks.length-1&&lastSection)renderSection(lastSection,sectionStart,i+1);});\n function renderSection(name,start,end){const n=document.createElement('div');n.className='timeline-section';n.style.setProperty('--timeline-tone',tone(name));const caption=document.createElement('span');caption.textContent=name;n.append(caption);n.dataset.sectionStart=left+start*(w+gap);n.dataset.sectionIndex=start;n.dataset.sectionCount=end-start;n.style.left=left+start*(w+gap)+'px';n.style.width=(end-start)*(w+gap)-gap+'px';scene.append(n);}\n const fadeId=root.id+'-timeline-tail',defs=el('defs',{}),fade=el('linearGradient',{id:fadeId,gradientUnits:'userSpaceOnUse'});defs.append(fade);[1,0].forEach((opacity,i)=>{const stop=el('stop',{offset:i?'100%':'0%','stop-opacity':opacity,style:'stop-color:var(--border)'});fade.append(stop);});\n const axis=el('path',{class:'timeline-axis',d:`M${left} 96H${left+(data.tasks.length-1)*(w+gap)}`});\n const tail=el('path',{class:'timeline-tail',fill:'none','stroke-width':1,stroke:`url(#${fadeId})`});\n data.tasks.forEach((t,i)=>{const x=left+i*(w+gap),column=document.createElement('div');column.className='timeline-column';column.dataset.period=t.id;column.style.setProperty('--timeline-tone',tone(t.section));column.style.left=x+'px';const b=document.createElement('button');b.type='button';b.className='timeline-period';b.textContent=t.name;column.append(b);const content=document.createElement('div');content.className='timeline-events';column.append(content);if(t.description){const d=document.createElement('div');d.className='timeline-description';d.textContent=t.description;d.style.cssText='font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere';content.append(d);}\n const stem=el('path',{class:'timeline-stem',d:`M${x} 101V116`}),point=el('circle',{class:'timeline-point',cx:x,cy:96,r:7,style:'--timeline-tone:'+tone(t.section)});\n const events=t.events.map((text,j)=>{const e=document.createElement('button');e.type='button';e.className='timeline-event';e.dataset.event=j;e.textContent=text;e.setAttribute('aria-label',[t.section,t.name,text].filter(Boolean).join(' · '));content.append(e);return e;});scene.append(column);const eventPoints=events.map(()=>el('circle',{class:'timeline-event-point',cx:x,cy:116,r:5.5,style:'--timeline-tone:'+tone(t.section)}));objects.push({t,column,b,events,eventPoints,point,stem});});\n const view=createMermaidViewport(root);\n // 完整图收紧列间距,文字在本列内换行,不改事件顺序。\n function layoutMode(){\n gap=view.expanded?24:40;w=view.expanded?184:216;\n const width=left*2+objects.length*(w+gap)-gap;\n root.style.maxWidth=view.expanded?width+'px':'';root.style.marginInline=view.expanded?'auto':'';\n objects.forEach((o,i)=>{o.column.style.width=w+'px';o.column.style.left=left+i*(w+gap)+'px';o.b.style.height='auto';o.b.style.fontSize='13px';});\n const ph=Math.max(38,...objects.map(o=>o.b.offsetHeight));let columnTop=42;\n root.querySelectorAll('.timeline-section').forEach(n=>{const start=Number(n.dataset.sectionIndex);n.style.left=left+start*(w+gap)+'px';n.style.width=Number(n.dataset.sectionCount)*(w+gap)-gap+'px';n.dataset.sectionStart=left+start*(w+gap);n.style.fontSize=view.expanded?'14px':'12px';});\n columnTop=Math.max(42,...[...root.querySelectorAll('.timeline-section')].map(n=>n.offsetHeight+12));const ay=columnTop+12+ph+16;\n objects.forEach((o,i)=>{const x=left+i*(w+gap)+12;o.column.style.top=columnTop+'px';o.b.style.height=ph+'px';o.point.setAttribute('cx',x);o.point.setAttribute('cy',ay);const centers=o.events.map(e=>columnTop+e.offsetTop+8+11);o.eventPoints.forEach((p,j)=>{p.setAttribute('cx',x);p.setAttribute('cy',centers[j]);});o.stem.setAttribute('d',centers.length?`M${x} ${ay+8}V${centers.at(-1)}`:'');});\n const height=Math.max(180,...objects.map(o=>o.column.offsetHeight+columnTop+24));scene.style.width=scene.style.minWidth=width+'px';scene.style.height=height+'px';svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);axis.setAttribute('d',`M${left+12} ${ay}H${left+12+(objects.length-1)*(w+gap)}`);const end=left+12+(objects.length-1)*(w+gap);tail.setAttribute('d',`M${end} ${ay}h120`);fade.setAttribute('x1',end);fade.setAttribute('x2',end+120);fade.setAttribute('y1',ay);fade.setAttribute('y2',ay);[axis,...objects.map(o=>o.stem)].forEach(p=>{p.style.strokeDasharray='none';});view.refresh();\n }\n root.addEventListener('qv:display-mode',()=>{layoutMode();if(!view.expanded)viewport.scrollLeft=view.savedInlinePosition?.left||0;});layoutMode();\n function clear(){root.querySelectorAll('[data-active]').forEach(n=>n.removeAttribute('data-active'));}\n const bind=(b,point)=>{b.onpointerenter=()=>{clear();b.dataset.active='true';if(point)point.dataset.active='true';};b.onpointerleave=()=>{if(!b.matches(':focus-visible')){b.removeAttribute('data-active');if(point)point.removeAttribute('data-active');}};b.onfocus=()=>{clear();b.dataset.active='true';if(point)point.dataset.active='true';const r=b.getBoundingClientRect(),v=viewport.getBoundingClientRect();if(r.left<v.left+12)viewport.scrollLeft-=v.left+12-r.left;else if(r.right>v.right-12)viewport.scrollLeft+=r.right-v.right+12;};b.onblur=()=>{b.removeAttribute('data-active');if(point)point.removeAttribute('data-active');};};objects.forEach(o=>{bind(o.b);o.events.forEach((e,j)=>bind(e,o.eventPoints[j]));});\n // 原生sticky与滚动同步合成,避免scroll事件回写left慢一帧造成边缘抖动。\n viewport.addEventListener('scroll',clear,{passive:true});root.addEventListener('qv:display-mode',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)');let running=[],armed=true;function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;const length=axis.getTotalLength();axis.style.strokeDasharray=`${length} ${length}`;running.push(animateMermaidEntry(axis,[{strokeDashoffset:length},{strokeDashoffset:0}],{duration:Math.max(480,Math.min(objects.length-1,10)*160),fill:'backwards'}));running.push(animateMermaidEntry(tail,[{opacity:0},{opacity:1}],{duration:240,delay:Math.min(objects.length-1,10)*160,fill:'backwards'}));objects.forEach((o,i)=>{const delay=Math.min(i,10)*160;running.push(animateMermaidEntry(o.column,[{opacity:0,transform:'translateY(8px)'},{opacity:1,transform:'translateY(0)'}],{duration:300,delay,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'}));running.push(animateMermaidEntry(o.point,[{opacity:0,r:'.32px'},{opacity:1,r:'7px'}],{duration:240,delay,fill:'backwards'}));o.eventPoints.forEach(p=>running.push(animateMermaidEntry(p,[{opacity:0},{opacity:1}],{duration:240,delay,fill:'backwards'})));const stemLength=o.stem.getTotalLength();o.stem.style.strokeDasharray=`${stemLength} ${stemLength}`;running.push(animateMermaidEntry(o.stem,[{strokeDashoffset:stemLength},{strokeDashoffset:0}],{duration:240,delay,fill:'backwards'}));});root.dataset.replays=String(+(root.dataset.replays||0)+1);}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\n// Mermaid 11.12 成员已拆分属性/方法,并给出可见性和分类符,不再解析源码签名。\nasync function readMermaidClassModel(db,descriptions={}) {\n const classes=[...db.getClasses()],raw=db.getData(),marks=new Set(['none','extension','composition','aggregation','dependency']);\n if(db.getNotes().length||db.styleClasses?.size||classes.some(([,n])=>n.styles?.length||n.cssClasses!=='default'||n.link||n.haveCallback||hasRichMermaidLabel(n.label)||[...n.members,...n.methods].some(a=>[a.id,a.parameters,a.returnType].some(hasRichMermaidLabel)))||raw.edges.some(e=>!marks.has(e.arrowTypeStart)||!marks.has(e.arrowTypeEnd)||hasRichMermaidLabel(e.label)))return {reason:'此类图包含尚未适配的注释、棒棒糖接口、链接、回调、富文本或自定义样式,保留原生语义'};\n const members=(list,kind)=>list.map(a=>({name:a.getDisplayDetails().displayText,kind,visibility:a.visibility,classifier:a.classifier}));\n const nodes=classes.map(([id,n])=>({id,name:(n.label||id)+(n.type?'<'+n.type+'>':''),technicalName:id,parent:n.parent||undefined,annotations:[...n.annotations],members:members(n.members,'attribute'),methods:members(n.methods,'method'),description:descriptions[id]||'',shape:'class'}));\n const groups=raw.nodes.filter(n=>n.isGroup).map(n=>({id:n.id,name:n.label||n.id,parent:n.parentId||undefined}));\n const unknown=Object.keys(descriptions).filter(id=>![...nodes,...groups].some(n=>n.id===id));if(unknown.length)throw new Error('说明引用了未知类或命名空间:'+unknown.join('、'));\n return {data:{kind:'class',direction:db.getDirection()||'TB',nodes,groups,layers:[],edges:raw.edges.map(e=>({id:e.id,from:e.start,to:e.end,label:e.label||'',startMark:e.arrowTypeStart,endMark:e.arrowTypeEnd,startMultiplicity:e.startLabelRight||'',endMultiplicity:e.endLabelLeft||'',async:e.pattern==='dashed'}))}};\n}\nasync function prepareMermaidClassLayout(data) {\n const measure=document.createElement('div');measure.style.cssText='position:absolute;left:-100000px;visibility:hidden;white-space:pre-wrap;overflow-wrap:anywhere;box-sizing:border-box';document.body.append(measure);\n const height=(text,width,font,padding)=>{measure.style.width=width+'px';measure.style.font=font;measure.style.padding=padding;measure.textContent=text;return measure.offsetHeight;};\n const mono='ui-monospace,SFMono-Regular,Menlo,Consolas,monospace',context=document.createElement('canvas').getContext('2d');context.font='12px '+mono;\n for(const n of data.nodes){\n n.layoutWidth=Math.max(240,Math.min(320,Math.max(context.measureText(n.name).width,...[...n.members,...n.methods].map(a=>context.measureText(a.name).width))+32));n.headerHeight=Math.max(52,height(n.name,n.layoutWidth,'600 14px/22px '+getComputedStyle(document.body).fontFamily,'14px 16px'))+(n.annotations.length?24:0);\n n.descriptionHeight=n.description?height(n.description,n.layoutWidth-20,'12px/18px '+getComputedStyle(document.body).fontFamily,'8px 10px')+8:0;\n for(const a of [...n.members,...n.methods])a.rowHeight=Math.max(30,height(a.name,n.layoutWidth-20,'12px/20px '+mono,'5px 10px'));\n n.layoutHeight=n.headerHeight+n.descriptionHeight+[n.members,n.methods].reduce((h,list)=>h+(list.length?44+list.reduce((sum,a)=>sum+a.rowHeight,0):0),0)+10;\n }\n context.font='12px '+getComputedStyle(document.body).fontFamily;for(const e of data.edges){e.labelWidth=Math.max(30,Math.min(180,context.measureText(e.label||'连接').width+12));e.labelHeight=height(e.label||'连接',e.labelWidth,'12px/18px '+getComputedStyle(document.body).fontFamily,'2px 6px');}\n measure.remove();\n for(const e of data.edges)if(e.from===e.to)e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';\n await prepareMermaidLayout(data);\n // 类图同样只在大量节点且明显长条时比较另一方向,不强制普通图趋近正方形。\n if(data.layout.width>(cfg.readingWidth||root.clientWidth)&&['LR','RL'].includes(data.direction)){\n const initial=structuredClone(data),ratio=d=>Math.abs(Math.log(d.layout.width/d.layout.height));data.direction=['LR','RL'].includes(data.direction)?'TB':'LR';\n for(const e of data.edges)if(e.from===e.to)e.fromSide=e.toSide=['LR','RL'].includes(data.direction)?'SOUTH':'EAST';\n await prepareMermaidLayout(data);if(data.layout.width>=initial.layout.width)Object.assign(data,initial);\n }\n\n // 自关联单独留出阅读通道,保留源端点及 UML 方向。\n const map=new Map(data.nodes.map(n=>[n.id,n]));\n for(const e of data.edges.filter(e=>e.from===e.to)){\n const n=map.get(e.from),a=e.points[0],b=e.points.at(-1),labelWidth=e.labelWidth;\n if(e.fromSide==='EAST'){\n const x=n.x+n.w+88;e.points=[a,[x,a[1]],[x,b[1]],b];e.labelPosition={x:x+labelWidth/2+8,y:(a[1]+b[1])/2};\n const right=x+labelWidth+24;data.layout.width=Math.max(data.layout.width,right+28);\n for(let id=n.parent;id;){const g=data.layout.groups.find(g=>g.id===id);if(!g)break;g.w=Math.max(g.w,right-g.x);id=g.parent;}\n }else{\n const y=n.y+n.h+72;e.points=[a,[a[0],y],[b[0],y],b];e.labelPosition={x:(a[0]+b[0])/2,y:y+20};\n const bottom=y+42;data.layout.height=Math.max(data.layout.height,bottom+28);\n for(let id=n.parent;id;){const g=data.layout.groups.find(g=>g.id===id);if(!g)break;g.h=Math.max(g.h,bottom-g.y);id=g.parent;}\n }\n }\n\n}\n\nfunction renderMermaidClass(root,data) {\n const scene=root.querySelector('.scene'),viewport=root.querySelector('.diagram-scroll'),svg=scene.querySelector('svg'),NS='http://www.w3.org/2000/svg';\n root.classList.add('qv-class');root.dataset.renderer='qv-class';root.dataset.state='enhanced';root.querySelector('.edge-key').hidden=true;viewport.setAttribute('aria-label','类结构图,可横向滚动');\n const style=document.createElement('style');style.textContent=`\n #${root.id}{--class-wire:light-dark(#727a82,#a3aab3)}\n #${root.id} .wires{z-index:1}\n #${root.id} .class-entity{z-index:2;position:absolute;border:1px solid var(--border);border-radius:14px;background:var(--background);overflow:hidden;transition:opacity 140ms ease,border-color 140ms ease;--class-type:light-dark(#287d7b,#80c3ba);--class-method:light-dark(#35659f,#98b9ed)}\n #${root.id} .class-header{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;width:100%;border:0;background:none;color:var(--foreground);font:inherit;font-size:14px;font-weight:600;padding:0 16px;text-align:left;white-space:normal;overflow-wrap:anywhere;line-height:22px;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-header small{color:var(--muted-foreground);font-size:12px;font-weight:400}\n #${root.id} .class-field{display:block;width:100%;min-height:30px;padding:5px 10px;white-space:pre-wrap;overflow-wrap:anywhere;box-sizing:border-box;line-height:20px;border:0;background:none;color:var(--foreground);font:12px/20px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;text-align:left;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-section-caption{height:28px;box-sizing:border-box;padding:7px 10px 3px;font-size:11px;color:var(--muted-foreground)}\n #${root.id} .class-panel{margin:0 10px 8px;padding:4px 0;border-radius:8px;background:color-mix(in srgb,var(--foreground) 4%,var(--background))}\n #${root.id} .class-description{margin:0 10px 8px;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--foreground) 4%,var(--background));font-size:12px;line-height:18px;color:var(--muted-foreground);overflow-wrap:anywhere}\n #${root.id} .class-token-type{color:var(--class-type)}\n #${root.id} .class-token-method{color:var(--class-method)}\n #${root.id} .class-token-muted{color:var(--muted-foreground)}\n #${root.id} .class-field[data-classifier=abstract]{font-style:italic}\n #${root.id} .class-field[data-classifier=static]{text-decoration:underline}\n #${root.id} .class-methods{border:0}\n #${root.id} .class-namespace{position:absolute;z-index:0;border:1px dashed color-mix(in srgb,var(--foreground) 18%,var(--background));border-radius:12px;background:var(--background);pointer-events:none}\n #${root.id} .class-namespace>span{position:absolute;left:14px;top:8px;padding:0 4px;font-size:12px;color:var(--muted-foreground);background:transparent}\n #${root.id} .class-marker path{stroke:currentColor;stroke-width:1.4;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke}\n #${root.id} .class-multiplicity{position:absolute;z-index:3;transform:translate(-50%,-50%);border:0;background:var(--background);padding:2px 4px;font:inherit;font-size:12px;line-height:18px;min-height:0;color:var(--muted-foreground);cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-field[data-active=true]{background:var(--accent)}\n #${root.id} .class-markers{color:var(--class-wire)}\n #${root.id} .class-wire{fill:none;stroke:var(--class-wire);stroke-width:1.4;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke}\n #${root.id} .class-label{position:absolute;z-index:3;transform:translate(-50%,-50%);padding:2px 6px;border:0;border-radius:4px;background:var(--background);color:var(--muted-foreground);font:inherit;font-size:12px;line-height:18px;min-height:0;max-width:180px;white-space:normal;overflow-wrap:anywhere;cursor:var(--cursor-interaction,pointer)}\n #${root.id} .class-tip{position:absolute;z-index:10;border-radius:12px;padding:12px 14px;background:var(--popover);color:var(--popover-foreground);font-size:12px;line-height:1.6;box-shadow:0 3px 12px #0002;pointer-events:none}\n `;root.append(style);\n const tip=document.createElement('div');tip.className='class-tip';tip.id=root.id+'-class-tip';tip.setAttribute('role','tooltip');tip.hidden=true;root.append(tip);\n const element=(tag,attrs,parent=svg)=>{const e=document.createElementNS(NS,tag);for(const [k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e;};\n const width=data.layout.width,height=data.layout.height,offset=(width-data.layout.width)/2;\n scene.style.cssText=`width:${width}px;min-width:${width}px;height:${height}px`;svg.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\n const nodes=data.nodes.map(n=>({...n,x:n.x+offset})),groups=data.layout.groups.map(n=>({...n,x:n.x+offset})),map=new Map([...nodes,...groups].map(n=>[n.id,n])),objects=[],edges=[];\n for(const g of groups){const box=document.createElement('div');box.className='class-namespace';box.dataset.namespace=g.id;box.style.cssText=`left:${g.x}px;top:${g.y}px;width:${g.w}px;height:${g.h}px`;const caption=document.createElement('span');caption.textContent=g.name;box.append(caption);scene.append(box);}\n for(const n of nodes){\n const box=document.createElement('div');box.className='class-entity';box.dataset.classEntity=n.id;box.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;\n const b=document.createElement('button');b.type='button';b.className='class-header';b.style.height=n.headerHeight+'px';b.dataset.classNode=n.id;if(n.annotations.length){const stereotype=document.createElement('small');stereotype.textContent=n.annotations.map(a=>'«'+a+'»').join(' ');b.append(stereotype);}const title=document.createElement('span');title.textContent=n.name;b.append(title);box.append(b);if(n.description){const d=document.createElement('div');d.className='class-description';d.textContent=n.description;box.append(d);}\n for(const [list,kind] of [[n.members,'attribute'],[n.methods,'method']]){const area=document.createElement('div');area.className='class-panel'+(kind==='method'?' class-methods':'');if(list.length){const caption=document.createElement('div');caption.className='class-section-caption';caption.textContent=(kind==='attribute'?'属性':'方法')+' · '+list.length;area.append(caption);}for(const member of list){const row=document.createElement('button');row.type='button';row.className='class-field';row.dataset.member=member.name;row.dataset.kind=kind;row.dataset.classifier=member.classifier==='*'?'abstract':member.classifier==='$'?'static':'';row.style.height=member.rowHeight+'px';syntax(row,member);row.setAttribute('aria-label',`${n.name} · ${kind==='attribute'?'属性':'方法'} · ${member.name}${member.classifier==='*'?' · 抽象':member.classifier==='$'?' · 静态':''}`);area.append(row);}if(list.length)box.append(area);}\n scene.append(box);objects.push({n,b,box});\n }\n function syntax(row,member){const text=member.name,span=(value,token)=>{if(!value)return;const s=document.createElement('span');s.className='class-token-'+token;s.textContent=value;row.append(s);};let offset=/^[+#~\\-]/.test(text)?1:0;span(text.slice(0,offset),'muted');const body=text.slice(offset),paren=body.indexOf('(');if(member.kind==='method'&&paren>=0){span(body.slice(0,paren),'method');const close=body.lastIndexOf(')');span(body.slice(paren,close+1),'muted');span(body.slice(close+1),'type');}else{const split=body.indexOf(' ');if(split>=0){span(body.slice(0,split),'type');row.append(document.createTextNode(body.slice(split)));}else row.append(document.createTextNode(body));}}\n const rounded=points=>{let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],l1=Math.hypot(b[0]-a[0],b[1]-a[1]),l2=Math.hypot(c[0]-b[0],c[1]-b[1]),r=Math.min(8,l1/2,l2/2);if(!l1||!l2)continue;d+=`L${b[0]-(b[0]-a[0])*r/l1} ${b[1]-(b[1]-a[1])*r/l1}Q${b[0]} ${b[1]} ${b[0]+(c[0]-b[0])*r/l2} ${b[1]+(c[1]-b[1])*r/l2}`;}return d+`L${points.at(-1)[0]} ${points.at(-1)[1]}`};\n const relationKind=e=>[e.startMark,e.endMark].includes('composition')?'组合':[e.startMark,e.endMark].includes('aggregation')?'聚合':[e.startMark,e.endMark].includes('extension')?(e.async?'实现':'继承'):[e.startMark,e.endMark].includes('dependency')?(e.async?'依赖':'关联'):'连接';\n function marker(point,next,kind,parent){const angle=Math.atan2(next[1]-point[1],next[0]-point[0]),size=kind==='extension'?10.392304845:12,scale=['extension','composition','aggregation'].includes(kind)?Math.min(1,Math.hypot(next[0]-point[0],next[1]-point[1])/size):1,g=element('g',{class:'class-marker','data-mark':kind,transform:`translate(${point[0]} ${point[1]}) rotate(${angle*180/Math.PI}) scale(${scale})`},parent);\n if(kind==='extension')element('path',{d:'M0 0L10.392304845 -6V6Z',fill:'var(--background)'},g);\n if(kind==='composition'||kind==='aggregation')element('path',{d:'M0 0L6 -6L12 0L6 6Z',fill:kind==='composition'?'currentColor':'var(--background)'},g);\n if(kind==='dependency')element('path',{d:'M6 -3L0 0L6 3',fill:'none'},g);return g;\n }\n for(const e of data.edges){const pts=e.points.map(([x,y])=>[x+offset,y]),line=pts.map(p=>[...p]);for(const [at,next,mark]of [[0,1,e.startMark],[line.length-1,line.length-2,e.endMark]])if(mark==='extension'||mark==='composition'||mark==='aggregation'){const p=line[at],n=pts[next],length=Math.hypot(n[0]-p[0],n[1]-p[1]),trim=Math.min(mark==='extension'?10.392304845:12,length);line[at]=[p[0]+(n[0]-p[0])*trim/length,p[1]+(n[1]-p[1])*trim/length];}\n const path=element('path',{d:rounded(line),class:'class-wire','data-class-edge':e.id,'data-from':e.from,'data-to':e.to,...(e.async?{'stroke-dasharray':'4 4'}:{})}),head=element('g',{class:'class-markers'});marker(pts[0],pts[1],e.startMark,head);marker(pts.at(-1),pts.at(-2),e.endMark,head);\n const detail=`${map.get(e.from).name}${e.startMultiplicity?' '+e.startMultiplicity:''} — ${map.get(e.to).name}${e.endMultiplicity?' '+e.endMultiplicity:''} · ${relationKind(e)}${e.label?' · '+e.label:''}`,p=e.labelPosition||path.getPointAtLength(path.getTotalLength()/2),label=document.createElement('button');label.type='button';label.className='class-label';label.textContent=e.label||relationKind(e);label.style.left=(e.labelPosition?p.x+offset:p.x)+'px';label.style.top=p.y+'px';label.setAttribute('aria-label',detail);scene.append(label);\n const multiplicities=[];for(const [point,next,text]of [[pts[0],pts[1],e.startMultiplicity],[pts.at(-1),pts.at(-2),e.endMultiplicity]])if(text){const length=Math.hypot(next[0]-point[0],next[1]-point[1]),ux=(next[0]-point[0])/length,uy=(next[1]-point[1])/length,sign=ux>=0?-1:1,b=document.createElement('button');b.type='button';b.className='class-multiplicity';b.textContent=text;b.setAttribute('aria-label',detail);b.style.left=(point[0]+ux*24-uy*sign*14)+'px';b.style.top=(point[1]+uy*24+ux*sign*14)+'px';scene.append(b);multiplicities.push(b);}\n edges.push({...e,path,head,label,multiplicities});\n }\n // 标签沿所属路径法向留8px间距,不能用背板擦断方向证据。\n for(const e of edges)if(e.label){const b=e.label,px=parseFloat(b.style.left),py=parseFloat(b.style.top),length=e.path.getTotalLength();let nearest,distance=Infinity,at=0;for(let d=0;d<=length;d+=3){const p=e.path.getPointAtLength(d),delta=Math.hypot(px-p.x,py-p.y);if(delta<distance){nearest=p;distance=delta;at=d;}}const a=e.path.getPointAtLength(Math.max(0,at-1)),c=e.path.getPointAtLength(Math.min(length,at+1));if(Math.abs(c.x-a.x)>=Math.abs(c.y-a.y)){const gap=b.offsetHeight/2+8;if(Math.abs(py-nearest.y)<gap)b.style.top=(nearest.y+(py<nearest.y?-gap:gap))+'px';}else{const gap=b.offsetWidth/2+8;if(Math.abs(px-nearest.x)<gap)b.style.left=(nearest.x+(px<nearest.x?-gap:gap))+'px';}}\n // 在所属路径旁寻找空位,保留多重性与卡片的阅读空间。\n const overlap=(a,b)=>Math.max(0,Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x))*Math.max(0,Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y));\n const labelBox=b=>({x:parseFloat(b.style.left)-b.offsetWidth/2-4,y:parseFloat(b.style.top)-b.offsetHeight/2-4,w:b.offsetWidth+8,h:b.offsetHeight+8});\n const occupied=[...nodes.map(n=>({x:n.x-4,y:n.y-4,w:n.w+8,h:n.h+8})),...edges.flatMap(e=>e.multiplicities.map(labelBox)),...groups.map(g=>({x:g.x,y:g.y,w:g.w,h:28}))];\n for(const e of edges){const b=e.label,w=b.offsetWidth,h=b.offsetHeight,origin={x:parseFloat(b.style.left),y:parseFloat(b.style.top)},length=e.path.getTotalLength(),candidates=[origin];\n for(let i=1;i<40;i++){const distance=length*i/40,p=e.path.getPointAtLength(distance),a=e.path.getPointAtLength(Math.max(0,distance-1)),z=e.path.getPointAtLength(Math.min(length,distance+1));if(Math.abs(z.x-a.x)>=Math.abs(z.y-a.y)){for(const sign of [-1,1])candidates.push({x:p.x,y:p.y+sign*(h/2+9)});}else for(const sign of [-1,1])candidates.push({x:p.x+sign*(w/2+9),y:p.y});}\n for(const c of candidates){const box={x:c.x-w/2-4,y:c.y-h/2-4,w:w+8,h:h+8};c.score=occupied.reduce((sum,o)=>sum+overlap(box,o),0)*1000+Math.hypot(c.x-origin.x,c.y-origin.y);if(box.x<0||box.y<0||box.x+box.w>width||box.y+box.h>height)c.score+=1e8;}\n candidates.sort((a,b)=>a.score-b.score);b.style.left=candidates[0].x+'px';b.style.top=candidates[0].y+'px';occupied.push(labelBox(b));\n }\n const view=createMermaidViewport(root);\n let hoverTimer=0,leaveTimer=0,focusSuppressed=false;const focusActions=new WeakMap();\n function clear(){clearTimeout(hoverTimer);clearTimeout(leaveTimer);tip.hidden=true;root.querySelectorAll('[data-active]').forEach(n=>n.removeAttribute('data-active'));objects.forEach(({box})=>{box.style.opacity='';box.style.borderColor='';});edges.forEach(e=>{e.path.style.opacity=e.head.style.opacity='';e.path.style.stroke=e.head.style.color='';e.multiplicities.forEach(b=>b.style.opacity='');e.label.style.opacity='';});}\n function select(trigger,id,edge){clear();trigger.dataset.active='true';const selected=edge?new Set([edge.from,edge.to]):new Set([id]);objects.forEach(({n,box})=>{box.style.opacity=selected.has(n.id)?'1':'.26';box.style.borderColor=selected.has(n.id)?'var(--viz-series-1)':'';});edges.forEach(e=>{const hit=edge?e===edge:e.from===id||e.to===id;e.path.style.opacity=e.head.style.opacity=hit?'1':'.14';e.path.style.stroke=e.head.style.color=hit?'var(--viz-series-1)':'';e.label.style.opacity=hit?'1':'.25';e.multiplicities.forEach(b=>b.style.opacity=hit?'1':'.25');});}\n function showRelation(trigger,edge){select(trigger,null,edge);}\n const bind=(b,activate)=>{focusActions.set(b,activate);b.onpointerenter=()=>{clearTimeout(hoverTimer);clearTimeout(leaveTimer);hoverTimer=setTimeout(activate,70);};b.onfocus=()=>{focusSuppressed=false;activate();};b.onclick=activate;};\n objects.forEach(({n,b,box})=>{bind(box,()=>select(b,n.id));bind(b,()=>select(b,n.id));box.querySelectorAll('.class-field').forEach(row=>bind(row,()=>select(row,n.id)));});edges.forEach(e=>{bind(e.label,()=>showRelation(e.label,e));e.multiplicities.forEach(b=>bind(b,()=>select(b,null,e)));});\n root.addEventListener('focusout',e=>{if(!root.contains(e.relatedTarget))clear();});\n scene.addEventListener('pointerout',e=>{if(e.relatedTarget&&scene.contains(e.relatedTarget))return;clearTimeout(hoverTimer);leaveTimer=setTimeout(clear,120);});viewport.addEventListener('scroll',()=>{clear();const active=document.activeElement;if(!focusSuppressed&&active?.matches(':focus-visible')&&focusActions.has(active))requestAnimationFrame(()=>{if(!focusSuppressed&&document.activeElement===active)focusActions.get(active)();});},{passive:true});root.addEventListener('qv:display-mode',clear);root.addEventListener('keydown',e=>{if(e.key==='Escape'){focusSuppressed=true;clear();}});\n const reduced=matchMedia('(prefers-reduced-motion:reduce)'),defs=element('defs',{});let running=[],armed=true;\n edges.forEach((e,i)=>{const length=e.path.getTotalLength(),id=root.id+'-class-mask-'+i,mask=element('mask',{id,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs);e.reveal=element('path',{d:e.path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-dasharray':`${length} ${length}`},mask);e.path.setAttribute('mask',`url(#${id})`)});\n function replay(){running.forEach(a=>a.cancel());running=[];if(reduced.matches||view.expanded)return;objects.forEach(({n,box})=>running.push(animateMermaidEntry(box,[{opacity:0,transform:'scale(.08)'},{opacity:1,transform:'scale(1)'}],{duration:240,delay:(n.layer||0)*180,fill:'backwards',easing:'cubic-bezier(.2,.65,.3,1)'})));edges.forEach(e=>{const delay=(map.get(e.from).layer||0)*180+110;running.push(animateMermaidEntry(e.reveal,[{strokeDashoffset:e.path.getTotalLength()},{strokeDashoffset:0}],{duration:650,delay,fill:'backwards'}));running.push(animateMermaidEntry(e.head,[{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'}));e.multiplicities.forEach(b=>running.push(animateMermaidEntry(b,[{opacity:0},{opacity:1}],{duration:50,delay:delay+650,fill:'backwards'})));if(e.label)running.push(animateMermaidEntry(e.label,[{opacity:0},{opacity:1}],{duration:100,delay:delay+600,fill:'backwards'}))});root.dataset.replays=String(+(root.dataset.replays||0)+1)}\n new IntersectionObserver(es=>{if(!es[0].isIntersecting)armed=true;else if(es[0].intersectionRatio>=.2&&armed){armed=false;replay()}},{threshold:[0,.2]}).observe(root);document.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else if(armed){armed=false;replay()}});root.addEventListener('qv:display-mode',()=>running.forEach(a=>a.cancel()));reduced.addEventListener('change',()=>running.forEach(a=>a.cancel()));view.refresh();\n}\n\nroot.setAttribute('aria-label',cfg.title||'架构图');\nlet data,sourceModel;\nconst viewport=root.querySelector('.diagram-scroll');\nlet diagramViewport;\nconst finishLoading=()=>{viewport.setAttribute('aria-busy','false');root.dispatchEvent(new Event('qv:ready',{bubbles:true}));};\nconst notice=root.querySelector('.qv-notice');\ntry {\n const mermaid=window.__mcxQVRuntime.mermaid;\n mermaid.initialize({startOnLoad:false,securityLevel:'strict',suppressErrorRendering:true,secure:['securityLevel','startOnLoad','suppressErrorRendering','maxTextSize'],maxTextSize:50000});\n if(!window.lucide&&document.readyState!=='complete')await new Promise(resolve=>window.addEventListener('load',resolve,{once:true}));\n const model=await readMermaidModel(mermaid,cfg.source,cfg.descriptions);\n if(model.data?.kind==='block'){renderMermaidBlock(root,model.data);finishLoading();return;}\n if(model.data?.kind==='c4'){renderMermaidC4(root,model.data);finishLoading();return;}\n if(model.data?.kind==='quadrant'){renderMermaidQuadrant(root,model.data);finishLoading();return;}\n if(model.data?.kind==='sankey'){await renderMermaidSankey(root,model.data);finishLoading();return;}\n if(model.data?.kind==='radar'){await renderMermaidRadar(root,model.data);finishLoading();return;}\n if(model.data?.kind==='xy'){await renderMermaidXY(root,model.data);finishLoading();return;}\n if(model.data?.kind==='pie'){await renderMermaidPie(root,model.data);finishLoading();return;}\n if(model.data?.kind==='kanban'){renderMermaidKanban(root,model.data);finishLoading();return;}\n if(model.data?.kind==='gantt'){await renderMermaidGantt(root,model.data);finishLoading();return;}\n if(model.data?.kind==='mindmap'){renderMermaidMindmap(root,model.data);finishLoading();return;}\n if(model.data?.kind==='journey'){renderMermaidJourney(root,model.data);finishLoading();return;}\n if(model.data?.kind==='timeline'){renderMermaidTimeline(root,model.data);finishLoading();return;}if(model.data?.kind==='git'){renderMermaidGit(root,model.data);finishLoading();return;}\n if(model.data?.kind==='class'){await prepareMermaidClassLayout(model.data);renderMermaidClass(root,model.data);finishLoading();return;}\n if(model.data?.kind==='er'){await prepareMermaidErLayout(model.data);renderMermaidEr(root,model.data);finishLoading();return;}\n if(model.data?.kind==='state'){await prepareMermaidStateLayout(model.data);renderMermaidState(root,model.data);finishLoading();return;}\n if(model.data?.kind==='sequence'){renderMermaidSequence(root,model.data,cfg);finishLoading();return;}\n if(!model.data){\n const result=await mermaid.render('mcx-qv-root-native',cfg.source);\n finishLoading();notice.hidden=false;scene.innerHTML=result.svg;root.querySelector('.edge-key').hidden=true;\n root.dataset.state='native';notice.textContent=model.reason+';当前显示 Mermaid 原生图。';return;\n }\n diagramViewport=createMermaidViewport(root);\n data=model.data;sourceModel=data;root.dataset.architecture=String(!!data.architecture);root.dataset.kind=data.kind||'';\n const iconNodes=data.nodes.filter(n=>!n.junction),explicitIcons=iconNodes.some(n=>n.explicitIcon),requiredIcons=cfg.icons==='required'||explicitIcons||(data.architecture&&cfg.icons!=='none');\n if(requiredIcons){const missing=iconNodes.filter(n=>!n.icon);if(missing.length)throw new Error('这些服务需要在 Mermaid 源码中指定语义图标:'+missing.map(n=>n.name).join('、'));if(!window.lucide)throw new Error('节点图标运行时尚未就绪');}\n root.dataset.hasGroups=String(!!data.groups?.length);\n \n if(data.kind==='flowchart'&&!data.navigation)await prepareFlowchartLayout(data,mermaid,cfg.source);else if(data.nativeGeometry)await prepareNativeGeometry(data,mermaid,cfg.source,data.db,data.architecture);else if(data.generic)await prepareMermaidLayout(data);scene.style.minWidth=(data.layout?data.layout.width:data.grouped?Math.max(736,Math.max(...data.layers.map((_,i)=>data.nodes.filter(n=>n.layer===i).length))*150-18):Math.max(560,data.layers.length*160+(data.layers.length-1)*48))+'px';root.dataset.state='enhanced';root.dataset.renderer='qv-architecture';notice.hidden=true;notice.textContent='';\n const missing=Object.keys(cfg.descriptions).filter(id=>![...sourceModel.nodes,...sourceModel.groups||[]].some(n=>n.id===id));\n if(missing.length){notice.hidden=false;notice.textContent='未匹配节点说明:'+missing.join('、');}\n} catch(error){finishLoading();notice.hidden=false;root.dataset.state='error';notice.setAttribute('role','alert');notice.textContent='无法绘制 Mermaid:'+String(error.message||error).slice(0,800);root.querySelector('.edge-key').hidden=true;return;}\n\nconst useIcons=!!window.lucide&&(sourceModel.nodes.some(n=>n.explicitIcon)||(cfg.icons!=='none'&&sourceModel.nodes.filter(n=>!n.junction).every(n=>n.icon)));\nlet architectureReader;\nconst tip=root.querySelector('.node-tip');let tipTimer,activeNode,leaveTimer,hoverTimer,pendingHover;\nconst NS='http://www.w3.org/2000/svg';function el(t,a,parent=svg){const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);parent.append(n);return n;}\n// 绘制结果始终可见,入场动画不作为显示的前置条件。\nlet nodes=[],arrows=[],edges=[],lastWidth=0,visible=false,armed=true,replayTimer,finishTimer;let motionSpecs=[],runningAnimations=[];root.dataset.motion='complete';\nconst legend=cfg.legend||{};\nfunction updateLegend(){for(const [selector,label,exists] of [['.solid-key',legend.solid,data.edges.some(e=>!e.async)],['.async-key',legend.dotted,data.edges.some(e=>e.async)]]){const item=root.querySelector(selector);item.hidden=!label||!exists;item.querySelector('.key-label').textContent=label||'';}\nroot.querySelector('.edge-key').hidden=[...root.querySelector('.edge-key').children].every(item=>item.hidden);}\nconst containmentLevels=new Map();\nfunction containmentLevel(id){\n if(containmentLevels.has(id))return containmentLevels.get(id);\n const group=sourceModel.groups?.find(g=>g.id===id);if(!group){containmentLevels.set(id,0);return 0;}\n const children=[...sourceModel.nodes,...sourceModel.groups].filter(n=>n.parent===id);\n const level=1+Math.max(0,...children.map(n=>containmentLevel(n.id)));containmentLevels.set(id,level);return level;\n}\nfunction setContainmentSurface(element,id){if(sourceModel.kind!=='flowchart'&&!sourceModel.architecture)return;const level=containmentLevel(id);element.dataset.containmentLevel=level;element.style.setProperty('--node-surface',level%2?'color-mix(in srgb,var(--muted) 18%,var(--background))':'var(--background)');}\nconst parents=new Map([...sourceModel.nodes,...sourceModel.groups||[]].map(n=>[n.id,n.parent]));\nfunction belongs(id,endpoint){while(id){if(id===endpoint)return true;id=parents.get(id);}return false;}\nfunction touches(e,id){return belongs(id,e.from)||belongs(id,e.to);}\nfunction relatedTo(id){const linked=data.edges.filter(e=>touches(e,id));return new Set(nodes.filter(n=>n.id===id||linked.some(e=>belongs(n.id,e.from)||belongs(n.id,e.to))).map(n=>n.id));}\nfunction styleArrows(id){arrows.forEach(a=>{const active=a.edges.some(e=>touches(e,id));a.el.style.fill=active?'var(--viz-series-1)':'';a.el.style.opacity=id?(active?.85:.055):1;});}\nfunction clear(){clearTimeout(hoverTimer);pendingHover=null;root.dataset.hoverNode='';clearTimeout(leaveTimer);architectureReader?.clear();root.querySelectorAll('.preview-item,.preview-link,.preview-arrow').forEach(el=>{el.style.opacity='';el.style.stroke='';el.style.fill='';});styleArrows(null);clearTimeout(tipTimer);tip.hidden=true;activeNode=null;nodes.forEach(n=>n.button.removeAttribute('aria-describedby'));nodes.forEach(n=>{n.button.style.opacity=1;n.button.style.borderColor='';n.button.style.removeProperty('--node-highlight');});edges.forEach(e=>{e.path.style.opacity='1';e.path.style.stroke='';e.path.style.strokeWidth=e.thick?'2':'';if(e.labelElement)e.labelElement.style.opacity='1';if(e.countElement)e.countElement.style.opacity='1';});}\nfunction focus(id){root.dataset.hoverNode=id;styleArrows(id);const related=relatedTo(id);nodes.forEach(n=>{n.button.style.opacity=related.has(n.id)?1:.22;n.button.style.borderColor=n.id===id?'var(--viz-series-1)':'';n.button.style.setProperty('--node-highlight',n.id===id?'var(--viz-series-1)':'var(--border)');});edges.forEach(e=>{const active=touches(e,id);e.path.style.opacity=active?.85:.055;e.path.style.stroke=active?'var(--viz-series-1)':'';e.path.style.strokeWidth=active?(e.thick?'2':'1.2'):(e.thick?'2':'');if(e.labelElement)e.labelElement.style.opacity=active?'1':'.22';if(e.countElement)e.countElement.style.opacity=active?'1':'.22';});}\nfunction rounded(points,radius=7){let d=`M${points[0][0]} ${points[0][1]}`;for(let i=1;i<points.length-1;i++){const a=points[i-1],b=points[i],c=points[i+1],u=Math.hypot(b[0]-a[0],b[1]-a[1]),v=Math.hypot(c[0]-b[0],c[1]-b[1]);if(!u||!v)continue;const r=Math.min(radius,u/2,v/2),pre=[b[0]+(a[0]-b[0])*r/u,b[1]+(a[1]-b[1])*r/u],post=[b[0]+(c[0]-b[0])*r/v,b[1]+(c[1]-b[1])*r/v];d+=`L${pre[0]} ${pre[1]}Q${b[0]} ${b[1]} ${post[0]} ${post[1]}`;}const last=points.at(-1);return d+`L${last[0]} ${last[1]}`;}\n// 快速掠过不切换焦点;跨节点保留旧状态,停稳后直接过渡到新状态。\nfunction queueNodeHover(n,event){\n if(sourceModel.kind!=='flowchart'){showTip(n,event);return;}\n clearTimeout(leaveTimer);\n clearTimeout(hoverTimer);pendingHover=null;if(activeNode===n.id)return;pendingHover=n.id;\n const pointer={clientX:event.clientX,clientY:event.clientY};\n hoverTimer=setTimeout(()=>{pendingHover=null;showTip(n,pointer);},140);\n}\nfunction leaveNodeHover(){\n clearTimeout(hoverTimer);pendingHover=null;clearTimeout(leaveTimer);\n if(!architectureReader?.focused)leaveTimer=setTimeout(clear,220);\n}\nfunction showTip(n,event){clearTimeout(leaveTimer);if(activeNode===n.id&&!architectureReader)return;if(root.dataset.motion==='running'){runningAnimations.forEach(a=>a.finish());root.dataset.motion='complete';}if(architectureReader){clear();activeNode=n.id;architectureReader.choose(n.id,n.button,!event);return;}clearTimeout(tipTimer);tip.hidden=true;nodes.forEach(m=>m.button.removeAttribute('aria-describedby'));focus(n.id);activeNode=n.id;if(!n.description?.trim())return;tipTimer=setTimeout(()=>{if(activeNode!==n.id)return;tip.textContent=n.description;tip.style.maxWidth=Math.min(260,root.clientWidth-16)+'px';tip.hidden=false;n.button.setAttribute('aria-describedby','mcx-qv-root-tip');const rr=root.getBoundingClientRect(),nr=n.button.getBoundingClientRect(),sx=scene.getBoundingClientRect().left-rr.left,sy=scene.getBoundingClientRect().top-rr.top,scale=diagramViewport.scale,box={x:nr.left-rr.left,y:nr.top-rr.top,w:nr.width,h:nr.height},w=tip.offsetWidth,h=tip.offsetHeight,px=event?.clientX?event.clientX-rr.left:box.x+box.w/2,py=event?.clientY?event.clientY-rr.top:box.y+box.h/2,gap=10;\nconst candidates=[[box.x+box.w+10,box.y-h-10],[box.x-w-10,box.y-h-10],[box.x+box.w+10,box.y+box.h+10],[box.x-w-10,box.y+box.h+10],[box.x+box.w+gap,py-h/2],[box.x-w-gap,py-h/2],[px-w/2,box.y-h-gap],[px-w/2,box.y+box.h+gap],[box.x,box.y-h-gap],[box.x+box.w-w,box.y+box.h+gap],...([22,38,54].flatMap(g=>[[box.x,box.y+box.h+g],[box.x+box.w-w,box.y+box.h+g],[box.x,box.y-h-g],[box.x+box.w-w,box.y-h-g]]))].map(([x,y])=>({x:Math.max(4,Math.min(root.clientWidth-w-4,x)),y:Math.max(0,Math.min(root.clientHeight-h,y))}));\nfunction overlap(a,b){return Math.max(0,Math.min(a.x+w,b.x+b.w)-Math.max(a.x,b.x))*Math.max(0,Math.min(a.y+h,b.y+b.h)-Math.max(a.y,b.y));}\nconst relatedNodes=relatedTo(n.id);\nfor(const c of candidates){c.score=nodes.reduce((sum,m)=>sum+overlap(c,{x:m.x*scale+sx,y:m.y*scale+sy,w:m.w*scale,h:m.h*scale})*(relatedNodes.has(m.id)?1000:1),0);for(const e of edges){const related=touches(e,n.id);for(const pt of e.samples){if(pt.x*scale+sx>c.x&&pt.x*scale+sx<c.x+w&&pt.y*scale+sy>c.y&&pt.y*scale+sy<c.y+h)c.score+=related?500:2;}}const dx=Math.max(c.x-px,0,px-c.x-w),dy=Math.max(c.y-py,0,py-c.y-h);c.score+=Math.hypot(dx,dy)*.5;}candidates.sort((a,b)=>a.score-b.score);tip.style.left=candidates[0].x+'px';tip.style.top=candidates[0].y+'px';},120);}\n\nfunction markerPath(x,y,ux,uy,type){const bx=x-5*ux,by=y-5*uy;\n if(type==='circle'){const cx=x-2.5*ux,cy=y-2.5*uy;return `M${cx-2.5} ${cy}a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0`;}\n if(type==='cross'){const cx=x-2.5*ux,cy=y-2.5*uy;return `M${cx-3} ${cy-2}l1 -1 5 5 -1 1Z M${cx+2} ${cy-3}l1 1 -5 5 -1 -1Z`;}\n return `M${x} ${y}L${bx-uy*2.2} ${by+ux*2.2}L${bx+uy*2.2} ${by-ux*2.2}Z`;\n}\nfunction drawNodeShape(n,button){\n button.classList.add('shaped');const svg=el('svg',{class:'node-outline',viewBox:`0 0 ${n.w} ${n.h}`},button),w=n.w,h=n.h;\n if(n.shape==='circle'||n.shape==='doublecircle'){el('ellipse',{cx:w/2,cy:h/2,rx:w/2-1,ry:h/2-1},svg);if(n.shape==='doublecircle')el('ellipse',{cx:w/2,cy:h/2,rx:w/2-6,ry:h/2-6,fill:'none'},svg);return;}\n if(n.shape==='stadium'){el('rect',{x:1,y:1,width:w-2,height:h-2,rx:h/2-1},svg);return;}\n if(n.shape==='cylinder'){el('path',{d:`M1 9C1 -2 ${w-1} -2 ${w-1} 9V${h-9}C${w-1} ${h+2} 1 ${h+2} 1 ${h-9}Z`,fill:'var(--node-surface,var(--background))'},svg);el('path',{d:`M1 9C1 20 ${w-1} 20 ${w-1} 9`,fill:'none'},svg);return;}\n const paths={diamond:`M${w/2} 1L${w-1} ${h/2}L${w/2} ${h-1}L1 ${h/2}Z`,hexagon:`M18 1H${w-18}L${w-1} ${h/2}L${w-18} ${h-1}H18L1 ${h/2}Z`,subroutine:`M1 1H${w-1}V${h-1}H1Z M9 1V${h-1} M${w-9} 1V${h-1}`,'lean-left':`M17 1H${w-1}L${w-17} ${h-1}H1Z`,'lean-right':`M1 1H${w-17}L${w-1} ${h-1}H17Z`,trapezoid:`M17 1H${w-17}L${w-1} ${h-1}H1Z`,'inv-trapezoid':`M1 1H${w-1}L${w-17} ${h-1}H17Z`,odd:`M17 1H${w-1}V${h-1}H17L1 ${h/2}Z`};el('path',{d:paths[n.shape]},svg);\n}\n\nfunction draw(){root.dataset.balanced=String(!!data.balanced);updateLegend();motionSpecs=[];runningAnimations.forEach(a=>a.cancel());runningAnimations=[];clear();clearTimeout(tipTimer);tip.hidden=true;const width=scene.clientWidth;if(!width)return;lastWidth=width;\nscene.querySelectorAll('.node,.layer,.edge-label,.edge-count,.qv-group').forEach(n=>{n.getAnimations().forEach(a=>a.cancel());n.remove();});svg.querySelectorAll('*').forEach(n=>n.getAnimations().forEach(a=>a.cancel()));root.querySelector('.edge-key').getAnimations().forEach(a=>a.cancel());svg.replaceChildren();nodes=[];arrows=[];edges=[];\nconst horizontal=!data.grouped&&['LR','RL'].includes(data.direction),complex=!horizontal;let height=0;\nconst layoutOffset=data.generic?Math.max(0,(width-data.layout.width)/2):0;\nif(data.generic){nodes=data.nodes.map(n=>({...n,x:n.x+layoutOffset}));for(const g of (sourceModel.kind==='flowchart'?[...data.layout.groups].sort((a,b)=>containmentLevel(b.id)-containmentLevel(a.id)):data.layout.groups)){const box=document.createElement('div');box.className='qv-group';box.dataset.group=g.id;box.style.cssText=`left:${g.x+layoutOffset}px;top:${g.y}px;width:${g.w}px;height:${g.h}px`;setContainmentSurface(box,g.id);const title=document.createElement(sourceModel.architecture?'button':'span');title.textContent=g.name;if(sourceModel.architecture){title.type='button';title.className='architecture-group-title';title.dataset.group=g.id;title.setAttribute('aria-label',g.name+',分组详情');title.onpointerenter=()=>architectureReader.choose(g.id,title);title.onpointerleave=leaveNodeHover;title.onfocus=()=>architectureReader.choose(g.id,title,true);title.onblur=clear;title.onclick=()=>architectureReader.choose(g.id,title);}box.append(title);scene.append(box);}}else if(horizontal){\n const count=data.layers.length,w=Math.min(160,(width-(count-1)*48)/count),gap=count>1?(width-w*count)/(count-1):0;\n const maxRows=Math.max(...data.layers.map((_,layer)=>data.nodes.filter(n=>n.layer===layer).length));\n const extent=(maxRows-1)*76+48;\n nodes=data.nodes.map(n=>{const peers=data.nodes.filter(m=>m.layer===n.layer),i=peers.indexOf(n),column=data.direction==='RL'?count-1-n.layer:n.layer;return {...n,x:column*(w+gap),y:8+(extent-((peers.length-1)*76+48))/2+i*76,w,h:48};});\n} else {\n let top=0;const order=data.layers.map((_,i)=>i);if(data.direction==='BT')order.reverse();\n order.forEach(layer=>{const label=data.layers[layer],members=data.nodes.filter(n=>n.layer===layer);\n if(label){const caption=document.createElement('div');caption.className='layer';caption.textContent=label;caption.style.top=top+'px';caption.style.setProperty('--rule-width',Math.max(0,width-64)+'px');scene.append(caption);top+=36;}\n const w=Math.min(154,(width-(members.length-1)*18)/members.length),h=48;\n {const gap=members.length>1?18:0,total=w*members.length+gap*(members.length-1);members.forEach((n,i)=>nodes.push({...n,x:(width-total)/2+i*(w+gap),y:top,w,h}));top+=h+70;}\n });\n}\nheight=data.layout?.height||Math.max(...nodes.map(n=>n.y+n.h));scene.style.height=height+'px';svg.setAttribute('viewBox',`0 0 ${width} ${height}`);\nconst defs=el('defs',{});\nconst map=Object.fromEntries([...(data.layout?.groups||[]).map(g=>({...g,x:g.x+layoutOffset})),...nodes].map(n=>[n.id,n]));\n// 同一侧同时承担出入时分成两个端口;同向连接仍共用端口。\nconst sides=new Map();\nfunction side(n,other){return complex?(other.y<n.y?'top':'bottom'):(other.x<n.x?'left':'right');}\nfor(const e of data.edges){for(const [id,other,incoming] of [[e.from,e.to,false],[e.to,e.from,true]]){const key=id+':'+side(map[id],map[other]);if(!sides.has(key))sides.set(key,new Set());sides.get(key).add(incoming?'in':'out');}}\nfunction port(n,other,incoming){const face=side(n,other),split=sides.get(n.id+':'+face).size===2,offset=split?(incoming?-7:7):0,gap=incoming?1:0;\n if(face==='top')return [n.x+n.w/2+offset,n.y-gap];\n if(face==='bottom')return [n.x+n.w/2+offset,n.y+n.h+gap];\n if(face==='left')return [n.x-gap,n.y+n.h/2+offset];\n return [n.x+n.w+gap,n.y+n.h/2+offset];\n}\ndata.edges.forEach((e,i)=>{const a=map[e.from],b=map[e.to],[sx,sy]=port(a,b,false),[tx,ty]=port(b,a,true);let d;\nif(e.curve){const c=e.curve.map((v,j)=>j%2===0?v+layoutOffset:v);d=`M${c[0]} ${c[1]}C${c[2]} ${c[3]} ${c[4]} ${c[5]} ${c[6]} ${c[7]}`;}else if(e.points){d=rounded(e.points.map(([x,y])=>[x+layoutOffset,y]),12);}else if(!complex){const mid=(sx+tx)/2;d=`M${sx} ${sy}C${mid} ${sy} ${mid} ${ty} ${tx} ${ty}`;}\nelse {const mid=(sy+ty)/2;d=`M${sx} ${sy}C${sx} ${mid} ${tx} ${mid} ${tx} ${ty}`;}\nconst path=el('path',{d,class:'wire','stroke-linejoin':'round','stroke-linecap':'round','data-edge':e.from+'-'+e.to,'data-from':e.from,'data-to':e.to});if(e.invisible)path.style.visibility='hidden';if(e.async)path.setAttribute('stroke-dasharray','4 4');if(e.thick)path.style.strokeWidth='2';const fullLength=path.getTotalLength(),tip=path.getPointAtLength(fullLength),near=path.getPointAtLength(Math.max(0,fullLength-.1)),rawAngle=Math.atan2(tip.y-near.y,tip.x-near.x),axis=Math.round(rawAngle/(Math.PI/2))*Math.PI/2,angle=Math.abs(rawAngle-axis)<.15?axis:rawAngle,ux=Math.cos(angle),uy=Math.sin(angle),bx=tip.x-(e.arrow?5:0)*ux,by=tip.y-(e.arrow?5:0)*uy;\npath.setAttribute('d',d.replace(/[-\\d.]+ [-\\d.]+$/,`${bx} ${by}`));\nconst length=path.getTotalLength();if(e.async)path.setAttribute('stroke-dashoffset',2-length%8);const samples=Array.from({length:Math.ceil(length/5)+1},(_,j)=>path.getPointAtLength(Math.min(length,j*5)));const edge={...e,path,samples};edges.push(edge);\nif(e.arrow&&!e.invisible){const key=[tip.x.toFixed(2),tip.y.toFixed(2),Math.round(angle*100),e.marker||'point'].join(':');let arrow=arrows.find(a=>a.key===key);if(!arrow){const head=el('path',{class:'arrow-head',d:markerPath(tip.x,tip.y,ux,uy,e.marker)});arrow={key,el:head,edges:[]};arrows.push(arrow);}arrow.edges.push(edge);}\n\nif(e.startArrow&&!e.invisible){const first=path.getPointAtLength(0),next=path.getPointAtLength(.1),angle=Math.atan2(first.y-next.y,first.x-next.x),ux=Math.cos(angle),uy=Math.sin(angle),head=el('path',{class:'arrow-head',d:markerPath(first.x,first.y,ux,uy,e.marker)});arrows.push({key:'start'+i,el:head,edges:[edge]});path.setAttribute('d',path.getAttribute('d').replace(/^M[-\\d.]+ [-\\d.]+/,`M${first.x-ux*5} ${first.y-uy*5}`));}\nif(e.bundleCount){const pt=path.getPointAtLength(length/2),badge=document.createElement('span');badge.className='edge-count';badge.textContent=e.bundleCount;badge.setAttribute('aria-label',e.bundleCount+' 条连接');badge.style.left=pt.x+'px';badge.style.top=pt.y+'px';scene.append(badge);edge.countElement=badge;}\nif(e.label){const pt=e.label&&typeof e.label==='string'&&e.labelPosition?{x:e.labelPosition.x+layoutOffset,y:e.labelPosition.y}:path.getPointAtLength(length/2),label=document.createElement('span');label.className='edge-label';label.textContent=e.label;label.style.left=pt.x+'px';label.style.top=(pt.y+(e.bundleCount?24:0))+'px';scene.append(label);edge.labelElement=label;}\nif(!reduced.matches){const maskId='mcx-qv-root-reveal-'+i,mask=el('mask',{id:maskId,maskUnits:'userSpaceOnUse',x:0,y:0,width,height},defs),reveal=el('path',{d:path.getAttribute('d'),fill:'none',stroke:'white','stroke-width':8,'stroke-linecap':'butt','stroke-dasharray':`${length} ${length}`},mask);path.setAttribute('mask',`url(#${maskId})`);motionSpecs.push(()=>animateMermaidEntry(reveal,[{strokeDashoffset:length},{strokeDashoffset:0}],{duration:650,delay:(a.layer||0)*220+110,easing:'linear',fill:'backwards'}));}\n});\nif(!reduced.matches)arrows.forEach(a=>{const layer=Math.min(...a.edges.map(e=>(map[e.from].layer||0)));motionSpecs.push(()=>animateMermaidEntry(a.el,[{opacity:0},{opacity:1}],{duration:50,delay:layer*220+760,fill:'backwards'}));});\nnodes.forEach(n=>{const button=document.createElement('button');button.type='button';button.className='node';button.dataset.layer=n.layer;button.dataset.shape=n.shape||'rect';if(n.junction)button.classList.add('junction');button.dataset.node=n.id;if(n.external)button.classList.add('external');button.style.cssText=`left:${n.x}px;top:${n.y}px;width:${n.w}px;height:${n.h}px`;setContainmentSurface(button,n.targetGroup!==undefined?n.targetGroup:n.id);button.setAttribute('aria-label',n.name);const label=document.createElement('span');label.className='name';if(useIcons&&!n.junction){const icon=document.createElement('i');icon.setAttribute('data-lucide',n.icon);icon.setAttribute('aria-hidden','true');label.append(icon);}if(!n.junction){if(sourceModel.architecture||sourceModel.kind==='flowchart'){const text=document.createElement('span');text.className=sourceModel.architecture?'architecture-name':'flowchart-name';text.textContent=n.name;label.append(text);}else label.append(document.createTextNode(n.name));}if(n.textWidth&&!n.groupCard){label.style.width=n.textWidth+'px';label.style.lineHeight=n.labelLine+'px';}button.append(label);\nif(n.external){if(n.targetGroup!==undefined){button.classList.add('external-group');const arrow=document.createElement('i');arrow.setAttribute('data-lucide','arrow-up-right');arrow.setAttribute('aria-hidden','true');label.append(arrow);label.style.width='100%';button.setAttribute('aria-label',n.name+','+(n.targetGroup===null?'返回总览':'进入此分组'));}else{const context=document.createElement('span');context.className='external-label';context.textContent='来自其他分组';button.append(context);}}\nif(n.shape&& !['rect','round'].includes(n.shape))drawNodeShape(n,button);button.onpointerenter=e=>queueNodeHover(n,e);button.onpointermove=e=>{if(activeNode!==n.id)queueNodeHover(n,e);};button.onpointerleave=leaveNodeHover;button.onfocus=()=>showTip(n);button.onblur=clear;if(sourceModel.architecture&&!button.onclick)button.onclick=()=>showTip(n,{type:'click'});scene.append(button);n.button=button;if(!reduced.matches)motionSpecs.push(()=>animateMermaidEntry(button,[{opacity:0,transform:'scale(.08)',offset:0},{opacity:1,transform:'scale(.7)',offset:.55},{opacity:1,transform:'scale(1)',offset:1}],{duration:240,delay:n.layer*220,easing:'cubic-bezier(.2,.65,.3,1)',fill:'backwards'}));});\nif(window.lucide)window.lucide.createIcons({attrs:{width:16,height:16}});\narchitectureReader?.refresh();diagramViewport.refresh();\n}\n\n\n\n\n\n\n\n\nif(sourceModel.architecture)architectureReader=createArchitectureReader(root,sourceModel,viewport,diagramViewport,focus,()=>clearTimeout(leaveTimer),leaveNodeHover);\n\nnew ResizeObserver(()=>requestAnimationFrame(()=>{if(architectureReader)architectureReader.refresh();else clear();})).observe(root);viewport.addEventListener('scroll',()=>{if(architectureReader)architectureReader.onScroll();else clear();},{passive:true});viewport.addEventListener('pointermove',e=>{if(sourceModel.kind==='flowchart'&&activeNode&&!e.target.closest('button.node')){clearTimeout(leaveTimer);leaveTimer=setTimeout(clear,220);}},{passive:true});root.onpointerleave=leaveNodeHover;document.addEventListener('keydown',e=>{if(e.key==='Escape')clear();});function playMotion(){if(diagramViewport.expanded){runningAnimations.forEach(a=>a.cancel());root.dataset.motion='complete';return;}clear();runningAnimations.forEach(a=>a.cancel());root.dataset.motion=reduced.matches?'complete':'running';root.dataset.replays=String(+(root.dataset.replays||0)+1);runningAnimations=reduced.matches?[]:motionSpecs.map(start=>start());clearTimeout(finishTimer);finishTimer=setTimeout(()=>root.dataset.motion='complete',mermaidEntryBudget);}\nfunction scheduleEntry(){clearTimeout(replayTimer);replayTimer=setTimeout(()=>{replayTimer=null;if(visible&&!document.hidden){playMotion();}else armed=true;},80);}\nfunction replay(){if(visible&&armed&&!document.hidden){armed=false;scheduleEntry();}}\nreduced.addEventListener('change',()=>{runningAnimations.forEach(a=>a.cancel());root.dataset.motion='complete';});\nnew IntersectionObserver(es=>{const e=es[0];if(!e.isIntersecting){visible=false;armed=true;}else if(e.intersectionRatio>=.2||(e.rootBounds&&e.boundingClientRect.height>e.rootBounds.height)){visible=true;replay();}},{threshold:[0,.2]}).observe(root);\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden)armed=true;else replay();});new ResizeObserver(()=>{if(scene.clientWidth!==lastWidth){draw();if(replayTimer)scheduleEntry();}}).observe(scene);draw();finishLoading();\nroot.addEventListener('qv:display-mode',()=>{clear();architectureReader?.refresh();clearTimeout(replayTimer);clearTimeout(finishTimer);runningAnimations.forEach(a=>a.cancel());root.dataset.motion='complete';});\n})();\n"}
|