@forge-kit/plugin-source-map-prase 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.
package/dist/icon.svg ADDED
@@ -0,0 +1,3 @@
1
+ <svg width="204" height="204" viewBox="0 0 204 204" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M178.275 203.725H25.475C11.4 203.725 0 192.325 0 178.25V25.475C0 11.4 11.4 0 25.475 0H178.275C192.35 0 203.75 11.4 203.75 25.475V178.275C203.725 192.35 192.35 203.725 178.275 203.725ZM191.15 24.3C191.15 17.85 185.45 12.6 178.4 12.6H25.35C18.3 12.6 12.6 17.85 12.6 24.3V38.325H191.15V24.3ZM191.15 50.95H12.6V178.175C12.6 185.35 18.3 191.15 25.35 191.15H178.4C185.45 191.15 191.15 185.35 191.15 178.175V50.95ZM149.275 154.4C147.125 156.525 143.675 156.525 141.525 154.4C140.5 153.4 139.925 152.025 139.925 150.575C139.925 149.15 140.5 147.775 141.525 146.75L169.725 123.725L141.525 100.7C140.5 99.7 139.925 98.325 139.925 96.875C139.925 95.425 140.5 94.05 141.525 93.05C143.675 90.95 147.15 90.95 149.275 93.05L181.825 119.625C182.975 120.75 183.45 122.225 183.375 123.7C183.45 125.175 182.975 126.675 181.825 127.775L149.275 154.4ZM94.125 170.7C92.625 173.25 90.225 173.7 86.7 172.65C83.825 171.8 83.2 167.875 84.725 165.3L111.9 73.925C111.9 73.925 116.725 70.475 119.3 71.975C120.55 72.675 121.45 73.85 121.825 75.225C122.2 76.6 122 78.075 121.3 79.3L94.125 170.7ZM62.225 154.4C60.075 156.525 56.625 156.525 54.475 154.4L21.925 127.85C20.775 126.725 20.3 125.225 20.375 123.775C20.3 122.3 20.775 120.8 21.925 119.7L54.475 93.125C56.625 91.025 60.075 91.025 62.225 93.125C63.25 94.125 63.85 95.5 63.85 96.95C63.85 98.4 63.275 99.775 62.225 100.775L34.025 123.8L62.225 146.825C63.25 147.825 63.85 149.2 63.85 150.65C63.85 152.1 63.25 153.4 62.225 154.4Z" fill=""/>
3
+ </svg>
package/dist/vite.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@forge-kit/plugin-source-map-prase",
3
+ "entry": "./dist/app.js",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "files": [
8
+ "dist",
9
+ "src"
10
+ ],
11
+ "author": "yu.pan <panyupy@vip.qq.com>",
12
+ "license": "MIT",
13
+ "version": "0.0.1",
14
+ "type": "module",
15
+ "scripts": {
16
+ "dev": "vite",
17
+ "build": "tsc -b && vite build",
18
+ "lint": "eslint .",
19
+ "preview": "vite preview"
20
+ },
21
+ "dependencies": {
22
+ "@dnd-kit/core": "^6.3.1",
23
+ "@dnd-kit/sortable": "^10.0.0",
24
+ "@dnd-kit/utilities": "^3.2.2",
25
+ "@forge-kit/component": "workspace:*",
26
+ "@forge-kit/icons": "workspace:*",
27
+ "@forge-kit/types": "workspace:*",
28
+ "classnames": "^2.5.1",
29
+ "react": "^19.2.0",
30
+ "react-dom": "^19.2.0",
31
+ "react-syntax-highlighter": "^16.1.0",
32
+ "source-map": "^0.7.6",
33
+ "source-map-js": "^1.2.1"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^9.39.1",
37
+ "@forge-kit/vite-plugin-forge-kit": "workspace:*",
38
+ "@types/node": "^24.10.1",
39
+ "@types/react": "^19.2.5",
40
+ "@types/react-dom": "^19.2.3",
41
+ "@types/react-syntax-highlighter": "^15.5.13",
42
+ "@vitejs/plugin-react": "^5.1.1",
43
+ "eslint": "^9.39.1",
44
+ "eslint-plugin-react-hooks": "^7.0.1",
45
+ "eslint-plugin-react-refresh": "^0.4.24",
46
+ "globals": "^16.5.0",
47
+ "typescript": "~5.9.3",
48
+ "typescript-eslint": "^8.46.4",
49
+ "vite": "npm:rolldown-vite@7.2.5"
50
+ },
51
+ "overrides": {
52
+ "vite": "npm:rolldown-vite@7.2.5"
53
+ }
54
+ }
package/src/App.less ADDED
@@ -0,0 +1,101 @@
1
+ html, body, #root {
2
+ -webkit-app-region: drag;
3
+ background-color: transparent;
4
+ margin: 0;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .forge-kit-plugin-source-map-prase {
10
+ width: 100%;
11
+ height: 100%;
12
+ overflow: hidden;
13
+
14
+ &-content {
15
+ width: 100%;
16
+ height: 100%;
17
+ overflow: hidden;
18
+ min-height: 0;
19
+ }
20
+
21
+ .top-cards {
22
+ flex: 1;
23
+ min-height: 0;
24
+ }
25
+
26
+ .trace-panel-card,
27
+ .input-panel-card,
28
+ .result-panel-card {
29
+
30
+ -webkit-app-region: no-drag;
31
+ border: 1px solid color-mix(in srgb, var(--gray-6) 75%, transparent);
32
+ background:
33
+ linear-gradient(180deg, color-mix(in srgb, var(--gray-2) 88%, transparent), color-mix(in srgb, var(--gray-1) 86%, transparent));
34
+ box-shadow:
35
+ inset 0 1px 0 rgba(255, 255, 255, 0.04),
36
+ 0 10px 24px rgba(0, 0, 0, 0.16);
37
+ }
38
+
39
+ .input-panel-card,
40
+ .trace-panel-card {
41
+ flex: 1;
42
+ min-height: 0;
43
+ }
44
+
45
+ .trace-panel-card {
46
+ display: flex;
47
+ }
48
+
49
+ .result-panel-card {
50
+ flex: 1;
51
+ min-height: 0;
52
+ display: flex;
53
+ }
54
+
55
+ .panel-shell {
56
+ flex: 1;
57
+ min-height: 0;
58
+ }
59
+
60
+ .panel-title {
61
+ display: block;
62
+ padding: 0 0 var(--space-3);
63
+ margin-bottom: var(--space-2);
64
+ border-bottom: solid 1px var(--gray-5);
65
+ font-size: 12px;
66
+ letter-spacing: 0.1em;
67
+ color: var(--gray-11);
68
+ text-transform: uppercase;
69
+ }
70
+
71
+ .trace-panel-content,
72
+ .result-panel-content {
73
+ padding-top: var(--space-3);
74
+ flex: 1;
75
+ min-height: 0;
76
+ }
77
+
78
+ .trace-panel-content {
79
+ overflow: hidden;
80
+ }
81
+
82
+ .result-panel-content {
83
+ overflow: auto;
84
+ }
85
+
86
+ .trace-chain-root {
87
+ height: 100%;
88
+ }
89
+
90
+ .output-meta {
91
+ display: inline-block;
92
+ margin-bottom: var(--space-2);
93
+ color: var(--gray-11);
94
+ font-size: 12px;
95
+ border: 1px solid var(--gray-6);
96
+ border-radius: 999px;
97
+ padding: 2px 10px;
98
+ background: color-mix(in srgb, var(--gray-3) 75%, transparent);
99
+ -webkit-app-region: no-drag;
100
+ }
101
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,141 @@
1
+ import * as React from "react";
2
+ import {Box, Flex, Text, Theme} from "@forge-kit/component";
3
+ import {CodeHighlight} from "@/components/code-highlight";
4
+ import {MapInputPanel} from "@/components/map-input-panel";
5
+ import {PanelCard} from "@/components/panel-card";
6
+ import {TraceChain} from "@/components/trace-chain";
7
+ import {
8
+ SourceMapUtils,
9
+ type ChainMapSlot,
10
+ type SourceMapChainState,
11
+ type TraceOutputState,
12
+ } from "@/utils/source-map";
13
+ import '@forge-kit/component/style.css'
14
+ import './App.less'
15
+
16
+ interface AppProps {
17
+ themeProps?: React.ComponentProps<typeof Theme>
18
+ }
19
+
20
+ interface InputState {
21
+ entryLine: string
22
+ entryColumn: string
23
+ contextLineRadius: string
24
+ }
25
+
26
+ const initialChainState: SourceMapChainState = {
27
+ sourceMaps: {},
28
+ sourceMapFileNames: [],
29
+ chainMapSlots: [],
30
+ entryFileName: "",
31
+ }
32
+
33
+ const initialInputState: InputState = {
34
+ entryLine: "",
35
+ entryColumn: "",
36
+ contextLineRadius: String(SourceMapUtils.DEFAULT_CONTEXT_LINE_RADIUS),
37
+ }
38
+
39
+ export const App: React.FC<AppProps> = (props) => {
40
+ const {themeProps} = props
41
+ const [chainState, setChainState] = React.useState<SourceMapChainState>(initialChainState)
42
+ const [inputState, setInputState] = React.useState<InputState>(initialInputState)
43
+ const [outputState, setOutputState] = React.useState<TraceOutputState>(SourceMapUtils.createInitialOutputState())
44
+
45
+ const traceChainDisplayData = React.useMemo(() => {
46
+ return SourceMapUtils.mapTraceForDisplay(outputState.traceData)
47
+ }, [outputState.traceData])
48
+
49
+ const applyChainSlots =(slots: ChainMapSlot[]) => {
50
+ setChainState(SourceMapUtils.buildChainStateFromSlots(slots))
51
+ }
52
+
53
+ const setOutputMessage = (message: string) => {
54
+ setOutputState((prev) => SourceMapUtils.patchOutputMessage(prev, message))
55
+ }
56
+
57
+ const handleAppendChainFile = async (file: File) => {
58
+ const slotFromFile = await SourceMapUtils.createChainSlotFromFile(file)
59
+ applyChainSlots([...chainState.chainMapSlots, slotFromFile])
60
+ }
61
+
62
+ const handleRemoveChainSlot = (slotId: string) => {
63
+ applyChainSlots(SourceMapUtils.removeChainSlot(chainState.chainMapSlots, slotId))
64
+ }
65
+
66
+ const handleReorderChainSlots = (nextIds: string[]) => {
67
+ applyChainSlots(SourceMapUtils.reorderChainSlots(chainState.chainMapSlots, nextIds))
68
+ }
69
+
70
+ const handleResolveTrace = async () => {
71
+ const validated = SourceMapUtils.validateResolveInput({
72
+ entryFileName: chainState.entryFileName,
73
+ entryLine: inputState.entryLine,
74
+ entryColumn: inputState.entryColumn,
75
+ contextLineRadius: inputState.contextLineRadius,
76
+ mapCount: chainState.sourceMapFileNames.length,
77
+ })
78
+
79
+ if (!validated.ok) return setOutputMessage(validated.message)
80
+
81
+ const nextOutput = await SourceMapUtils.resolveTraceOutput({
82
+ entry: validated.entry,
83
+ contextLineRadius: validated.contextLineRadius,
84
+ sourceMaps: chainState.sourceMaps,
85
+ sourceMapFileNames: chainState.sourceMapFileNames,
86
+ })
87
+ setOutputState(nextOutput)
88
+ }
89
+
90
+ return (
91
+ <Theme className="forge-kit-plugin-source-map-prase" accentColor="gray" appearance="dark" grayColor="slate"
92
+ radius="full" panelBackground="translucent" {...themeProps}>
93
+ <Flex className="forge-kit-plugin-source-map-prase-content" direction="column" gap="3">
94
+ <Flex className="top-cards" gap="3" align="stretch">
95
+ <PanelCard title="TRACE CHAIN" cardClassName="trace-panel-card" contentClassName="trace-panel-content">
96
+ <TraceChain
97
+ slots={chainState.chainMapSlots.map((slot) => ({
98
+ id: slot.id,
99
+ mapFileName: slot.mapFileName,
100
+ error: slot.error,
101
+ }))}
102
+ data={traceChainDisplayData}
103
+ onReorder={handleReorderChainSlots}
104
+ onDelete={handleRemoveChainSlot}
105
+ className="trace-chain-root"
106
+ />
107
+ </PanelCard>
108
+
109
+ <MapInputPanel
110
+ chainDepth={chainState.chainMapSlots.length}
111
+ entryFileName={chainState.entryFileName}
112
+ inputConfig={inputState}
113
+ onInputConfigChange={setInputState}
114
+ onResolve={handleResolveTrace}
115
+ onAppendMapFile={handleAppendChainFile}
116
+ />
117
+ </Flex>
118
+
119
+ <PanelCard
120
+ title="TRACE OUTPUT"
121
+ cardClassName="result-panel-card"
122
+ contentClassName="result-panel-content"
123
+ contentProps={{direction: "column"}}
124
+ >
125
+ {outputState.resolvedSourceMeta && (
126
+ <Text className="output-meta">
127
+ {`目标位置: ${outputState.resolvedSourceMeta.sourceFile}:${outputState.resolvedSourceMeta.lineNumber}:${outputState.resolvedSourceMeta.columnNumber}`}
128
+ </Text>
129
+ )}
130
+ <Box>
131
+ <CodeHighlight
132
+ code={outputState.traceCode}
133
+ highlightLines={outputState.traceHighlightLines}
134
+ className="code-highlight-view"
135
+ />
136
+ </Box>
137
+ </PanelCard>
138
+ </Flex>
139
+ </Theme>
140
+ )
141
+ }
@@ -0,0 +1,41 @@
1
+ import * as React from "react";
2
+ import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'
3
+ import {oneDark} from 'react-syntax-highlighter/dist/esm/styles/prism'
4
+
5
+ interface CodeHighlightProps {
6
+ code?: string
7
+ className?: string
8
+ highlightLines?: number[]
9
+ }
10
+
11
+ export const CodeHighlight: React.FC<CodeHighlightProps> = (props) => {
12
+ const {code = "", className, highlightLines = []} = props
13
+
14
+ const baseLineStyle = {
15
+ backgroundColor: 'var(--color-background)',
16
+ display: 'block',
17
+ }
18
+
19
+ const highlightLineStyle = {
20
+ backgroundColor: 'color-mix(in srgb, var(--color-background) 55%, var(--gray-8) 45%)',
21
+ display: 'block',
22
+ }
23
+
24
+ const generateLineProps = (lineNum: number) => {
25
+ return highlightLines.includes(lineNum) ? {style: highlightLineStyle} : {style: baseLineStyle}
26
+ }
27
+
28
+ return (
29
+ <SyntaxHighlighter
30
+ wrapLines
31
+ showLineNumbers
32
+ style={oneDark}
33
+ language="javascript"
34
+ className={className}
35
+ lineProps={generateLineProps}
36
+ customStyle={{background: "var(--color-background)"}}
37
+ >
38
+ {code}
39
+ </SyntaxHighlighter>
40
+ )
41
+ }
@@ -0,0 +1,95 @@
1
+ .forge-kit-plugin-source-map-prase {
2
+ .input-panel-card {
3
+ flex: 1;
4
+ display: flex;
5
+
6
+ .input-panel-content {
7
+ flex: 1;
8
+ padding-top: var(--space-3);
9
+ overflow: auto;
10
+ gap: 10px;
11
+ }
12
+
13
+ .input-footer {
14
+ display: grid;
15
+ grid-template-rows: auto minmax(0, 1fr);
16
+ row-gap: 10px;
17
+ border-top: 1px solid var(--gray-5);
18
+ padding-top: 10px;
19
+ flex: 1;
20
+ }
21
+ }
22
+
23
+ .input-summary-grid {
24
+ display: grid;
25
+ grid-template-columns: repeat(2, minmax(0, 1fr));
26
+ gap: 8px;
27
+ }
28
+
29
+ .input-summary-item {
30
+ border: 1px solid var(--gray-5);
31
+ border-radius: var(--radius-3);
32
+ padding: 8px 10px;
33
+ background: var(--gray-2);
34
+ }
35
+
36
+ .input-summary-label {
37
+ display: block;
38
+ color: var(--gray-10);
39
+ font-size: 12px;
40
+ margin-bottom: 4px;
41
+ }
42
+
43
+ .input-summary-value {
44
+ display: block;
45
+ color: var(--gray-12);
46
+ font-size: 13px;
47
+ font-weight: 600;
48
+ word-break: break-all;
49
+ }
50
+
51
+ .input-hints {
52
+ border: 1px dashed var(--gray-6);
53
+ border-radius: var(--radius-3);
54
+ padding: 8px 10px;
55
+ background: color-mix(in srgb, var(--gray-2) 70%, transparent);
56
+ }
57
+
58
+ .input-hints-title {
59
+ display: block;
60
+ font-size: 12px;
61
+ font-weight: 600;
62
+ color: var(--gray-11);
63
+ margin-bottom: 6px;
64
+ }
65
+
66
+ .input-hints-item {
67
+ display: block;
68
+ font-size: 12px;
69
+ line-height: 1.6;
70
+ color: var(--gray-11);
71
+ }
72
+
73
+ .field-row-compact {
74
+ flex: 1;
75
+ }
76
+
77
+ .entry-actions {
78
+ display: flex;
79
+ flex: 1;
80
+ }
81
+
82
+ .entry-action-row {
83
+ width: auto;
84
+ }
85
+
86
+ .field-label {
87
+ color: var(--gray-11);
88
+ font-size: 12px;
89
+ }
90
+
91
+ .rt-TextFieldRoot {
92
+ border-radius: 6px;
93
+ }
94
+
95
+ }
@@ -0,0 +1,128 @@
1
+ import * as React from "react";
2
+ import {Box, Button, Flex, Text, TextField} from "@forge-kit/component";
3
+ import {PanelCard} from "@/components/panel-card";
4
+ import {noop} from "@/utils";
5
+ import "./index.less";
6
+
7
+ interface MapInputConfig {
8
+ entryLine: string
9
+ entryColumn: string
10
+ contextLineRadius: string
11
+ }
12
+
13
+ interface MapInputPanelProps {
14
+ chainDepth: number
15
+ entryFileName: string
16
+ inputConfig: MapInputConfig
17
+ onInputConfigChange: (next: MapInputConfig) => void
18
+ onResolve: () => void
19
+ onAppendMapFile: (file: File) => void
20
+ }
21
+
22
+ interface MapInputFieldProps {
23
+ title?: string
24
+ value?: string
25
+ min?: number
26
+ placeholder?: string
27
+ onChange?: (value: string) => void
28
+ }
29
+
30
+ const MapInputField: React.FC<MapInputFieldProps> = (props) => {
31
+ const {title, value, min, placeholder, onChange = noop} = props
32
+ return (
33
+ <Flex className="field-row-compact" direction="column" gap="1">
34
+ <Text className="field-label">{title}</Text>
35
+ <TextField.Root
36
+ type="number"
37
+ min={min}
38
+ value={value}
39
+ onChange={(event) => onChange(event.target.value)}
40
+ placeholder={placeholder}
41
+ />
42
+ </Flex>
43
+ )
44
+ }
45
+
46
+ export const MapInputPanel: React.FC<MapInputPanelProps> = (props) => {
47
+ const {
48
+ chainDepth,
49
+ entryFileName,
50
+ inputConfig,
51
+ onInputConfigChange,
52
+ onResolve,
53
+ onAppendMapFile,
54
+ } = props
55
+
56
+ const openFilePicker = () => {
57
+ const fileInput = document.createElement("input")
58
+ fileInput.type = "file"
59
+ fileInput.accept = ".map,application/json"
60
+ fileInput.onchange = () => {
61
+ const file = fileInput.files?.[0]
62
+ if (file) {
63
+ onAppendMapFile(file)
64
+ }
65
+ }
66
+ fileInput.click()
67
+ }
68
+
69
+ const handleInputConfigChange = (field: keyof MapInputConfig, value: string) => {
70
+ onInputConfigChange({...inputConfig, [field]: value})
71
+ }
72
+
73
+ return (
74
+ <PanelCard
75
+ title="MAP INPUT"
76
+ cardClassName="input-panel-card"
77
+ contentClassName="input-panel-content"
78
+ contentProps={{direction: "column", gap: "3"}}
79
+ >
80
+ <Box className="input-summary-grid">
81
+ <Box className="input-summary-item">
82
+ <Text className="input-summary-label">入口 Map</Text>
83
+ <Text className="input-summary-value">{entryFileName || "-"}</Text>
84
+ </Box>
85
+ <Box className="input-summary-item">
86
+ <Text className="input-summary-label">链路层数</Text>
87
+ <Text className="input-summary-value">{`${chainDepth} 层`}</Text>
88
+ </Box>
89
+ </Box>
90
+
91
+ <Box className="input-hints">
92
+ <Text className="input-hints-title">使用说明</Text>
93
+ <Text className="input-hints-item">1. 按解析链路顺序添加 map 文件。</Text>
94
+ <Text className="input-hints-item">2. 入口 Map 会自动取第一层的 output file。</Text>
95
+ <Text className="input-hints-item">3. TRACE CHAIN 支持拖动序号来切换 map 顺序。</Text>
96
+ </Box>
97
+
98
+ <Box className="input-footer">
99
+ <Flex gap="2" className="entry-row" align="end" wrap="wrap">
100
+ <MapInputField
101
+ title="入口行号"
102
+ min={1}
103
+ value={inputConfig.entryLine}
104
+ onChange={(value) => handleInputConfigChange("entryLine", value)}
105
+ />
106
+ <MapInputField
107
+ title="入口列号"
108
+ min={0}
109
+ value={inputConfig.entryColumn}
110
+ onChange={(value) => handleInputConfigChange("entryColumn", value)}
111
+ />
112
+ <MapInputField
113
+ title="上下文行数"
114
+ min={0}
115
+ value={inputConfig.contextLineRadius}
116
+ onChange={(value) => handleInputConfigChange("contextLineRadius", value)}
117
+ />
118
+ </Flex>
119
+ <Flex className="entry-actions" direction="column" justify="center" align="center" gap="2">
120
+ <Flex className="entry-action-row" gap="2" wrap="wrap" justify="center" align="center">
121
+ <Button size="2" radius="large" onClick={onResolve}>解析链路</Button>
122
+ <Button size="2" radius="large" variant="outline" onClick={openFilePicker}>添加 map 文件</Button>
123
+ </Flex>
124
+ </Flex>
125
+ </Box>
126
+ </PanelCard>
127
+ )
128
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import {Card, Flex, Text} from "@forge-kit/component";
3
+
4
+ interface PanelCardProps {
5
+ title: string
6
+ cardClassName: string
7
+ contentClassName: string
8
+ contentProps?: Omit<React.ComponentProps<typeof Flex>, "className" | "children">
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ export const PanelCard: React.FC<PanelCardProps> = (props) => {
13
+ const {title, cardClassName, contentClassName, contentProps, children} = props
14
+
15
+ return (
16
+ <Card className={cardClassName}>
17
+ <Flex className="panel-shell" direction="column">
18
+ <Text className="panel-title">{title}</Text>
19
+ <Flex className={contentClassName} {...contentProps}>
20
+ {children}
21
+ </Flex>
22
+ </Flex>
23
+ </Card>
24
+ )
25
+ }