@snack-kit/scripts 0.2.0 → 0.3.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 CHANGED
@@ -142,6 +142,13 @@ module.exports = (config) => {
142
142
 
143
143
  ## Changelog
144
144
 
145
+ ### 0.3.0
146
+
147
+ - 新增:调试窗口 topbar 视口尺寸切换(PC / Tablet / Mobile / 自定义),偏好持久化到 localStorage
148
+ - 新增:调试窗口 topbar 内容区背景切换(默认 / 白色 / 深色 / 棋盘格透明检测)
149
+ - 新增:调试窗口 topbar 内容区缩放控制(50% / 75% / 100% / 125% / 150%)
150
+ - 新增:调试窗口 topbar 刷新模块按钮,强制重新 mount 当前模块
151
+
145
152
  ### 0.2.0
146
153
 
147
154
  - 新增:`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,
@@ -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/* 等依赖)
@@ -25,9 +90,15 @@ function createResolve(extraAlias = {}) {
25
90
  assets: path.join(process.env.PROJECT_PATH, 'src/assets'),
26
91
  // @snack-kit/core 的 package.json module 字段路径有误,直接指向 cjs
27
92
  '@snack-kit/core': path.resolve(__dirname, '../node_modules/@snack-kit/core/dist/cjs/index.js'),
28
- // 强制 dev 模板使用 snack-scripts 自身的 react,避免项目中旧版 react 被命中
29
- 'react': path.resolve(__dirname, '../node_modules/react'),
30
- 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'),
93
+ // 优先使用宿主项目自己的 react,保证 React 版本与项目一致(兼容 React 17/18/19)
94
+ // react/react-dom 使用目录别名(不加 $),使子路径(如 react/jsx-runtime)也走项目自身版本
95
+ // react-dom/client react-dom 更长,enhanced-resolve 会优先匹配更具体的 key
96
+ 'react-dom/client': reactDomClient,
97
+ 'react': react,
98
+ 'react-dom': reactDom,
99
+ // axios >= 1.x 使用子路径 import 'process/browser',webpack 5 的 fallback 不处理子路径,
100
+ // 需显式 alias 保证可解析(@snack-kit/lib >= 0.7.0 起依赖新版 axios)
101
+ ...(processBrowserPath && { 'process/browser': processBrowserPath }),
31
102
  ...extraAlias
32
103
  }
33
104
  };
@@ -137,6 +208,33 @@ const filesystemCache = {
137
208
  allowCollectingMemory: true
138
209
  };
139
210
 
211
+ /**
212
+ * 解析 webpack 模块,优先使用宿主项目的 webpack 实例。
213
+ * 保证 webpack config 文件与 webpack CLI 进程使用同一个 webpack 实例,
214
+ * 避免多实例导致的 instanceof 检查失败(Critical dependency / Compilation 错误)。
215
+ */
216
+ function resolveProjectWebpack() {
217
+ const searchPaths = [process.env.PROJECT_PATH, __dirname].filter(Boolean);
218
+ try {
219
+ return require(require.resolve('webpack', { paths: searchPaths }));
220
+ } catch (_) {
221
+ return require('webpack');
222
+ }
223
+ }
224
+
225
+ /**
226
+ * 创建 process/Buffer polyfill 的 ProvidePlugin 配置(可选)。
227
+ * 若 process/browser 可解析,则自动注入,使项目代码中裸用的 `process` 全局变量可正常工作。
228
+ * 返回 plugin 实例数组,为空数组则表示无需注入。
229
+ */
230
+ function createPolyfillPlugins(webpack) {
231
+ const processBrowserPath = resolveProcessBrowser();
232
+ if (!processBrowserPath) return [];
233
+ return [
234
+ new webpack.ProvidePlugin({ process: 'process/browser' })
235
+ ];
236
+ }
237
+
140
238
  module.exports = {
141
239
  createResolve,
142
240
  createSwcRule,
@@ -144,5 +242,8 @@ module.exports = {
144
242
  createOptimization,
145
243
  filesystemCache,
146
244
  styleRule,
147
- assetRule
245
+ assetRule,
246
+ resolveProjectWebpack,
247
+ resolveLoader,
248
+ createPolyfillPlugins
148
249
  };
@@ -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.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "snack-cli package scripts Powered by Para FED",
5
5
  "bin": {
6
6
  "snack-scripts": "./bin/main.js"
@@ -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
- <div className="sd-pane__content">
330
- {Module && <Module />}
331
- </div>
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
- <div className="sd-pane__content">
568
+ <PaneContent width={width} scale={scale} bg={bg}>
340
569
  {tips}
341
- {SettingFC && <SettingFC />}
342
- </div>
570
+ {SettingFC && <SettingFC key={refreshKey} />}
571
+ </PaneContent>
343
572
  </div>
344
573
  </>
345
574
  ) : (
346
575
  <div className="sd-pane sd-pane--full">
347
- <div className="sd-pane__content">
348
- {Module && <Module />}
349
- </div>
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;
@@ -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 ─────────────────────────────────────────────────────────────