@snack-kit/scripts 0.2.0 → 0.4.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/README.md +13 -0
- package/config/webpack.dev.config.js +5 -3
- package/config/webpack.shared.js +116 -5
- package/config/webpack.snack.config.js +5 -3
- package/package.json +2 -2
- package/template/dev/App.tsx +243 -14
- package/template/dev/index.scss +142 -0
package/README.md
CHANGED
|
@@ -142,6 +142,19 @@ module.exports = (config) => {
|
|
|
142
142
|
|
|
143
143
|
## Changelog
|
|
144
144
|
|
|
145
|
+
### 0.4.0
|
|
146
|
+
|
|
147
|
+
- 修复:调试窗口 topbar 工具区样式细节优化
|
|
148
|
+
- 修复:启动调试运行错误的问题
|
|
149
|
+
- 修复:修复 core 依赖版本错误的问题
|
|
150
|
+
|
|
151
|
+
### 0.3.0
|
|
152
|
+
|
|
153
|
+
- 新增:调试窗口 topbar 视口尺寸切换(PC / Tablet / Mobile / 自定义),偏好持久化到 localStorage
|
|
154
|
+
- 新增:调试窗口 topbar 内容区背景切换(默认 / 白色 / 深色 / 棋盘格透明检测)
|
|
155
|
+
- 新增:调试窗口 topbar 内容区缩放控制(50% / 75% / 100% / 125% / 150%)
|
|
156
|
+
- 新增:调试窗口 topbar 刷新模块按钮,强制重新 mount 当前模块
|
|
157
|
+
|
|
145
158
|
### 0.2.0
|
|
146
159
|
|
|
147
160
|
- 新增:`MIGRATION.md` 迁移指南,说明从 `@para-snack/*` / `@paraview/*` 迁移到 `@snack-kit/*` 的完整步骤
|
|
@@ -8,10 +8,10 @@ const path = require('path');
|
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
10
10
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
11
|
-
const webpack = require('webpack');
|
|
12
11
|
const { version } = require('../package.json');
|
|
13
12
|
const { injectIndexFile } = require('../utils/package');
|
|
14
|
-
const { createResolve, createModuleRules, createOptimization } = require('./webpack.shared');
|
|
13
|
+
const { createResolve, createModuleRules, createOptimization, resolveProjectWebpack, resolveLoader, createPolyfillPlugins } = require('./webpack.shared');
|
|
14
|
+
const webpack = resolveProjectWebpack();
|
|
15
15
|
|
|
16
16
|
const packageJson = JSON.parse(process.env.PROJECT_PACKAGE_JSON);
|
|
17
17
|
const snackConfig = JSON.parse(process.env.PROJECT_SNACK_CONFIG);
|
|
@@ -79,7 +79,8 @@ let conf = {
|
|
|
79
79
|
template: templatePkgPath || path.join(templatePath, 'index.html')
|
|
80
80
|
}),
|
|
81
81
|
new webpack.HotModuleReplacementPlugin(),
|
|
82
|
-
new ReactRefreshWebpackPlugin()
|
|
82
|
+
new ReactRefreshWebpackPlugin(),
|
|
83
|
+
...createPolyfillPlugins(webpack)
|
|
83
84
|
],
|
|
84
85
|
module: {
|
|
85
86
|
rules: createModuleRules({ withRefresh: true, withAsset: true })
|
|
@@ -87,6 +88,7 @@ let conf = {
|
|
|
87
88
|
resolve: createResolve({
|
|
88
89
|
template: path.join(__dirname, '../template')
|
|
89
90
|
}),
|
|
91
|
+
resolveLoader,
|
|
90
92
|
optimization: createOptimization(false, true),
|
|
91
93
|
devServer: {
|
|
92
94
|
open: true,
|
package/config/webpack.shared.js
CHANGED
|
@@ -6,11 +6,76 @@
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const TerserPlugin = require('terser-webpack-plugin');
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* 获取项目自身的 react/react-dom 路径。
|
|
11
|
+
* 优先使用宿主项目的 React(保持版本一致),若项目未安装则 fallback 到 snack-scripts 内置的 React 19。
|
|
12
|
+
*
|
|
13
|
+
* 同时始终提供 react-dom/client 的别名:
|
|
14
|
+
* - React 18+ → 使用项目自身的 react-dom/client
|
|
15
|
+
* - React 17 → 使用 scripts 内置 React 19 的 react-dom/client(仅供 webpack 静态解析使用,
|
|
16
|
+
* 运行时因 major < 18 不会执行该分支,不影响实际行为)
|
|
17
|
+
* 这样可让 @snack-kit/core 中的动态 require('react-dom/client') 在所有 React 版本下都能
|
|
18
|
+
* 被 webpack 静态解析,避免产生 Critical dependency 警告。
|
|
19
|
+
*/
|
|
20
|
+
function resolveProjectReact() {
|
|
21
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
22
|
+
const scriptsReactDomClient = path.resolve(__dirname, '../node_modules/react-dom/client.js');
|
|
23
|
+
const scriptsReact = path.resolve(__dirname, '../node_modules/react');
|
|
24
|
+
const scriptsReactDom = path.resolve(__dirname, '../node_modules/react-dom');
|
|
25
|
+
if (!projectPath) {
|
|
26
|
+
return { react: scriptsReact, reactDom: scriptsReactDom, reactDomClient: scriptsReactDomClient };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const reactPkgPath = path.join(projectPath, 'node_modules/react/package.json');
|
|
30
|
+
const reactPkg = require(reactPkgPath);
|
|
31
|
+
const majorVersion = parseInt(reactPkg.version.split('.')[0], 10);
|
|
32
|
+
const projectReactDom = path.join(projectPath, 'node_modules/react-dom');
|
|
33
|
+
// React 18+ 项目:使用项目自身的 react-dom/client
|
|
34
|
+
// React 17 项目:使用 scripts 内置的 react-dom/client(运行时不会执行该分支)
|
|
35
|
+
const reactDomClient = majorVersion >= 18
|
|
36
|
+
? path.join(projectReactDom, 'client.js')
|
|
37
|
+
: scriptsReactDomClient;
|
|
38
|
+
return {
|
|
39
|
+
react: path.join(projectPath, 'node_modules/react'),
|
|
40
|
+
reactDom: projectReactDom,
|
|
41
|
+
reactDomClient,
|
|
42
|
+
};
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return { react: scriptsReact, reactDom: scriptsReactDom, reactDomClient: scriptsReactDomClient };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* loader 解析配置:确保 webpack 能从 snack-scripts 自身 node_modules 找到 swc-loader 等 loader。
|
|
50
|
+
* 当 @snack-kit/scripts 以软链接方式安装时,其依赖不会被 npm hoist,需显式指定 loader 搜索路径。
|
|
51
|
+
*/
|
|
52
|
+
const resolveLoader = {
|
|
53
|
+
modules: [
|
|
54
|
+
path.resolve(__dirname, '../node_modules'),
|
|
55
|
+
'node_modules'
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 尝试解析 process/browser polyfill 路径。
|
|
61
|
+
* 优先从宿主项目查找,其次从 snack-scripts 自身查找,找不到返回 null。
|
|
62
|
+
*/
|
|
63
|
+
function resolveProcessBrowser() {
|
|
64
|
+
const searchPaths = [process.env.PROJECT_PATH, path.resolve(__dirname, '../')].filter(Boolean);
|
|
65
|
+
try {
|
|
66
|
+
return require.resolve('process/browser', { paths: searchPaths });
|
|
67
|
+
} catch (_) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
9
72
|
/**
|
|
10
73
|
* 获取公共 resolve 配置
|
|
11
74
|
* @param {object} extraAlias 额外的别名
|
|
12
75
|
*/
|
|
13
76
|
function createResolve(extraAlias = {}) {
|
|
77
|
+
const { react, reactDom, reactDomClient } = resolveProjectReact();
|
|
78
|
+
const processBrowserPath = resolveProcessBrowser();
|
|
14
79
|
return {
|
|
15
80
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
16
81
|
// 优先从 snack-scripts 自身 node_modules 解析(保证 template 文件能找到 @snack-kit/* 等依赖)
|
|
@@ -24,10 +89,26 @@ function createResolve(extraAlias = {}) {
|
|
|
24
89
|
package: path.join(process.env.PROJECT_PATH, 'src/package'),
|
|
25
90
|
assets: path.join(process.env.PROJECT_PATH, 'src/assets'),
|
|
26
91
|
// @snack-kit/core 的 package.json module 字段路径有误,直接指向 cjs
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
92
|
+
// 优先使用宿主项目安装的 @snack-kit/core,fallback 到 scripts 自身的(兼容 file: 软链)
|
|
93
|
+
'@snack-kit/core': (() => {
|
|
94
|
+
const projectCorePath = process.env.PROJECT_PATH
|
|
95
|
+
? path.join(process.env.PROJECT_PATH, 'node_modules/@snack-kit/core/dist/cjs/index.js')
|
|
96
|
+
: null;
|
|
97
|
+
const scriptsCorePath = path.resolve(__dirname, '../node_modules/@snack-kit/core/dist/cjs/index.js');
|
|
98
|
+
if (projectCorePath) {
|
|
99
|
+
try { require.resolve(projectCorePath); return projectCorePath; } catch (_) {}
|
|
100
|
+
}
|
|
101
|
+
return scriptsCorePath;
|
|
102
|
+
})(),
|
|
103
|
+
// 优先使用宿主项目自己的 react,保证 React 版本与项目一致(兼容 React 17/18/19)
|
|
104
|
+
// react/react-dom 使用目录别名(不加 $),使子路径(如 react/jsx-runtime)也走项目自身版本
|
|
105
|
+
// react-dom/client 比 react-dom 更长,enhanced-resolve 会优先匹配更具体的 key
|
|
106
|
+
'react-dom/client': reactDomClient,
|
|
107
|
+
'react': react,
|
|
108
|
+
'react-dom': reactDom,
|
|
109
|
+
// axios >= 1.x 使用子路径 import 'process/browser',webpack 5 的 fallback 不处理子路径,
|
|
110
|
+
// 需显式 alias 保证可解析(@snack-kit/lib >= 0.7.0 起依赖新版 axios)
|
|
111
|
+
...(processBrowserPath && { 'process/browser': processBrowserPath }),
|
|
31
112
|
...extraAlias
|
|
32
113
|
}
|
|
33
114
|
};
|
|
@@ -137,6 +218,33 @@ const filesystemCache = {
|
|
|
137
218
|
allowCollectingMemory: true
|
|
138
219
|
};
|
|
139
220
|
|
|
221
|
+
/**
|
|
222
|
+
* 解析 webpack 模块,优先使用宿主项目的 webpack 实例。
|
|
223
|
+
* 保证 webpack config 文件与 webpack CLI 进程使用同一个 webpack 实例,
|
|
224
|
+
* 避免多实例导致的 instanceof 检查失败(Critical dependency / Compilation 错误)。
|
|
225
|
+
*/
|
|
226
|
+
function resolveProjectWebpack() {
|
|
227
|
+
const searchPaths = [process.env.PROJECT_PATH, __dirname].filter(Boolean);
|
|
228
|
+
try {
|
|
229
|
+
return require(require.resolve('webpack', { paths: searchPaths }));
|
|
230
|
+
} catch (_) {
|
|
231
|
+
return require('webpack');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 创建 process/Buffer polyfill 的 ProvidePlugin 配置(可选)。
|
|
237
|
+
* 若 process/browser 可解析,则自动注入,使项目代码中裸用的 `process` 全局变量可正常工作。
|
|
238
|
+
* 返回 plugin 实例数组,为空数组则表示无需注入。
|
|
239
|
+
*/
|
|
240
|
+
function createPolyfillPlugins(webpack) {
|
|
241
|
+
const processBrowserPath = resolveProcessBrowser();
|
|
242
|
+
if (!processBrowserPath) return [];
|
|
243
|
+
return [
|
|
244
|
+
new webpack.ProvidePlugin({ process: 'process/browser' })
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
|
|
140
248
|
module.exports = {
|
|
141
249
|
createResolve,
|
|
142
250
|
createSwcRule,
|
|
@@ -144,5 +252,8 @@ module.exports = {
|
|
|
144
252
|
createOptimization,
|
|
145
253
|
filesystemCache,
|
|
146
254
|
styleRule,
|
|
147
|
-
assetRule
|
|
255
|
+
assetRule,
|
|
256
|
+
resolveProjectWebpack,
|
|
257
|
+
resolveLoader,
|
|
258
|
+
createPolyfillPlugins
|
|
148
259
|
};
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const minimist = require('minimist');
|
|
10
|
-
const webpack = require('webpack');
|
|
11
10
|
const { SnackPlugin, createExternals } = require('../utils/package');
|
|
12
|
-
const { createResolve, createModuleRules, createOptimization, filesystemCache } = require('./webpack.shared');
|
|
11
|
+
const { createResolve, createModuleRules, createOptimization, filesystemCache, resolveProjectWebpack, resolveLoader, createPolyfillPlugins } = require('./webpack.shared');
|
|
12
|
+
const webpack = resolveProjectWebpack();
|
|
13
13
|
|
|
14
14
|
const argv = minimist(process.argv.slice(2), { default: { mode: 'development' } });
|
|
15
15
|
const isDevelopment = argv.mode === 'development';
|
|
@@ -50,7 +50,8 @@ let conf = {
|
|
|
50
50
|
banner: 'if(typeof window!=="undefined"&&window.snackdefine){var define=window.snackdefine;}',
|
|
51
51
|
raw: true
|
|
52
52
|
}),
|
|
53
|
-
new SnackPlugin()
|
|
53
|
+
new SnackPlugin(),
|
|
54
|
+
...createPolyfillPlugins(webpack)
|
|
54
55
|
],
|
|
55
56
|
externals: {
|
|
56
57
|
react: 'react',
|
|
@@ -63,6 +64,7 @@ let conf = {
|
|
|
63
64
|
rules: createModuleRules({ withAsset: true })
|
|
64
65
|
},
|
|
65
66
|
resolve: createResolve(),
|
|
67
|
+
resolveLoader,
|
|
66
68
|
optimization: createOptimization(isProduction)
|
|
67
69
|
};
|
|
68
70
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snack-kit/scripts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "snack-cli package scripts Powered by Para FED",
|
|
5
5
|
"bin": {
|
|
6
6
|
"snack-scripts": "./bin/main.js"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
|
|
36
36
|
"react": "^19.0.0",
|
|
37
37
|
"react-dom": "^19.0.0",
|
|
38
|
-
"@snack-kit/core": "
|
|
38
|
+
"@snack-kit/core": "^0.3.0",
|
|
39
39
|
"@snack-kit/lib": "^0.6.0",
|
|
40
40
|
"@swc/core": "^1.2.100",
|
|
41
41
|
"babel-loader": "^9.1.3",
|
package/template/dev/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
|
2
|
+
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
|
3
3
|
import logo from 'template/logo.svg';
|
|
4
4
|
import Core from '@snack-kit/core';
|
|
5
5
|
|
|
@@ -24,8 +24,6 @@ const resolveRouter = (hash: string) => {
|
|
|
24
24
|
return name ? { name, setting } : null;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
27
|
// ─── Theme Hook ───────────────────────────────────────────────────────────────
|
|
30
28
|
|
|
31
29
|
const THEME_KEY = 'snack-dev-theme';
|
|
@@ -44,6 +42,78 @@ const useTheme = () => {
|
|
|
44
42
|
return { dark, toggle };
|
|
45
43
|
};
|
|
46
44
|
|
|
45
|
+
// ─── Viewport Hook ────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
type ViewportPreset = 'pc' | 'tablet' | 'mobile' | 'custom';
|
|
48
|
+
|
|
49
|
+
const VIEWPORT_PRESETS: { key: ViewportPreset; label: string; width: number | null }[] = [
|
|
50
|
+
{ key: 'pc', label: 'PC', width: null },
|
|
51
|
+
{ key: 'tablet', label: 'Tablet', width: 768 },
|
|
52
|
+
{ key: 'mobile', label: 'Mobile', width: 375 },
|
|
53
|
+
{ key: 'custom', label: '自定义', width: null },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const VIEWPORT_KEY = 'snack-dev-viewport';
|
|
57
|
+
|
|
58
|
+
const useViewport = () => {
|
|
59
|
+
const saved = (() => { try { return JSON.parse(localStorage.getItem(VIEWPORT_KEY) || 'null'); } catch { return null; } })();
|
|
60
|
+
const [preset, setPreset] = useState<ViewportPreset>(saved?.preset || 'pc');
|
|
61
|
+
const [customWidth, setCustomWidth] = useState<number>(saved?.customWidth || 414);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
try { localStorage.setItem(VIEWPORT_KEY, JSON.stringify({ preset, customWidth })); } catch {}
|
|
65
|
+
}, [preset, customWidth]);
|
|
66
|
+
|
|
67
|
+
const width = useMemo(() => {
|
|
68
|
+
const p = VIEWPORT_PRESETS.find(x => x.key === preset);
|
|
69
|
+
if (!p) return null;
|
|
70
|
+
if (preset === 'custom') return customWidth;
|
|
71
|
+
return p.width;
|
|
72
|
+
}, [preset, customWidth]);
|
|
73
|
+
|
|
74
|
+
return { preset, setPreset, customWidth, setCustomWidth, width };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ─── Background Hook ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
type BgPreset = 'default' | 'white' | 'dark' | 'checker';
|
|
80
|
+
|
|
81
|
+
const BG_PRESETS: { key: BgPreset; label: string; title: string }[] = [
|
|
82
|
+
{ key: 'default', label: '默认', title: '跟随主题' },
|
|
83
|
+
{ key: 'white', label: '白色', title: '白色背景' },
|
|
84
|
+
{ key: 'dark', label: '深色', title: '深色背景' },
|
|
85
|
+
{ key: 'checker', label: '棋盘', title: '透明检测' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const BG_KEY = 'snack-dev-bg';
|
|
89
|
+
|
|
90
|
+
const useBgPreset = () => {
|
|
91
|
+
const [bg, setBg] = useState<BgPreset>(() => {
|
|
92
|
+
try { return (localStorage.getItem(BG_KEY) as BgPreset) || 'default'; } catch { return 'default'; }
|
|
93
|
+
});
|
|
94
|
+
const set = useCallback((v: BgPreset) => {
|
|
95
|
+
setBg(v);
|
|
96
|
+
try { localStorage.setItem(BG_KEY, v); } catch {}
|
|
97
|
+
}, []);
|
|
98
|
+
return { bg, set };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ─── Scale Hook ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const SCALE_PRESETS = [50, 75, 100, 125, 150];
|
|
104
|
+
const SCALE_KEY = 'snack-dev-scale';
|
|
105
|
+
|
|
106
|
+
const useScale = () => {
|
|
107
|
+
const [scale, setScale] = useState<number>(() => {
|
|
108
|
+
try { return Number(localStorage.getItem(SCALE_KEY)) || 100; } catch { return 100; }
|
|
109
|
+
});
|
|
110
|
+
const set = useCallback((v: number) => {
|
|
111
|
+
setScale(v);
|
|
112
|
+
try { localStorage.setItem(SCALE_KEY, String(v)); } catch {}
|
|
113
|
+
}, []);
|
|
114
|
+
return { scale, set };
|
|
115
|
+
};
|
|
116
|
+
|
|
47
117
|
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
|
48
118
|
|
|
49
119
|
const Sidebar = ({ projectPkg, version, packages, dark, onToggleTheme }: any) => {
|
|
@@ -228,12 +298,155 @@ const HomeView = ({ packages, mosuleMaps, onOpen }: any) => (
|
|
|
228
298
|
</div>
|
|
229
299
|
);
|
|
230
300
|
|
|
301
|
+
// ─── Topbar Tools ─────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/** 视口尺寸切换 */
|
|
304
|
+
const ViewportToggle = ({ preset, setPreset, customWidth, setCustomWidth }: any) => {
|
|
305
|
+
const [editing, setEditing] = useState(false);
|
|
306
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
307
|
+
|
|
308
|
+
const handleCustomClick = () => {
|
|
309
|
+
setPreset('custom');
|
|
310
|
+
setEditing(true);
|
|
311
|
+
setTimeout(() => inputRef.current?.select(), 0);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const handleInputBlur = () => setEditing(false);
|
|
315
|
+
const handleInputKeyDown = (e: React.KeyboardEvent) => {
|
|
316
|
+
if (e.key === 'Enter') (e.target as HTMLInputElement).blur();
|
|
317
|
+
};
|
|
318
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
319
|
+
const v = parseInt(e.target.value, 10);
|
|
320
|
+
if (!isNaN(v) && v > 0) setCustomWidth(v);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div className="sd-topbar__group">
|
|
325
|
+
<span className="sd-topbar__group-label">视口</span>
|
|
326
|
+
<div className="sd-topbar__seg">
|
|
327
|
+
{VIEWPORT_PRESETS.filter(p => p.key !== 'custom').map(p => (
|
|
328
|
+
<button
|
|
329
|
+
key={p.key}
|
|
330
|
+
className={`sd-topbar__seg-btn ${preset === p.key ? 'sd-topbar__seg-btn--active' : ''}`}
|
|
331
|
+
onClick={() => setPreset(p.key)}
|
|
332
|
+
title={p.width ? `${p.width}px` : '全宽'}
|
|
333
|
+
>
|
|
334
|
+
{p.label}
|
|
335
|
+
</button>
|
|
336
|
+
))}
|
|
337
|
+
{preset === 'custom' && editing ? (
|
|
338
|
+
<input
|
|
339
|
+
ref={inputRef}
|
|
340
|
+
className="sd-topbar__custom-input"
|
|
341
|
+
type="number"
|
|
342
|
+
min={1}
|
|
343
|
+
defaultValue={customWidth}
|
|
344
|
+
onChange={handleInputChange}
|
|
345
|
+
onBlur={handleInputBlur}
|
|
346
|
+
onKeyDown={handleInputKeyDown}
|
|
347
|
+
/>
|
|
348
|
+
) : (
|
|
349
|
+
<button
|
|
350
|
+
className={`sd-topbar__seg-btn ${preset === 'custom' ? 'sd-topbar__seg-btn--active' : ''}`}
|
|
351
|
+
onClick={handleCustomClick}
|
|
352
|
+
title="自定义宽度"
|
|
353
|
+
>
|
|
354
|
+
{preset === 'custom' ? `${customWidth}px` : '自定义'}
|
|
355
|
+
</button>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** 背景切换 */
|
|
363
|
+
const BgToggle = ({ bg, set }: any) => (
|
|
364
|
+
<div className="sd-topbar__group">
|
|
365
|
+
<span className="sd-topbar__group-label">背景</span>
|
|
366
|
+
<div className="sd-topbar__seg">
|
|
367
|
+
{BG_PRESETS.map(p => (
|
|
368
|
+
<button
|
|
369
|
+
key={p.key}
|
|
370
|
+
className={`sd-topbar__seg-btn ${bg === p.key ? 'sd-topbar__seg-btn--active' : ''}`}
|
|
371
|
+
onClick={() => set(p.key)}
|
|
372
|
+
title={p.title}
|
|
373
|
+
>
|
|
374
|
+
{p.key === 'checker' ? (
|
|
375
|
+
<span className="sd-checker-icon" />
|
|
376
|
+
) : p.label}
|
|
377
|
+
</button>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
/** 缩放控制 */
|
|
384
|
+
const ScaleToggle = ({ scale, set }: any) => (
|
|
385
|
+
<div className="sd-topbar__group">
|
|
386
|
+
<span className="sd-topbar__group-label">缩放</span>
|
|
387
|
+
<div className="sd-topbar__seg">
|
|
388
|
+
{SCALE_PRESETS.map(v => (
|
|
389
|
+
<button
|
|
390
|
+
key={v}
|
|
391
|
+
className={`sd-topbar__seg-btn ${scale === v ? 'sd-topbar__seg-btn--active' : ''}`}
|
|
392
|
+
onClick={() => set(v)}
|
|
393
|
+
>
|
|
394
|
+
{v}%
|
|
395
|
+
</button>
|
|
396
|
+
))}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
/** 刷新模块 */
|
|
402
|
+
const RefreshBtn = ({ onRefresh }: any) => (
|
|
403
|
+
<button className="sd-topbar__icon-btn" onClick={onRefresh} title="重新挂载模块">
|
|
404
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
|
405
|
+
<path d="M11.5 2.5A5.5 5.5 0 1 0 12 7"/>
|
|
406
|
+
<path d="M12 2.5V5.5H9"/>
|
|
407
|
+
</svg>
|
|
408
|
+
</button>
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// ─── Pane Content Wrapper ─────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
const PaneContent = ({ width, scale, bg, children }: any) => {
|
|
414
|
+
const style: React.CSSProperties = {};
|
|
415
|
+
if (width) {
|
|
416
|
+
style.width = width;
|
|
417
|
+
style.margin = '0 auto';
|
|
418
|
+
style.flexShrink = 0;
|
|
419
|
+
}
|
|
420
|
+
if (scale !== 100) {
|
|
421
|
+
style.transform = `scale(${scale / 100})`;
|
|
422
|
+
style.transformOrigin = 'top left';
|
|
423
|
+
if (width) {
|
|
424
|
+
style.width = width;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return (
|
|
428
|
+
<div className={`sd-pane__content sd-pane__content--bg-${bg}`}>
|
|
429
|
+
<div style={style}>
|
|
430
|
+
{children}
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
};
|
|
435
|
+
|
|
231
436
|
// ─── Debug View ───────────────────────────────────────────────────────────────
|
|
232
437
|
|
|
233
438
|
const DebugView = ({ arg, mosuleMaps, sdk, settingPanel, onBack, packages }: any) => {
|
|
234
439
|
const mod = mosuleMaps[arg.name];
|
|
235
440
|
const pkgInfo = packages.find((p: any) => p.name === arg.name);
|
|
236
441
|
|
|
442
|
+
// topbar 状态
|
|
443
|
+
const { preset, setPreset, customWidth, setCustomWidth, width } = useViewport();
|
|
444
|
+
const { bg, set: setBg } = useBgPreset();
|
|
445
|
+
const { scale, set: setScale } = useScale();
|
|
446
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
447
|
+
|
|
448
|
+
const handleRefresh = useCallback(() => setRefreshKey(k => k + 1), []);
|
|
449
|
+
|
|
237
450
|
const { Module, SettingFC, tips } = useMemo(() => {
|
|
238
451
|
if (!mod) return { Module: null, SettingFC: null, tips: null };
|
|
239
452
|
|
|
@@ -264,7 +477,7 @@ const DebugView = ({ arg, mosuleMaps, sdk, settingPanel, onBack, packages }: any
|
|
|
264
477
|
</div>
|
|
265
478
|
);
|
|
266
479
|
return { Module: ModuleFC, SettingFC: ModuleSettingFC, tips: warn };
|
|
267
|
-
}, [arg.name, arg.setting]);
|
|
480
|
+
}, [arg.name, arg.setting, refreshKey]);
|
|
268
481
|
|
|
269
482
|
if (!mod) {
|
|
270
483
|
return (
|
|
@@ -316,6 +529,22 @@ const DebugView = ({ arg, mosuleMaps, sdk, settingPanel, onBack, packages }: any
|
|
|
316
529
|
</button>
|
|
317
530
|
)}
|
|
318
531
|
</div>
|
|
532
|
+
<div className="sd-topbar__sep" />
|
|
533
|
+
{/* 工具区 */}
|
|
534
|
+
<div className="sd-topbar__tools">
|
|
535
|
+
<ViewportToggle
|
|
536
|
+
preset={preset}
|
|
537
|
+
setPreset={setPreset}
|
|
538
|
+
customWidth={customWidth}
|
|
539
|
+
setCustomWidth={setCustomWidth}
|
|
540
|
+
/>
|
|
541
|
+
<div className="sd-topbar__tool-sep" />
|
|
542
|
+
<BgToggle bg={bg} set={setBg} />
|
|
543
|
+
<div className="sd-topbar__tool-sep" />
|
|
544
|
+
<ScaleToggle scale={scale} set={setScale} />
|
|
545
|
+
<div className="sd-topbar__tool-sep" />
|
|
546
|
+
<RefreshBtn onRefresh={handleRefresh} />
|
|
547
|
+
</div>
|
|
319
548
|
</div>
|
|
320
549
|
|
|
321
550
|
<div className="sd-debug__body">
|
|
@@ -326,9 +555,9 @@ const DebugView = ({ arg, mosuleMaps, sdk, settingPanel, onBack, packages }: any
|
|
|
326
555
|
<svg width="8" height="9" viewBox="0 0 8 9" fill="currentColor"><path d="M0 0v9l8-4.5L0 0z"/></svg>
|
|
327
556
|
主模块
|
|
328
557
|
</div>
|
|
329
|
-
<
|
|
330
|
-
{Module && <Module />}
|
|
331
|
-
</
|
|
558
|
+
<PaneContent width={width} scale={scale} bg={bg}>
|
|
559
|
+
{Module && <Module key={refreshKey} />}
|
|
560
|
+
</PaneContent>
|
|
332
561
|
</div>
|
|
333
562
|
<div className="sd-pane__divider" />
|
|
334
563
|
<div className="sd-pane sd-pane--right">
|
|
@@ -336,17 +565,17 @@ const DebugView = ({ arg, mosuleMaps, sdk, settingPanel, onBack, packages }: any
|
|
|
336
565
|
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" stroke="currentColor" strokeWidth="1.2"><circle cx="5.5" cy="5.5" r="1.8"/><path d="M5.5.5v1M5.5 9.5v1M.5 5.5h1M9.5 5.5h1M2 2l.7.7M8.3 8.3l.7.7M2 9l.7-.7M8.3 2.7l.7-.7"/></svg>
|
|
337
566
|
设置模块
|
|
338
567
|
</div>
|
|
339
|
-
<
|
|
568
|
+
<PaneContent width={width} scale={scale} bg={bg}>
|
|
340
569
|
{tips}
|
|
341
|
-
{SettingFC && <SettingFC />}
|
|
342
|
-
</
|
|
570
|
+
{SettingFC && <SettingFC key={refreshKey} />}
|
|
571
|
+
</PaneContent>
|
|
343
572
|
</div>
|
|
344
573
|
</>
|
|
345
574
|
) : (
|
|
346
575
|
<div className="sd-pane sd-pane--full">
|
|
347
|
-
<
|
|
348
|
-
{Module && <Module />}
|
|
349
|
-
</
|
|
576
|
+
<PaneContent width={width} scale={scale} bg={bg}>
|
|
577
|
+
{Module && <Module key={refreshKey} />}
|
|
578
|
+
</PaneContent>
|
|
350
579
|
</div>
|
|
351
580
|
)}
|
|
352
581
|
</div>
|
|
@@ -392,4 +621,4 @@ const App = (props: Props) => {
|
|
|
392
621
|
);
|
|
393
622
|
};
|
|
394
623
|
|
|
395
|
-
export default App;
|
|
624
|
+
export default App;
|
package/template/dev/index.scss
CHANGED
|
@@ -581,6 +581,123 @@ button {
|
|
|
581
581
|
}
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
// ─── Topbar Tools ─────────────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
.sd-topbar__tools {
|
|
587
|
+
display: flex;
|
|
588
|
+
align-items: center;
|
|
589
|
+
gap: 6px;
|
|
590
|
+
flex-shrink: 0;
|
|
591
|
+
overflow-x: auto;
|
|
592
|
+
min-width: 0;
|
|
593
|
+
|
|
594
|
+
&::-webkit-scrollbar { display: none; }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.sd-topbar__tool-sep {
|
|
598
|
+
width: 1px;
|
|
599
|
+
height: 14px;
|
|
600
|
+
background: var(--border);
|
|
601
|
+
flex-shrink: 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.sd-topbar__group {
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
gap: 5px;
|
|
608
|
+
flex-shrink: 0;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.sd-topbar__group-label {
|
|
612
|
+
font-size: 10px;
|
|
613
|
+
font-family: var(--font-mono);
|
|
614
|
+
color: var(--text-3);
|
|
615
|
+
letter-spacing: .04em;
|
|
616
|
+
white-space: nowrap;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.sd-topbar__seg {
|
|
620
|
+
display: flex;
|
|
621
|
+
align-items: center;
|
|
622
|
+
background: var(--bg-2);
|
|
623
|
+
border: 1px solid var(--border);
|
|
624
|
+
border-radius: var(--radius-sm);
|
|
625
|
+
overflow: hidden;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.sd-topbar__seg-btn {
|
|
629
|
+
display: inline-flex;
|
|
630
|
+
align-items: center;
|
|
631
|
+
justify-content: center;
|
|
632
|
+
padding: 3px 8px;
|
|
633
|
+
font-size: 11px;
|
|
634
|
+
color: var(--text-2);
|
|
635
|
+
white-space: nowrap;
|
|
636
|
+
transition: background .12s, color .12s;
|
|
637
|
+
min-width: 32px;
|
|
638
|
+
line-height: 1;
|
|
639
|
+
|
|
640
|
+
&:hover { background: var(--bg-3); color: var(--text); }
|
|
641
|
+
|
|
642
|
+
&--active {
|
|
643
|
+
background: var(--bg-1);
|
|
644
|
+
color: var(--accent-a);
|
|
645
|
+
box-shadow: 0 0 0 1px rgba(71,218,181,.3) inset;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
& + & {
|
|
649
|
+
border-left: 1px solid var(--border-soft);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.sd-topbar__custom-input {
|
|
654
|
+
width: 58px;
|
|
655
|
+
padding: 3px 6px;
|
|
656
|
+
font-size: 11px;
|
|
657
|
+
font-family: var(--font-mono);
|
|
658
|
+
color: var(--accent-a);
|
|
659
|
+
background: var(--bg-1);
|
|
660
|
+
border: none;
|
|
661
|
+
outline: none;
|
|
662
|
+
border-left: 1px solid var(--border-soft);
|
|
663
|
+
text-align: center;
|
|
664
|
+
|
|
665
|
+
&::-webkit-inner-spin-button,
|
|
666
|
+
&::-webkit-outer-spin-button { -webkit-appearance: none; }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.sd-topbar__icon-btn {
|
|
670
|
+
display: inline-flex;
|
|
671
|
+
align-items: center;
|
|
672
|
+
justify-content: center;
|
|
673
|
+
width: 26px;
|
|
674
|
+
height: 26px;
|
|
675
|
+
border-radius: var(--radius-sm);
|
|
676
|
+
color: var(--text-3);
|
|
677
|
+
transition: background .15s, color .15s;
|
|
678
|
+
flex-shrink: 0;
|
|
679
|
+
|
|
680
|
+
&:hover {
|
|
681
|
+
background: var(--bg-2);
|
|
682
|
+
color: var(--accent-a);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 棋盘格图标
|
|
687
|
+
.sd-checker-icon {
|
|
688
|
+
display: inline-block;
|
|
689
|
+
width: 10px;
|
|
690
|
+
height: 10px;
|
|
691
|
+
background-image:
|
|
692
|
+
linear-gradient(45deg, var(--text-3) 25%, transparent 25%),
|
|
693
|
+
linear-gradient(-45deg, var(--text-3) 25%, transparent 25%),
|
|
694
|
+
linear-gradient(45deg, transparent 75%, var(--text-3) 75%),
|
|
695
|
+
linear-gradient(-45deg, transparent 75%, var(--text-3) 75%);
|
|
696
|
+
background-size: 5px 5px;
|
|
697
|
+
background-position: 0 0, 0 2.5px, 2.5px -2.5px, -2.5px 0;
|
|
698
|
+
border-radius: 1px;
|
|
699
|
+
}
|
|
700
|
+
|
|
584
701
|
// ─── Debug Body + Panes ───────────────────────────────────────────────────────
|
|
585
702
|
|
|
586
703
|
.sd-debug__body {
|
|
@@ -625,6 +742,31 @@ button {
|
|
|
625
742
|
flex: 1;
|
|
626
743
|
overflow: auto;
|
|
627
744
|
padding: 16px;
|
|
745
|
+
transition: background .2s;
|
|
746
|
+
|
|
747
|
+
// 背景模式
|
|
748
|
+
&--bg-default { background: var(--bg); }
|
|
749
|
+
&--bg-white { background: #ffffff; }
|
|
750
|
+
&--bg-dark { background: #1a1a1a; }
|
|
751
|
+
&--bg-checker {
|
|
752
|
+
background-color: #e0e0e0;
|
|
753
|
+
background-image:
|
|
754
|
+
linear-gradient(45deg, #bbb 25%, transparent 25%),
|
|
755
|
+
linear-gradient(-45deg, #bbb 25%, transparent 25%),
|
|
756
|
+
linear-gradient(45deg, transparent 75%, #bbb 75%),
|
|
757
|
+
linear-gradient(-45deg, transparent 75%, #bbb 75%);
|
|
758
|
+
background-size: 16px 16px;
|
|
759
|
+
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
|
760
|
+
|
|
761
|
+
[data-theme="dark"] & {
|
|
762
|
+
background-color: #2a2a2a;
|
|
763
|
+
background-image:
|
|
764
|
+
linear-gradient(45deg, #333 25%, transparent 25%),
|
|
765
|
+
linear-gradient(-45deg, #333 25%, transparent 25%),
|
|
766
|
+
linear-gradient(45deg, transparent 75%, #333 75%),
|
|
767
|
+
linear-gradient(-45deg, transparent 75%, #333 75%);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
628
770
|
}
|
|
629
771
|
|
|
630
772
|
// ─── Debug Error ─────────────────────────────────────────────────────────────
|