@midscene/visualizer 0.0.1

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.
Files changed (92) hide show
  1. package/.eslintrc.js +9 -0
  2. package/README.md +24 -0
  3. package/dist/es/assets/logo-plain.16842bbc.svg +70 -0
  4. package/dist/es/assets/logo-plain2.16842bbc.svg +70 -0
  5. package/dist/es/component/blackboard.css +25 -0
  6. package/dist/es/component/blackboard.js +256 -0
  7. package/dist/es/component/color.js +34 -0
  8. package/dist/es/component/common.css +0 -0
  9. package/dist/es/component/detail-panel.css +34 -0
  10. package/dist/es/component/detail-panel.js +106 -0
  11. package/dist/es/component/detail-side.css +99 -0
  12. package/dist/es/component/detail-side.js +285 -0
  13. package/dist/es/component/global-hover-preview.css +19 -0
  14. package/dist/es/component/global-hover-preview.js +44 -0
  15. package/dist/es/component/misc.js +24 -0
  16. package/dist/es/component/panel-title.css +8 -0
  17. package/dist/es/component/panel-title.js +9 -0
  18. package/dist/es/component/side-item.js +0 -0
  19. package/dist/es/component/sidebar.css +87 -0
  20. package/dist/es/component/sidebar.js +175 -0
  21. package/dist/es/component/store.js +128 -0
  22. package/dist/es/component/timeline.css +18 -0
  23. package/dist/es/component/timeline.js +438 -0
  24. package/dist/es/index.css +89 -0
  25. package/dist/es/index.js +174 -0
  26. package/dist/es/utils.js +76 -0
  27. package/dist/lib/assets/logo-plain.16842bbc.svg +70 -0
  28. package/dist/lib/assets/logo-plain2.16842bbc.svg +70 -0
  29. package/dist/lib/component/blackboard.css +25 -0
  30. package/dist/lib/component/blackboard.js +286 -0
  31. package/dist/lib/component/color.js +59 -0
  32. package/dist/lib/component/common.css +0 -0
  33. package/dist/lib/component/detail-panel.css +34 -0
  34. package/dist/lib/component/detail-panel.js +136 -0
  35. package/dist/lib/component/detail-side.css +99 -0
  36. package/dist/lib/component/detail-side.js +313 -0
  37. package/dist/lib/component/global-hover-preview.css +19 -0
  38. package/dist/lib/component/global-hover-preview.js +64 -0
  39. package/dist/lib/component/misc.js +48 -0
  40. package/dist/lib/component/panel-title.css +8 -0
  41. package/dist/lib/component/panel-title.js +29 -0
  42. package/dist/lib/component/side-item.js +1 -0
  43. package/dist/lib/component/sidebar.css +87 -0
  44. package/dist/lib/component/sidebar.js +198 -0
  45. package/dist/lib/component/store.js +153 -0
  46. package/dist/lib/component/timeline.css +18 -0
  47. package/dist/lib/component/timeline.js +466 -0
  48. package/dist/lib/index.css +89 -0
  49. package/dist/lib/index.js +202 -0
  50. package/dist/lib/utils.js +111 -0
  51. package/dist/types/component/blackboard.d.ts +4 -0
  52. package/dist/types/component/color.d.ts +2 -0
  53. package/dist/types/component/detail-panel.d.ts +4 -0
  54. package/dist/types/component/detail-side.d.ts +4 -0
  55. package/dist/types/component/global-hover-preview.d.ts +4 -0
  56. package/dist/types/component/misc.d.ts +2 -0
  57. package/dist/types/component/panel-title.d.ts +6 -0
  58. package/dist/types/component/side-item.d.ts +0 -0
  59. package/dist/types/component/sidebar.d.ts +4 -0
  60. package/dist/types/component/store.d.ts +35 -0
  61. package/dist/types/component/timeline.d.ts +4 -0
  62. package/dist/types/index.d.ts +4 -0
  63. package/dist/types/utils.d.ts +5 -0
  64. package/docs/index.tsx +6 -0
  65. package/modern.config.ts +15 -0
  66. package/package.json +46 -0
  67. package/src/component/assets/logo-plain.svg +70 -0
  68. package/src/component/assets/logo-plain2.svg +70 -0
  69. package/src/component/blackboard.less +37 -0
  70. package/src/component/blackboard.tsx +293 -0
  71. package/src/component/color.tsx +34 -0
  72. package/src/component/common.less +21 -0
  73. package/src/component/detail-panel.less +47 -0
  74. package/src/component/detail-panel.tsx +124 -0
  75. package/src/component/detail-side.less +131 -0
  76. package/src/component/detail-side.tsx +361 -0
  77. package/src/component/global-hover-preview.less +23 -0
  78. package/src/component/global-hover-preview.tsx +50 -0
  79. package/src/component/misc.tsx +20 -0
  80. package/src/component/panel-title.less +11 -0
  81. package/src/component/panel-title.tsx +11 -0
  82. package/src/component/side-item.tsx +0 -0
  83. package/src/component/sidebar.less +122 -0
  84. package/src/component/sidebar.tsx +205 -0
  85. package/src/component/store.tsx +151 -0
  86. package/src/component/timeline.less +25 -0
  87. package/src/component/timeline.tsx +486 -0
  88. package/src/global.d.ts +11 -0
  89. package/src/index.less +113 -0
  90. package/src/index.tsx +210 -0
  91. package/src/utils.ts +58 -0
  92. package/tsconfig.json +24 -0
@@ -0,0 +1,37 @@
1
+ @import './common.less';
2
+
3
+ .blackboard {
4
+ .footer {
5
+ color: #aaa;
6
+ }
7
+
8
+ ul {
9
+ padding-left: 0px;
10
+ }
11
+
12
+ li {
13
+ list-style: none;
14
+ }
15
+
16
+ .bottom-tip {
17
+ height: 30px;
18
+ }
19
+
20
+ .bottom-tip-item {
21
+ max-width: 500px;
22
+ color: #AAA;
23
+ text-overflow: ellipsis;
24
+ word-wrap: break-word;
25
+ }
26
+ }
27
+
28
+ .blackboard-filter {
29
+ margin: 10px 0;
30
+ }
31
+
32
+ .blackboard-main-content {
33
+ canvas {
34
+ width: 100%;
35
+ border: 1px solid @heavy-border-color;
36
+ }
37
+ }
@@ -0,0 +1,293 @@
1
+ 'use client';
2
+ import * as PIXI from 'pixi.js';
3
+ import { ReactElement, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Checkbox } from 'antd';
5
+ import type { CheckboxProps } from 'antd';
6
+ import { Rect } from '../../../midscene/dist/types';
7
+ import { highlightColorForType, colorForName } from './color';
8
+ import './blackboard.less';
9
+ import { useBlackboardPreference, useInsightDump } from './store';
10
+
11
+ const itemFillAlpha = 0.3;
12
+ const bgOnAlpha = 0.8;
13
+ const bgOffAlpha = 0.3;
14
+ const noop = () => {
15
+ // noop
16
+ };
17
+
18
+ const BlackBoard = (): JSX.Element => {
19
+ const dump = useInsightDump((store) => store.data);
20
+ const setHighlightSectionNames = useInsightDump((store) => store.setHighlightSectionNames);
21
+ const setHighlightElements = useInsightDump((store) => store.setHighlightElements);
22
+ const highlightSectionNames = useInsightDump((store) => store.highlightSectionNames);
23
+ const highlightElements = useInsightDump((store) => store.highlightElements);
24
+
25
+ const { context, matchedSection: sections, matchedElement: elements } = dump!;
26
+ const { size, screenshotBase64 } = context;
27
+
28
+ const screenWidth = size.width;
29
+ const screenHeight = size.height;
30
+
31
+ const domRef = useRef<HTMLDivElement>(null); // Should be HTMLDivElement not HTMLInputElement
32
+ const app = useMemo<PIXI.Application>(() => new PIXI.Application(), []);
33
+ const [appInitialed, setAppInitialed] = useState(false);
34
+
35
+ const itemMarkContainer = useMemo(() => new PIXI.Container(), []);
36
+ const textContainer = useMemo(() => new PIXI.Container(), []);
37
+
38
+ // key overlays
39
+ const pixiBgRef = useRef<PIXI.Sprite>();
40
+ const { bgVisible, setBgVisible, textsVisible, setTextsVisible } = useBlackboardPreference();
41
+
42
+ useEffect(() => {
43
+ Promise.resolve(
44
+ (async () => {
45
+ if (!domRef.current || !screenWidth) {
46
+ return;
47
+ }
48
+ await app.init({ width: screenWidth, height: screenHeight, background: 0xffffff });
49
+ const canvasEl = domRef.current;
50
+ domRef.current.appendChild(app.canvas); // Ensure app.view is appended
51
+ const { clientWidth } = domRef.current.parentElement!;
52
+ const targetHeight = window.innerHeight * 0.7;
53
+ const viewportRatio = clientWidth / targetHeight;
54
+ if (screenWidth / screenHeight <= viewportRatio) {
55
+ const ratio = targetHeight / screenHeight;
56
+ canvasEl.style.width = `${Math.floor(screenWidth * ratio)}px`;
57
+ canvasEl.style.height = `${Math.floor(screenHeight * ratio)}px`;
58
+ }
59
+
60
+ app.stage.addChild(itemMarkContainer);
61
+ app.stage.addChild(textContainer);
62
+
63
+ setAppInitialed(true);
64
+ })(),
65
+ );
66
+
67
+ // Clean up the PIXI application when the component unmounts
68
+ return () => {
69
+ console.log('will destroy');
70
+ app?.destroy(true, { children: true, texture: true });
71
+ };
72
+ }, [app, screenWidth, screenHeight]);
73
+
74
+ // draw all texts on PIXI app
75
+ useEffect(() => {
76
+ if (!appInitialed) {
77
+ return;
78
+ }
79
+
80
+ // draw the screenshot base64
81
+ const img = new Image();
82
+ img.src = screenshotBase64;
83
+ img.onload = () => {
84
+ const screenshotTexture = PIXI.Texture.from(img);
85
+ const screenshotSprite = new PIXI.Sprite(screenshotTexture);
86
+ screenshotSprite.x = 0;
87
+ screenshotSprite.y = 0;
88
+ screenshotSprite.width = screenWidth;
89
+ screenshotSprite.height = screenHeight;
90
+ app.stage.addChildAt(screenshotSprite, 0);
91
+ pixiBgRef.current = screenshotSprite;
92
+ screenshotSprite.alpha = bgVisible ? bgOnAlpha : bgOffAlpha;
93
+ };
94
+ }, [app.stage, appInitialed]);
95
+
96
+ const rectMarkForItem = (
97
+ rect: Rect,
98
+ name: string,
99
+ themeColor: string,
100
+ alpha: number,
101
+ onPointOver: () => void,
102
+ onPointerOut: () => void,
103
+ ) => {
104
+ const { left, top, width, height } = rect;
105
+ const graphics = new PIXI.Graphics();
106
+ graphics.beginFill(themeColor, alpha);
107
+ graphics.lineStyle(1, themeColor, 1);
108
+ graphics.drawRect(left, top, width, height);
109
+ graphics.endFill();
110
+ graphics.interactive = true;
111
+ graphics.on('pointerover', onPointOver);
112
+ graphics.on('pointerout', onPointerOut);
113
+
114
+ const nameFontSize = 18;
115
+ const texts = new PIXI.Text(name, {
116
+ fontSize: nameFontSize,
117
+ fill: 0x0,
118
+ });
119
+ texts.x = left;
120
+ texts.y = Math.max(top - (nameFontSize + 4), 0);
121
+ return [graphics, texts];
122
+ };
123
+
124
+ const { highlightSectionRects, highlightElementRects } = useMemo(() => {
125
+ const highlightSectionRects: Rect[] = [];
126
+ const highlightElementRects: Rect[] = [];
127
+
128
+ itemMarkContainer.removeChildren();
129
+ textContainer.removeChildren();
130
+
131
+ sections.forEach((section) => {
132
+ // draw a section overlay
133
+ const ifHighlight = highlightSectionNames.includes(section.name);
134
+ if (ifHighlight) {
135
+ highlightSectionRects.push(section.rect);
136
+ }
137
+ const [graphics, texts] = rectMarkForItem(
138
+ section.rect,
139
+ section.name,
140
+ ifHighlight ? highlightColorForType('section') : colorForName('section', section.name),
141
+ ifHighlight ? 1 : itemFillAlpha,
142
+ () => {
143
+ setHighlightSectionNames([section.name]);
144
+ },
145
+ () => {
146
+ setHighlightSectionNames([]);
147
+ },
148
+ );
149
+ itemMarkContainer.addChild(graphics);
150
+ textContainer.addChild(texts);
151
+ });
152
+
153
+ // some are tmp highlights, draw them separately
154
+ highlightElements.forEach((element) => {
155
+ const { rect } = element;
156
+ highlightElementRects.push(rect);
157
+ if (elements.includes(element)) {
158
+ return;
159
+ }
160
+ const [graphics, texts] = rectMarkForItem(rect, '', highlightColorForType('element'), 1, noop, noop);
161
+ itemMarkContainer.addChild(graphics);
162
+ textContainer.addChild(texts);
163
+ });
164
+
165
+ // element mark
166
+ elements.forEach((element) => {
167
+ const { rect, content } = element;
168
+ const ifHighlight = highlightElements.includes(element);
169
+ const [graphics, texts] = rectMarkForItem(
170
+ rect,
171
+ content,
172
+ ifHighlight ? highlightColorForType('element') : colorForName('element', content),
173
+ ifHighlight ? 1 : itemFillAlpha,
174
+ () => {
175
+ setHighlightElements([element]);
176
+ },
177
+ () => {
178
+ setHighlightElements([]);
179
+ },
180
+ );
181
+ itemMarkContainer.addChild(graphics);
182
+ textContainer.addChild(texts);
183
+ });
184
+
185
+ sections.forEach((section) => {
186
+ const { content } = section;
187
+ const ifHighlight = highlightSectionNames.includes(section.name);
188
+
189
+ const sectionTheme = ifHighlight ? '#FFFFFF' : colorForName('section', section.name);
190
+
191
+ content.forEach((text) => {
192
+ const { content, rect } = text;
193
+ const { left, top } = rect;
194
+ const style = new PIXI.TextStyle({
195
+ wordWrap: true,
196
+ wordWrapWidth: rect.width,
197
+ fontSize: 18,
198
+ fill: sectionTheme,
199
+ });
200
+ const textElement = new PIXI.Text(content, style);
201
+ textElement.x = left;
202
+ textElement.y = top;
203
+ textContainer.addChild(textElement);
204
+
205
+ const textBorder = new PIXI.Graphics();
206
+ textBorder.beginFill(0xaaaaaa, 0.2);
207
+ textBorder.lineStyle(1, 0x0, 1);
208
+ textBorder.drawRect(left, top, rect.width, rect.height);
209
+ textBorder.endFill();
210
+ textContainer.addChild(textBorder);
211
+ });
212
+ });
213
+ textContainer.visible = textsVisible;
214
+ return {
215
+ highlightSectionRects,
216
+ highlightElementRects,
217
+ };
218
+ }, [
219
+ app,
220
+ appInitialed,
221
+ sections,
222
+ highlightSectionNames,
223
+ highlightElements,
224
+ // bgVisible,
225
+ // textsVisible,
226
+ ]);
227
+
228
+ const onSetBg: CheckboxProps['onChange'] = (e) => {
229
+ setBgVisible(e.target.checked);
230
+ if (pixiBgRef.current) {
231
+ pixiBgRef.current.alpha = e.target.checked ? bgOnAlpha : bgOffAlpha;
232
+ }
233
+ };
234
+
235
+ const onSetTextsVisible: CheckboxProps['onChange'] = (e) => {
236
+ setTextsVisible(e.target.checked);
237
+ textContainer.visible = e.target.checked;
238
+ };
239
+
240
+ let bottomTipA: ReactElement | null = null;
241
+ if (highlightElementRects.length === 1) {
242
+ bottomTipA = (
243
+ <div className="bottom-tip">
244
+ <div className="bottom-tip-item">Element: {JSON.stringify(highlightElementRects[0])}</div>
245
+ </div>
246
+ );
247
+ } else if (highlightElementRects.length > 1) {
248
+ bottomTipA = (
249
+ <div className="bottom-tip">
250
+ <div className="bottom-tip-item">Element: {JSON.stringify(highlightElementRects)}</div>
251
+ </div>
252
+ );
253
+ }
254
+
255
+ let bottomTipB: ReactElement | null = null;
256
+ if (highlightSectionRects.length === 1) {
257
+ bottomTipB = (
258
+ <div className="bottom-tip">
259
+ <div className="bottom-tip-item">Section: {JSON.stringify(highlightSectionRects[0])}</div>
260
+ </div>
261
+ );
262
+ } else if (highlightSectionRects.length > 1) {
263
+ bottomTipB = (
264
+ <div className="bottom-tip">
265
+ <div className="bottom-tip-item">Sections: {JSON.stringify(highlightSectionRects)}</div>
266
+ </div>
267
+ );
268
+ }
269
+
270
+ return (
271
+ <div className="blackboard">
272
+ <div className="blackboard-main-content" style={{ width: '100%' }} ref={domRef} />
273
+ <div className="blackboard-filter">
274
+ <div className="overlay-control">
275
+ <Checkbox checked={bgVisible} onChange={onSetBg}>
276
+ Screenshot
277
+ </Checkbox>
278
+ <Checkbox checked={textsVisible} onChange={onSetTextsVisible}>
279
+ Text Mark
280
+ </Checkbox>
281
+ </div>
282
+ </div>
283
+ <div className="bottom-tip">
284
+ {bottomTipA}
285
+ {bottomTipB}
286
+ </div>
287
+
288
+ {/* {footer} */}
289
+ </div>
290
+ );
291
+ };
292
+
293
+ export default BlackBoard;
@@ -0,0 +1,34 @@
1
+ // https://coolors.co/palettes/popular/#01204e
2
+ const sectionColor = ['#028391'];
3
+ const elementColor = ['#fb6107'];
4
+ const highlightColorForSection = '#01204E';
5
+ const highlightColorForElement = '#F56824'; // @main-orange
6
+
7
+ function djb2Hash(str?: string): number {
8
+ if (!str) {
9
+ console.warn('djb2Hash: empty string');
10
+ str = 'unnamed';
11
+ }
12
+ let hash = 5381;
13
+ for (let i = 0; i < str.length; i++) {
14
+ hash = (hash << 5) + hash + str.charCodeAt(i); // hash * 33 + c
15
+ }
16
+ return hash >>> 0; // Convert to unsigned 32
17
+ }
18
+
19
+ export function colorForName(type: 'section' | 'element', name: string): string {
20
+ const hashNumber = djb2Hash(name);
21
+ if (type === 'section') {
22
+ return sectionColor[hashNumber % sectionColor.length];
23
+ } else {
24
+ return elementColor[hashNumber % elementColor.length];
25
+ }
26
+ }
27
+
28
+ export function highlightColorForType(type: 'section' | 'element'): string {
29
+ if (type === 'section') {
30
+ return highlightColorForSection;
31
+ } else {
32
+ return highlightColorForElement;
33
+ }
34
+ }
@@ -0,0 +1,21 @@
1
+ @main-blue: #06B1AB;
2
+ @sub-blue: #A3D6D2;
3
+
4
+ @main-orange: #F9483E;
5
+
6
+ @side-bg: #ECECEC;
7
+ @title-bg: #DDDDDD;
8
+ @border-color: #CCCCCC;
9
+ @heavy-border-color: #888;
10
+
11
+ @selected-bg: @main-blue;
12
+ @selected-text: #ffffff;
13
+ @hover-bg: @sub-blue;
14
+ @hover-text: #333;
15
+
16
+
17
+ @weak-text: #777;
18
+ @weak-bg: #F3F3F3;
19
+
20
+ @side-horizontal-padding: 10px;
21
+ @side-vertical-spacing: 10px;
@@ -0,0 +1,47 @@
1
+ @import './common.less';
2
+
3
+ @layout-space: 10px;
4
+
5
+ .detail-panel {
6
+ display: flex;
7
+ flex-direction: column;
8
+ height: 100%;
9
+ padding: @layout-space;
10
+ background: #FFF;
11
+
12
+ .ant-segmented{
13
+ padding: 0;
14
+ }
15
+
16
+ .view-switcher{
17
+ margin-bottom: @layout-space;
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ .detail-content{
22
+ box-sizing: border-box;
23
+ justify-content: center;
24
+ flex-direction: column;
25
+ display: flex;
26
+ flex-grow: 1;
27
+ }
28
+
29
+ .blackboard {
30
+ margin: 0 auto;
31
+ }
32
+
33
+ .screenshot-item {
34
+ margin-bottom: @layout-space;
35
+
36
+ .screenshot-item-title {
37
+ // font-weight: bold;
38
+ margin-bottom: 5px;
39
+ }
40
+
41
+ img {
42
+ border: 1px solid @heavy-border-color;
43
+ max-width: 100%;
44
+ }
45
+ }
46
+
47
+ }
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+ import './detail-panel.less';
3
+ import { Segmented, ConfigProvider } from 'antd';
4
+ import { CameraOutlined, FileTextOutlined, ScheduleOutlined } from '@ant-design/icons';
5
+ import { useEffect, useState } from 'react';
6
+ import BlackBoard from './blackboard';
7
+ import { useExecutionDump, useInsightDump } from '@/component/store';
8
+ import { timeStr, filterBase64Value } from '@/utils';
9
+
10
+ const ScreenshotItem = (props: { time: string; img: string }) => {
11
+ return (
12
+ <div className="screenshot-item">
13
+ <div className="screenshot-item-title">{props.time}</div>
14
+ <div>
15
+ <img src={props.img} />
16
+ </div>
17
+ </div>
18
+ );
19
+ };
20
+
21
+ const VIEW_TYPE_SCREENSHOT = 'screenshot';
22
+ const VIEW_TYPE_JSON = 'json';
23
+ const VIEW_TYPE_BLACKBOARD = 'blackboard';
24
+
25
+ const DetailPanel = (): JSX.Element => {
26
+ const dumpId = useInsightDump((store) => store._loadId);
27
+ const blackboardViewAvailable = Boolean(dumpId);
28
+ const activeTask = useExecutionDump((store) => store.activeTask);
29
+ const [preferredViewType, setViewType] = useState(dumpId ? VIEW_TYPE_BLACKBOARD : VIEW_TYPE_SCREENSHOT);
30
+
31
+ const viewType =
32
+ preferredViewType === VIEW_TYPE_BLACKBOARD && !dumpId ? VIEW_TYPE_SCREENSHOT : preferredViewType;
33
+
34
+ let content;
35
+ if (!activeTask) {
36
+ content = <div>please select a task</div>;
37
+ } else if (viewType === VIEW_TYPE_JSON) {
38
+ content = (
39
+ <div className="json-content">{filterBase64Value(JSON.stringify(activeTask, undefined, 2))}</div>
40
+ );
41
+ } else if (viewType === VIEW_TYPE_BLACKBOARD) {
42
+ if (dumpId) {
43
+ content = <BlackBoard key={`${dumpId}`} />;
44
+ } else {
45
+ content = <div>invalid view</div>;
46
+ }
47
+ } else if (viewType === VIEW_TYPE_SCREENSHOT) {
48
+ if (activeTask.recorder?.length) {
49
+ content = (
50
+ <div>
51
+ {activeTask.recorder
52
+ .filter((item) => item.screenshot)
53
+ .map((item, index) => {
54
+ const fullTime = timeStr(item.ts);
55
+ const str = item.timing ? `${fullTime} / ${item.timing}` : fullTime;
56
+ return <ScreenshotItem key={index} time={str} img={item.screenshot!} />;
57
+ })}
58
+ </div>
59
+ );
60
+ } else {
61
+ content = <div>no screenshot</div>; // TODO: pretty error message
62
+ }
63
+ }
64
+
65
+ useEffect(() => {
66
+ // hit `Tab` to toggle viewType
67
+ const handleKeyDown = (e: KeyboardEvent) => {
68
+ if (e.key === 'Tab') {
69
+ if (viewType === VIEW_TYPE_BLACKBOARD) {
70
+ setViewType(VIEW_TYPE_SCREENSHOT);
71
+ } else if (viewType === VIEW_TYPE_SCREENSHOT) {
72
+ setViewType(VIEW_TYPE_JSON);
73
+ } else {
74
+ setViewType(blackboardViewAvailable ? VIEW_TYPE_BLACKBOARD : VIEW_TYPE_SCREENSHOT);
75
+ }
76
+ e.preventDefault();
77
+ }
78
+ };
79
+
80
+ document.addEventListener('keydown', handleKeyDown);
81
+
82
+ return () => {
83
+ document.removeEventListener('keydown', handleKeyDown);
84
+ };
85
+ });
86
+
87
+ const options = [
88
+ { label: 'Screenshots', value: VIEW_TYPE_SCREENSHOT, icon: <CameraOutlined /> },
89
+ { label: 'JSON View', value: VIEW_TYPE_JSON, icon: <FileTextOutlined /> },
90
+ ];
91
+ if (blackboardViewAvailable) {
92
+ options.unshift({ label: 'Visualization', value: VIEW_TYPE_BLACKBOARD, icon: <ScheduleOutlined /> });
93
+ }
94
+ return (
95
+ <div className="detail-panel">
96
+ <div className="view-switcher">
97
+ <ConfigProvider
98
+ theme={{
99
+ components: {
100
+ Segmented: {
101
+ itemSelectedBg: '#D7D7D7',
102
+ itemSelectedColor: '#000000',
103
+ // itemHoverColor: '#ffffff',
104
+ // itemHoverBg: '#A3D6D2', // @sub-blue
105
+ /* 这里是你的组件 token */
106
+ },
107
+ },
108
+ }}
109
+ >
110
+ <Segmented
111
+ options={options}
112
+ value={viewType}
113
+ onChange={(value: any) => {
114
+ setViewType(value);
115
+ }}
116
+ />
117
+ </ConfigProvider>
118
+ </div>
119
+ <div className="detail-content">{content}</div>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ export default DetailPanel;
@@ -0,0 +1,131 @@
1
+ @import './common.less';
2
+
3
+ @subSpacing: 5px;
4
+ @lightTextColor: #777;
5
+
6
+ .detail-side {
7
+ h2 {
8
+ padding-top: 0;
9
+ margin-top: 0;
10
+ margin-bottom: 4px;
11
+ }
12
+
13
+ .ant-tag{
14
+ margin-top: 2px;
15
+ }
16
+
17
+ .meta-kv {
18
+ padding: @side-vertical-spacing @side-horizontal-padding calc(@side-vertical-spacing + 4px);
19
+ .meta {
20
+ box-sizing: border-box;
21
+ padding: 2px 0;
22
+ width: 100%;
23
+ display: flex;
24
+ flex-direction: row;
25
+ line-height: 1.5;
26
+
27
+ .meta-key {
28
+ // font-weight: bold;
29
+ width:100px;
30
+ text-align: right;
31
+ padding-right: 10px;
32
+ // color: #999;
33
+ }
34
+
35
+ .meta-value {
36
+ flex: 1;
37
+ }
38
+ }
39
+ }
40
+
41
+ .item-list {
42
+ padding: @side-vertical-spacing @side-horizontal-padding;
43
+
44
+ cursor:default;
45
+ margin-bottom: 10px;
46
+
47
+ .item {
48
+ padding: 16px @side-horizontal-padding @side-horizontal-padding;
49
+ transition: .1s;
50
+ border: 1px solid #DDD;
51
+ border-radius: @subSpacing;
52
+ margin-bottom: @side-vertical-spacing;
53
+ position: relative;
54
+
55
+ &.item-lite {
56
+ border: none;
57
+ padding: 0;
58
+ }
59
+ }
60
+
61
+ .item-highlight {
62
+ color: #FFF;
63
+ .subtitle {
64
+ color: #CCC;
65
+ }
66
+ }
67
+
68
+ .item-extra {
69
+ position: absolute;
70
+ right: @side-horizontal-padding;
71
+ top: @side-horizontal-padding;
72
+ color: @lightTextColor;
73
+ }
74
+
75
+ .title-right-padding {
76
+ padding-right: 15px;
77
+ }
78
+
79
+ .title {
80
+ font-size: 18px;
81
+ font-weight: bold;
82
+ margin-bottom: @subSpacing;
83
+
84
+ .title-tag {
85
+ display: inline-block;
86
+ margin-left: 6px;
87
+ color: @lightTextColor;
88
+ font-size: 14px;
89
+ line-height: 18px;
90
+ }
91
+ }
92
+
93
+ .subtitle {
94
+ font-weight: normal;
95
+ font-size: 14px;
96
+ color: #777;
97
+ }
98
+
99
+ .description {
100
+ margin-top: @subSpacing;
101
+ }
102
+
103
+ .description-content {
104
+ font-size: 14px;
105
+ margin-top: @side-horizontal-padding;
106
+ white-space: break-spaces;
107
+ margin: 0;
108
+ }
109
+
110
+ .element-button:hover {
111
+ // font-weight: bold;
112
+ color: #fff;
113
+ background: @main-orange;
114
+ }
115
+
116
+ .section-button:hover {
117
+ color: #fff;
118
+ background: #01204E;
119
+ }
120
+ }
121
+
122
+ .context {
123
+ pre {
124
+ text-wrap: balance;
125
+ }
126
+ }
127
+
128
+ .item-list-space-up {
129
+ margin-top: @side-vertical-spacing;
130
+ }
131
+ }