@forgecharts/sdk 1.1.23 → 1.1.27
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/package.json +28 -4
- package/src/internal.ts +1 -1
- package/src/react/canvas/ChartCanvas.tsx +984 -0
- package/src/react/canvas/ChartContextMenu.tsx +60 -0
- package/src/react/canvas/ChartSettingsDialog.tsx +133 -0
- package/src/react/canvas/IndicatorLabel.tsx +347 -0
- package/src/react/canvas/IndicatorPane.tsx +503 -0
- package/src/react/canvas/PointerOverlay.tsx +126 -0
- package/src/react/canvas/toolbars/LeftToolbar.tsx +1096 -0
- package/src/react/hooks/useChartCapabilities.ts +76 -0
- package/src/react/index.ts +51 -0
- package/src/react/internal.ts +62 -0
- package/src/react/shell/ManagedAppShell.tsx +699 -0
- package/src/react/trading/TradingBridge.ts +156 -0
- package/src/react/workspace/ChartWorkspace.tsx +228 -0
- package/src/react/workspace/FloatingPanel.tsx +131 -0
- package/src/react/workspace/IndicatorsDialog.tsx +246 -0
- package/src/react/workspace/LayoutMenu.tsx +345 -0
- package/src/react/workspace/SymbolSearchDialog.tsx +377 -0
- package/src/react/workspace/TabBar.tsx +87 -0
- package/src/react/workspace/toolbars/BottomToolbar.tsx +372 -0
- package/src/react/workspace/toolbars/RightToolbar.tsx +46 -0
- package/src/react/workspace/toolbars/TopToolbar.tsx +431 -0
- package/tsconfig.json +2 -1
- package/tsup.config.ts +4 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChartContextMenu — right-click context menu for the chart canvas.
|
|
3
|
+
*
|
|
4
|
+
* Placed at fixed (x, y) on screen. Clicking outside dismisses it via the
|
|
5
|
+
* transparent backdrop. The parent is responsible for calling onClose.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onSettings: () => void;
|
|
13
|
+
onClearDrawings: () => void;
|
|
14
|
+
onClearIndicators: () => void;
|
|
15
|
+
onResetView: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function ChartContextMenu({
|
|
19
|
+
x,
|
|
20
|
+
y,
|
|
21
|
+
onClose,
|
|
22
|
+
onSettings,
|
|
23
|
+
onClearDrawings,
|
|
24
|
+
onClearIndicators,
|
|
25
|
+
onResetView,
|
|
26
|
+
}: Props) {
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<div className="chart-ctx-backdrop" onClick={onClose} onContextMenu={(e) => { e.preventDefault(); onClose(); }} />
|
|
30
|
+
<div className="chart-ctx-menu" style={{ left: x, top: y }}>
|
|
31
|
+
<button
|
|
32
|
+
className="chart-ctx-item"
|
|
33
|
+
onClick={() => { onSettings(); onClose(); }}
|
|
34
|
+
>
|
|
35
|
+
Settings…
|
|
36
|
+
</button>
|
|
37
|
+
<div className="chart-ctx-divider" />
|
|
38
|
+
<button
|
|
39
|
+
className="chart-ctx-item"
|
|
40
|
+
onClick={() => { onResetView(); onClose(); }}
|
|
41
|
+
>
|
|
42
|
+
Reset Chart View
|
|
43
|
+
</button>
|
|
44
|
+
<div className="chart-ctx-divider" />
|
|
45
|
+
<button
|
|
46
|
+
className="chart-ctx-item chart-ctx-item--danger"
|
|
47
|
+
onClick={() => { onClearIndicators(); onClose(); }}
|
|
48
|
+
>
|
|
49
|
+
Clear Indicators
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
className="chart-ctx-item chart-ctx-item--danger"
|
|
53
|
+
onClick={() => { onClearDrawings(); onClose(); }}
|
|
54
|
+
>
|
|
55
|
+
Clear Drawings
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChartSettingsDialog — canvas settings modal.
|
|
3
|
+
*
|
|
4
|
+
* Sections (sidebar nav):
|
|
5
|
+
* • Canvas — background color, grid visibility, grid color
|
|
6
|
+
*
|
|
7
|
+
* Additional sections can be added by extending SECTIONS and adding a matching
|
|
8
|
+
* content renderer in renderContent().
|
|
9
|
+
*
|
|
10
|
+
* The parent passes `initialSettings` and receives the final values via `onApply`.
|
|
11
|
+
* Nothing is persisted here; persistence (localStorage / DB) lives in the parent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
|
|
16
|
+
export type CanvasSettings = {
|
|
17
|
+
background: string;
|
|
18
|
+
gridColor: string;
|
|
19
|
+
gridVisible: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Section = 'canvas';
|
|
23
|
+
|
|
24
|
+
const SECTIONS: { id: Section; label: string }[] = [
|
|
25
|
+
{ id: 'canvas', label: 'Canvas' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
type Props = {
|
|
29
|
+
initialSettings: CanvasSettings;
|
|
30
|
+
onApply: (settings: CanvasSettings) => void;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ChartSettingsDialog({ initialSettings, onApply, onClose }: Props) {
|
|
35
|
+
const [activeSection, setActiveSection] = useState<Section>('canvas');
|
|
36
|
+
const [settings, setSettings] = useState<CanvasSettings>(initialSettings);
|
|
37
|
+
|
|
38
|
+
const update = <K extends keyof CanvasSettings>(key: K, value: CanvasSettings[K]) =>
|
|
39
|
+
setSettings((prev) => ({ ...prev, [key]: value }));
|
|
40
|
+
|
|
41
|
+
const handleApply = () => {
|
|
42
|
+
onApply(settings);
|
|
43
|
+
onClose();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="csettings-backdrop" onClick={onClose}>
|
|
48
|
+
<div className="csettings-dialog" onClick={(e) => e.stopPropagation()}>
|
|
49
|
+
|
|
50
|
+
{/* ── Header ──────────────────────────────────────────────────── */}
|
|
51
|
+
<div className="csettings-header">
|
|
52
|
+
<span className="csettings-title">Chart Settings</span>
|
|
53
|
+
<button className="csettings-close" onClick={onClose} aria-label="Close">✕</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="csettings-body">
|
|
57
|
+
{/* ── Sidebar ─────────────────────────────────────────────── */}
|
|
58
|
+
<nav className="csettings-sidebar">
|
|
59
|
+
{SECTIONS.map(({ id, label }) => (
|
|
60
|
+
<button
|
|
61
|
+
key={id}
|
|
62
|
+
className={`csettings-nav-item${activeSection === id ? ' is-active' : ''}`}
|
|
63
|
+
onClick={() => setActiveSection(id)}
|
|
64
|
+
>
|
|
65
|
+
{label}
|
|
66
|
+
</button>
|
|
67
|
+
))}
|
|
68
|
+
</nav>
|
|
69
|
+
|
|
70
|
+
{/* ── Content ─────────────────────────────────────────────── */}
|
|
71
|
+
<div className="csettings-content">
|
|
72
|
+
{activeSection === 'canvas' && (
|
|
73
|
+
<div className="csettings-section">
|
|
74
|
+
|
|
75
|
+
<div className="csettings-row">
|
|
76
|
+
<label className="csettings-label">Background</label>
|
|
77
|
+
<div className="csettings-color-wrap">
|
|
78
|
+
<input
|
|
79
|
+
type="color"
|
|
80
|
+
className="csettings-color-input"
|
|
81
|
+
value={settings.background}
|
|
82
|
+
onChange={(e) => update('background', e.target.value)}
|
|
83
|
+
/>
|
|
84
|
+
<span className="csettings-color-hex">{settings.background}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="csettings-row">
|
|
89
|
+
<label className="csettings-label">Grid Lines</label>
|
|
90
|
+
<label className="csettings-toggle">
|
|
91
|
+
<input
|
|
92
|
+
type="checkbox"
|
|
93
|
+
checked={settings.gridVisible}
|
|
94
|
+
onChange={(e) => update('gridVisible', e.target.checked)}
|
|
95
|
+
/>
|
|
96
|
+
<span className="csettings-toggle-track" />
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{settings.gridVisible && (
|
|
101
|
+
<div className="csettings-row">
|
|
102
|
+
<label className="csettings-label">Grid Color</label>
|
|
103
|
+
<div className="csettings-color-wrap">
|
|
104
|
+
<input
|
|
105
|
+
type="color"
|
|
106
|
+
className="csettings-color-input"
|
|
107
|
+
value={settings.gridColor}
|
|
108
|
+
onChange={(e) => update('gridColor', e.target.value)}
|
|
109
|
+
/>
|
|
110
|
+
<span className="csettings-color-hex">{settings.gridColor}</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* ── Footer ──────────────────────────────────────────────────── */}
|
|
121
|
+
<div className="csettings-footer">
|
|
122
|
+
<button className="csettings-btn csettings-btn--ghost" onClick={onClose}>
|
|
123
|
+
Cancel
|
|
124
|
+
</button>
|
|
125
|
+
<button className="csettings-btn csettings-btn--primary" onClick={handleApply}>
|
|
126
|
+
Apply
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndicatorLabel — hover-menu label rendered over a chart pane.
|
|
3
|
+
*
|
|
4
|
+
* Shows the indicator display name in the top-left of a pane.
|
|
5
|
+
* On hover a gear (configure) and × (remove) button appear inline, giving the
|
|
6
|
+
* appearance that the menu is part of the label pill.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import type { IndicatorConfig, IndicatorInstance } from '@forgecharts/types';
|
|
11
|
+
|
|
12
|
+
// ─── Script title extractor ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function _scriptTitle(src: string): string {
|
|
15
|
+
const m = src.match(/indicator\s*\(\s*["']([^"']+)["']/);
|
|
16
|
+
return m ? m[1]! : 'Script';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Display name helper ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export function indicatorDisplayName(config: IndicatorConfig): string {
|
|
22
|
+
const p = (config.params ?? {}) as Record<string, unknown>;
|
|
23
|
+
switch (config.type) {
|
|
24
|
+
case 'sma': return `SMA(${p['period'] ?? 20})`;
|
|
25
|
+
case 'ema': return `EMA(${p['period'] ?? 20})`;
|
|
26
|
+
case 'wma': return `WMA(${p['period'] ?? 20})`;
|
|
27
|
+
case 'rsi': return `RSI(${p['period'] ?? 14})`;
|
|
28
|
+
case 'macd': return `MACD(${p['fast'] ?? 12},${p['slow'] ?? 26},${p['signal'] ?? 9})`;
|
|
29
|
+
case 'bbands': return `BB(${p['period'] ?? 20},${p['multiplier'] ?? 2})`;
|
|
30
|
+
case 'stochastic': return `Stoch(${p['k'] ?? 14},${p['d'] ?? 3})`;
|
|
31
|
+
case 'atr': return `ATR(${p['period'] ?? 14})`;
|
|
32
|
+
case 'obv': return 'OBV';
|
|
33
|
+
case 'volume': return 'Volume';
|
|
34
|
+
case 'script': return config.script ? _scriptTitle(config.script) : 'Script';
|
|
35
|
+
default: return (config.type as string).toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Param definitions for config dialog ─────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
type ParamDef = { key: string; label: string; min?: number; step?: number };
|
|
42
|
+
|
|
43
|
+
const PARAM_DEFS: Record<string, ParamDef[]> = {
|
|
44
|
+
sma: [{ key: 'period', label: 'Period', min: 1 }],
|
|
45
|
+
ema: [{ key: 'period', label: 'Period', min: 1 }],
|
|
46
|
+
wma: [{ key: 'period', label: 'Period', min: 1 }],
|
|
47
|
+
rsi: [{ key: 'period', label: 'Period', min: 1 }],
|
|
48
|
+
macd: [
|
|
49
|
+
{ key: 'fast', label: 'Fast', min: 1 },
|
|
50
|
+
{ key: 'slow', label: 'Slow', min: 1 },
|
|
51
|
+
{ key: 'signal', label: 'Signal', min: 1 },
|
|
52
|
+
],
|
|
53
|
+
bbands: [
|
|
54
|
+
{ key: 'period', label: 'Period', min: 1 },
|
|
55
|
+
{ key: 'multiplier', label: 'Multiplier', min: 0.1, step: 0.1 },
|
|
56
|
+
],
|
|
57
|
+
stochastic: [
|
|
58
|
+
{ key: 'k', label: 'K Period', min: 1 },
|
|
59
|
+
{ key: 'd', label: 'D Period', min: 1 },
|
|
60
|
+
],
|
|
61
|
+
atr: [{ key: 'period', label: 'Period', min: 1 }],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ─── IndicatorLabel ────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function IndicatorLabel({
|
|
67
|
+
indicator,
|
|
68
|
+
color,
|
|
69
|
+
onRemove,
|
|
70
|
+
onConfigure,
|
|
71
|
+
}: {
|
|
72
|
+
indicator: IndicatorInstance;
|
|
73
|
+
color?: string;
|
|
74
|
+
onRemove: () => void;
|
|
75
|
+
onConfigure: () => void;
|
|
76
|
+
}) {
|
|
77
|
+
const [hovered, setHovered] = useState(false);
|
|
78
|
+
const name = indicatorDisplayName(indicator.config);
|
|
79
|
+
const hasDefs = Boolean(PARAM_DEFS[indicator.config.type]);
|
|
80
|
+
|
|
81
|
+
const btnStyle: React.CSSProperties = {
|
|
82
|
+
display: 'flex',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
width: 18,
|
|
86
|
+
height: '100%',
|
|
87
|
+
border: 'none',
|
|
88
|
+
borderLeft: '1px solid rgba(255,255,255,0.08)',
|
|
89
|
+
background: 'transparent',
|
|
90
|
+
color: '#787b86',
|
|
91
|
+
cursor: 'pointer',
|
|
92
|
+
fontSize: 10,
|
|
93
|
+
padding: 0,
|
|
94
|
+
lineHeight: 1,
|
|
95
|
+
flexShrink: 0,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
style={{
|
|
101
|
+
display: 'inline-flex',
|
|
102
|
+
alignItems: 'stretch',
|
|
103
|
+
height: 18,
|
|
104
|
+
borderRadius: 3,
|
|
105
|
+
overflow: 'hidden',
|
|
106
|
+
background: 'rgba(0,0,0,0.5)',
|
|
107
|
+
cursor: 'default',
|
|
108
|
+
userSelect: 'none',
|
|
109
|
+
}}
|
|
110
|
+
onMouseEnter={() => setHovered(true)}
|
|
111
|
+
onMouseLeave={() => setHovered(false)}
|
|
112
|
+
>
|
|
113
|
+
{/* Name */}
|
|
114
|
+
<span
|
|
115
|
+
style={{
|
|
116
|
+
color: color ?? '#d1d4dc',
|
|
117
|
+
fontSize: 11,
|
|
118
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
119
|
+
fontWeight: 500,
|
|
120
|
+
padding: '0 5px',
|
|
121
|
+
display: 'flex',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{name}
|
|
126
|
+
</span>
|
|
127
|
+
|
|
128
|
+
{/* Hover actions */}
|
|
129
|
+
{hovered && (
|
|
130
|
+
<>
|
|
131
|
+
{hasDefs && (
|
|
132
|
+
<button
|
|
133
|
+
onClick={(e) => { e.stopPropagation(); onConfigure(); }}
|
|
134
|
+
title="Configure"
|
|
135
|
+
style={btnStyle}
|
|
136
|
+
>
|
|
137
|
+
⚙
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
<button
|
|
141
|
+
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
|
142
|
+
title="Remove"
|
|
143
|
+
style={{ ...btnStyle, borderRadius: '0 3px 3px 0' }}
|
|
144
|
+
>
|
|
145
|
+
✕
|
|
146
|
+
</button>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── IndicatorConfigDialog ─────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export function IndicatorConfigDialog({
|
|
156
|
+
indicator,
|
|
157
|
+
onSave,
|
|
158
|
+
onClose,
|
|
159
|
+
}: {
|
|
160
|
+
indicator: IndicatorInstance;
|
|
161
|
+
onSave: (newConfig: IndicatorConfig) => void;
|
|
162
|
+
onClose: () => void;
|
|
163
|
+
}) {
|
|
164
|
+
const defs = PARAM_DEFS[indicator.config.type] ?? [];
|
|
165
|
+
const [values, setValues] = useState<Record<string, string>>(() => {
|
|
166
|
+
const p = (indicator.config.params ?? {}) as Record<string, unknown>;
|
|
167
|
+
return Object.fromEntries(defs.map((d) => [d.key, String(p[d.key] ?? '')]));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const handleSave = () => {
|
|
171
|
+
const newParams: Record<string, number | string | boolean> = {
|
|
172
|
+
...(indicator.config.params ?? {}),
|
|
173
|
+
};
|
|
174
|
+
for (const def of defs) {
|
|
175
|
+
const v = parseFloat(values[def.key] ?? '');
|
|
176
|
+
if (!isNaN(v)) newParams[def.key] = v;
|
|
177
|
+
}
|
|
178
|
+
onSave({ ...indicator.config, params: newParams });
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const name = indicatorDisplayName(indicator.config);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
style={{
|
|
186
|
+
position: 'fixed',
|
|
187
|
+
inset: 0,
|
|
188
|
+
zIndex: 1000,
|
|
189
|
+
display: 'flex',
|
|
190
|
+
alignItems: 'center',
|
|
191
|
+
justifyContent: 'center',
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
{/* Backdrop */}
|
|
195
|
+
<div
|
|
196
|
+
onClick={onClose}
|
|
197
|
+
style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.55)' }}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
{/* Panel */}
|
|
201
|
+
<div
|
|
202
|
+
style={{
|
|
203
|
+
position: 'relative',
|
|
204
|
+
background: '#1c2030',
|
|
205
|
+
border: '1px solid #2a2e39',
|
|
206
|
+
borderRadius: 8,
|
|
207
|
+
padding: '20px 24px',
|
|
208
|
+
minWidth: 280,
|
|
209
|
+
zIndex: 1,
|
|
210
|
+
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
{/* Header */}
|
|
214
|
+
<div
|
|
215
|
+
style={{
|
|
216
|
+
display: 'flex',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
justifyContent: 'space-between',
|
|
219
|
+
marginBottom: 20,
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<span
|
|
223
|
+
style={{
|
|
224
|
+
color: '#d1d4dc',
|
|
225
|
+
fontSize: 14,
|
|
226
|
+
fontWeight: 600,
|
|
227
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{name}
|
|
231
|
+
</span>
|
|
232
|
+
<button
|
|
233
|
+
onClick={onClose}
|
|
234
|
+
style={{
|
|
235
|
+
background: 'transparent',
|
|
236
|
+
border: 'none',
|
|
237
|
+
color: '#787b86',
|
|
238
|
+
cursor: 'pointer',
|
|
239
|
+
fontSize: 16,
|
|
240
|
+
lineHeight: 1,
|
|
241
|
+
padding: 0,
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
✕
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Fields */}
|
|
249
|
+
{defs.map((def) => (
|
|
250
|
+
<div key={def.key} style={{ marginBottom: 14 }}>
|
|
251
|
+
<label
|
|
252
|
+
style={{
|
|
253
|
+
display: 'block',
|
|
254
|
+
color: '#787b86',
|
|
255
|
+
fontSize: 11,
|
|
256
|
+
marginBottom: 4,
|
|
257
|
+
textTransform: 'uppercase',
|
|
258
|
+
letterSpacing: '0.05em',
|
|
259
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{def.label}
|
|
263
|
+
</label>
|
|
264
|
+
<input
|
|
265
|
+
type="number"
|
|
266
|
+
value={values[def.key] ?? ''}
|
|
267
|
+
min={def.min}
|
|
268
|
+
step={def.step ?? 1}
|
|
269
|
+
onChange={(e) =>
|
|
270
|
+
setValues((v) => ({ ...v, [def.key]: e.target.value }))
|
|
271
|
+
}
|
|
272
|
+
style={{
|
|
273
|
+
width: '100%',
|
|
274
|
+
background: '#131722',
|
|
275
|
+
border: '1px solid #2a2e39',
|
|
276
|
+
borderRadius: 4,
|
|
277
|
+
padding: '6px 8px',
|
|
278
|
+
color: '#d1d4dc',
|
|
279
|
+
fontSize: 13,
|
|
280
|
+
outline: 'none',
|
|
281
|
+
boxSizing: 'border-box',
|
|
282
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
))}
|
|
287
|
+
|
|
288
|
+
{defs.length === 0 && (
|
|
289
|
+
<p
|
|
290
|
+
style={{
|
|
291
|
+
color: '#787b86',
|
|
292
|
+
fontSize: 12,
|
|
293
|
+
margin: '0 0 16px',
|
|
294
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
No configurable parameters.
|
|
298
|
+
</p>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{/* Actions */}
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
display: 'flex',
|
|
305
|
+
gap: 8,
|
|
306
|
+
justifyContent: 'flex-end',
|
|
307
|
+
marginTop: defs.length > 0 ? 8 : 0,
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
<button
|
|
311
|
+
onClick={onClose}
|
|
312
|
+
style={{
|
|
313
|
+
background: 'transparent',
|
|
314
|
+
border: '1px solid #2a2e39',
|
|
315
|
+
borderRadius: 4,
|
|
316
|
+
color: '#787b86',
|
|
317
|
+
padding: '6px 16px',
|
|
318
|
+
cursor: 'pointer',
|
|
319
|
+
fontSize: 13,
|
|
320
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
Cancel
|
|
324
|
+
</button>
|
|
325
|
+
{defs.length > 0 && (
|
|
326
|
+
<button
|
|
327
|
+
onClick={handleSave}
|
|
328
|
+
style={{
|
|
329
|
+
background: '#2962ff',
|
|
330
|
+
border: 'none',
|
|
331
|
+
borderRadius: 4,
|
|
332
|
+
color: '#fff',
|
|
333
|
+
padding: '6px 16px',
|
|
334
|
+
cursor: 'pointer',
|
|
335
|
+
fontSize: 13,
|
|
336
|
+
fontWeight: 500,
|
|
337
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
Apply
|
|
341
|
+
</button>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|