@rspress/plugin-preview 1.44.0 → 1.45.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/dist/index.js CHANGED
@@ -608,7 +608,9 @@ var __webpack_exports__ = {};
608
608
  (0, external_node_path_namespaceObject.join)(staticPath, 'global-components', 'ContainerFixedPerComp.tsx')
609
609
  ]
610
610
  },
611
- globalUIComponents: 'fixed-with-per-comp' === position || 'fixed' === position ? [
611
+ globalUIComponents: 'fixed-with-per-comp' === position ? [
612
+ (0, external_node_path_namespaceObject.join)(staticPath, 'global-components', 'DeviceFixedPerComp.tsx')
613
+ ] : 'fixed' === position ? [
612
614
  (0, external_node_path_namespaceObject.join)(staticPath, 'global-components', 'Device.tsx')
613
615
  ] : [],
614
616
  globalStyles: (0, external_node_path_namespaceObject.join)(staticPath, 'global-styles', `${previewMode}.css`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rspress/plugin-preview",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
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": {
@@ -25,8 +25,8 @@
25
25
  "@rsbuild/plugin-solid": "~1.0.5",
26
26
  "lodash": "4.17.21",
27
27
  "qrcode.react": "^3.2.0",
28
- "@rspress/shared": "1.44.0",
29
- "@rspress/theme-default": "1.44.0"
28
+ "@rspress/shared": "1.45.0",
29
+ "@rspress/theme-default": "1.45.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@rslib/core": "~0.6.9",
@@ -45,7 +45,7 @@
45
45
  "unist-util-visit": "^4.1.2"
46
46
  },
47
47
  "peerDependencies": {
48
- "@rspress/core": "^1.44.0",
48
+ "@rspress/core": "^1.45.0",
49
49
  "react": ">=17.0.0",
50
50
  "react-router-dom": "^6.8.1"
51
51
  },
@@ -1,4 +1,7 @@
1
- import { NoSSR, usePageData, withBase } from '@rspress/core/runtime';
1
+ import { NoSSR, useLang, usePageData, withBase } from '@rspress/core/runtime';
2
+ import { type MouseEvent, useCallback, useState } from 'react';
3
+ import IconCode from './icons/Code';
4
+ import { publishIframeUrl } from './useIframeUrlPerComp';
2
5
 
3
6
  type ContainerProps = {
4
7
  children: React.ReactNode[];
@@ -6,7 +9,7 @@ type ContainerProps = {
6
9
  demoId: string;
7
10
  };
8
11
 
9
- const ContainerFixedPerComp: React.FC<ContainerProps> = props => {
12
+ const MobileContainerFixedPerComp: React.FC<ContainerProps> = props => {
10
13
  const { children, demoId } = props;
11
14
  const { page } = usePageData();
12
15
  const url = `/~demo/${demoId}`;
@@ -22,24 +25,76 @@ const ContainerFixedPerComp: React.FC<ContainerProps> = props => {
22
25
  return '';
23
26
  };
24
27
 
28
+ const setIframeUrl = () => {
29
+ const url = getPageUrl();
30
+ const fixedIframe = document.querySelector('.rspress-fixed-iframe');
31
+ fixedIframe?.setAttribute('src', url);
32
+ publishIframeUrl(url);
33
+ };
34
+
35
+ return (
36
+ <div className="rspress-preview-code" onClick={setIframeUrl}>
37
+ {children}
38
+ </div>
39
+ );
40
+ };
41
+
42
+ const ContainerFixedPerComp = (props: ContainerProps) => {
43
+ const { children, isMobile } = props;
44
+ const [showCode, setShowCode] = useState(false);
45
+ const lang = useLang();
46
+
47
+ const toggleCode = useCallback(
48
+ (ev: MouseEvent<HTMLButtonElement>) => {
49
+ if (!showCode) {
50
+ ev.currentTarget.blur();
51
+ }
52
+ setShowCode(!showCode);
53
+ },
54
+ [showCode],
55
+ );
56
+
25
57
  return (
26
- <NoSSR>
27
- <div className="rspress-preview">
28
- <div className="rspress-preview-wrapper">
29
- <div
30
- className="rspress-preview-code"
31
- onClick={() => {
32
- const fixedIframe = document.querySelector(
33
- '.rspress-fixed-iframe',
34
- );
35
- fixedIframe?.setAttribute('src', getPageUrl());
36
- }}
37
- >
38
- {children?.[0]}
58
+ <>
59
+ {isMobile === 'true' ? (
60
+ <MobileContainerFixedPerComp {...props} />
61
+ ) : (
62
+ <NoSSR>
63
+ <div className="rspress-preview">
64
+ <div>
65
+ <div className="rspress-preview-card">
66
+ <div
67
+ style={{
68
+ overflow: 'auto',
69
+ flex: 'auto',
70
+ }}
71
+ >
72
+ {children?.[1]}
73
+ </div>
74
+ <div className="rspress-preview-operations web">
75
+ <button
76
+ onClick={toggleCode}
77
+ aria-label={lang === 'zh' ? '收起代码' : 'Collapse Code'}
78
+ className={showCode ? 'button-expanded' : ''}
79
+ >
80
+ <IconCode />
81
+ </button>
82
+ </div>
83
+ </div>
84
+ <div
85
+ className={`${
86
+ showCode
87
+ ? 'rspress-preview-code-show'
88
+ : 'rspress-preview-code-hide'
89
+ }`}
90
+ >
91
+ {children?.[0]}
92
+ </div>
93
+ </div>
39
94
  </div>
40
- </div>
41
- </div>
42
- </NoSSR>
95
+ </NoSSR>
96
+ )}
97
+ </>
43
98
  );
44
99
  };
45
100
 
@@ -0,0 +1,106 @@
1
+ import {
2
+ NoSSR,
3
+ usePageData,
4
+ useWindowSize,
5
+ withBase,
6
+ } from '@rspress/core/runtime';
7
+ import { useCallback, useEffect, useState } from 'react';
8
+ // @ts-ignore
9
+ import { normalizeId } from '../../dist/utils';
10
+ import MobileOperation from './common/mobile-operation';
11
+ import { publishIframeUrl, useIframeUrlPerComp } from './useIframeUrlPerComp';
12
+ import './Device.scss';
13
+
14
+ export default () => {
15
+ const { page } = usePageData();
16
+ const pageName = `${normalizeId(page.pagePath)}`;
17
+ const demoId = `_${pageName}`;
18
+ const url = `~demo/${demoId}`;
19
+ const { haveDemos } = page;
20
+
21
+ const getPageUrl = (url: string) => {
22
+ if (page?.devPort) {
23
+ return `http://localhost:${page.devPort}/${demoId}`;
24
+ }
25
+ if (typeof window !== 'undefined') {
26
+ return `${window.location.origin}${withBase(url)}`;
27
+ }
28
+ // Do nothing in ssr
29
+ return '';
30
+ };
31
+
32
+ const initialUrl = getPageUrl(url);
33
+
34
+ const iframeUrl = useIframeUrlPerComp() ?? initialUrl;
35
+
36
+ const resetIframeUrl = useCallback(() => {
37
+ publishIframeUrl(initialUrl);
38
+ }, [initialUrl, url]);
39
+
40
+ useEffect(() => {
41
+ publishIframeUrl(initialUrl);
42
+ }, []);
43
+
44
+ const [asideWidth, setAsideWidth] = useState('0px');
45
+ const { width: innerWidth } = useWindowSize();
46
+ const [iframeKey, setIframeKey] = useState(0);
47
+ const refresh = useCallback(() => {
48
+ setIframeKey(Math.random());
49
+ }, []);
50
+
51
+ // get default value from root
52
+ // listen resize and re-render
53
+ useEffect(() => {
54
+ const root = document.querySelector(':root');
55
+ if (root) {
56
+ const defaultAsideWidth =
57
+ getComputedStyle(root).getPropertyValue('--rp-aside-width');
58
+ setAsideWidth(defaultAsideWidth);
59
+ }
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ const node = document.querySelector('.rspress-doc-container');
64
+ const { style } = document.documentElement;
65
+ if (haveDemos) {
66
+ if (innerWidth > 1280) {
67
+ node?.setAttribute(
68
+ 'style',
69
+ 'padding-right: calc(var(--rp-device-width) + var(--rp-preview-padding) * 2)',
70
+ );
71
+ } else if (innerWidth > 960) {
72
+ node?.setAttribute(
73
+ 'style',
74
+ `padding-right: calc(${
75
+ innerWidth - 1280
76
+ }px + var(--rp-device-width) + var(--rp-preview-padding) * 2)`,
77
+ );
78
+ } else {
79
+ node?.removeAttribute('style');
80
+ }
81
+ style.setProperty('--rp-aside-width', '0px');
82
+ } else {
83
+ node?.removeAttribute('style');
84
+ style.setProperty('--rp-aside-width', asideWidth);
85
+ }
86
+ }, [haveDemos, asideWidth, innerWidth]);
87
+
88
+ return haveDemos ? (
89
+ <div className="rspress-fixed-device">
90
+ <NoSSR>
91
+ <iframe
92
+ // refresh when load the iframe, then remove NoSSR
93
+ src={iframeUrl}
94
+ className="rspress-fixed-iframe"
95
+ key={iframeKey}
96
+ ></iframe>
97
+ </NoSSR>
98
+ <MobileOperation
99
+ url={iframeUrl}
100
+ className="rspress-fixed-operation"
101
+ refresh={refresh}
102
+ goBack={iframeUrl !== initialUrl ? resetIframeUrl : undefined}
103
+ />
104
+ </div>
105
+ ) : null;
106
+ };
@@ -7,6 +7,7 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
+ import IconBack from '../icons/Back';
10
11
  import IconLaunch from '../icons/Launch';
11
12
  import IconQrcode from '../icons/Qrcode';
12
13
  import IconRefresh from '../icons/Refresh';
@@ -15,21 +16,24 @@ import './index.scss';
15
16
  const locales = {
16
17
  zh: {
17
18
  refresh: '刷新页面',
19
+ goBack: '返回',
18
20
  open: '在新页面打开',
19
21
  },
20
22
  en: {
21
23
  refresh: 'Refresh',
24
+ goBack: 'Go back',
22
25
  open: 'Open in new page',
23
26
  },
24
27
  };
25
28
 
26
- export default (props: {
29
+ const MobileOperation = (props: {
27
30
  url: string;
28
31
  className?: string;
29
- refresh: () => void;
32
+ refresh?: () => void;
33
+ goBack?: () => void;
30
34
  }) => {
35
+ const { url, className = '', refresh, goBack } = props;
31
36
  const [showQRCode, setShowQRCode] = useState(false);
32
- const { url, className = '', refresh } = props;
33
37
  const lang = useLang();
34
38
  const triggerRef = useRef(null);
35
39
  const t = lang === 'zh' ? locales.zh : locales.en;
@@ -80,22 +84,46 @@ export default (props: {
80
84
 
81
85
  return (
82
86
  <div className={`rspress-preview-operations mobile ${className}`}>
83
- <button onClick={refresh} aria-label={t.refresh}>
84
- <IconRefresh />
85
- </button>
87
+ {goBack && (
88
+ <button
89
+ onClick={goBack}
90
+ aria-label={t.goBack}
91
+ className="rspress-preview-operations-back"
92
+ >
93
+ <IconBack />
94
+ </button>
95
+ )}
96
+ {refresh && (
97
+ <button
98
+ onClick={refresh}
99
+ aria-label={t.refresh}
100
+ className="rspress-preview-operations-refresh"
101
+ >
102
+ <IconRefresh />
103
+ </button>
104
+ )}
86
105
  <div className="relative" ref={triggerRef}>
87
106
  {showQRCode && (
88
107
  <div className="rspress-preview-qrcode">
89
108
  <QRCodeSVG value={url} size={96} />
90
109
  </div>
91
110
  )}
92
- <button onClick={toggleQRCode}>
111
+ <button
112
+ onClick={toggleQRCode}
113
+ className="rspress-preview-operations-qrcode"
114
+ >
93
115
  <IconQrcode />
94
116
  </button>
95
117
  </div>
96
- <button onClick={openNewPage} aria-label={t.open}>
118
+ <button
119
+ onClick={openNewPage}
120
+ aria-label={t.open}
121
+ className="rspress-preview-operations-open"
122
+ >
97
123
  <IconLaunch />
98
124
  </button>
99
125
  </div>
100
126
  );
101
127
  };
128
+
129
+ export default MobileOperation;
@@ -0,0 +1,31 @@
1
+ const Back = ({ color = 'currentColor', ...props }) => {
2
+ return (
3
+ <svg
4
+ width="1em"
5
+ height="1em"
6
+ viewBox="0 0 48 48"
7
+ fill="none"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ stroke={color}
10
+ strokeWidth="4"
11
+ {...props}
12
+ >
13
+ <path
14
+ d="M12.9998 8L6 14L12.9998 21"
15
+ stroke="#333"
16
+ stroke-width="4"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ <path
21
+ d="M6 14H28.9938C35.8768 14 41.7221 19.6204 41.9904 26.5C42.2739 33.7696 36.2671 40 28.9938 40H11.9984"
22
+ stroke="#333"
23
+ stroke-width="4"
24
+ stroke-linecap="round"
25
+ stroke-linejoin="round"
26
+ />
27
+ </svg>
28
+ );
29
+ };
30
+
31
+ export default Back;
@@ -0,0 +1,39 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ const useForceRefresh = () => {
4
+ const [_, setKey] = useState(0);
5
+
6
+ const forceRefresh = useCallback(() => {
7
+ return setKey(prevKey => prevKey + 1);
8
+ }, [setKey]);
9
+
10
+ return forceRefresh;
11
+ };
12
+
13
+ let iframeUrl: string | undefined;
14
+
15
+ const observer = new Map<string, () => void>();
16
+
17
+ const publishIframeUrl = (url: string) => {
18
+ iframeUrl = url;
19
+ for (const [, refresh] of observer) {
20
+ refresh();
21
+ }
22
+ };
23
+
24
+ const useIframeUrlPerComp = () => {
25
+ const forceRefresh = useForceRefresh();
26
+
27
+ useEffect(() => {
28
+ const id = Math.random().toString(36).slice(5);
29
+ observer.set(id, forceRefresh);
30
+
31
+ return () => {
32
+ observer.delete(id);
33
+ };
34
+ }, []);
35
+
36
+ return iframeUrl;
37
+ };
38
+
39
+ export { useIframeUrlPerComp, publishIframeUrl };