@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.
@@ -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
+ }