@izumisy/vitepress-plugin-react-preview 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.d.mts +39 -0
- package/dist/index.mjs +108 -0
- package/package.json +42 -0
- package/src/PreviewBlock.vue +281 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IzumiSy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @izumisy/vitepress-plugin-react-preview
|
|
2
|
+
|
|
3
|
+
VitePress plugin for rendering live React component previews inside your VitePress documentation site.
|
|
4
|
+
|
|
5
|
+
Transforms `` ```tsx preview `` fenced blocks in Markdown into interactive previews rendered via iframes, with full style isolation and dark mode support.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **markdown-it plugin** — rewrites `` ```tsx preview `` blocks into `<PreviewBlock>` Vue components at the Markdown parsing stage
|
|
10
|
+
- **Vite plugin** — serves preview modules and standalone preview pages using `@izumisy/vite-plugin-react-preview` under the hood
|
|
11
|
+
- **Style isolation** — inline previews render inside iframes; standalone previews run in a separate page
|
|
12
|
+
- **Dark mode** — syncs with VitePress theme toggle via `postMessage`
|
|
13
|
+
- **Build support** — emits standalone HTML pages (`/__preview/{blockId}.html`) during `vitepress build`
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @izumisy/vitepress-plugin-react-preview
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
In your VitePress config (`.vitepress/config.ts`):
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineConfig } from "vitepress";
|
|
27
|
+
import { createMrpPlugin } from "@izumisy/vitepress-plugin-react-preview";
|
|
28
|
+
|
|
29
|
+
const mrp = createMrpPlugin({
|
|
30
|
+
css: "@my-lib/styles", // optional: CSS to inject into previews
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
markdown: {
|
|
35
|
+
config(md) {
|
|
36
|
+
md.use(mrp.markdownIt);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
vite: {
|
|
40
|
+
plugins: [mrp.vite()],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then use `<PreviewBlock>` in your theme if needed:
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<script setup>
|
|
49
|
+
import PreviewBlock from "@izumisy/vitepress-plugin-react-preview/PreviewBlock.vue";
|
|
50
|
+
</script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Preview Syntax
|
|
54
|
+
|
|
55
|
+
Works with the same `` ```tsx preview `` syntax as `@izumisy/md-react-preview`:
|
|
56
|
+
|
|
57
|
+
````md
|
|
58
|
+
```tsx preview
|
|
59
|
+
import { Button } from "@my-lib"
|
|
60
|
+
|
|
61
|
+
<Button>Click me</Button>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```tsx preview standalone
|
|
65
|
+
import { Sheet } from "@my-lib"
|
|
66
|
+
|
|
67
|
+
<Sheet.Root>...</Sheet.Root>
|
|
68
|
+
```
|
|
69
|
+
````
|
|
70
|
+
|
|
71
|
+
- **inline** (default) — rendered within the page via iframe
|
|
72
|
+
- **standalone** — shows a code block with a link to a full-viewport preview page
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PreviewBlockEntry } from "@izumisy/vite-plugin-react-preview";
|
|
2
|
+
import * as markdown_it0 from "markdown-it";
|
|
3
|
+
import { Plugin } from "vite";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
type MrpVitePressOptions = {
|
|
7
|
+
/** CSS file to inject into preview blocks (e.g. your component library's stylesheet) */css?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Recursively find all .md files under a directory.
|
|
11
|
+
* @internal Exported for testing.
|
|
12
|
+
*/
|
|
13
|
+
declare function findMarkdownFiles(dir: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Pre-scan markdown files to populate the block registry before Rollup
|
|
16
|
+
* resolves the registry virtual module.
|
|
17
|
+
* @internal Exported for testing.
|
|
18
|
+
*/
|
|
19
|
+
declare function scanMarkdownBlocks(root: string, blockRegistry: Map<string, PreviewBlockEntry>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Create a md-react-preview plugin instance for VitePress.
|
|
22
|
+
*
|
|
23
|
+
* Returns an object with:
|
|
24
|
+
* - `markdownIt`: markdown-it plugin that transforms ` ```tsx preview ` fences
|
|
25
|
+
* - `vite()`: Vite plugins that serve preview blocks with live React components
|
|
26
|
+
*/
|
|
27
|
+
declare function createMrpPlugin(options?: MrpVitePressOptions): {
|
|
28
|
+
/**
|
|
29
|
+
* markdown-it plugin that transforms ` ```tsx preview ` fences into
|
|
30
|
+
* `<PreviewBlock>` Vue components and registers blocks in the shared registry.
|
|
31
|
+
*/
|
|
32
|
+
markdownIt: (md: markdown_it0.default) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Vite plugins that provide virtual modules for preview blocks.
|
|
35
|
+
*/
|
|
36
|
+
vite(): Plugin[];
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { MrpVitePressOptions, createMrpPlugin, findMarkdownFiles, scanMarkdownBlocks };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { createBasePreviewPlugin, createPreviewBuildPlugin, parseMeta, simpleHash } from "@izumisy/vite-plugin-react-preview";
|
|
4
|
+
//#region src/markdown-it-plugin.ts
|
|
5
|
+
/**
|
|
6
|
+
* Create a markdown-it plugin that transforms ` ```tsx preview ` fenced code blocks
|
|
7
|
+
* into a `<PreviewBlock>` Vue component with the code passed as props.
|
|
8
|
+
*
|
|
9
|
+
* Blocks are registered in the shared registry so the preview Vite plugin
|
|
10
|
+
* can serve the preview pages.
|
|
11
|
+
*
|
|
12
|
+
* The code is base64-encoded to avoid escaping issues in HTML attributes.
|
|
13
|
+
*/
|
|
14
|
+
function createMarkdownItPlugin(blockRegistry) {
|
|
15
|
+
return function markdownItPreviewPlugin(md) {
|
|
16
|
+
const defaultFence = md.renderer.rules.fence;
|
|
17
|
+
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|
18
|
+
const token = tokens[idx];
|
|
19
|
+
const info = token.info.trim();
|
|
20
|
+
if (info.startsWith("tsx preview")) {
|
|
21
|
+
const code = token.content.replace(/\n$/, "");
|
|
22
|
+
const encodedCode = Buffer.from(code).toString("base64");
|
|
23
|
+
const blockId = simpleHash(`${env.relativePath || "unknown"}:${token.map?.[0] ?? 0}`);
|
|
24
|
+
const meta = parseMeta(info.slice(11));
|
|
25
|
+
const isStandalone = meta.standalone === "true";
|
|
26
|
+
blockRegistry.set(blockId, {
|
|
27
|
+
code,
|
|
28
|
+
sourceFile: "",
|
|
29
|
+
wrap: meta.wrap,
|
|
30
|
+
height: meta.height,
|
|
31
|
+
standalone: isStandalone
|
|
32
|
+
});
|
|
33
|
+
const highlighted = md.options.highlight?.(code, "tsx", "") ?? "";
|
|
34
|
+
return `<PreviewBlock code="${encodedCode}" block-id="${blockId}" highlighted="${Buffer.from(highlighted).toString("base64")}"${meta.height ? ` height="${meta.height}"` : ""}${meta.wrap ? ` wrap="${meta.wrap}"` : ""}${meta.align ? ` align="${meta.align}"` : ""}${isStandalone ? ` standalone="true"` : ""} />\n`;
|
|
35
|
+
}
|
|
36
|
+
return defaultFence(tokens, idx, options, env, self);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/index.ts
|
|
42
|
+
/**
|
|
43
|
+
* Recursively find all .md files under a directory.
|
|
44
|
+
* @internal Exported for testing.
|
|
45
|
+
*/
|
|
46
|
+
function findMarkdownFiles(dir) {
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const entry of readdirSync(dir)) {
|
|
49
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
50
|
+
const full = join(dir, entry);
|
|
51
|
+
if (statSync(full).isDirectory()) results.push(...findMarkdownFiles(full));
|
|
52
|
+
else if (full.endsWith(".md")) results.push(full);
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pre-scan markdown files to populate the block registry before Rollup
|
|
58
|
+
* resolves the registry virtual module.
|
|
59
|
+
* @internal Exported for testing.
|
|
60
|
+
*/
|
|
61
|
+
function scanMarkdownBlocks(root, blockRegistry) {
|
|
62
|
+
const mdFiles = findMarkdownFiles(root);
|
|
63
|
+
for (const file of mdFiles) {
|
|
64
|
+
const content = readFileSync(file, "utf-8");
|
|
65
|
+
const relativePath = relative(root, file);
|
|
66
|
+
const fenceRe = /^(`{3,})\s*(\S.*?)?\n([\s\S]*?)^\1\s*$/gm;
|
|
67
|
+
let m;
|
|
68
|
+
while ((m = fenceRe.exec(content)) !== null) {
|
|
69
|
+
const info = (m[2] || "").trim();
|
|
70
|
+
if (info.startsWith("tsx preview")) {
|
|
71
|
+
const code = m[3].replace(/\n$/, "");
|
|
72
|
+
const blockId = simpleHash(`${relativePath}:${content.slice(0, m.index).split("\n").length - 1}`);
|
|
73
|
+
const meta = parseMeta(info.slice(11));
|
|
74
|
+
blockRegistry.set(blockId, {
|
|
75
|
+
code,
|
|
76
|
+
sourceFile: "",
|
|
77
|
+
wrap: meta.wrap,
|
|
78
|
+
height: meta.height,
|
|
79
|
+
standalone: meta.standalone === "true"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a md-react-preview plugin instance for VitePress.
|
|
87
|
+
*
|
|
88
|
+
* Returns an object with:
|
|
89
|
+
* - `markdownIt`: markdown-it plugin that transforms ` ```tsx preview ` fences
|
|
90
|
+
* - `vite()`: Vite plugins that serve preview blocks with live React components
|
|
91
|
+
*/
|
|
92
|
+
function createMrpPlugin(options = {}) {
|
|
93
|
+
const blockRegistry = /* @__PURE__ */ new Map();
|
|
94
|
+
return {
|
|
95
|
+
markdownIt: createMarkdownItPlugin(blockRegistry),
|
|
96
|
+
vite() {
|
|
97
|
+
return [...createBasePreviewPlugin("mrp-vitepress-preview", {
|
|
98
|
+
blockRegistry,
|
|
99
|
+
cssImport: options.css
|
|
100
|
+
}), createPreviewBuildPlugin({
|
|
101
|
+
blockRegistry,
|
|
102
|
+
scanBlocks: (root) => scanMarkdownBlocks(root, blockRegistry)
|
|
103
|
+
})];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { createMrpPlugin, findMarkdownFiles, scanMarkdownBlocks };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@izumisy/vitepress-plugin-react-preview",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VitePress plugin for rendering React component previews via iframe",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"default": "./dist/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./PreviewBlock.vue": "./src/PreviewBlock.vue"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**",
|
|
15
|
+
"src/PreviewBlock.vue"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@izumisy/vite-plugin-react-preview": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/markdown-it": "^14.1.2",
|
|
22
|
+
"@types/node": "^22",
|
|
23
|
+
"markdown-it": "^14.1.0",
|
|
24
|
+
"tsdown": "^0.21.3",
|
|
25
|
+
"typescript": "~5.9.3",
|
|
26
|
+
"vite": "^6.3.5",
|
|
27
|
+
"vitest": "^4.1.2"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"markdown-it": ">=14",
|
|
31
|
+
"react": ">=18",
|
|
32
|
+
"react-dom": ">=18",
|
|
33
|
+
"vite": ">=6",
|
|
34
|
+
"vue": ">=3"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "tsdown --watch",
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"type-check": "tsc --incremental",
|
|
40
|
+
"test": "vitest run"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
code: string;
|
|
6
|
+
blockId: string;
|
|
7
|
+
highlighted?: string;
|
|
8
|
+
height?: string;
|
|
9
|
+
wrap?: string;
|
|
10
|
+
align?: string;
|
|
11
|
+
standalone?: string;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const isStandalone = computed(() => props.standalone === "true");
|
|
15
|
+
const showCode = ref(true);
|
|
16
|
+
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
|
17
|
+
const iframeHeight = ref(props.height ? Number(props.height) : 150);
|
|
18
|
+
|
|
19
|
+
const decodedCode = computed(() => {
|
|
20
|
+
try {
|
|
21
|
+
return atob(props.code);
|
|
22
|
+
} catch {
|
|
23
|
+
return props.code;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const decodedHighlighted = computed(() => {
|
|
28
|
+
if (!props.highlighted) return "";
|
|
29
|
+
try {
|
|
30
|
+
return atob(props.highlighted);
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const currentTheme = ref(
|
|
37
|
+
typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
|
38
|
+
? "dark"
|
|
39
|
+
: "light"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const previewUrl = computed(() => {
|
|
43
|
+
const params = new URLSearchParams({ theme: currentTheme.value });
|
|
44
|
+
if (props.wrap) params.set("wrap", props.wrap);
|
|
45
|
+
if (props.align) params.set("align", props.align);
|
|
46
|
+
return `/__preview/${props.blockId}?${params}`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let themeObserver: MutationObserver | null = null;
|
|
50
|
+
|
|
51
|
+
function syncThemeToIframe() {
|
|
52
|
+
const iframe = iframeRef.value;
|
|
53
|
+
if (iframe?.contentWindow) {
|
|
54
|
+
iframe.contentWindow.postMessage(
|
|
55
|
+
{ type: "mrp-theme", theme: currentTheme.value },
|
|
56
|
+
"*"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onMessage(e: MessageEvent) {
|
|
62
|
+
if (
|
|
63
|
+
e.data?.type === "mrp-resize" &&
|
|
64
|
+
e.data?.blockId === props.blockId
|
|
65
|
+
) {
|
|
66
|
+
iframeHeight.value = e.data.height;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
window.addEventListener("message", onMessage);
|
|
72
|
+
|
|
73
|
+
themeObserver = new MutationObserver(() => {
|
|
74
|
+
const isDark = document.documentElement.classList.contains("dark");
|
|
75
|
+
const newTheme = isDark ? "dark" : "light";
|
|
76
|
+
if (currentTheme.value !== newTheme) {
|
|
77
|
+
currentTheme.value = newTheme;
|
|
78
|
+
syncThemeToIframe();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
themeObserver.observe(document.documentElement, {
|
|
82
|
+
attributes: true,
|
|
83
|
+
attributeFilter: ["class"],
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
onBeforeUnmount(() => {
|
|
88
|
+
window.removeEventListener("message", onMessage);
|
|
89
|
+
themeObserver?.disconnect();
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<template>
|
|
94
|
+
<div class="mrp-preview vp-raw">
|
|
95
|
+
<template v-if="isStandalone">
|
|
96
|
+
<a
|
|
97
|
+
:href="previewUrl"
|
|
98
|
+
target="_blank"
|
|
99
|
+
rel="noopener noreferrer"
|
|
100
|
+
class="mrp-preview-standalone-link"
|
|
101
|
+
>
|
|
102
|
+
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5">
|
|
103
|
+
<path d="M7 3H3v10h10V9" />
|
|
104
|
+
<path d="M10 2h4v4" />
|
|
105
|
+
<path d="M14 2L7 9" />
|
|
106
|
+
</svg>
|
|
107
|
+
<span class="mrp-preview-standalone-text">
|
|
108
|
+
<span class="mrp-preview-standalone-title">Open full-page preview</span>
|
|
109
|
+
<span class="mrp-preview-standalone-desc">This component requires a full viewport to render correctly.</span>
|
|
110
|
+
</span>
|
|
111
|
+
</a>
|
|
112
|
+
</template>
|
|
113
|
+
<template v-else>
|
|
114
|
+
<div class="mrp-preview-render">
|
|
115
|
+
<iframe
|
|
116
|
+
ref="iframeRef"
|
|
117
|
+
:src="previewUrl"
|
|
118
|
+
:style="{ height: iframeHeight + 'px' }"
|
|
119
|
+
class="mrp-preview-iframe"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="mrp-preview-actions">
|
|
123
|
+
<a
|
|
124
|
+
:href="previewUrl"
|
|
125
|
+
target="_blank"
|
|
126
|
+
rel="noopener noreferrer"
|
|
127
|
+
class="mrp-preview-fullscreen-link"
|
|
128
|
+
>
|
|
129
|
+
Open full preview
|
|
130
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
131
|
+
<path d="M7 3H3v10h10V9" />
|
|
132
|
+
<path d="M10 2h4v4" />
|
|
133
|
+
<path d="M14 2L7 9" />
|
|
134
|
+
</svg>
|
|
135
|
+
</a>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
138
|
+
<div class="mrp-preview-code">
|
|
139
|
+
<button
|
|
140
|
+
class="mrp-preview-toggle"
|
|
141
|
+
@click="showCode = !showCode"
|
|
142
|
+
>
|
|
143
|
+
<svg
|
|
144
|
+
width="16"
|
|
145
|
+
height="16"
|
|
146
|
+
viewBox="0 0 16 16"
|
|
147
|
+
fill="none"
|
|
148
|
+
stroke="currentColor"
|
|
149
|
+
stroke-width="1.5"
|
|
150
|
+
stroke-linecap="round"
|
|
151
|
+
stroke-linejoin="round"
|
|
152
|
+
:class="{ 'mrp-chevron-open': showCode }"
|
|
153
|
+
>
|
|
154
|
+
<path d="M6 4l4 4-4 4" />
|
|
155
|
+
</svg>
|
|
156
|
+
Code
|
|
157
|
+
</button>
|
|
158
|
+
<div v-if="showCode && decodedHighlighted" v-html="decodedHighlighted" />
|
|
159
|
+
<div v-else-if="showCode" class="language-tsx vp-adaptive-theme">
|
|
160
|
+
<pre><code>{{ decodedCode }}</code></pre>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<style scoped>
|
|
167
|
+
.mrp-preview {
|
|
168
|
+
border: 1px solid var(--vp-c-divider);
|
|
169
|
+
border-radius: 8px;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
margin: 16px 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.mrp-preview-render {
|
|
175
|
+
background-color: var(--vp-c-bg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.mrp-preview-iframe {
|
|
179
|
+
display: block;
|
|
180
|
+
width: 100%;
|
|
181
|
+
border: none;
|
|
182
|
+
transition: height 0.15s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.mrp-preview-code {
|
|
186
|
+
border-top: 1px solid var(--vp-c-divider);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.mrp-preview-toggle {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: 4px;
|
|
193
|
+
width: 100%;
|
|
194
|
+
padding: 8px 12px;
|
|
195
|
+
background: none;
|
|
196
|
+
border: none;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
font-size: 13px;
|
|
199
|
+
color: var(--vp-c-text-2);
|
|
200
|
+
font-family: inherit;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.mrp-preview-toggle:hover {
|
|
204
|
+
color: var(--vp-c-text-1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.mrp-chevron-open {
|
|
208
|
+
transform: rotate(90deg);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.mrp-preview-actions {
|
|
212
|
+
display: flex;
|
|
213
|
+
justify-content: flex-end;
|
|
214
|
+
padding: 6px 12px;
|
|
215
|
+
border-top: 1px solid var(--vp-c-divider);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.mrp-preview-fullscreen-link {
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 4px;
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
color: var(--vp-c-text-3);
|
|
224
|
+
text-decoration: none;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.mrp-preview-fullscreen-link:hover {
|
|
228
|
+
color: var(--vp-c-text-1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.mrp-preview-code svg {
|
|
232
|
+
transition: transform 0.15s;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.mrp-preview-code pre {
|
|
236
|
+
margin: 0 !important;
|
|
237
|
+
border-radius: 0 !important;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.mrp-preview-code :deep(pre) {
|
|
241
|
+
margin: 0 !important;
|
|
242
|
+
border-radius: 0 !important;
|
|
243
|
+
padding: 16px !important;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.mrp-preview-code :deep(pre code) {
|
|
247
|
+
font-size: 13px !important;
|
|
248
|
+
line-height: 1.6 !important;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.mrp-preview-standalone-link {
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 12px;
|
|
255
|
+
padding: 20px 16px;
|
|
256
|
+
text-decoration: none;
|
|
257
|
+
color: var(--vp-c-text-2);
|
|
258
|
+
transition: background-color 0.15s;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.mrp-preview-standalone-link:hover {
|
|
262
|
+
background-color: var(--vp-c-bg-soft);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.mrp-preview-standalone-text {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
gap: 2px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.mrp-preview-standalone-title {
|
|
272
|
+
font-size: 14px;
|
|
273
|
+
font-weight: 500;
|
|
274
|
+
color: var(--vp-c-text-1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.mrp-preview-standalone-desc {
|
|
278
|
+
font-size: 12px;
|
|
279
|
+
color: var(--vp-c-text-3);
|
|
280
|
+
}
|
|
281
|
+
</style>
|