@rspress/plugin-preview 2.0.0-rc.1 → 2.0.0-rc.2

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/dist/utils.js CHANGED
@@ -7,10 +7,6 @@ const normalizeId = (routePath)=>{
7
7
  const result = routePath.replace(/\.(.*)?$/, '');
8
8
  return toValidVarName(result);
9
9
  };
10
- const injectDemoBlockImport = (str, path)=>`
11
- import DemoBlock from ${JSON.stringify(path)};
12
- ${str}
13
- `;
14
10
  const getLangFileExt = (lang)=>{
15
11
  switch(lang){
16
12
  case 'jsx':
@@ -22,4 +18,4 @@ const getLangFileExt = (lang)=>{
22
18
  return lang;
23
19
  }
24
20
  };
25
- export { generateId, getLangFileExt, injectDemoBlockImport, normalizeId, toValidVarName };
21
+ export { generateId, getLangFileExt, normalizeId, toValidVarName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rspress/plugin-preview",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.0-rc.2",
4
4
  "description": "A plugin for rspress to preview the code block in markdown/mdx file.",
5
5
  "bugs": "https://github.com/web-infra-dev/rspress/issues",
6
6
  "repository": {
@@ -23,32 +23,29 @@
23
23
  "static"
24
24
  ],
25
25
  "dependencies": {
26
- "@rsbuild/core": "~1.6.6",
26
+ "@rsbuild/core": "~1.6.13",
27
27
  "@rsbuild/plugin-babel": "~1.0.6",
28
28
  "@rsbuild/plugin-react": "~1.4.2",
29
- "@rsbuild/plugin-solid": "~1.0.6",
30
- "lodash": "4.17.21",
31
29
  "qrcode.react": "^4.2.0"
32
30
  },
33
31
  "devDependencies": {
34
- "@rslib/core": "0.17.2",
35
- "@types/lodash": "^4.17.20",
32
+ "@rslib/core": "0.18.3",
36
33
  "@types/mdast": "^4.0.4",
37
34
  "@types/node": "^22.8.1",
38
- "@types/react": "^19.2.5",
35
+ "@types/react": "^19.2.7",
39
36
  "@types/react-dom": "^19.2.3",
40
37
  "mdast-util-mdx-jsx": "^3.2.0",
41
38
  "mdast-util-mdxjs-esm": "^2.0.1",
42
- "react": "^19.2.0",
43
- "react-dom": "^19.2.0",
44
- "react-router-dom": "^6.30.2",
39
+ "react": "^19.2.1",
40
+ "react-dom": "^19.2.1",
41
+ "react-router-dom": "^7.10.1",
45
42
  "rsbuild-plugin-publint": "^0.3.3",
46
43
  "typescript": "^5.8.2",
47
44
  "unified": "^11.0.5",
48
45
  "unist-util-visit": "^5.0.0"
49
46
  },
50
47
  "peerDependencies": {
51
- "@rspress/core": "^2.0.0-rc.1",
48
+ "@rspress/core": "^2.0.0-rc.2",
52
49
  "react": ">=18.0.0",
53
50
  "react-router-dom": "^6.8.1"
54
51
  },
@@ -1,48 +1,56 @@
1
- .fixed-device {
1
+ /* FixedDevice Component Styles (BEM) */
2
+
3
+ :root {
4
+ --rp-preview-padding: 32px;
5
+
6
+ --rp-device-width: 360px;
7
+ --rp-device-height: 640px;
8
+ --rp-device-border-radius: 20px;
9
+ --rp-device-border: 1px solid #e5e6e8;
10
+ }
11
+
12
+ /* Block: rp-fixed-device */
13
+ .rp-fixed-device {
2
14
  display: none;
3
15
  position: fixed;
4
- top: calc(var(--rp-nav-height) + var(--rp-preview-padding));
16
+ top: calc(
17
+ var(--rp-nav-height) + var(--rp-sidebar-menu-height)+
18
+ var(--rp-preview-padding) + var(--rp-banner-height, 0px)
19
+ );
20
+ right: max(
21
+ calc(
22
+ var(--rp-outline-margin-right) + var(--rp-outline-width) -
23
+ var(--rp-device-width)
24
+ ),
25
+ 0px
26
+ );
5
27
  overflow: hidden;
28
+ z-index: 100;
6
29
  }
7
30
 
8
- .fixed-iframe {
31
+ /* Element: __iframe */
32
+ .rp-fixed-device__iframe {
9
33
  height: var(--rp-device-height);
10
34
  max-height: calc(
11
35
  100vh - var(--rp-preview-padding) * 2 - var(--rp-nav-height)
12
36
  );
13
- width: 360px;
37
+ width: var(--rp-device-width);
14
38
  pointer-events: auto;
15
39
  border-radius: var(--rp-device-border-radius) var(--rp-device-border-radius) 0
16
40
  0;
17
41
  border: var(--rp-device-border);
18
42
  }
19
43
 
20
- .fixed-operation {
44
+ /* Element: __operations */
45
+ .rp-fixed-device__operations {
21
46
  border: var(--rp-device-border);
22
47
  border-top: 0;
23
48
  border-radius: 0 0 var(--rp-device-border-radius)
24
49
  var(--rp-device-border-radius);
25
50
  }
26
51
 
27
- :root {
28
- --rp-device-width: 360px;
29
- --rp-device-height: 640px;
30
- --rp-device-border-radius: 20px;
31
- --rp-device-border: 1px solid #e5e6e8;
32
- }
33
-
34
52
  @media (min-width: 960px) {
35
- .fixed-device {
36
- display: inline;
37
- left: calc(1280px - var(--rp-device-width) - var(--rp-preview-padding));
38
- right: auto;
39
- }
40
- }
41
-
42
- @media (min-width: 1280px) {
43
- .fixed-device {
53
+ .rp-fixed-device {
44
54
  display: inline;
45
- right: var(--rp-preview-padding);
46
- left: auto;
47
55
  }
48
56
  }
@@ -0,0 +1,68 @@
1
+ import { NoSSR, useDark, usePage, withBase } from '@rspress/core/runtime';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ // @ts-expect-error
4
+ import { normalizeId } from '../../dist/utils';
5
+ import MobileOperation from './common/PreviewOperations';
6
+ import './FixedDevice.css';
7
+
8
+ export default () => {
9
+ const { page } = usePage();
10
+ const pageName = `${normalizeId(page.pagePath)}`;
11
+ const demoId = `_${pageName}`;
12
+ const url = `~demo/${demoId}`;
13
+ const { haveIframeFixedDemos } = page;
14
+
15
+ const getPageUrl = (url: string) => {
16
+ if (page?.devPort) {
17
+ return `http://localhost:${page.devPort}/${demoId}`;
18
+ }
19
+ return withBase(url);
20
+ };
21
+ const [iframeKey, setIframeKey] = useState(0);
22
+ const dark = useDark();
23
+ const refresh = useCallback(() => {
24
+ setIframeKey(Math.random());
25
+ }, []);
26
+
27
+ const iframeRef = useRef<HTMLIFrameElement>(null);
28
+
29
+ useEffect(() => {
30
+ iframeRef.current?.contentWindow?.postMessage(
31
+ {
32
+ type: 'theme-change',
33
+ dark,
34
+ },
35
+ '*',
36
+ );
37
+ }, [dark]);
38
+
39
+ return haveIframeFixedDemos ? (
40
+ <div className="rp-fixed-device">
41
+ {/* hide the outline */}
42
+ <style>{`@media (min-width: 1280px) {
43
+ .rp-doc-layout__outline {
44
+ display: none;
45
+ }
46
+ }
47
+ @media (min-width: 960px) and (max-width: 1280px) {
48
+ .rp-doc-layout__doc-container {
49
+ padding-right: calc(var(--rp-device-width) + var(--rp-preview-padding));
50
+ }
51
+ }
52
+ `}</style>
53
+ <NoSSR>
54
+ <iframe
55
+ src={getPageUrl(url)}
56
+ className="rp-fixed-device__iframe"
57
+ key={iframeKey}
58
+ ref={iframeRef}
59
+ />
60
+ </NoSSR>
61
+ <MobileOperation
62
+ url={getPageUrl(url)}
63
+ className="rp-fixed-device__operations"
64
+ refresh={refresh}
65
+ />
66
+ </div>
67
+ ) : null;
68
+ };
@@ -0,0 +1,133 @@
1
+ /* Preview Component Styles (BEM) */
2
+
3
+ :root {
4
+ --rp-preview-button-hover-bg: #e5e6eb;
5
+ --rp-preview-button-bg: #e5e6eb;
6
+ }
7
+
8
+ .dark {
9
+ --rp-preview-button-hover-bg: #c5c5c5;
10
+ --rp-preview-button-bg: #c5c5c5;
11
+ }
12
+
13
+ /* Block: rp-preview */
14
+ .rp-preview {
15
+ margin: 16px 0;
16
+ }
17
+
18
+ /* ==========================================================================
19
+ Preview--internal (internal mode)
20
+ ========================================================================== */
21
+
22
+ .rp-preview--internal .rp-codeblock {
23
+ margin: 0;
24
+ border-radius: 0 0 var(--rp-radius) var(--rp-radius);
25
+ border-top: 0px;
26
+ }
27
+
28
+ .rp-preview--internal__card {
29
+ padding: 16px;
30
+ position: relative;
31
+ border: 1px solid var(--rp-container-details-border);
32
+ border-radius: var(--rp-radius);
33
+ display: flex;
34
+ }
35
+
36
+ .rp-preview--internal__card__content {
37
+ overflow: auto;
38
+ flex: auto;
39
+ }
40
+
41
+ /* Code collapse/expand animation using CSS Grid */
42
+ .rp-preview--internal--show-code .rp-preview--internal__card {
43
+ border-radius: var(--rp-radius) var(--rp-radius) 0 0;
44
+ transition: border-radius 0.2s ease-out;
45
+ }
46
+
47
+ .rp-preview--internal__code-wrapper {
48
+ display: grid;
49
+ grid-template-rows: 0fr;
50
+ transition:
51
+ grid-template-rows 0.2s ease-out,
52
+ opacity 0.2s ease-out;
53
+ opacity: 0;
54
+ }
55
+
56
+ .rp-preview--internal__code-wrapper--visible {
57
+ grid-template-rows: 1fr;
58
+ opacity: 1;
59
+ }
60
+
61
+ .rp-preview--internal__code {
62
+ overflow: hidden;
63
+ }
64
+
65
+ .rp-preview-operations__button--expanded {
66
+ background: var(--rp-c-bg-mute);
67
+ box-shadow: inset 1px 1px 0 1px var(--rp-c-divider-light);
68
+ }
69
+
70
+ /* ==========================================================================
71
+ Preview--iframe-follow (iframe-follow mode)
72
+ ========================================================================== */
73
+
74
+ .rp-preview--iframe-follow {
75
+ border: 1px solid var(--rp-container-details-border);
76
+ border-radius: var(--rp-radius);
77
+ display: flex;
78
+ flex-direction: column;
79
+ }
80
+
81
+ @media (min-width: 960px) {
82
+ .rp-preview--iframe-follow {
83
+ flex-direction: row;
84
+ }
85
+ }
86
+
87
+ .rp-preview--iframe-follow__code {
88
+ position: relative;
89
+ overflow: hidden;
90
+ flex: 1 1 auto;
91
+ min-height: 300px;
92
+ }
93
+
94
+ @media (min-width: 960px) {
95
+ .rp-preview--iframe-follow__code {
96
+ max-height: 700px;
97
+ min-height: auto;
98
+ }
99
+ }
100
+
101
+ .rp-preview--iframe-follow__code .rp-codeblock {
102
+ border: none;
103
+ margin: 0;
104
+ }
105
+
106
+ .rp-preview--iframe-follow__code .rp-codeblock,
107
+ .rp-preview--iframe-follow__code .rp-codeblock__content,
108
+ .rp-preview--iframe-follow__code
109
+ .rp-codeblock__content
110
+ .rp-codeblock__content__scroll-container {
111
+ height: 100%;
112
+ }
113
+
114
+ .rp-preview--iframe-follow__device {
115
+ position: relative;
116
+ flex: 1 1 auto;
117
+ border-top: 1px solid var(--rp-container-details-border);
118
+ display: flex;
119
+ flex-direction: column;
120
+ }
121
+
122
+ @media (min-width: 960px) {
123
+ .rp-preview--iframe-follow__device {
124
+ border-top: none;
125
+ border-left: 1px solid var(--rp-container-details-border);
126
+ }
127
+ }
128
+
129
+ .rp-preview--iframe-follow__device iframe {
130
+ border-bottom: 1px solid var(--rp-container-details-border);
131
+ height: 100%;
132
+ flex: 1 1 auto;
133
+ }
@@ -0,0 +1,130 @@
1
+ import { NoSSR, useDark, usePageData, withBase } from '@rspress/core/runtime';
2
+ import {
3
+ type MouseEvent,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import MobileOperation from './common/PreviewOperations';
10
+ import IconCode from './icons/Code';
11
+ import './Preview.css';
12
+
13
+ type PreviewProps = {
14
+ children: React.ReactNode[];
15
+ previewMode: 'internal' | 'iframe-follow';
16
+ demoId: string;
17
+ };
18
+
19
+ interface BasePreviewProps {
20
+ children: React.ReactNode[];
21
+ getPageUrl: () => string;
22
+ }
23
+
24
+ const PreviewIframeFollow: React.FC<BasePreviewProps> = ({
25
+ children,
26
+ getPageUrl,
27
+ }) => {
28
+ const [iframeKey, setIframeKey] = useState(0);
29
+ const refresh = useCallback(() => {
30
+ setIframeKey(Math.random());
31
+ }, []);
32
+
33
+ const iframeRef = useRef<HTMLIFrameElement>(null);
34
+ const dark = useDark();
35
+ useEffect(() => {
36
+ iframeRef.current?.contentWindow?.postMessage(
37
+ {
38
+ type: 'theme-change',
39
+ dark,
40
+ },
41
+ '*',
42
+ );
43
+ }, [dark]);
44
+
45
+ return (
46
+ <div className="rp-preview rp-not-doc rp-preview--iframe-follow">
47
+ <div className="rp-preview--iframe-follow__code">{children?.[0]}</div>
48
+ <div className="rp-preview--iframe-follow__device">
49
+ <iframe
50
+ className="rp-preview--iframe-follow__device__iframe"
51
+ src={getPageUrl()}
52
+ key={iframeKey}
53
+ ref={iframeRef}
54
+ />
55
+ <MobileOperation url={getPageUrl()} refresh={refresh} />
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ const PreviewInternal: React.FC<{ children: React.ReactNode[] }> = ({
62
+ children,
63
+ }) => {
64
+ const [showCode, setShowCode] = useState(false);
65
+
66
+ const toggleCode = useCallback(
67
+ (ev: MouseEvent<HTMLButtonElement>) => {
68
+ if (!showCode) {
69
+ ev.currentTarget.blur();
70
+ }
71
+ setShowCode(!showCode);
72
+ },
73
+ [showCode],
74
+ );
75
+
76
+ return (
77
+ <div
78
+ className={`rp-preview rp-not-doc rp-preview--internal ${showCode ? 'rp-preview--internal--show-code' : ''}`}
79
+ >
80
+ <div className="rp-preview--internal__card">
81
+ <div className="rp-preview--internal__card__content">
82
+ {children?.[1]}
83
+ </div>
84
+ <div className="rp-preview-operations rp-preview-operations--web">
85
+ <button
86
+ onClick={toggleCode}
87
+ aria-label="Collapse Code"
88
+ className={`rp-preview-operations__button ${showCode ? 'rp-preview-operations__button--expanded' : ''}`}
89
+ >
90
+ <IconCode />
91
+ </button>
92
+ </div>
93
+ </div>
94
+ <div
95
+ className={`rp-preview--internal__code-wrapper ${
96
+ showCode ? 'rp-preview--internal__code-wrapper--visible' : ''
97
+ }`}
98
+ >
99
+ <div className="rp-preview--internal__code">{children?.[0]}</div>
100
+ </div>
101
+ </div>
102
+ );
103
+ };
104
+
105
+ const Preview: React.FC<PreviewProps> = props => {
106
+ const { children, previewMode, demoId } = props;
107
+ const { page } = usePageData();
108
+
109
+ const getPageUrl = useCallback(() => {
110
+ const url = `/~demo/${demoId}`;
111
+ if (page?.devPort) {
112
+ return `http://localhost:${page.devPort}/${demoId}`;
113
+ }
114
+ return withBase(url);
115
+ }, [page?.devPort, demoId]);
116
+
117
+ return (
118
+ <NoSSR>
119
+ {previewMode === 'iframe-follow' ? (
120
+ <PreviewIframeFollow getPageUrl={getPageUrl}>
121
+ {children}
122
+ </PreviewIframeFollow>
123
+ ) : (
124
+ <PreviewInternal>{children}</PreviewInternal>
125
+ )}
126
+ </NoSSR>
127
+ );
128
+ };
129
+
130
+ export default Preview;
@@ -0,0 +1,61 @@
1
+ /* PreviewOperations Component Styles (BEM) */
2
+
3
+ /* Block: rp-preview-operations */
4
+ .rp-preview-operations {
5
+ display: flex;
6
+ }
7
+
8
+ /* Modifier: --mobile */
9
+ .rp-preview-operations--mobile {
10
+ justify-content: flex-end;
11
+ width: 100%;
12
+ padding: 6px;
13
+ }
14
+
15
+ /* Modifier: --web */
16
+ .rp-preview-operations--web {
17
+ justify-content: center;
18
+ align-items: center;
19
+ flex: none;
20
+ }
21
+
22
+ /* Element: __button */
23
+ .rp-preview-operations__button {
24
+ width: 28px;
25
+ height: 28px;
26
+ cursor: pointer;
27
+ padding: 0;
28
+ text-align: center;
29
+ border-radius: 50%;
30
+ border: 1px solid transparent;
31
+ background-color: var(--rp-c-bg-soft);
32
+ margin-left: 14px;
33
+
34
+ transition: background-color 0.2s ease-out;
35
+ }
36
+
37
+ .rp-preview-operations__button:hover {
38
+ background-color: var(--rp-preview-button-hover-bg);
39
+ }
40
+
41
+ .rp-preview-operations__button svg {
42
+ display: inline-block;
43
+ vertical-align: -2px;
44
+ }
45
+
46
+ /* Element: __qrcode */
47
+ .rp-preview-operations__qrcode {
48
+ position: relative;
49
+ }
50
+
51
+ /* Element: __qrcode-popup */
52
+ .rp-preview-operations__qrcode-popup {
53
+ background-color: #fff;
54
+ width: 120px;
55
+ height: 120px;
56
+ position: absolute;
57
+ top: -132px;
58
+ right: -46px;
59
+ padding: 12px;
60
+ z-index: 999;
61
+ }
@@ -10,7 +10,7 @@ import {
10
10
  import IconLaunch from '../icons/Launch';
11
11
  import IconQrcode from '../icons/Qrcode';
12
12
  import IconRefresh from '../icons/Refresh';
13
- import './index.css';
13
+ import './PreviewOperations.css';
14
14
 
15
15
  const locales = {
16
16
  zh: {
@@ -79,21 +79,34 @@ export default (props: {
79
79
  }, [showQRCode]);
80
80
 
81
81
  return (
82
- <div className={`rspress-preview-operations mobile ${className}`}>
83
- <button onClick={refresh} aria-label={t.refresh}>
82
+ <div
83
+ className={`rp-preview-operations rp-preview-operations--mobile ${className}`}
84
+ >
85
+ <button
86
+ className="rp-preview-operations__button"
87
+ onClick={refresh}
88
+ aria-label={t.refresh}
89
+ >
84
90
  <IconRefresh />
85
91
  </button>
86
- <div className="relative" ref={triggerRef}>
92
+ <div className="rp-preview-operations__qrcode" ref={triggerRef}>
87
93
  {showQRCode && (
88
- <div className="rspress-preview-qrcode">
94
+ <div className="rp-preview-operations__qrcode-popup">
89
95
  <QRCodeSVG value={url} size={96} />
90
96
  </div>
91
97
  )}
92
- <button onClick={toggleQRCode}>
98
+ <button
99
+ className="rp-preview-operations__button"
100
+ onClick={toggleQRCode}
101
+ >
93
102
  <IconQrcode />
94
103
  </button>
95
104
  </div>
96
- <button onClick={openNewPage} aria-label={t.open}>
105
+ <button
106
+ className="rp-preview-operations__button"
107
+ onClick={openNewPage}
108
+ aria-label={t.open}
109
+ >
97
110
  <IconLaunch />
98
111
  </button>
99
112
  </div>
@@ -3,11 +3,10 @@
3
3
  --rp-iframe-nav-bg: #ffffff;
4
4
  }
5
5
 
6
- /* TODO: support dark mode */
7
- /* .dark {
6
+ .dark {
8
7
  --rp-iframe-bg: #242424;
9
8
  --rp-iframe-nav-bg: #191d24;
10
- } */
9
+ }
11
10
 
12
11
  body {
13
12
  background-color: var(--rp-iframe-bg) !important;
@@ -26,7 +25,7 @@ body {
26
25
  }
27
26
  /* #endregion copied from preflight.css */
28
27
 
29
- .preview-nav {
28
+ .rp-preview-nav {
30
29
  position: relative;
31
30
  display: flex;
32
31
  align-items: center;
@@ -0,0 +1,25 @@
1
+ const storageKey = 'rspress-plugin-preview-theme-appearance';
2
+
3
+ function setDocumentTheme(isDark) {
4
+ if (isDark) {
5
+ document.documentElement.classList.add('dark');
6
+ document.documentElement.style.colorScheme = 'dark';
7
+ } else {
8
+ document.documentElement.classList.remove('dark');
9
+ document.documentElement.style.colorScheme = 'light';
10
+ }
11
+ localStorage.setItem(
12
+ 'rspress-plugin-preview-theme-appearance',
13
+ isDark ? 'dark' : 'light',
14
+ );
15
+ }
16
+
17
+ const saved = localStorage.getItem(storageKey) || 'light';
18
+ setDocumentTheme(saved === 'dark');
19
+
20
+ window.addEventListener('message', event => {
21
+ if (event.data.type === 'theme-change') {
22
+ const isDark = event.data.dark;
23
+ setDocumentTheme(isDark);
24
+ }
25
+ });