@izumisy/md-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 +116 -0
- package/app/index.html +41 -0
- package/app/src/app.tsx +219 -0
- package/app/src/code-block.tsx +182 -0
- package/app/src/main.tsx +9 -0
- package/app/src/mdx-components.tsx +103 -0
- package/app/src/preview-block.tsx +217 -0
- package/app/src/theme.tsx +45 -0
- package/app/src/virtual-modules.d.ts +26 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +59 -0
- package/dist/index.d.mts +3370 -0
- package/dist/index.mjs +2 -0
- package/dist/server-C2ZxWhHj.mjs +275 -0
- package/package.json +60 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { CodeBlock } from "./code-block";
|
|
3
|
+
import { useTheme } from "./theme";
|
|
4
|
+
|
|
5
|
+
function ChevronIcon({ open }: { open: boolean }) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
width="16"
|
|
9
|
+
height="16"
|
|
10
|
+
viewBox="0 0 16 16"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth="1.5"
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
style={{
|
|
17
|
+
transition: "transform 0.15s",
|
|
18
|
+
transform: open ? "rotate(90deg)" : "rotate(0deg)",
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<path d="M6 4l4 4-4 4" />
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ExternalLinkIcon() {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
width="14"
|
|
30
|
+
height="14"
|
|
31
|
+
viewBox="0 0 16 16"
|
|
32
|
+
fill="none"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
strokeWidth="1.5"
|
|
35
|
+
strokeLinecap="round"
|
|
36
|
+
strokeLinejoin="round"
|
|
37
|
+
>
|
|
38
|
+
<path d="M7 3H3v10h10V9" />
|
|
39
|
+
<path d="M10 2h4v4" />
|
|
40
|
+
<path d="M14 2L7 9" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CodeSection({
|
|
46
|
+
code,
|
|
47
|
+
open,
|
|
48
|
+
onToggle,
|
|
49
|
+
}: {
|
|
50
|
+
code: string;
|
|
51
|
+
open: boolean;
|
|
52
|
+
onToggle: () => void;
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<div style={{ borderTop: "1px solid var(--ms-border)" }}>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={onToggle}
|
|
59
|
+
style={{
|
|
60
|
+
display: "flex",
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
gap: 4,
|
|
63
|
+
width: "100%",
|
|
64
|
+
padding: "8px 12px",
|
|
65
|
+
background: "none",
|
|
66
|
+
border: "none",
|
|
67
|
+
cursor: "pointer",
|
|
68
|
+
fontSize: 13,
|
|
69
|
+
color: "var(--ms-fg-muted)",
|
|
70
|
+
fontFamily: "inherit",
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<ChevronIcon open={open} />
|
|
74
|
+
Code
|
|
75
|
+
</button>
|
|
76
|
+
{open && (
|
|
77
|
+
<CodeBlock className="language-tsx" noBorderRadius>
|
|
78
|
+
{code}
|
|
79
|
+
</CodeBlock>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function PreviewBlock({
|
|
86
|
+
code,
|
|
87
|
+
blockId,
|
|
88
|
+
height,
|
|
89
|
+
wrap,
|
|
90
|
+
align,
|
|
91
|
+
standalone,
|
|
92
|
+
}: {
|
|
93
|
+
code: string;
|
|
94
|
+
blockId: string;
|
|
95
|
+
height?: string;
|
|
96
|
+
wrap?: string;
|
|
97
|
+
align?: string;
|
|
98
|
+
standalone?: boolean;
|
|
99
|
+
}) {
|
|
100
|
+
const DEFAULT_HEIGHT = 200;
|
|
101
|
+
const [open, setOpen] = useState(true);
|
|
102
|
+
const [iframeHeight, setIframeHeight] = useState(height ? Number(height) : DEFAULT_HEIGHT);
|
|
103
|
+
const { colorScheme } = useTheme();
|
|
104
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
105
|
+
|
|
106
|
+
// Build the initial URL (theme is applied via postMessage after load)
|
|
107
|
+
const params = new URLSearchParams({ theme: colorScheme });
|
|
108
|
+
if (wrap) params.set("wrap", wrap);
|
|
109
|
+
if (align) params.set("align", align);
|
|
110
|
+
const previewUrl = `/__preview/${blockId}?${params}`;
|
|
111
|
+
|
|
112
|
+
// Sync theme changes to the iframe via postMessage instead of reloading
|
|
113
|
+
const initialColorScheme = useRef(colorScheme);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (colorScheme === initialColorScheme.current) return;
|
|
116
|
+
iframeRef.current?.contentWindow?.postMessage({ type: "mrp-theme", theme: colorScheme }, "*");
|
|
117
|
+
}, [colorScheme]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
function onMessage(e: MessageEvent) {
|
|
121
|
+
if (e.data?.type === "mrp-resize" && e.data?.blockId === blockId) {
|
|
122
|
+
setIframeHeight(e.data.height);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
window.addEventListener("message", onMessage);
|
|
126
|
+
return () => window.removeEventListener("message", onMessage);
|
|
127
|
+
}, [blockId]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
style={{
|
|
132
|
+
border: "1px solid var(--ms-border)",
|
|
133
|
+
borderRadius: 8,
|
|
134
|
+
overflow: "hidden",
|
|
135
|
+
margin: "16px 0",
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{standalone ? (
|
|
139
|
+
<a
|
|
140
|
+
href={previewUrl}
|
|
141
|
+
target="_blank"
|
|
142
|
+
rel="noopener noreferrer"
|
|
143
|
+
style={{
|
|
144
|
+
display: "flex",
|
|
145
|
+
alignItems: "center",
|
|
146
|
+
gap: 12,
|
|
147
|
+
padding: "20px 16px",
|
|
148
|
+
textDecoration: "none",
|
|
149
|
+
color: "var(--ms-fg-muted)",
|
|
150
|
+
}}
|
|
151
|
+
onMouseEnter={(e) => {
|
|
152
|
+
e.currentTarget.style.backgroundColor = "var(--ms-code-bg)";
|
|
153
|
+
}}
|
|
154
|
+
onMouseLeave={(e) => {
|
|
155
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<ExternalLinkIcon />
|
|
159
|
+
<span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
160
|
+
<span
|
|
161
|
+
style={{
|
|
162
|
+
fontSize: 14,
|
|
163
|
+
fontWeight: 500,
|
|
164
|
+
color: "var(--ms-fg)",
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
Open full-page preview
|
|
168
|
+
</span>
|
|
169
|
+
<span style={{ fontSize: 12, color: "var(--ms-fg-muted)" }}>
|
|
170
|
+
This component requires a full viewport to render correctly.
|
|
171
|
+
</span>
|
|
172
|
+
</span>
|
|
173
|
+
</a>
|
|
174
|
+
) : (
|
|
175
|
+
<div style={{ position: "relative" }}>
|
|
176
|
+
<a
|
|
177
|
+
href={previewUrl}
|
|
178
|
+
target="_blank"
|
|
179
|
+
rel="noopener noreferrer"
|
|
180
|
+
title="Open in separate page"
|
|
181
|
+
style={{
|
|
182
|
+
position: "absolute",
|
|
183
|
+
top: 8,
|
|
184
|
+
right: 8,
|
|
185
|
+
display: "flex",
|
|
186
|
+
alignItems: "center",
|
|
187
|
+
justifyContent: "center",
|
|
188
|
+
width: 28,
|
|
189
|
+
height: 28,
|
|
190
|
+
borderRadius: 6,
|
|
191
|
+
border: "1px solid var(--ms-border)",
|
|
192
|
+
background: "var(--ms-bg)",
|
|
193
|
+
color: "var(--ms-fg-muted)",
|
|
194
|
+
cursor: "pointer",
|
|
195
|
+
zIndex: 1,
|
|
196
|
+
textDecoration: "none",
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<ExternalLinkIcon />
|
|
200
|
+
</a>
|
|
201
|
+
<iframe
|
|
202
|
+
ref={iframeRef}
|
|
203
|
+
src={previewUrl}
|
|
204
|
+
style={{
|
|
205
|
+
display: "block",
|
|
206
|
+
width: "100%",
|
|
207
|
+
height: iframeHeight,
|
|
208
|
+
border: "none",
|
|
209
|
+
transition: "height 0.15s ease",
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
<CodeSection code={code} open={open} onToggle={() => setOpen((v) => !v)} />
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
type ColorScheme = "light" | "dark";
|
|
4
|
+
|
|
5
|
+
interface ThemeContextValue {
|
|
6
|
+
colorScheme: ColorScheme;
|
|
7
|
+
toggle: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ThemeContext = createContext<ThemeContextValue>({
|
|
11
|
+
colorScheme: "light",
|
|
12
|
+
toggle: () => {},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function getSystemScheme(): ColorScheme {
|
|
16
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
20
|
+
const [colorScheme, setColorScheme] = useState<ColorScheme>(getSystemScheme);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const root = document.documentElement;
|
|
24
|
+
root.setAttribute("data-theme", colorScheme);
|
|
25
|
+
root.classList.remove("light", "dark");
|
|
26
|
+
root.classList.add(colorScheme);
|
|
27
|
+
}, [colorScheme]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
31
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
32
|
+
setColorScheme(e.matches ? "dark" : "light");
|
|
33
|
+
};
|
|
34
|
+
mq.addEventListener("change", handler);
|
|
35
|
+
return () => mq.removeEventListener("change", handler);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const toggle = () => setColorScheme((s) => (s === "light" ? "dark" : "light"));
|
|
39
|
+
|
|
40
|
+
return <ThemeContext.Provider value={{ colorScheme, toggle }}>{children}</ThemeContext.Provider>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useTheme() {
|
|
44
|
+
return useContext(ThemeContext);
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare module "virtual:previewer-entries" {
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
interface PreviewEntryFrontmatter {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PreviewEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
Component: ComponentType;
|
|
10
|
+
frontmatter: PreviewEntryFrontmatter;
|
|
11
|
+
/** File path relative to the host project root */
|
|
12
|
+
filePath: string;
|
|
13
|
+
}
|
|
14
|
+
export const entries: PreviewEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module "virtual:previewer-css" {}
|
|
18
|
+
|
|
19
|
+
declare module "virtual:mrp-preview-registry" {
|
|
20
|
+
export const registry: Record<
|
|
21
|
+
string,
|
|
22
|
+
() => Promise<{ default: import("react").FC; css: string }>
|
|
23
|
+
>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare const __PREVIEWER_TITLE__: string;
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as runPreview, r as startDev, t as runBuild } from "./server-C2ZxWhHj.mjs";
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
|
+
import { loadConfig } from "c12";
|
|
5
|
+
import fg from "fast-glob";
|
|
6
|
+
//#region src/cli.ts
|
|
7
|
+
async function resolveRunOptions(cwd) {
|
|
8
|
+
const { config } = await loadConfig({
|
|
9
|
+
name: "mrp",
|
|
10
|
+
cwd
|
|
11
|
+
});
|
|
12
|
+
const cfg = config ?? {};
|
|
13
|
+
const resolveFiles = () => fg(cfg.glob ?? "docs/**/*.md", {
|
|
14
|
+
cwd,
|
|
15
|
+
absolute: true
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
cwd,
|
|
19
|
+
config: cfg,
|
|
20
|
+
resolveFiles
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
runMain(defineCommand({
|
|
24
|
+
meta: {
|
|
25
|
+
name: "mrp",
|
|
26
|
+
description: "md-react-preview CLI"
|
|
27
|
+
},
|
|
28
|
+
subCommands: {
|
|
29
|
+
dev: defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "dev",
|
|
32
|
+
description: "Start the dev server"
|
|
33
|
+
},
|
|
34
|
+
async run() {
|
|
35
|
+
await startDev(await resolveRunOptions(process.cwd()));
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
build: defineCommand({
|
|
39
|
+
meta: {
|
|
40
|
+
name: "build",
|
|
41
|
+
description: "Build for production"
|
|
42
|
+
},
|
|
43
|
+
async run() {
|
|
44
|
+
await runBuild(await resolveRunOptions(process.cwd()));
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
preview: defineCommand({
|
|
48
|
+
meta: {
|
|
49
|
+
name: "preview",
|
|
50
|
+
description: "Preview the production build locally"
|
|
51
|
+
},
|
|
52
|
+
async run() {
|
|
53
|
+
await runPreview(await resolveRunOptions(process.cwd()));
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
//#endregion
|
|
59
|
+
export {};
|