-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
index.tsx
256 lines (246 loc) · 10.7 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
import { ReactComponent as IconCheck } from '@ant-design/icons-svg/inline-svg/outlined/check.svg';
import { ReactComponent as IconCodeSandbox } from '@ant-design/icons-svg/inline-svg/outlined/code-sandbox.svg';
import { ReactComponent as IconEdit } from '@ant-design/icons-svg/inline-svg/outlined/edit.svg';
import { ReactComponent as IconSketch } from '@ant-design/icons-svg/inline-svg/outlined/sketch.svg';
import { ReactComponent as IconStackBlitz } from '@ant-design/icons-svg/inline-svg/outlined/thunderbolt.svg';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import {
getSketchJSON,
openCodeSandbox,
openStackBlitz,
useDemo,
useIntl,
type IPreviewerProps,
} from 'dumi';
import SourceCode from 'dumi/theme/builtins/SourceCode';
import PreviewerActionsExtra from 'dumi/theme/slots/PreviewerActionsExtra';
import SourceCodeEditor from 'dumi/theme/slots/SourceCodeEditor';
import Tabs from 'rc-tabs';
import React, { useRef, useState, type FC, type ReactNode } from 'react';
import './index.less';
export interface IPreviewerActionsProps extends IPreviewerProps {
/**
* disabled actions
*/
disabledActions?: ('CSB' | 'STACKBLITZ' | 'EXTERNAL' | 'HTML2SKETCH')[];
extra?: ReactNode;
forceShowCode?: boolean;
demoContainer: HTMLDivElement | HTMLIFrameElement;
onSourceTranspile?: (
args:
| { err: Error; source?: null }
| { err?: null; source: Record<string, string> },
) => void;
onSourceChange?: (source: Record<string, string>) => void;
}
const IconCode: FC = () => (
<svg viewBox="0 0 200 117">
<path d="M59.688 2.578c-3.438-3.437-8.438-3.437-11.563 0L3.75 48.516c-5 5.937-5 14.062 0 19.062l44.063 45.938c1.562 1.562 4.062 2.5 5.937 2.5s4.063-.938 5.938-2.5c3.437-3.438 3.437-8.438 0-11.563l-42.5-43.437 42.5-44.063c3.437-3.437 3.437-8.437 0-11.875Zm135.937 45.938L151.25 2.578c-3.438-3.437-8.438-3.437-11.563 0-3.125 3.438-3.437 8.438 0 11.563l42.5 44.375-42.5 44.062c-3.437 3.438-3.437 8.438 0 11.563 1.563 1.562 3.438 2.5 5.938 2.5 2.5 0 4.063-.938 5.938-2.5l44.062-45.938c5.625-5.625 5.625-13.75 0-19.687Zm-75.938-45c-4.062-1.563-9.062.937-10.937 5l-34.063 95c-1.562 4.062.938 9.062 5 10.937.938 0 1.563.938 2.5.938 3.438 0 6.563-2.5 7.5-5.938l35-95.937c1.563-4.063-.937-9.063-5-10Z" />
</svg>
);
const IconCodeExpand: FC = () => (
<svg viewBox="0 0 200 117">
<path d="M59.688 2.578c-3.438-3.437-8.438-3.437-11.563 0L3.75 48.516c-5 5.937-5 14.062 0 19.062l44.063 45.938c1.562 1.562 4.062 2.5 5.937 2.5s4.063-.938 5.938-2.5c3.437-3.438 3.437-8.438 0-11.563l-42.5-43.437 42.5-44.063c3.437-3.437 3.437-8.437 0-11.875Zm135.937 45.938L151.25 2.578c-3.438-3.437-8.438-3.437-11.563 0-3.125 3.438-3.437 8.438 0 11.563l42.5 44.375-42.5 44.062c-3.437 3.438-3.437 8.438 0 11.563 1.563 1.562 3.438 2.5 5.938 2.5 2.5 0 4.063-.938 5.938-2.5l44.062-45.938c5.625-5.625 5.625-13.75 0-19.687Z" />
</svg>
);
const IconExternalLink: FC = () => (
<svg viewBox="0 0 1024 1024">
<path d="M853.333 469.333A42.667 42.667 0 0 0 810.667 512v256A42.667 42.667 0 0 1 768 810.667H256A42.667 42.667 0 0 1 213.333 768V256A42.667 42.667 0 0 1 256 213.333h256A42.667 42.667 0 0 0 512 128H256a128 128 0 0 0-128 128v512a128 128 0 0 0 128 128h512a128 128 0 0 0 128-128V512a42.667 42.667 0 0 0-42.667-42.667z" />
<path d="M682.667 213.333h67.413L481.707 481.28a42.667 42.667 0 0 0 0 60.587 42.667 42.667 0 0 0 60.586 0L810.667 273.92v67.413A42.667 42.667 0 0 0 853.333 384 42.667 42.667 0 0 0 896 341.333V170.667A42.667 42.667 0 0 0 853.333 128H682.667a42.667 42.667 0 0 0 0 85.333z" />
</svg>
);
const PreviewerActions: FC<IPreviewerActionsProps> = (props) => {
const intl = useIntl();
const files = Object.entries(props.asset.dependencies).filter(
([, { type }]) => type === 'FILE',
);
const { renderOpts } = useDemo(props.asset.id)!;
const [activeKey, setActiveKey] = useState(0);
const [showCode, setShowCode] = useState(
props.forceShowCode || props.defaultShowCode,
);
const copyTimer = useRef<number>();
const [isCopied, setIsCopied] = useState(false);
const isSingleFile = files.length === 1;
const lang = (files[activeKey][0].match(/\.([^.]+)$/)?.[1] || 'text') as any;
return (
<>
<div className="dumi-default-previewer-actions">
{!props.disabledActions?.includes('CSB') && (
<button
className="dumi-default-previewer-action-btn"
type="button"
data-dumi-tooltip={intl.formatMessage({
id: 'previewer.actions.codesandbox',
})}
onClick={() => openCodeSandbox(props)}
>
<IconCodeSandbox />
</button>
)}
{!props.disabledActions?.includes('STACKBLITZ') && (
<button
className="dumi-default-previewer-action-btn"
type="button"
data-dumi-tooltip={intl.formatMessage({
id: 'previewer.actions.stackblitz',
})}
onClick={() => openStackBlitz(props)}
>
<IconStackBlitz />
</button>
)}
{!props.disabledActions?.includes('HTML2SKETCH') && getSketchJSON && (
<span
className="dumi-default-previewer-action-btn dumi-default-previewer-action-sketch"
data-dumi-tooltip={intl.formatMessage({
id: 'previewer.actions.sketch',
})}
data-copied={isCopied || undefined}
>
{isCopied ? <IconCheck /> : <IconSketch />}
<select
value=""
onChange={(ev) => {
const { value: type } = ev.target;
switch (type) {
case 'group':
case 'symbol':
getSketchJSON(props.demoContainer, { type }).then(
(data) => {
copy(JSON.stringify(data));
setIsCopied(true);
clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(
() => setIsCopied(false),
2000,
);
},
);
break;
case 'guide':
window.open('https://d.umijs.org/config#html2sketch');
break;
default:
}
}}
>
<option value="" hidden></option>
<option value="group">
{intl.formatMessage({ id: 'previewer.actions.sketch.group' })}
</option>
<option value="symbol">
{intl.formatMessage({ id: 'previewer.actions.sketch.symbol' })}
</option>
<option value="-" disabled>
{intl.formatMessage({ id: 'previewer.actions.sketch.divider' })}
</option>
<option value="guide">
{intl.formatMessage({ id: 'previewer.actions.sketch.guide' })}
</option>
</select>
</span>
)}
{!props.disabledActions?.includes('EXTERNAL') && (
<a
target="_blank"
rel="noreferrer"
href={props.demoUrl}
className="dumi-default-previewer-action-btn"
data-dumi-tooltip={intl.formatMessage({
id: 'previewer.actions.separate',
})}
>
<IconExternalLink />
</a>
)}
{props.extra}
<PreviewerActionsExtra {...props} />
{!props.forceShowCode && (
<button
className="dumi-default-previewer-action-btn"
type="button"
onClick={() => setShowCode((prev) => !prev)}
data-dumi-tooltip={intl.formatMessage({
id: `previewer.actions.code.${showCode ? 'shrink' : 'expand'}`,
})}
>
{showCode ? <IconCodeExpand /> : <IconCode />}
</button>
)}
</div>
{showCode && (
<>
<div className="dumi-default-previewer-sources">
<Tabs
className={classNames(
'dumi-default-previewer-tabs',
isSingleFile && 'dumi-default-previewer-tabs-single',
)}
prefixCls="dumi-default-tabs"
moreIcon="···"
defaultActiveKey={String(activeKey)}
onChange={(key) => setActiveKey(Number(key))}
items={files.map(([filename], i) => ({
key: String(i),
// remove leading ./ prefix
label: filename.replace(/^\.\//, ''),
// only support to edit entry file currently
children:
i === 0 && renderOpts?.compile ? (
<SourceCodeEditor
lang={lang}
initialValue={files[i][1].value.trim()}
onChange={(code) => {
props.onSourceChange?.({ [files[i][0]]: code });
// FIXME: remove before publish
props.onSourceTranspile?.({
source: { [files[i][0]]: code },
});
}}
extra={
<button
type="button"
className="dumi-default-previewer-editor-tip-btn"
data-dumi-tooltip={intl.formatMessage({
id: 'previewer.actions.code.editable',
})}
>
<IconEdit />
</button>
}
/>
) : (
<SourceCode
lang={lang}
extra={
// only show readonly tip for non-entry files
// because readonly entry file means live compile is not available for this demo tech stack
i !== 0 && (
<button
type="button"
className="dumi-default-previewer-editor-tip-btn"
data-dumi-tooltip={intl.formatMessage({
id: 'previewer.actions.code.readonly',
})}
data-readonly
>
<span></span>
<IconEdit />
</button>
)
}
>
{files[activeKey][1].value.trim()}
</SourceCode>
),
}))}
/>
</div>
</>
)}
</>
);
};
export default PreviewerActions;