@coding-script/script-engine 0.0.3 → 0.0.5

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
@@ -9,7 +9,8 @@
9
9
  - **动态类型自动补全**:基于 `ScriptMetadata` 提供变量名补全和点号链式访问补全(如 `request.test.name`)
10
10
  - **Groovy 语法提示**:内置 `if`/`for`/`while`/`println`/`return` 等常用 Groovy 语法片段
11
11
  - **代码格式化**:内置 Groovy 格式化器,也可通过 `onFormat` 自定义格式化逻辑
12
- - **属性面板**:右侧侧边栏展示主函数签名、变量、数据类型(字段和方法),支持折叠/展开和拖拽调节宽度
12
+ - **属性面板**:右侧侧边栏展示主函数签名、函数入参、绑定参数、数据类型(字段和方法),支持折叠/展开和拖拽调节宽度
13
+ - **脚本说明**:工具栏"脚本说明"按钮,点击展开/收起脚本描述弹框,支持多行文本和 `key: value` 格式高亮
13
14
  - **主题切换**:暗色/亮色两套主题,编辑器、补全弹窗、属性面板同步切换,编辑器内部管理主题状态
14
15
  - **全屏模式**:支持 CSS 全屏(`position: fixed` 覆盖视口),不影响编辑器内容
15
16
  - **自定义工具栏**:通过 `toolbarExtra` 传入任意 React 节点
@@ -127,7 +128,44 @@ function App() {
127
128
 
128
129
  | 属性 | 类型 | 默认值 | 说明 |
129
130
  |---|---|---|---|
130
- | `toolbarExtra` | `React.ReactNode` | `undefined` | 工具栏额外内容,渲染在内置按钮之后,可传入任意 JSX |
131
+ | `toolbar` | `ToolbarItem[]` | `undefined` | 工具栏自定义按钮列表,渲染在内置按钮之后、`toolbarExtra` 之前 |
132
+ | `toolbarExtra` | `React.ReactNode` | `undefined` | 工具栏额外内容,渲染在最后,可传入任意 JSX |
133
+
134
+ `ToolbarItem` 类型:
135
+
136
+ ```typescript
137
+ interface ToolbarItem {
138
+ key: string; // 唯一标识
139
+ label: ReactNode; // 按钮内容
140
+ title: string; // 鼠标悬停提示
141
+ backgroundColor: string;
142
+ hoverBackgroundColor: string;
143
+ textColor: string;
144
+ borderColor: string;
145
+ onClick: () => void;
146
+ }
147
+ ```
148
+
149
+ **使用按钮列表:**
150
+
151
+ ```tsx
152
+ <ScriptCodeEditor
153
+ toolbar={[
154
+ {
155
+ key: 'save',
156
+ label: '💾 保存',
157
+ title: '保存脚本',
158
+ backgroundColor: '#007bff',
159
+ hoverBackgroundColor: '#0056b3',
160
+ textColor: '#fff',
161
+ borderColor: '#007bff',
162
+ onClick: () => saveCode(),
163
+ },
164
+ ]}
165
+ />
166
+ ```
167
+
168
+ **使用自定义内容:**
131
169
 
132
170
  ```tsx
133
171
  <ScriptCodeEditor
@@ -154,7 +192,7 @@ function App() {
154
192
  interface ScriptMetadata {
155
193
  /** 主函数名称(可选) */
156
194
  mainMethod?: string;
157
- /** 主函数描述信息(可选,提供后在属性面板主函数名旁显示悬浮提示) */
195
+ /** 脚本说明(可选,提供后在工具栏显示"脚本说明"按钮,点击展开/收起描述弹框) */
158
196
  description?: string;
159
197
  /** 注入变量(如 $request,name 含 $ 前缀) */
160
198
  binds: ScriptBindInfo[];
@@ -15,10 +15,12 @@ function buildMemberCompletions(typeInfo) {
15
15
  });
16
16
  for (const func of typeInfo.functions){
17
17
  const sig = formatFunctionSignature(func);
18
+ const ret = func.returnType ? ` → ${func.returnType}` : '';
19
+ const detail = func.description ? `${sig}${ret} — ${func.description}` : `${sig}${ret}`;
18
20
  options.push({
19
21
  label: func.name,
20
22
  type: 'function',
21
- detail: func.description ? `${sig} — ${func.description}` : sig,
23
+ detail,
22
24
  info: func.description || void 0,
23
25
  apply: `${func.name}()`,
24
26
  boost: -1
@@ -2,7 +2,7 @@ import react, { useState } from "react";
2
2
  const FunctionRow = ({ func, colors })=>{
3
3
  const [hovered, setHovered] = useState(false);
4
4
  const params = func.parameters.map((p)=>`${p.name}: ${p.dataType}`).join(', ');
5
- const sig = `${func.name}(${params})`;
5
+ const sig = `${func.name}(${params})${func.returnType ? ` → ${func.returnType}` : ''}`;
6
6
  return /*#__PURE__*/ react.createElement("div", {
7
7
  style: {
8
8
  padding: '4px 12px 4px 28px',
@@ -35,7 +35,13 @@ const FunctionRow = ({ func, colors })=>{
35
35
  color: colors.textSecondary,
36
36
  fontSize: 11
37
37
  }
38
- }, "(", params, ")")), func.description && /*#__PURE__*/ react.createElement("div", {
38
+ }, "(", params, ")"), func.returnType && /*#__PURE__*/ react.createElement("span", {
39
+ style: {
40
+ color: colors.typeColor,
41
+ fontSize: 11,
42
+ marginLeft: 'auto'
43
+ }
44
+ }, "→ ", func.returnType)), func.description && /*#__PURE__*/ react.createElement("div", {
39
45
  style: {
40
46
  fontSize: 11,
41
47
  color: colors.textSecondary,
@@ -1,7 +1,7 @@
1
1
  export { themes, SCROLL_CLASS, ensureScrollbarStyle } from './theme-colors';
2
2
  export type { ThemeColors } from './theme-colors';
3
3
  export { ToolbarButton } from './toolbar-button';
4
- export type { ToolbarButtonProps } from './toolbar-button';
4
+ export type { ToolbarButtonProps } from '../types';
5
5
  export { Toolbar } from './toolbar';
6
6
  export type { ToolbarProps } from './toolbar';
7
7
  export { ExpandSidebarButton } from './expand-sidebar-button';
@@ -1,51 +1,6 @@
1
- import react, { useCallback, useEffect, useRef, useState } from "react";
1
+ import react from "react";
2
2
  import { SectionHeader } from "./section-header.js";
3
- const TOOLTIP_MARGIN = 6;
4
- const TOOLTIP_MAX_WIDTH = 380;
5
- function calcTooltipPos(anchorRect, tooltipEl) {
6
- const vw = window.innerWidth;
7
- const vh = window.innerHeight;
8
- const tooltipH = tooltipEl?.scrollHeight ?? 200;
9
- const spaceBelow = vh - anchorRect.bottom - TOOLTIP_MARGIN;
10
- const spaceAbove = anchorRect.top - TOOLTIP_MARGIN;
11
- const placeBelow = spaceBelow >= Math.min(tooltipH, 260) || spaceBelow >= spaceAbove;
12
- const top = placeBelow ? anchorRect.bottom + TOOLTIP_MARGIN : Math.max(TOOLTIP_MARGIN, anchorRect.top - tooltipH - TOOLTIP_MARGIN);
13
- let left = anchorRect.left;
14
- const maxRight = vw - TOOLTIP_MARGIN;
15
- if (left + TOOLTIP_MAX_WIDTH > maxRight) left = Math.max(TOOLTIP_MARGIN, maxRight - TOOLTIP_MAX_WIDTH);
16
- const maxWidth = Math.min(TOOLTIP_MAX_WIDTH, maxRight - left);
17
- const maxHeight = placeBelow ? spaceBelow - TOOLTIP_MARGIN : spaceAbove - TOOLTIP_MARGIN;
18
- return {
19
- top,
20
- left,
21
- maxWidth,
22
- maxHeight: Math.max(maxHeight, 80)
23
- };
24
- }
25
- const MainFunctionSection = ({ metadata, colors })=>{
26
- const [showTip, setShowTip] = useState(false);
27
- const [pos, setPos] = useState(null);
28
- const anchorRef = useRef(null);
29
- const tooltipRef = useRef(null);
30
- const updatePos = useCallback(()=>{
31
- if (!anchorRef.current) return;
32
- const rect = anchorRef.current.getBoundingClientRect();
33
- setPos(calcTooltipPos(rect, tooltipRef.current));
34
- }, []);
35
- useEffect(()=>{
36
- if (!showTip) return;
37
- updatePos();
38
- window.addEventListener('scroll', updatePos, true);
39
- window.addEventListener('resize', updatePos);
40
- return ()=>{
41
- window.removeEventListener('scroll', updatePos, true);
42
- window.removeEventListener('resize', updatePos);
43
- };
44
- }, [
45
- showTip,
46
- updatePos
47
- ]);
48
- return /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement(SectionHeader, {
3
+ const MainFunctionSection = ({ metadata, colors })=>/*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement(SectionHeader, {
49
4
  colors: colors,
50
5
  label: "主函数"
51
6
  }), /*#__PURE__*/ react.createElement("div", {
@@ -70,30 +25,7 @@ const MainFunctionSection = ({ metadata, colors })=>{
70
25
  color: colors.text,
71
26
  fontWeight: 500
72
27
  }
73
- }, metadata.mainMethod), metadata.description && /*#__PURE__*/ react.createElement("span", {
74
- ref: anchorRef,
75
- style: {
76
- display: 'inline-flex',
77
- cursor: 'help',
78
- flexShrink: 0
79
- },
80
- onMouseEnter: ()=>setShowTip(true),
81
- onMouseLeave: ()=>setShowTip(false)
82
- }, /*#__PURE__*/ react.createElement("span", {
83
- style: {
84
- display: 'inline-flex',
85
- alignItems: 'center',
86
- justifyContent: 'center',
87
- width: 14,
88
- height: 14,
89
- borderRadius: '50%',
90
- border: `1px solid ${colors.textSecondary}`,
91
- color: colors.textSecondary,
92
- fontSize: 10,
93
- lineHeight: 1,
94
- fontWeight: 600
95
- }
96
- }, "?")), /*#__PURE__*/ react.createElement("span", {
28
+ }, metadata.mainMethod), /*#__PURE__*/ react.createElement("span", {
97
29
  style: {
98
30
  color: colors.textSecondary,
99
31
  fontSize: 11
@@ -104,30 +36,5 @@ const MainFunctionSection = ({ metadata, colors })=>{
104
36
  fontSize: 11,
105
37
  marginLeft: 'auto'
106
38
  }
107
- }, "→ ", metadata.returnType))), showTip && pos && metadata.description && /*#__PURE__*/ react.createElement("div", {
108
- ref: tooltipRef,
109
- onMouseEnter: ()=>setShowTip(true),
110
- onMouseLeave: ()=>setShowTip(false),
111
- style: {
112
- position: 'fixed',
113
- top: pos.top,
114
- left: pos.left,
115
- maxWidth: pos.maxWidth,
116
- maxHeight: pos.maxHeight,
117
- overflowY: 'auto',
118
- padding: '8px 12px',
119
- background: colors.headerBg,
120
- border: `1px solid ${colors.border}`,
121
- borderRadius: 6,
122
- color: colors.text,
123
- fontSize: 12,
124
- lineHeight: 1.65,
125
- whiteSpace: 'pre-wrap',
126
- wordBreak: 'break-word',
127
- zIndex: 9999,
128
- boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
129
- pointerEvents: 'auto'
130
- }
131
- }, metadata.description));
132
- };
39
+ }, "→ ", metadata.returnType))));
133
40
  export { MainFunctionSection };
@@ -3,5 +3,7 @@ import type { ThemeColors } from './theme-colors';
3
3
  export interface SectionHeaderProps {
4
4
  colors: ThemeColors;
5
5
  label: string;
6
+ /** 标题右侧的额外操作区域(如按钮、图标) */
7
+ extra?: React.ReactNode;
6
8
  }
7
9
  export declare const SectionHeader: React.FC<SectionHeaderProps>;
@@ -1,6 +1,9 @@
1
1
  import react from "react";
2
- const SectionHeader = ({ colors, label })=>/*#__PURE__*/ react.createElement("div", {
2
+ const SectionHeader = ({ colors, label, extra })=>/*#__PURE__*/ react.createElement("div", {
3
3
  style: {
4
+ display: 'flex',
5
+ alignItems: 'center',
6
+ justifyContent: 'space-between',
4
7
  padding: '6px 12px 4px',
5
8
  fontSize: 11,
6
9
  textTransform: 'uppercase',
@@ -8,5 +11,10 @@ const SectionHeader = ({ colors, label })=>/*#__PURE__*/ react.createElement("di
8
11
  color: colors.textSecondary,
9
12
  fontWeight: 600
10
13
  }
11
- }, label);
14
+ }, /*#__PURE__*/ react.createElement("span", null, label), extra && /*#__PURE__*/ react.createElement("div", {
15
+ style: {
16
+ display: 'flex',
17
+ alignItems: 'center'
18
+ }
19
+ }, extra));
12
20
  export { SectionHeader };
@@ -1,11 +1,3 @@
1
1
  import React from 'react';
2
- export interface ToolbarButtonProps {
3
- label: React.ReactNode;
4
- title: string;
5
- bg: string;
6
- hoverBg: string;
7
- color: string;
8
- border: string;
9
- onClick: () => void;
10
- }
11
- export declare const ToolbarButton: React.FC<ToolbarButtonProps>;
2
+ import type { ToolbarButtonProps } from '../types';
3
+ export declare const ToolbarButton: React.ForwardRefExoticComponent<ToolbarButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -1,7 +1,9 @@
1
- import react, { useState } from "react";
2
- const ToolbarButton = ({ label, title, bg, hoverBg, color, border, onClick })=>{
1
+ import react, { forwardRef, useState } from "react";
2
+ const ToolbarButton = /*#__PURE__*/ forwardRef(({ label, title, backgroundColor, hoverBackgroundColor, textColor, borderColor, onClick, active }, ref)=>{
3
3
  const [hovered, setHovered] = useState(false);
4
+ const bg = active ? hoverBackgroundColor : hovered ? hoverBackgroundColor : backgroundColor;
4
5
  return /*#__PURE__*/ react.createElement("button", {
6
+ ref: ref,
5
7
  onClick: onClick,
6
8
  onMouseEnter: ()=>setHovered(true),
7
9
  onMouseLeave: ()=>setHovered(false),
@@ -9,9 +11,9 @@ const ToolbarButton = ({ label, title, bg, hoverBg, color, border, onClick })=>{
9
11
  display: 'inline-flex',
10
12
  alignItems: 'center',
11
13
  gap: 4,
12
- background: hovered ? hoverBg : bg,
13
- border: `1px solid ${border}`,
14
- color,
14
+ background: bg,
15
+ border: `1px solid ${borderColor}`,
16
+ color: textColor,
15
17
  cursor: 'pointer',
16
18
  fontSize: 12,
17
19
  padding: '4px 10px',
@@ -22,5 +24,6 @@ const ToolbarButton = ({ label, title, bg, hoverBg, color, border, onClick })=>{
22
24
  },
23
25
  title: title
24
26
  }, label);
25
- };
27
+ });
28
+ ToolbarButton.displayName = 'ToolbarButton';
26
29
  export { ToolbarButton };
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import type { ToolbarItem } from '../types';
2
3
  export interface ToolbarProps {
3
4
  title?: string;
4
5
  theme: 'dark' | 'light';
@@ -11,6 +12,9 @@ export interface ToolbarProps {
11
12
  enableFullscreen?: boolean;
12
13
  isFullscreen?: boolean;
13
14
  onToggleFullscreen?: () => void;
15
+ toolbar?: ToolbarItem[];
14
16
  toolbarExtra?: React.ReactNode;
17
+ /** 脚本说明内容(有值时显示"脚本说明"按钮) */
18
+ description?: string;
15
19
  }
16
20
  export declare const Toolbar: React.FC<ToolbarProps>;
@@ -1,7 +1,8 @@
1
- import react from "react";
1
+ import react, { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ToolbarButton } from "./toolbar-button.js";
3
3
  import { FormatIcon } from "./format-icon.js";
4
4
  import { MaximizeIcon, MinimizeIcon } from "./fullscreen-icon.js";
5
+ import { themes } from "./theme-colors.js";
5
6
  const HammerIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
6
7
  width: "14",
7
8
  height: "14",
@@ -28,13 +29,103 @@ const HammerIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
28
29
  opacity: 0.8,
29
30
  transform: "rotate(-25 12.25 14.5)"
30
31
  }));
31
- const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat, onFormat, enableCompile, onCompile, enableFullscreen, isFullscreen, onToggleFullscreen, toolbarExtra })=>{
32
+ const TIP_MARGIN = 6;
33
+ const TIP_MAX_WIDTH = 380;
34
+ function calcTooltipPos(anchorRect, el) {
35
+ const vw = window.innerWidth;
36
+ const vh = window.innerHeight;
37
+ const tipH = el?.scrollHeight ?? 200;
38
+ const spaceBelow = vh - anchorRect.bottom - TIP_MARGIN;
39
+ const spaceAbove = anchorRect.top - TIP_MARGIN;
40
+ const placeBelow = spaceBelow >= Math.min(tipH, 260) || spaceBelow >= spaceAbove;
41
+ const top = placeBelow ? anchorRect.bottom + TIP_MARGIN : Math.max(TIP_MARGIN, anchorRect.top - tipH - TIP_MARGIN);
42
+ let left = anchorRect.left;
43
+ const maxRight = vw - TIP_MARGIN;
44
+ if (left + TIP_MAX_WIDTH > maxRight) left = Math.max(TIP_MARGIN, maxRight - TIP_MAX_WIDTH);
45
+ const maxWidth = Math.min(TIP_MAX_WIDTH, maxRight - left);
46
+ const maxHeight = placeBelow ? spaceBelow - TIP_MARGIN : spaceAbove - TIP_MARGIN;
47
+ return {
48
+ top,
49
+ left,
50
+ maxWidth,
51
+ maxHeight: Math.max(maxHeight, 80)
52
+ };
53
+ }
54
+ const KV_RE = /^([^:]+):\s*([\s\S]+)$/;
55
+ function renderDescLine(line, accent) {
56
+ const m = line.match(KV_RE);
57
+ if (m) return /*#__PURE__*/ react.createElement("span", null, /*#__PURE__*/ react.createElement("span", {
58
+ style: {
59
+ color: accent,
60
+ fontWeight: 600
61
+ }
62
+ }, m[1]), /*#__PURE__*/ react.createElement("span", null, ": ", m[2]));
63
+ return line;
64
+ }
65
+ const QuestionIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
66
+ width: "13",
67
+ height: "13",
68
+ viewBox: "0 0 24 24",
69
+ fill: "none",
70
+ stroke: color,
71
+ strokeWidth: 2.2,
72
+ strokeLinecap: "round",
73
+ strokeLinejoin: "round",
74
+ style: {
75
+ flexShrink: 0
76
+ }
77
+ }, /*#__PURE__*/ react.createElement("circle", {
78
+ cx: "12",
79
+ cy: "12",
80
+ r: "10"
81
+ }), /*#__PURE__*/ react.createElement("path", {
82
+ d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"
83
+ }), /*#__PURE__*/ react.createElement("line", {
84
+ x1: "12",
85
+ y1: "17",
86
+ x2: "12.01",
87
+ y2: "17"
88
+ }));
89
+ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat, onFormat, enableCompile, onCompile, enableFullscreen, isFullscreen, onToggleFullscreen, toolbar, toolbarExtra, description })=>{
32
90
  const isDark = 'dark' === theme;
91
+ const colors = themes[theme];
33
92
  const borderColor = isDark ? '#434343' : '#d9d9d9';
34
93
  const toolbarBg = isDark ? '#282c34' : '#fafafa';
35
94
  const toolbarText = isDark ? '#abb2bf' : '#333';
36
95
  const btnBg = isDark ? '#2c313a' : '#f0f0f0';
37
96
  const btnHoverBg = isDark ? '#3e4451' : '#e0e0e0';
97
+ const [showDesc, setShowDesc] = useState(false);
98
+ const [tipPos, setTipPos] = useState(null);
99
+ const descBtnRef = useRef(null);
100
+ const tipRef = useRef(null);
101
+ const updateTipPos = useCallback(()=>{
102
+ if (!descBtnRef.current) return;
103
+ setTipPos(calcTooltipPos(descBtnRef.current.getBoundingClientRect(), tipRef.current));
104
+ }, []);
105
+ useEffect(()=>{
106
+ if (!showDesc) return;
107
+ updateTipPos();
108
+ window.addEventListener('scroll', updateTipPos, true);
109
+ window.addEventListener('resize', updateTipPos);
110
+ return ()=>{
111
+ window.removeEventListener('scroll', updateTipPos, true);
112
+ window.removeEventListener('resize', updateTipPos);
113
+ };
114
+ }, [
115
+ showDesc,
116
+ updateTipPos
117
+ ]);
118
+ useEffect(()=>{
119
+ if (!showDesc) setTipPos(null);
120
+ }, [
121
+ showDesc
122
+ ]);
123
+ useEffect(()=>{
124
+ if (!description) setShowDesc(false);
125
+ }, [
126
+ description
127
+ ]);
128
+ const descLines = description ? description.replace(/\\n/g, '\n').split('\n').filter((l)=>l.trim()) : [];
38
129
  const handleThemeToggle = ()=>{
39
130
  const next = isDark ? 'light' : 'dark';
40
131
  onThemeChange?.(next);
@@ -64,33 +155,45 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
64
155
  style: {
65
156
  marginRight: 'auto'
66
157
  }
158
+ }), descLines.length > 0 && /*#__PURE__*/ react.createElement(ToolbarButton, {
159
+ ref: descBtnRef,
160
+ label: /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(QuestionIcon, {
161
+ color: "currentColor"
162
+ }), "脚本说明"),
163
+ title: showDesc ? '隐藏脚本说明' : '查看脚本说明',
164
+ backgroundColor: isDark ? '#2d5a27' : '#e6f4e5',
165
+ hoverBackgroundColor: isDark ? '#3a7033' : '#d4ecd3',
166
+ textColor: isDark ? '#98c379' : '#389e0d',
167
+ borderColor: isDark ? '#2d5a27' : '#b7eb8f',
168
+ active: showDesc,
169
+ onClick: ()=>setShowDesc((v)=>!v)
67
170
  }), enableThemeToggle && /*#__PURE__*/ react.createElement(ToolbarButton, {
68
171
  label: isDark ? '🌞 浅色模式' : '🌙 深色模式',
69
172
  title: isDark ? '切换到浅色主题' : '切换到深色主题',
70
- bg: btnBg,
71
- hoverBg: btnHoverBg,
72
- color: toolbarText,
73
- border: borderColor,
173
+ backgroundColor: btnBg,
174
+ hoverBackgroundColor: btnHoverBg,
175
+ textColor: toolbarText,
176
+ borderColor: borderColor,
74
177
  onClick: handleThemeToggle
75
178
  }), enableFormat && onFormat && /*#__PURE__*/ react.createElement(ToolbarButton, {
76
179
  label: /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(FormatIcon, {
77
180
  color: isDark ? '#61afef' : '#1677ff'
78
181
  }), "格式化"),
79
182
  title: "格式化代码",
80
- bg: isDark ? '#1e3a5f' : '#e6f4ff',
81
- hoverBg: isDark ? '#264a73' : '#bae0ff',
82
- color: isDark ? '#61afef' : '#1677ff',
83
- border: isDark ? '#1e3a5f' : '#91caff',
183
+ backgroundColor: isDark ? '#1e3a5f' : '#e6f4ff',
184
+ hoverBackgroundColor: isDark ? '#264a73' : '#bae0ff',
185
+ textColor: isDark ? '#61afef' : '#1677ff',
186
+ borderColor: isDark ? '#1e3a5f' : '#91caff',
84
187
  onClick: onFormat
85
188
  }), enableCompile && onCompile && /*#__PURE__*/ react.createElement(ToolbarButton, {
86
189
  label: /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(HammerIcon, {
87
190
  color: isDark ? '#98c379' : '#389e0d'
88
191
  }), "编译验证"),
89
192
  title: "编译并验证脚本",
90
- bg: isDark ? '#2d5a27' : '#e6f4e5',
91
- hoverBg: isDark ? '#3a7033' : '#d4ecd3',
92
- color: isDark ? '#98c379' : '#389e0d',
93
- border: isDark ? '#2d5a27' : '#b7eb8f',
193
+ backgroundColor: isDark ? '#2d5a27' : '#e6f4e5',
194
+ hoverBackgroundColor: isDark ? '#3a7033' : '#d4ecd3',
195
+ textColor: isDark ? '#98c379' : '#389e0d',
196
+ borderColor: isDark ? '#2d5a27' : '#b7eb8f',
94
197
  onClick: onCompile
95
198
  }), enableFullscreen && /*#__PURE__*/ react.createElement(ToolbarButton, {
96
199
  label: /*#__PURE__*/ react.createElement(react.Fragment, null, isFullscreen ? /*#__PURE__*/ react.createElement(MinimizeIcon, {
@@ -99,11 +202,46 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
99
202
  color: isDark ? '#c678dd' : '#722ed1'
100
203
  }), isFullscreen ? '退出全屏' : '全屏模式'),
101
204
  title: isFullscreen ? '退出全屏(ESC)' : '全屏显示',
102
- bg: isDark ? '#3a2d5a' : '#f3e8ff',
103
- hoverBg: isDark ? '#4a3d6a' : '#e9d5ff',
104
- color: isDark ? '#c678dd' : '#722ed1',
105
- border: isDark ? '#3a2d5a' : '#d3adf7',
205
+ backgroundColor: isDark ? '#3a2d5a' : '#f3e8ff',
206
+ hoverBackgroundColor: isDark ? '#4a3d6a' : '#e9d5ff',
207
+ textColor: isDark ? '#c678dd' : '#722ed1',
208
+ borderColor: isDark ? '#3a2d5a' : '#d3adf7',
106
209
  onClick: ()=>onToggleFullscreen?.()
107
- }), toolbarExtra);
210
+ }), toolbar?.map((item)=>/*#__PURE__*/ react.createElement(ToolbarButton, {
211
+ key: item.key,
212
+ label: item.label,
213
+ title: item.title,
214
+ backgroundColor: item.backgroundColor,
215
+ hoverBackgroundColor: item.hoverBackgroundColor,
216
+ textColor: item.textColor,
217
+ borderColor: item.borderColor,
218
+ onClick: item.onClick
219
+ })), toolbarExtra, showDesc && tipPos && descLines.length > 0 && /*#__PURE__*/ react.createElement("div", {
220
+ ref: tipRef,
221
+ style: {
222
+ position: 'fixed',
223
+ top: tipPos.top,
224
+ left: tipPos.left,
225
+ maxWidth: tipPos.maxWidth,
226
+ maxHeight: tipPos.maxHeight,
227
+ overflowY: 'auto',
228
+ padding: '10px 14px',
229
+ background: colors.headerBg,
230
+ border: `1px solid ${colors.border}`,
231
+ borderRadius: 6,
232
+ color: colors.text,
233
+ fontSize: 12,
234
+ lineHeight: 1.65,
235
+ wordBreak: 'break-word',
236
+ zIndex: 9999,
237
+ boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
238
+ pointerEvents: 'auto'
239
+ }
240
+ }, descLines.map((line, i)=>/*#__PURE__*/ react.createElement("div", {
241
+ key: i,
242
+ style: {
243
+ marginBottom: i < descLines.length - 1 ? 4 : 0
244
+ }
245
+ }, renderDescLine(line, colors.accent)))));
108
246
  };
109
247
  export { Toolbar };
@@ -3,21 +3,24 @@ import { SectionHeader } from "./section-header.js";
3
3
  import { VariableRow } from "./variable-row.js";
4
4
  const VariablesSection = ({ sortedBinds, sortedRequests, colors })=>{
5
5
  if (0 === sortedBinds.length && 0 === sortedRequests.length) return null;
6
- return /*#__PURE__*/ react.createElement("div", null, /*#__PURE__*/ react.createElement(SectionHeader, {
6
+ return /*#__PURE__*/ react.createElement("div", null, sortedRequests.length > 0 && /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(SectionHeader, {
7
7
  colors: colors,
8
- label: "变量"
8
+ label: "函数入参"
9
+ }), sortedRequests.map((req)=>/*#__PURE__*/ react.createElement(VariableRow, {
10
+ key: req.name,
11
+ name: req.name,
12
+ dataType: req.dataType,
13
+ description: req.description,
14
+ colors: colors
15
+ }))), sortedBinds.length > 0 && /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(SectionHeader, {
16
+ colors: colors,
17
+ label: "绑定参数"
9
18
  }), sortedBinds.map((bind)=>/*#__PURE__*/ react.createElement(VariableRow, {
10
19
  key: bind.name,
11
20
  name: bind.name,
12
21
  dataType: bind.dataType,
13
22
  description: bind.description,
14
23
  colors: colors
15
- })), sortedRequests.map((req)=>/*#__PURE__*/ react.createElement(VariableRow, {
16
- key: req.name,
17
- name: req.name,
18
- dataType: req.dataType,
19
- description: req.description,
20
- colors: colors
21
- })));
24
+ }))));
22
25
  };
23
26
  export { VariablesSection };
@@ -181,7 +181,7 @@ function buildLayoutExtensions(fontSize, minHeight, maxHeight, isFullscreen) {
181
181
  });
182
182
  }
183
183
  const ScriptCodeEditor = (props)=>{
184
- const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange, placeholder = '请输入 Groovy 脚本...', defaultTheme, title, metadata, defaultSidebarOpen, enableThemeToggle, enableFormat, enableCompile, enableFullscreen, toolbarExtra, options = {} } = props;
184
+ const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange, placeholder = '请输入 Groovy 脚本...', defaultTheme, title, metadata, defaultSidebarOpen, enableThemeToggle, enableFormat, enableCompile, enableFullscreen, toolbar, toolbarExtra, options = {} } = props;
185
185
  const { fontSize = 14, minHeight = 300, maxHeight = 300 } = options;
186
186
  const [internalTheme, setInternalTheme] = useState(defaultTheme ?? 'dark');
187
187
  useEffect(()=>{
@@ -368,7 +368,9 @@ const ScriptCodeEditor = (props)=>{
368
368
  enableFullscreen: enableFullscreen,
369
369
  isFullscreen: isFullscreen,
370
370
  onToggleFullscreen: ()=>setIsFullscreen((prev)=>!prev),
371
- toolbarExtra: toolbarExtra
371
+ toolbar: toolbar,
372
+ toolbarExtra: toolbarExtra,
373
+ description: metadata?.description
372
374
  }), /*#__PURE__*/ react.createElement("div", {
373
375
  style: {
374
376
  display: 'flex',
@@ -1,4 +1,28 @@
1
1
  import type React from 'react';
2
+ /** 工具栏按钮属性 */
3
+ export interface ToolbarButtonProps {
4
+ /** 按钮内容(支持 ReactNode,可包含图标 + 文字) */
5
+ label: React.ReactNode;
6
+ /** 鼠标悬停提示文字(原生 title 属性) */
7
+ title: string;
8
+ /** 按钮背景色 */
9
+ backgroundColor?: string;
10
+ /** 鼠标悬停时的背景色 */
11
+ hoverBackgroundColor?: string;
12
+ /** 文字颜色 */
13
+ textColor?: string;
14
+ /** 边框颜色 */
15
+ borderColor?: string;
16
+ /** 点击回调 */
17
+ onClick: () => void;
18
+ /** 是否处于激活状态(可选,激活时使用 hoverBackgroundColor 作为背景色) */
19
+ active?: boolean;
20
+ }
21
+ /** 工具栏自定义按钮项(用于 toolbar 数组) */
22
+ export interface ToolbarItem extends ToolbarButtonProps {
23
+ /** 唯一标识(React key) */
24
+ key: string;
25
+ }
2
26
  /** 函数参数信息 */
3
27
  export interface ScriptParameterInfo {
4
28
  dataType: string;
@@ -77,6 +101,8 @@ export interface ScriptCodeEditorProps {
77
101
  onFormat?: () => void;
78
102
  /** 编译/测试脚本回调(enableCompile 时需提供) */
79
103
  onCompile?: (code: string) => void;
104
+ /** 工具栏自定义按钮列表(渲染在内置按钮之后) */
105
+ toolbar?: ToolbarItem[];
80
106
  /** 工具栏额外内容(渲染在内置按钮之后,任意 React 节点) */
81
107
  toolbarExtra?: React.ReactNode;
82
108
  /** 其他选项 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coding-script/script-engine",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "script-engine components",
5
5
  "keywords": [
6
6
  "coding-script",