minecodex 2.0.38 → 2.0.40

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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/apps/dashboard/assets/preview-content.js +3 -0
  3. package/features/markdown-reading/src/adapters/codex/lists.mjs +0 -26
  4. package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +5 -3
  5. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +1 -1
  6. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +1 -108
  7. package/features/markdown-reading/src/adapters/codex/toc.mjs +1 -1
  8. package/features/markdown-reading/src/injection-source.mjs +8 -10
  9. package/features/markdown-reading/src/markdown-reading.mjs +5 -2
  10. package/features/mermaid-diagrams/assets/qv-dependencies.js +4412 -0
  11. package/features/mermaid-diagrams/assets/qv-export.js +2 -0
  12. package/features/mermaid-diagrams/assets/qv-loading.css +4 -0
  13. package/features/mermaid-diagrams/assets/qv-template.json +1 -0
  14. package/features/mermaid-diagrams/assets/workflow.svg +15 -0
  15. package/features/mermaid-diagrams/codex-feature.json +31 -0
  16. package/features/mermaid-diagrams/src/adapters/codex/mermaid-viewer.mjs +95 -0
  17. package/features/mermaid-diagrams/src/adapters/codex/mermaid.mjs +91 -0
  18. package/features/mermaid-diagrams/src/adapters/codex/native.mjs +34 -0
  19. package/features/mermaid-diagrams/src/adapters/codex/runtime.mjs +42 -0
  20. package/features/mermaid-diagrams/src/injection-source.mjs +26 -0
  21. package/features/mermaid-diagrams/src/mermaid/export.mjs +83 -0
  22. package/features/mermaid-diagrams/src/mermaid/frame.mjs +109 -0
  23. package/features/mermaid-diagrams/src/mermaid/loading.mjs +27 -0
  24. package/features/mermaid-diagrams/src/mermaid/theme.css +8 -0
  25. package/features/mermaid-diagrams/src/mermaid/viewport.mjs +75 -0
  26. package/features/mermaid-diagrams/src/mermaid-diagrams.mjs +85 -0
  27. package/features/mermaid-diagrams/vendor/lucide-LICENSE +43 -0
  28. package/features/mermaid-diagrams/vendor/qv/LICENSE +21 -0
  29. package/features/mermaid-diagrams/vendor/qv/THIRD_PARTY_NOTICES.txt +3009 -0
  30. package/features/mermaid-diagrams/vendor/qv/provenance.json +55 -0
  31. package/features/usage-meter/src/adapters/codex/renderer.mjs +11 -3
  32. package/features/usage-meter/src/usage-meter.mjs +19 -4
  33. package/package.json +19 -5
  34. package/packages/cli/src/config.mjs +2 -1
  35. package/packages/cli/src/control-server.mjs +1 -1
  36. package/packages/codex-adapter/src/markdown/block-actions.mjs +107 -0
  37. package/packages/codex-adapter/src/markdown/editor-api.mjs +26 -0
  38. package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/preview.mjs +2 -8
  39. package/packages/plugin/assets/app.js +2 -2
  40. /package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/markdown-native.mjs +0 -0
  41. /package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/providers.mjs +0 -0
  42. /package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/toc-native.mjs +0 -0
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <rect width="8" height="8" x="3" y="3" rx="2" />
13
+ <path d="M7 11v4a2 2 0 0 0 2 2h4" />
14
+ <rect width="8" height="8" x="13" y="13" rx="2" />
15
+ </svg>
@@ -0,0 +1,31 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "mermaid-diagrams",
4
+ "version": "0.1.0",
5
+ "label": {
6
+ "en": "Mermaid Diagrams (Experimental)",
7
+ "zh-CN": "Mermaid 图表增强(实验性)"
8
+ },
9
+ "description": {
10
+ "en": "Opt in to enhanced Markdown diagrams with zoom and PNG download. Off by default.",
11
+ "zh-CN": "在 Markdown 预览中增强图表,支持缩放与 PNG 下载。实验性功能,默认关闭。"
12
+ },
13
+ "entry": {
14
+ "kind": "page-script",
15
+ "script": "src/mermaid-diagrams.mjs",
16
+ "sourceFactory": "src/injection-source.mjs",
17
+ "resources": {
18
+ "mermaidStyles": "src/mermaid/theme.css",
19
+ "qvLoadingStyles": "assets/qv-loading.css",
20
+ "qvExport": "assets/qv-export.js",
21
+ "qvTemplate": "assets/qv-template.json",
22
+ "qvDependencies": "assets/qv-dependencies.js"
23
+ }
24
+ },
25
+ "icon": {
26
+ "resource": "assets/workflow.svg"
27
+ },
28
+ "placement": {
29
+ "order": 91
30
+ }
31
+ }
@@ -0,0 +1,95 @@
1
+ import { readQvTheme } from '../../mermaid/frame.mjs';
2
+ export function createMarkdownMermaidControls(native) {
3
+ const { React, Actions, Button, CopyButton, Tooltip, ExpandIcon, DownloadIcon, downloadImage, TablePreview } = native;
4
+ const h = React.createElement, chinese = document.documentElement.lang.startsWith('zh');
5
+ const download = chinese ? '下载图表' : 'Download diagram';
6
+ const expand = chinese ? '放大图表' : 'Expand diagram';
7
+ function configureShell(area) {
8
+ const shell=area.parentElement.parentElement;
9
+ shell.style.paddingTop='64px';shell.style.paddingBottom='48px';
10
+ shell.style.paddingInline='clamp(16px, 4vw, 48px)';
11
+ }
12
+ function CanvasMeasure({onMeasured}) {
13
+ const ref=React.useRef(null);
14
+ React.useLayoutEffect(()=>{
15
+ const area=ref.current.closest('[data-testid="image-preview-dismiss-area"]');
16
+ configureShell(area);
17
+ let ticket=requestAnimationFrame(()=>{ticket=requestAnimationFrame(()=>onMeasured({width:area.clientWidth,height:area.clientHeight}));});
18
+ return ()=>cancelAnimationFrame(ticket);
19
+ },[onMeasured]);
20
+ return h('div',{ref});
21
+ }
22
+ function FrameView({ item, size, palette, onViewport }) {
23
+ const ref = React.useRef(null);
24
+ React.useLayoutEffect(() => {
25
+ const slot = ref.current, frame = item.controller.frame;
26
+ item.expanded = true;
27
+ slot.moveBefore(frame, null);
28
+ frame.style.width = size.width + 'px'; frame.style.height = size.height + 'px';
29
+ frame.style.transformOrigin = '0 0'; item.controller.mode('viewer');
30
+ const wrapper = slot.parentElement;
31
+ const area = slot.closest('[data-testid="image-preview-dismiss-area"]');
32
+ configureShell(area);
33
+ const update = () => { const scale=wrapper.getBoundingClientRect().width/size.width;frame.style.transform=`scale(${scale})`;frame.dataset.zoom=String(scale); };
34
+ const measure = () => {
35
+ const rect = area.getBoundingClientRect();
36
+ onViewport(previous => previous?.width === rect.width && previous?.height === rect.height
37
+ ? previous : { width: rect.width, height: rect.height });
38
+ };
39
+ const observer = new ResizeObserver(update); observer.observe(wrapper); update();
40
+ const viewportObserver = new ResizeObserver(measure); viewportObserver.observe(area); measure();
41
+ let drag = null;
42
+ item.controller.input(input => {
43
+ const rect = frame.getBoundingClientRect(), scale = rect.width / size.width;
44
+ if (input.kind === 'wheel') {
45
+ area.dispatchEvent(new WheelEvent('wheel',{bubbles:true,cancelable:true,ctrlKey:true,deltaY:input.deltaY,deltaMode:input.deltaMode,clientX:rect.left+input.x*scale,clientY:rect.top+input.y*scale}));
46
+ }
47
+ if (input.kind === 'start') drag = {left:area.scrollLeft,top:area.scrollTop};
48
+ if (input.kind === 'drag' && drag) { area.scrollLeft = drag.left-input.dx*scale; area.scrollTop = drag.top-input.dy*scale; }
49
+ if (input.kind === 'end') drag = null;
50
+ if (input.kind === 'pan') {area.scrollLeft+=input.dx;area.scrollTop+=input.dy;}
51
+ });
52
+ return () => {
53
+ observer.disconnect(); viewportObserver.disconnect(); item.controller.input(null); item.expanded = false;
54
+ if (item.slot.isConnected) item.slot.moveBefore(frame, null);
55
+ frame.style.transform = ''; frame.style.transformOrigin = ''; frame.style.width = '';
56
+ frame.style.height = item.height + 'px'; item.controller.mode('inline');
57
+ };
58
+ }, [item,size.width]);
59
+ React.useLayoutEffect(()=>{item.controller.frame.style.height=size.height+'px';},[item,size.height]);
60
+ return h('div',{ref,className:'mcx-reading-qv-slot',style:palette});
61
+ }
62
+ return function Controls({ item }) {
63
+ const [open,setOpen] = React.useState(false), [size,setSize] = React.useState(null);
64
+ const [downloading,setDownloading] = React.useState(false), [failed,setFailed] = React.useState(false);
65
+ const [measuring,setMeasuring] = React.useState(false);
66
+ const pendingMeasurement = React.useRef(null);
67
+ const onMeasured = React.useCallback(canvas=>{pendingMeasurement.current?.resolve(canvas);pendingMeasurement.current=null;setMeasuring(false);},[]);
68
+ React.useEffect(()=>()=>{pendingMeasurement.current?.reject(new Error('diagram-disposed'));},[]);
69
+ const [viewport,setViewport] = React.useState(null);
70
+ const [ready,setReady] = React.useState(item.ready), [palette,setPalette] = React.useState({});
71
+ const trigger = React.useRef(null);
72
+ React.useEffect(() => { item.setReady=setReady; item.setNaturalHeight=height=>setSize(size=>size&&size.height!==height?{...size,height}:size); item.close=()=>setOpen(false); return ()=>{item.setReady=null;item.setNaturalHeight=null;item.close=null;}; }, [item]);
73
+ const save = async () => {
74
+ if (downloading || !item.ready) return;
75
+ setDownloading(true);setFailed(false);
76
+ try {
77
+ const canvas = open && viewport ? viewport : await new Promise((resolve,reject)=>{pendingMeasurement.current={resolve,reject};setMeasuring(true);});
78
+ const blob = await item.export(canvas);
79
+ if (!await downloadImage({downloadSrc:blob,suggestedFilename:'mermaid-diagram.png'})) throw new Error('diagram-download-failed');
80
+ } catch { setFailed(true); }
81
+ finally { setDownloading(false); }
82
+ };
83
+ // 把占画幅 90% 的适配尺寸交给原生组件,作为其 100% 缩放基准。
84
+ const fit = viewport && size ? .9 * Math.min(viewport.width / size.width, viewport.height / size.height) : 1;
85
+ return h(React.Fragment,null,
86
+ h(Actions, { 'data-visible':true, 'data-markdown-copy':'exclude' },
87
+ h(CopyButton,{ariaLabel:chinese?'复制图表源码':'Copy diagram source',buttonText:chinese?'复制图表源码':'Copy diagram source',iconOnly:true,variant:'blockAction',onCopy:()=>navigator.clipboard.writeText(item.source).then(()=>true)}),
88
+ h(Tooltip,{tooltipContent:failed?(chinese?'下载失败,点击重试':'Download failed. Retry'):download,delayOpen:true},h(Button,{'aria-label':download,'aria-busy':downloading,disabled:!ready||downloading,color:'blockAction',radius:'full',size:'inputIcon',onClick:save},h(DownloadIcon,{className:'icon-xs'}))),
89
+ h(Tooltip,{tooltipContent:expand,delayOpen:true},h(Button,{ref:trigger,'aria-label':expand,'aria-expanded':open,disabled:!ready,color:'blockAction',radius:'full',size:'inputIcon',onClick:()=>{setFailed(false);const theme=readQvTheme(item.host);setPalette({background:theme.tokens['--background'],color:theme.tokens['--foreground']});setViewport(null);setSize(item.controller.size());setOpen(true);}},h(ExpandIcon)))),
90
+ measuring?h('div',{'aria-hidden':true,inert:true,style:{position:'fixed',inset:0,width:'100vw',height:'100dvh',visibility:'hidden',pointerEvents:'none',zIndex:-1}},
91
+ h(TablePreview,{src:'',alt:expand,open:true,inline:true,imageSurface:'card',showDownload:false,showZoomControls:true,onOpenChange:()=>{},zoomableContent:{width:1,height:1,content:h(CanvasMeasure,{onMeasured})}})):null,
92
+ open&&size?h(TablePreview,{src:'',alt:expand,open:true,dialogAriaLabel:expand,closeAriaLabel:chinese?'关闭图表预览':'Close diagram preview',imageSurface:'card',caption:failed?(chinese?'下载失败,请重试。':'Download failed. Please retry.'):undefined,showDownload:true,downloadAriaLabel:download,onDownload:save,showZoomControls:true,disableOpenAnimation:true,onOpenChange:setOpen,onOpenAutoFocus:event=>{event.preventDefault();item.controller.frame.focus({preventScroll:true});},onCloseAutoFocus:event=>{event.preventDefault();trigger.current?.focus({preventScroll:true});},
93
+ zoomableContent:{width:size.width*fit,height:size.height*fit,content:h(FrameView,{item,size,palette,onViewport:setViewport})}}):null);
94
+ };
95
+ }
@@ -0,0 +1,91 @@
1
+ import { createQvResources, createQvFrame, readQvTheme } from '../../mermaid/frame.mjs';
2
+ import { exportQvDiagram } from '../../mermaid/export.mjs';
3
+ import { createQvLoading } from '../../mermaid/loading.mjs';
4
+ import { captureMarkdownProviders } from '../../../../../packages/codex-adapter/src/markdown/providers.mjs';
5
+ import { createMarkdownMermaidControls } from './mermaid-viewer.mjs';
6
+
7
+ export function installMarkdownMermaid(view, api, native, codex, resources, onStatus = () => {}) {
8
+ if (!Element.prototype.moveBefore) throw new Error('qv-state-preserving-move-unavailable');
9
+ const { Decoration, StateField, StateEffect, Compartment, WidgetType, Prec } = api;
10
+ const assets = createQvResources(resources), mounted = new Set();
11
+ const Controls = createMarkdownMermaidControls(native);
12
+ const withProviders = captureMarkdownProviders(view.dom, codex);
13
+ let disposed = false;
14
+ const available = () => [...mounted].every(item => item.ready);
15
+ const report = () => { if (!disposed) onStatus(available()); };
16
+ class Diagram extends WidgetType {
17
+ constructor(source) { super(); this.source = source; }
18
+ eq(other) { return this.source === other.source; }
19
+ toDOM() {
20
+ const host = document.createElement('div'), slot = document.createElement('div'), actions = document.createElement('div');
21
+ const status = document.createElement('div');
22
+ const chinese = document.documentElement.lang.startsWith('zh');
23
+ status.className = 'mcx-reading-qv-status'; status.setAttribute('role', 'status');
24
+ status.setAttribute('aria-label', chinese ? '正在加载图表…' : 'Loading diagram…');
25
+ host.className = 'mcx-reading-qv-widget'; slot.className = 'mcx-reading-qv-inline'; actions.className = 'mcx-reading-qv-actions';
26
+ host.append(actions, slot); slot.append(status);
27
+ const finishLoading = createQvLoading(status);
28
+ const item = { host, slot, source:this.source, height:280, ready:false, expanded:false };
29
+ slot.style.height = '280px';
30
+ item.controller = createQvFrame(this.source, assets, readQvTheme(view.dom), height => {
31
+ if (disposed || view.destroyed) return;
32
+ if (item.expanded) {item.setNaturalHeight?.(height);return;}
33
+ item.height = height; slot.style.height = height + 'px';
34
+ item.controller.frame.style.height = height + 'px'; view.requestMeasure();
35
+ }, state => {
36
+ finishLoading();
37
+ status.removeAttribute('aria-label');
38
+ item.ready = state === 'enhanced' || state === 'native';
39
+ status.hidden = item.ready;
40
+ if (!item.ready) status.textContent = chinese ? '图表加载失败,可复制源码查看。' : 'Diagram failed to load. Copy the source to inspect it.';
41
+ item.controller.frame.style.visibility = item.ready ? '' : 'hidden';
42
+ item.setReady?.(item.ready); report();
43
+ }, () => item.close?.());
44
+ item.controller.frame.style.visibility = 'hidden';
45
+ slot.append(item.controller.frame);
46
+ item.export = async canvas => exportQvDiagram(item.controller.frame, await assets.exporter(item.controller.frame.contentDocument),canvas);
47
+ const root = native.ReactDOM.createRoot(actions);
48
+ root.render(withProviders(native.React, native.React.createElement(Controls, {item})));
49
+ item.dispose = () => { finishLoading(); root.unmount(); item.controller.dispose(); mounted.delete(item); report(); };
50
+ host._mcxQv = item; mounted.add(item); report();
51
+ return host;
52
+ }
53
+ destroy(host) { host._mcxQv?.dispose(); }
54
+ ignoreEvent() { return true; }
55
+ }
56
+ function build(state) {
57
+ const ranges = [];
58
+ state.tree?.iterate({ enter(n) {
59
+ if (n.name !== 'FencedCode') return;
60
+ const node = n.node, info = node.getChild('CodeInfo');
61
+ if (!info || state.sliceDoc(info.from, info.to).split(/\s/,1)[0] !== 'mermaid'
62
+ || node.getChildren('CodeMark').length !== 2) return false;
63
+ const source = node.getChildren('CodeText').map(n=>state.sliceDoc(n.from,n.to)).join('');
64
+ if (source.length > 50000) return false;
65
+ ranges.push(Decoration.replace({ block:true, widget:new Diagram(source) })
66
+ .range(state.doc.lineAt(n.from).from, state.doc.lineAt(n.to).to));
67
+ return false;
68
+ } });
69
+ return Decoration.set(ranges, true);
70
+ }
71
+ const field = StateField.define({ create:build, update(value,tr) {
72
+ return tr.docChanged || tr.startState.tree !== tr.state.tree ? build(tr.state) : value;
73
+ }, provide:field=>view.constructor.decorations.from(field) });
74
+ const compartment = new Compartment();
75
+ view.dispatch({ effects:StateEffect.appendConfig.of(compartment.of(Prec.highest(field))) });
76
+ const themes = new MutationObserver(() => {
77
+ if (disposed) return;
78
+ const theme = readQvTheme(view.dom);
79
+ for (const item of mounted) item.controller.theme(theme);
80
+ });
81
+ themes.observe(document.documentElement,{attributes:true,attributeFilter:['class','style','data-theme']});
82
+ const cleanup = () => {
83
+ if (disposed) return;
84
+ disposed = true; themes.disconnect();
85
+ if (!view.destroyed) view.dispatch({effects:compartment.reconfigure([])});
86
+ for (const item of [...mounted]) item.dispose();
87
+ assets.dispose();
88
+ };
89
+ cleanup.available = available;
90
+ return cleanup;
91
+ }
@@ -0,0 +1,34 @@
1
+ import { saveQvDiagram } from '../../mermaid/export.mjs';
2
+
3
+ // 图表下载协议只由实验性功能加载,阅读增强不依赖此能力。
4
+ export async function loadNativeMermaidActions(native) {
5
+ const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
6
+ .map(node => node.href || node.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
7
+ const response = await fetch(url);
8
+ if (!response.ok) throw new Error('native-diagram-source-unavailable');
9
+ const source = await response.text();
10
+ const section = marker => {
11
+ const at = source.indexOf(marker);
12
+ if (at < 0) throw new Error('native-diagram-contract-changed');
13
+ return source.slice(source.lastIndexOf('function ', at), source.indexOf('function ', at));
14
+ };
15
+ const preview = section('id:`imagePreviewDialog.label`');
16
+ const downloadSection = preview.slice(preview.indexOf('suggestedFilename:', preview.indexOf('downloadSrc:')));
17
+ const downloadIcon = downloadSection.match(/children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`icon-xs`\}/)?.[1];
18
+ const requestName = section('Failed to download image').match(/await ([\w$]+)\(`save-file`,/)?.[1];
19
+ const imports = new Map();
20
+ for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
21
+ for (const entry of match[1].split(',')) {
22
+ const parts = entry.trim().split(/\s+as\s+/);
23
+ imports.set(parts.at(-1), { name:parts[0], url:new URL(match[2],url).href });
24
+ }
25
+ const binding = async name => {
26
+ const item = imports.get(name);
27
+ if (!item) throw new Error('native-diagram-save-request-unavailable');
28
+ return (await import(item.url))[item.name];
29
+ };
30
+ const [DownloadIcon, saveFile] = await Promise.all([binding(downloadIcon),binding(requestName)]);
31
+ return { ...native, DownloadIcon, downloadImage({downloadSrc,suggestedFilename}) {
32
+ return saveQvDiagram(downloadSrc,saveFile,markdownDownloadsDirectory,suggestedFilename);
33
+ } };
34
+ }
@@ -0,0 +1,42 @@
1
+ import { loadMarkdownListAPI } from '../../../../../packages/codex-adapter/src/markdown/editor-api.mjs';
2
+ import { loadNativeMarkdownToc } from '../../../../../packages/codex-adapter/src/markdown/toc-native.mjs';
3
+ import { loadNativeMarkdownTableActions } from '../../../../../packages/codex-adapter/src/markdown/block-actions.mjs';
4
+ import { loadNativeMermaidActions } from './native.mjs';
5
+ import { installMarkdownMermaid } from './mermaid.mjs';
6
+
7
+ export function createMermaidRuntime(codex, resources, capabilities = {
8
+ loadMarkdownListAPI, loadNativeMarkdownToc, loadNativeMarkdownTableActions,
9
+ loadNativeMermaidActions, installMarkdownMermaid,
10
+ }) {
11
+ let native;
12
+ function load() {
13
+ native ??= Promise.resolve().then(() => capabilities.loadNativeMarkdownToc())
14
+ .then(value => capabilities.loadNativeMarkdownTableActions(value))
15
+ .then(value => capabilities.loadNativeMermaidActions(value));
16
+ native.catch(() => { native = null; });
17
+ return native;
18
+ }
19
+ return { attach({editor, content}) {
20
+ const view = content.cmTile?.view;
21
+ let disposed = false, available = false, cleanup;
22
+ const current = () => !disposed && !view?.destroyed;
23
+ const report = value => {
24
+ if (!current()) return;
25
+ available = value;
26
+ codex.report('mermaid-diagrams-renderer', value, value ? null : 'mermaid-renderer-unavailable');
27
+ };
28
+ const ready = (async () => {
29
+ try {
30
+ if (!view || view.dom !== editor || !view.state.tree) throw new Error('native-markdown-view-unavailable');
31
+ const [api, actions] = await Promise.all([capabilities.loadMarkdownListAPI(view), load()]);
32
+ if (!current()) return;
33
+ cleanup = capabilities.installMarkdownMermaid(view,api,actions,codex,resources,report);
34
+ report(cleanup.available?.() ?? true);
35
+ } catch { report(false); }
36
+ })();
37
+ return { ready, checks: () => [{name:'mermaid-diagrams-renderer',available}], dispose() {
38
+ if (disposed) return;
39
+ disposed = true; cleanup?.();
40
+ } };
41
+ } };
42
+ }
@@ -0,0 +1,26 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { createMarkdownPreviewAdapter } from '../../../packages/codex-adapter/src/markdown/preview.mjs';
5
+ import { loadMarkdownListAPI } from '../../../packages/codex-adapter/src/markdown/editor-api.mjs';
6
+ import { captureMarkdownProviders } from '../../../packages/codex-adapter/src/markdown/providers.mjs';
7
+ import { loadNativeMarkdownToc } from '../../../packages/codex-adapter/src/markdown/toc-native.mjs';
8
+ import { loadNativeMarkdownRenderer } from '../../../packages/codex-adapter/src/markdown/markdown-native.mjs';
9
+ import { markdownExportAlias, loadNativeMarkdownTableActions } from '../../../packages/codex-adapter/src/markdown/block-actions.mjs';
10
+ import { createQvResources, createQvFrame, readQvTheme } from './mermaid/frame.mjs';
11
+ import { exportQvDiagram, saveQvDiagram } from './mermaid/export.mjs';
12
+ import { createQvLoading } from './mermaid/loading.mjs';
13
+ import { createMarkdownMermaidControls } from './adapters/codex/mermaid-viewer.mjs';
14
+ import { installMarkdownMermaid } from './adapters/codex/mermaid.mjs';
15
+ import { loadNativeMermaidActions } from './adapters/codex/native.mjs';
16
+ import { createMermaidRuntime } from './adapters/codex/runtime.mjs';
17
+
18
+ export async function createPageScriptSource() {
19
+ const bindings = [createMarkdownPreviewAdapter, loadMarkdownListAPI, captureMarkdownProviders,
20
+ loadNativeMarkdownToc, loadNativeMarkdownRenderer, markdownExportAlias, loadNativeMarkdownTableActions,
21
+ createQvResources, createQvFrame, readQvTheme, exportQvDiagram, saveQvDiagram, createQvLoading,
22
+ createMarkdownMermaidControls, installMarkdownMermaid, loadNativeMermaidActions, createMermaidRuntime];
23
+ const declarations = bindings.map(binding => `const ${binding.name} = ${binding.toString()};`).join('\n');
24
+ const entry = await readFile(new URL('./mermaid-diagrams.mjs',import.meta.url),'utf8');
25
+ return `const markdownDownloadsDirectory=${JSON.stringify(join(homedir(),'Downloads'))};\n${declarations}\n${entry}`;
26
+ }
@@ -0,0 +1,83 @@
1
+ // 导出副本保留原始排版;正文和原生查看器的缩放、滚动与动画不受影响。
2
+ export async function exportQvDiagram(frame, toBlob, canvasSize) {
3
+ const doc = frame.contentDocument, root = doc.querySelector('.qv-mermaid');
4
+ await doc.fonts.ready;
5
+ await Promise.all((root.getAnimations?.({subtree:true})||[])
6
+ .filter(a=>a.playState==='running'&&Number.isFinite(a.effect?.getComputedTiming().endTime))
7
+ .map(a=>a.finished.catch(()=>{})));
8
+ // 雷达顶点由 rAF 更新属性;只等待正在变化的几何稳定,不修改原图。
9
+ if (root.classList?.contains('qv-radar')) {
10
+ let last='',stable=0;
11
+ for (let i=0;i<60&&stable<3;i++) {
12
+ const current=[...root.querySelectorAll('polygon')].map(n=>n.getAttribute('points')).join('|');
13
+ stable=current===last?stable+1:0;last=current;
14
+ if(stable<3)await new Promise(resolve=>setTimeout(resolve,20));
15
+ }
16
+ }
17
+ const scene = root.querySelector('.scene'), viewport = root.querySelector('.diagram-scroll');
18
+ const width = Math.ceil(Math.max(root.clientWidth, scene.offsetWidth));
19
+ const copy = root.cloneNode(true);
20
+ copy.classList.add('mcx-qv-export');
21
+ copy.querySelectorAll('.scope-nav,.edge-key,.scope-status,[popover],[role="tooltip"],.node-tip,.seq-tip,.qv-notice').forEach(node => node.remove());
22
+ copy.style.cssText += `;position:absolute;left:0;top:0;width:${width}px;max-width:none;margin:0;transform:translateX(-100000px);overflow:visible;`;
23
+ const copiedViewport = copy.querySelector('.diagram-scroll'), copiedScene = copy.querySelector('.scene');
24
+ copiedViewport.style.cssText += `;width:${viewport.offsetWidth}px;height:${scene.offsetHeight}px;overflow:visible;padding-bottom:0;transform:none;`;
25
+ copiedScene.style.cssText += `;width:${scene.offsetWidth}px;height:${scene.offsetHeight}px;min-width:${scene.offsetWidth}px;transform:none;`;
26
+ copy.querySelectorAll('svg [mask]').forEach(node => {
27
+ if(node.getAttribute('mask').includes('reveal'))node.removeAttribute('mask');
28
+ });
29
+ copy.querySelectorAll('svg [clip-path]').forEach(node => {
30
+ if(node.getAttribute('clip-path').includes('sankey-mask'))node.removeAttribute('clip-path');
31
+ });
32
+ const style = doc.createElement('style');
33
+ style.textContent = `#${root.id}.mcx-qv-export *{animation:none!important;transition:none!important}`;
34
+ copy.append(style);
35
+ doc.body.append(copy);
36
+ try {
37
+ // html-to-image 深拷贝 SVG 而不逐个解析其子节点样式;先冻结线条、文字和渐变。
38
+ const svgProperties=['fill','fill-opacity','stroke','stroke-width','stroke-opacity','stroke-dasharray','stroke-dashoffset','stroke-linecap','stroke-linejoin','vector-effect','opacity','visibility','font-family','font-size','font-weight','text-anchor','dominant-baseline','stop-color','stop-opacity','mask','clip-path'];
39
+ for(const node of copy.querySelectorAll('svg,svg *')) {
40
+ const computed=frame.contentWindow.getComputedStyle(node);
41
+ for(const name of svgProperties)node.style.setProperty(name,computed.getPropertyValue(name));
42
+ }
43
+ copy.querySelectorAll('svg style').forEach(node=>node.remove());
44
+ // 系统字体在 SVG 图像内的字宽可能有细微差异;冻结原本单行的文字,避免导出时多换一行。
45
+ for(const node of copy.querySelectorAll('*')) {
46
+ if(node.namespaceURI!=='http://www.w3.org/1999/xhtml'||![...node.childNodes].some(n=>n.nodeType===3&&n.textContent.trim()))continue;
47
+ const textStyle=frame.contentWindow.getComputedStyle(node),line=parseFloat(textStyle.lineHeight);
48
+ if(Number.isFinite(line)&&node.getBoundingClientRect().height<=line+.5)node.style.whiteSpace='nowrap';
49
+ }
50
+ const customProperties = new Set(Array.from(frame.contentWindow.getComputedStyle(root)).filter(name=>name.startsWith('--')));
51
+ for(const node of copy.querySelectorAll('*'))for(const name of node.style)if(name.startsWith('--'))customProperties.add(name);
52
+ const height = Math.ceil(Math.max(copy.offsetHeight,copy.scrollHeight||0));
53
+ const blob = await toBlob(copy, { width, height, pixelRatio:canvasSize?2*Math.max(1,.9*Math.min(canvasSize.width/width,canvasSize.height/height)):2, skipAutoScale:true, filter:node=>node.tagName!=='STYLE',
54
+ includeStyleProperties:['display','position','left','top','right','bottom','width','height','min-width','max-width','min-height','max-height','box-sizing','padding','margin','overflow','background-color','background-image','background-size','background-position','color','font-family','font-size','font-weight','font-style','line-height','letter-spacing','text-align','text-decoration','text-overflow','white-space','word-break','overflow-wrap','flex-direction','flex-wrap','flex-grow','flex-shrink','flex-basis','align-items','align-content','align-self','justify-content','gap','row-gap','column-gap','grid-template-columns','grid-template-rows','grid-auto-flow','vertical-align','border','border-top','border-right','border-bottom','border-left','border-radius','box-shadow','opacity','transform','transform-origin','z-index','visibility','fill','fill-opacity','stroke','stroke-width','stroke-opacity','stroke-dasharray','stroke-dashoffset','stroke-linecap','stroke-linejoin','vector-effect','mask','clip-path','stop-color','stop-opacity','color-scheme',...customProperties],
55
+ backgroundColor:frame.contentWindow.getComputedStyle(doc.body).backgroundColor,
56
+ style:{position:'relative',left:'0',top:'0',transform:'none'} });
57
+ if (!blob) throw new Error('diagram-export-empty');
58
+ if (!canvasSize) return blob;
59
+ const canvas=doc.createElement('canvas');
60
+ canvas.width=Math.round(canvasSize.width*2);canvas.height=Math.round(canvasSize.height*2);
61
+ const context=canvas.getContext('2d'),image=new frame.contentWindow.Image();
62
+ const url=frame.contentWindow.URL.createObjectURL(blob);
63
+ try {
64
+ await new Promise((resolve,reject)=>{image.onload=resolve;image.onerror=()=>reject(new Error('diagram-export-image-failed'));image.src=url;});
65
+ context.fillStyle=frame.contentWindow.getComputedStyle(doc.body).backgroundColor;
66
+ context.fillRect(0,0,canvas.width,canvas.height);
67
+ const fit=.9*Math.min(canvas.width/image.naturalWidth,canvas.height/image.naturalHeight);
68
+ const w=image.naturalWidth*fit,h=image.naturalHeight*fit;
69
+ context.drawImage(image,(canvas.width-w)/2,(canvas.height-h)/2,w,h);
70
+ const result=await new Promise(resolve=>canvas.toBlob(resolve,'image/png'));
71
+ if(!result)throw new Error('diagram-export-empty');
72
+ return result;
73
+ } finally {frame.contentWindow.URL.revokeObjectURL(url);}
74
+
75
+ } finally { copy.remove(); }
76
+ }
77
+
78
+ export async function saveQvDiagram(blob, saveFile, directory, filename) {
79
+ const bytes = new Uint8Array(await blob.arrayBuffer());
80
+ let binary='';
81
+ for (let start=0;start<bytes.length;start+=32768) binary+=String.fromCharCode(...bytes.subarray(start,start+32768));
82
+ return saveFile('save-file',{params:{kind:'contents',suggestedFilename:directory+'/'+filename,contentsBase64:btoa(binary)}});
83
+ }
@@ -0,0 +1,109 @@
1
+ export function markdownMermaidSource(fence) {
2
+ const match = String(fence || '').match(/^(`{3,}|~{3,})mermaid[ \t]*\r?\n([\s\S]*?)\r?\n\1\s*$/);
3
+ return match && match[2].length <= 50000 ? match[2] : null;
4
+ }
5
+
6
+ // 同一文档共享脚本 URL,减少源码复制和 Blob 创建,并允许浏览器复用编译缓存。
7
+ // 各帧仍独立求值;URL 由引用计数持有,最后一个预览释放时回收。
8
+ export function createQvResources(resources) {
9
+ if (!resources.qvTemplate?.script || !resources.qvDependencies) throw new Error('qv-resources-unavailable');
10
+ const store = (globalThis.__mcxQvScripts ??= new Map());
11
+ const acquire = (text) => {
12
+ const held = store.get(text);
13
+ if (held) { held.refs += 1; return held.url; }
14
+ const entry = { url: URL.createObjectURL(new Blob([text], { type: 'text/javascript' })), refs: 1 };
15
+ store.set(text, entry);
16
+ return entry.url;
17
+ };
18
+ const release = (url) => {
19
+ for (const [text, entry] of store) {
20
+ if (entry.url !== url) continue;
21
+ if (--entry.refs > 0) return;
22
+ URL.revokeObjectURL(entry.url);
23
+ store.delete(text);
24
+ return;
25
+ }
26
+ };
27
+ const dependencies = acquire(resources.qvDependencies);
28
+ const renderer = acquire(resources.qvTemplate.script);
29
+ let exporter;
30
+ return { template: resources.qvTemplate, dependencies, renderer,
31
+ async exporter(doc) {
32
+ if (!resources.qvExport) throw new Error('qv-export-unavailable');
33
+ exporter ??= acquire(resources.qvExport);
34
+ const child = doc.defaultView;
35
+ child.__mcxQVExportReady ??= new Promise((resolve,reject) => {
36
+ const script = doc.createElement('script');script.src=exporter;
37
+ script.onload=()=>{script.remove();resolve(child.__mcxQVExport);};
38
+ script.onerror=()=>{script.remove();delete child.__mcxQVExportReady;reject(new Error('qv-export-load-failed'));};
39
+ doc.head.append(script);
40
+ });
41
+ return (await child.__mcxQVExportReady).toBlob;
42
+ },
43
+ dispose() { release(dependencies); release(renderer); if (exporter) release(exporter); } };
44
+ }
45
+
46
+ export function createQvFrame(source, assets, theme, onHeight, onReady, onEscape) {
47
+ const frame = document.createElement('iframe'), token = crypto.randomUUID();
48
+ frame.className = 'mcx-reading-qv-frame';
49
+ frame.title = document.documentElement.lang.startsWith('zh') ? 'QV Mermaid 图表' : 'QV Mermaid diagram';
50
+ frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
51
+ frame.style.height = '240px';
52
+ let disposed = false, height = 240, input = null;
53
+ const timeout = setTimeout(() => { if (!disposed) onReady('error'); }, 30000);
54
+ const config = { source, title: '', descriptions: {}, icons: 'auto', navigation: 'expanded', readingWidth: theme.readingWidth };
55
+ const encode = (value) => JSON.stringify(value).replace(/</g, '\\u003c');
56
+ const bootstrap = `const token=${encode(token)};window.__mcxDiagramConfig=${encode(config)};window.openai={displayMode:'inline',maxHeight:innerHeight};
57
+ const send=()=>{const root=document.querySelector('.qv-mermaid');parent.postMessage({type:'mcx-qv',token,state:root.dataset.state,height:Math.ceil(root.offsetHeight)},'*');};
58
+ const theme=value=>{for(const [key,v]of Object.entries(value.tokens))document.documentElement.style.setProperty(key,v);document.documentElement.style.colorScheme=value.scheme;document.body.style.fontFamily=value.font;document.body.style.fontSize=value.size;};theme(${encode(theme)});
59
+ const load=value=>new Promise((resolve,reject)=>{const url=String(value);if(!/^blob:/.test(url))return reject(new Error('QV runtime unavailable'));const script=document.createElement('script');script.src=url;script.onload=()=>resolve();script.onerror=()=>reject(new Error('QV runtime unavailable'));document.body.append(script);});
60
+ window.addEventListener('message',async event=>{const data=event.data;if(event.source!==parent||data?.token!==token)return;if(data.type==='boot'){try{await load(data.dependencies);await load(data.renderer);}catch(error){const root=document.querySelector('.qv-mermaid');root.dataset.state='error';root.querySelector('.qv-notice').hidden=false;root.querySelector('.qv-notice').textContent=error.message;send();}}if(data.type==='theme')theme(data.theme);if(data.type==='mode'){window.openai={displayMode:data.mode,maxHeight:innerHeight};dispatchEvent(new CustomEvent('openai:set_globals',{detail:{globals:window.openai}}));}});
61
+ document.addEventListener('mcx:qv-viewer-input',event=>parent.postMessage({type:'mcx-qv-viewer-input',token,input:event.detail},'*'));
62
+ document.addEventListener('qv:ready',()=>queueMicrotask(send));document.addEventListener('keydown',event=>{if(event.key==='Escape')parent.postMessage({type:'mcx-qv-escape',token},'*');});
63
+ new ResizeObserver(send).observe(document.querySelector('.qv-mermaid'));parent.postMessage({type:'mcx-qv-bootstrap',token},'*');`;
64
+ const html = `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src blob: 'unsafe-inline';style-src 'unsafe-inline';img-src data: blob:;font-src data:;connect-src 'none'"><style>html,body{margin:0;overflow:hidden}body{color:var(--foreground);background:var(--background);font-size:14px;line-height:1.5}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}button,input{font:inherit}.scope-toggle{display:flex;align-items:center;gap:8px}.form-check-input{accent-color:var(--viz-series-1)}</style></head><body>${assets.template.markup}<script>${bootstrap}</script></body></html>`;
65
+ frame.srcdoc = html;
66
+ function message(event) {
67
+ if (disposed || event.source !== frame.contentWindow || event.data?.token !== token) return;
68
+ if (event.data.type === 'mcx-qv-bootstrap') {
69
+ frame.contentWindow.postMessage({type:'boot',token,dependencies:assets.dependencies,renderer:assets.renderer},'*');
70
+ return;
71
+ }
72
+ if (event.data.type === 'mcx-qv-escape') { onEscape(); return; }
73
+ if (event.data.type === 'mcx-qv-viewer-input') { input?.(event.data.input); return; }
74
+ if (event.data.type !== 'mcx-qv' || event.data.state === 'loading') return;
75
+ clearTimeout(timeout);
76
+ onReady(event.data.state);
77
+ const next = event.data.height;
78
+ if (Number.isFinite(next) && next > 0 && next <= 100000 && next !== height) {
79
+ height = next; onHeight(next);
80
+ }
81
+ }
82
+ window.addEventListener('message', message);
83
+ return { frame, get height() { return height; },
84
+ size() {
85
+ const root = frame.contentDocument.querySelector('.qv-mermaid'), viewport = root.querySelector('.diagram-scroll'), scene = root.querySelector('.scene');
86
+ return { width:Math.max(root.clientWidth,scene.offsetWidth), height:root.offsetHeight-viewport.offsetHeight+scene.offsetHeight };
87
+ },
88
+ input(handler) { input = handler; },
89
+ mode(mode) { frame.contentWindow?.postMessage({ type: 'mode', token, mode }, '*'); },
90
+ theme(theme) { frame.contentWindow?.postMessage({ type: 'theme', token, theme }, '*'); },
91
+ dispose() { disposed = true; clearTimeout(timeout); window.removeEventListener('message', message); frame.remove(); } };
92
+ }
93
+
94
+ export function readQvTheme(element) {
95
+ const style = getComputedStyle(element), get = (name) => style.getPropertyValue(name).trim();
96
+ const background = get('--mcx-color-token-main-surface-primary'), foreground = get('--mcx-color-text-foreground');
97
+ const tokens = { '--background': background, '--foreground': foreground,
98
+ '--muted': get('--mcx-color-token-bg-secondary'), '--muted-foreground': get('--mcx-color-text-foreground-secondary'),
99
+ '--border': get('--mcx-color-border'), '--popover': background, '--popover-foreground': foreground,
100
+ '--secondary': get('--mcx-color-token-bg-secondary'), '--secondary-foreground': foreground };
101
+ const colors = ['#3b82f6','#10b981','#8b5cf6','#f59e0b','#ec4899','#06b6d4'];
102
+ colors.forEach((color, index) => tokens[`--viz-series-${index+1}`] = get(`--viz-series-${index+1}`) || color);
103
+ const probe = document.createElement('div');
104
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;width:var(--mcx-reading-width, calc(var(--thread-content-max-width) - var(--padding-toolbar) * 2));height:0';
105
+ element.append(probe);
106
+ const readingWidth = probe.getBoundingClientRect().width;
107
+ probe.remove();
108
+ return { readingWidth, tokens, scheme: style.colorScheme, font: style.fontFamily, size: style.fontSize };
109
+ }
@@ -0,0 +1,27 @@
1
+ // 由 scripts/build-qv-mermaid.mjs 从 QV chart-loading.html 生成,请勿手改。
2
+ export function createQvLoading(region) {
3
+ const field=document.createElement('div'),canvas=document.createElement('canvas');
4
+ field.className='qv-loading-field';canvas.className='qv-loading-canvas';field.setAttribute('aria-hidden','true');field.append(canvas);region.append(field);
5
+ region.setAttribute('data-mcx-qv-loading','');region.setAttribute('aria-busy','true');
6
+ const ctx=canvas.getContext('2d'),reduced=matchMedia('(prefers-reduced-motion: reduce)');
7
+ let frame=0,last=0,start=0,done=false,geometry;
8
+ const clamp=x=>Math.max(0,Math.min(1,x)),smooth=x=>x*x*(3-2*x),cubic=x=>x<.5?4*x*x*x:1-Math.pow(-2*x+2,3)/2,lerp=(a,b,t)=>a+(b-a)*t;
9
+ const random=(a,b)=>lerp(a,b,Math.random()),cycle=t=>{const p=t%1;return p<.5?p*2:2-p*2;};
10
+ // 对照本机 image-loading-state:固定点阵、双明暗场、30fps 与相同运动参数;不依赖宿主 React。
11
+ const durations=[4500,6330,5600,5750,3600,2400].map(n=>n*1.2*random(1,1.35)),phases=durations.map(()=>Math.random());
12
+ const bounds=[[random(.1,.32),random(.68,.9)],[random(.1,.32),random(.68,.9)],[random(.68,.9),random(.1,.32)],[random(.68,.9),random(.1,.32)],[random(.42,.52),random(.62,.75)],[random(.5,.62),random(.74,.9)]];
13
+ function resize(){const {width,height}=field.getBoundingClientRect(),dpr=Math.max(1,devicePixelRatio||1),spacing=Math.max(1,12/dpr);canvas.width=Math.floor(width*dpr);canvas.height=Math.floor(height*dpr);geometry={width,height,dpr,spacing,cols:Math.max(1,Math.floor(width/spacing)),rows:Math.max(1,Math.floor(height/spacing))};schedule();}
14
+ function paint(now){frame=0;if(done||document.hidden||!ctx)return;if(!reduced.matches&&now-last<1000/30){schedule();return;}last=now;if(!start)start=now;const t=reduced.matches?0:now-start,g=geometry;if(!g||!g.width||!g.height)return;
15
+ const values=durations.map((d,i)=>lerp(...bounds[i],(i===0||i===2?cubic:smooth)(cycle(t/d+phases[i])))),[x1,y1,x2,y2]=values,r1=.78*values[4],r2=.78*values[5];
16
+ ctx.setTransform(g.dpr,0,0,g.dpr,0,0);ctx.clearRect(0,0,g.width,g.height);ctx.fillStyle=getComputedStyle(field).color;
17
+ const radius=Math.max(.8,g.spacing*.075),ox=(g.width-(g.cols-1)*g.spacing)/2,oy=(g.height-(g.rows-1)*g.spacing)/2;
18
+ for(let y=0;y<g.rows;y++)for(let x=0;x<g.cols;x++){const nx=g.cols===1?.5:x/(g.cols-1),ny=g.rows===1?.5:y/(g.rows-1),a=Math.pow(clamp((1-smooth(clamp(Math.hypot(nx-x1,ny-y1)/r1)))*1.2+(1-smooth(clamp(Math.hypot(nx-x2,ny-y2)/r2)))*.82),1.18);if(a<=.03)continue;ctx.globalAlpha=a;ctx.beginPath();ctx.arc(ox+x*g.spacing,oy+y*g.spacing,radius,0,Math.PI*2);ctx.fill();}
19
+ ctx.globalAlpha=1;if(!reduced.matches)schedule();}
20
+ function schedule(){if(!done&&!frame&&!document.hidden)frame=requestAnimationFrame(paint);}
21
+ const observer=new ResizeObserver(resize);observer.observe(field);
22
+ function visibility(){if(document.hidden){cancelAnimationFrame(frame);frame=0;}else schedule();}
23
+ function motion(){cancelAnimationFrame(frame);frame=0;schedule();}
24
+ document.addEventListener('visibilitychange',visibility);reduced.addEventListener('change',motion);
25
+
26
+ return function finish(){if(done)return;done=true;cancelAnimationFrame(frame);observer.disconnect();document.removeEventListener('visibilitychange',visibility);reduced.removeEventListener('change',motion);field.remove();region.removeAttribute('data-mcx-qv-loading');region.setAttribute('aria-busy','false');};
27
+ }
@@ -0,0 +1,8 @@
1
+ .mcx-reading-qv-widget { position:relative; padding-top:44px; }
2
+ .mcx-reading-qv-frame { display:block; width:100%; border:0; }
3
+ .mcx-reading-qv-actions { position:absolute; inset:0 0 auto; height:44px; }
4
+ .mcx-reading-qv-actions [data-block-actions] { opacity:1; pointer-events:auto; }
5
+ .mcx-reading-qv-inline { position:relative; }
6
+ .mcx-reading-qv-status:not([hidden]) { position:absolute; inset:0; display:grid; place-items:center; }
7
+ .mcx-reading-qv-slot { position:relative; width:100%; height:100%; overflow:hidden; }
8
+ .mcx-reading-qv-slot > iframe { position:absolute; inset:0 auto auto 0; max-width:none; }