@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,431 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import type { ChartTheme, IndicatorConfig } from '@forgecharts/types';
3
+ import { LayoutMenu } from '../LayoutMenu';
4
+ import { SymbolSearchDialog } from '../SymbolSearchDialog';
5
+ import type { ISymbolResolver } from '@forgecharts/types';
6
+ import { IndicatorsDialog } from '../IndicatorsDialog';
7
+
8
+ // ─── Timeframe data ───────────────────────────────────────────────────────────
9
+
10
+ export const DEFAULT_FAVORITES: string[] = ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'];
11
+
12
+ const TF_LABELS: Record<string, string> = {
13
+ '1m': '1 minute', '2m': '2 minutes', '3m': '3 minutes', '4m': '4 minutes',
14
+ '5m': '5 minutes', '10m': '10 minutes', '15m': '15 minutes', '20m': '20 minutes',
15
+ '30m': '30 minutes', '45m': '45 minutes',
16
+ '1h': '1 hour', '2h': '2 hours', '3h': '3 hours', '4h': '4 hours',
17
+ '6h': '6 hours', '8h': '8 hours', '12h': '12 hours',
18
+ '1d': '1 day', '2d': '2 days', '3d': '3 days', '1w': '1 week',
19
+ '2w': '2 weeks', '1M': '1 month', '3M': '3 months', '6M': '6 months', '12M': '12 months',
20
+ };
21
+
22
+ const TF_GROUPS_ALL: { label: string; items: string[] }[] = [
23
+ { label: 'Minutes', items: ['1m', '2m', '3m', '4m', '5m', '10m', '15m', '20m', '30m', '45m'] },
24
+ { label: 'Hours', items: ['1h', '2h', '3h', '4h', '6h', '8h', '12h'] },
25
+ { label: 'Days', items: ['1d', '2d', '3d', '1w', '2w', '1M', '3M', '6M', '12M'] },
26
+ ];
27
+
28
+ // ─── Timeframe dropdown ───────────────────────────────────────────────────────
29
+
30
+ function TimeframeDropdown({
31
+ timeframe,
32
+ customTimeframes,
33
+ favorites,
34
+ onSelect,
35
+ onToggleFavorite,
36
+ onAddCustom,
37
+ onClose,
38
+ }: {
39
+ timeframe: string;
40
+ customTimeframes: string[];
41
+ favorites: string[];
42
+ onSelect: (tf: string) => void;
43
+ onToggleFavorite: (tf: string) => void;
44
+ onAddCustom: (tf: string) => void;
45
+ onClose: () => void;
46
+ }) {
47
+ const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
48
+ const [addingCustom, setAddingCustom] = useState(false);
49
+ const [input, setInput] = useState('');
50
+ const ref = useRef<HTMLDivElement>(null);
51
+
52
+ useEffect(() => {
53
+ const handler = (e: MouseEvent) => {
54
+ if (ref.current && !ref.current.contains(e.target as Node)) onClose();
55
+ };
56
+ document.addEventListener('mousedown', handler);
57
+ return () => document.removeEventListener('mousedown', handler);
58
+ }, [onClose]);
59
+
60
+ const commit = () => {
61
+ const val = input.trim();
62
+ if (!val) return;
63
+ onAddCustom(val);
64
+ onSelect(val);
65
+ setInput('');
66
+ onClose();
67
+ };
68
+
69
+ const toggleGroup = (label: string) =>
70
+ setCollapsed((prev) => ({ ...prev, [label]: !prev[label] }));
71
+
72
+ return (
73
+ <div ref={ref} className="tf-dropdown">
74
+ <button
75
+ className="tf-dropdown-add-custom"
76
+ onClick={() => setAddingCustom((a) => !a)}
77
+ >
78
+ + Add custom interval...
79
+ </button>
80
+
81
+ {addingCustom && (
82
+ <div className="tf-dropdown-add-row">
83
+ <input
84
+ className="tf-dropdown-input"
85
+ type="text"
86
+ value={input}
87
+ placeholder="e.g. 2h, 45m"
88
+ onChange={(e) => setInput(e.target.value)}
89
+ onKeyDown={(e) => { if (e.key === 'Enter') commit(); }}
90
+ autoFocus
91
+ />
92
+ <button
93
+ className="tf-dropdown-add-btn"
94
+ disabled={!input.trim()}
95
+ onClick={commit}
96
+ >
97
+ Add
98
+ </button>
99
+ </div>
100
+ )}
101
+
102
+ {TF_GROUPS_ALL.map((group) => (
103
+ <div key={group.label}>
104
+ <button
105
+ className="tf-dropdown-group-hdr"
106
+ onClick={() => toggleGroup(group.label)}
107
+ >
108
+ <span>{group.label}</span>
109
+ <span className={`tf-dropdown-chevron${collapsed[group.label] ? ' collapsed' : ''}`}>∧</span>
110
+ </button>
111
+ {!collapsed[group.label] && group.items.map((tf) => (
112
+ <button
113
+ key={tf}
114
+ className="tf-dropdown-row"
115
+ onClick={() => { onSelect(tf); onClose(); }}
116
+ >
117
+ <span>{TF_LABELS[tf] ?? tf}</span>
118
+ <button
119
+ className="tf-star-btn"
120
+ title={favorites.includes(tf) ? 'Remove from toolbar' : 'Add to toolbar'}
121
+ onClick={(e) => { e.stopPropagation(); onToggleFavorite(tf); }}
122
+ >
123
+ <span className={`tf-star${favorites.includes(tf) ? ' favorited' : ''}`}>★</span>
124
+ </button>
125
+ </button>
126
+ ))}
127
+ </div>
128
+ ))}
129
+
130
+ {customTimeframes.length > 0 && (
131
+ <div>
132
+ <button
133
+ className="tf-dropdown-group-hdr"
134
+ onClick={() => toggleGroup('Custom')}
135
+ >
136
+ <span>Custom</span>
137
+ <span className={`tf-dropdown-chevron${collapsed['Custom'] ? ' collapsed' : ''}`}>∧</span>
138
+ </button>
139
+ {!collapsed['Custom'] && customTimeframes.map((tf) => (
140
+ <button
141
+ key={tf}
142
+ className="tf-dropdown-row"
143
+ onClick={() => { onSelect(tf); onClose(); }}
144
+ >
145
+ <span>{tf}</span>
146
+ </button>
147
+ ))}
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ // ─── TopToolbar ─────────────────────────────────────────────────────────────
155
+
156
+ type Props = {
157
+ symbol: string;
158
+ timeframe: string;
159
+ theme: ChartTheme;
160
+ customTimeframes: string[];
161
+ favorites: string[];
162
+ onSymbolChange: (s: string) => void;
163
+ onTimeframeChange: (tf: string) => void;
164
+ onAddCustomTimeframe: (tf: string) => void;
165
+ onFavoritesChange: (favs: string[]) => void;
166
+ onAddIndicator: (config: IndicatorConfig) => void;
167
+ onToggleTheme: () => void;
168
+ onCopyScreenshot: () => void;
169
+ onDownloadScreenshot: () => void;
170
+ onFullscreen: () => void;
171
+ isFullscreen: boolean;
172
+ currentLayoutName?: string | undefined;
173
+ currentLayoutId?: string | undefined;
174
+ autoSave: boolean;
175
+ onSaveLayout: (name: string) => Promise<void>;
176
+ onLoadLayout: (layoutId: string) => Promise<void>;
177
+ onRenameLayout: (newName: string) => Promise<void>;
178
+ onCopyLayout: (newName: string) => Promise<void>;
179
+ onToggleAutoSave: (enabled: boolean) => void;
180
+ onDeleteLayout: (id: string) => Promise<void>;
181
+ onOpenLayoutInNewTab: (id: string) => Promise<void>;
182
+ /** Called to fetch current saved layouts. Passed through to LayoutMenu. */
183
+ onFetchLayouts: () => Promise<import('../LayoutMenu').LayoutRecord[]>;
184
+ /** Resolver used by the symbol search dialog. */
185
+ symbolResolver: ISymbolResolver;
186
+ /** Only rendered when a managed license is active */
187
+ showTradeButton?: boolean;
188
+ tradeDrawerOpen?: boolean;
189
+ onToggleTradeDrawer?: () => void;
190
+ };
191
+
192
+ export function TopToolbar({
193
+ symbol, timeframe, theme, customTimeframes, favorites,
194
+ onSymbolChange, onTimeframeChange, onAddCustomTimeframe, onFavoritesChange,
195
+ onAddIndicator,
196
+ onToggleTheme,
197
+ onCopyScreenshot, onDownloadScreenshot, onFullscreen, isFullscreen,
198
+ currentLayoutName, currentLayoutId, autoSave,
199
+ onSaveLayout, onLoadLayout, onRenameLayout, onCopyLayout, onToggleAutoSave,
200
+ onDeleteLayout, onOpenLayoutInNewTab, onFetchLayouts, symbolResolver,
201
+ showTradeButton, tradeDrawerOpen, onToggleTradeDrawer,
202
+ }: Props) {
203
+ const [tfOpen, setTfOpen] = useState(false);
204
+ const [symOpen, setSymOpen] = useState(false);
205
+ const [indOpen, setIndOpen] = useState(false);
206
+ const [ssOpen, setSsOpen] = useState(false);
207
+ const ssRef = useRef<HTMLDivElement>(null);
208
+ const tfRef = useRef<HTMLDivElement>(null);
209
+
210
+ useEffect(() => {
211
+ if (!ssOpen) return;
212
+ const handler = (e: MouseEvent) => {
213
+ if (ssRef.current && !ssRef.current.contains(e.target as Node)) setSsOpen(false);
214
+ };
215
+ document.addEventListener('mousedown', handler);
216
+ return () => document.removeEventListener('mousedown', handler);
217
+ }, [ssOpen]);
218
+
219
+ // Close dropdown on outside click is handled inside TimeframeDropdown itself.
220
+
221
+ return (
222
+ <div
223
+ style={{
224
+ display: 'flex',
225
+ alignItems: 'center',
226
+ gap: 8,
227
+ padding: '6px 12px',
228
+ background: 'var(--toolbar-bg)',
229
+ borderRadius: 8,
230
+ flexShrink: 0,
231
+ }}
232
+ >
233
+ {/* Logo */}
234
+ <span style={{ fontWeight: 700, letterSpacing: '-0.5px', color: 'var(--accent)' }}>
235
+ ForgeCharts
236
+ </span>
237
+
238
+ <div className="toolbar-sep" />
239
+
240
+ {/* Symbol picker */}
241
+ <button
242
+ className="sym-trigger"
243
+ onClick={() => setSymOpen(true)}
244
+ title="Search symbols"
245
+ >
246
+ <svg viewBox="0 0 14 14" width="12" height="12" stroke="currentColor"
247
+ fill="none" strokeWidth="1.8" strokeLinecap="round">
248
+ <circle cx="5.5" cy="5.5" r="4" />
249
+ <line x1="9" y1="9" x2="13" y2="13" />
250
+ </svg>
251
+ <span>{symbol}</span>
252
+ <svg viewBox="0 0 10 6" width="8" height="6" fill="currentColor">
253
+ <path d="M0 0l5 6 5-6z" />
254
+ </svg>
255
+ </button>
256
+
257
+ {symOpen && (
258
+ <SymbolSearchDialog
259
+ current={symbol}
260
+ onSelect={onSymbolChange}
261
+ onClose={() => setSymOpen(false)}
262
+ symbolResolver={symbolResolver}
263
+ />
264
+ )}
265
+
266
+ <div className="toolbar-sep" />
267
+
268
+ {/* Timeframe buttons + dropdown */}
269
+ <div ref={tfRef} style={{ display: 'flex', gap: 2, position: 'relative' }}>
270
+ {favorites.map((tf) => (
271
+ <button
272
+ key={tf}
273
+ className={tf === timeframe ? 'active' : undefined}
274
+ onClick={() => onTimeframeChange(tf)}
275
+ >
276
+ {tf}
277
+ </button>
278
+ ))}
279
+
280
+ {/* Extra button for active non-favorited timeframe */}
281
+ {!favorites.includes(timeframe) && timeframe && (
282
+ <button className="active" style={{ fontStyle: 'italic' }}>
283
+ {timeframe}
284
+ </button>
285
+ )}
286
+
287
+ {/* More timeframes trigger */}
288
+ <button
289
+ className={tfOpen ? 'active' : undefined}
290
+ onClick={() => setTfOpen((o) => !o)}
291
+ title="More timeframes"
292
+ style={{ fontSize: 11, padding: '4px 7px' }}
293
+ >
294
+
295
+ </button>
296
+
297
+ {tfOpen && (
298
+ <TimeframeDropdown
299
+ timeframe={timeframe}
300
+ customTimeframes={customTimeframes}
301
+ favorites={favorites}
302
+ onSelect={onTimeframeChange}
303
+ onToggleFavorite={(tf) => {
304
+ const next = favorites.includes(tf)
305
+ ? favorites.filter((f) => f !== tf)
306
+ : [...favorites, tf];
307
+ onFavoritesChange(next);
308
+ }}
309
+ onAddCustom={onAddCustomTimeframe}
310
+ onClose={() => setTfOpen(false)}
311
+ />
312
+ )}
313
+ </div>
314
+
315
+ <div className="toolbar-sep" />
316
+
317
+ {/* Indicators button */}
318
+ <button
319
+ className={`ind-trigger${indOpen ? ' active' : ''}`}
320
+ onClick={() => setIndOpen((o) => !o)}
321
+ title="Indicators"
322
+ >
323
+ <svg viewBox="0 0 16 16" width="13" height="13" stroke="currentColor" fill="none" strokeWidth="1.6" strokeLinecap="round">
324
+ <polyline points="2,12 6,7 9,10 14,4" />
325
+ <line x1="11" y1="2" x2="14" y2="2" />
326
+ <line x1="14" y1="2" x2="14" y2="5" />
327
+ </svg>
328
+ Indicators
329
+ </button>
330
+
331
+ {indOpen && (
332
+ <IndicatorsDialog
333
+ onAdd={(config: IndicatorConfig) => { onAddIndicator(config); }}
334
+ onClose={() => setIndOpen(false)}
335
+ />
336
+ )}
337
+
338
+ {/* Layout + theme toggle — pinned to right edge */}
339
+ <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}>
340
+ {showTradeButton && (
341
+ <>
342
+ <button
343
+ className={`top-trade-btn${tradeDrawerOpen ? ' active' : ''}`}
344
+ onClick={onToggleTradeDrawer}
345
+ title="Trade — Connect your broker"
346
+ style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 8px', fontSize: 12 }}
347
+ >
348
+ <svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="1.6">
349
+ <rect x="1.5" y="3" width="13" height="10" rx="1.5" />
350
+ <line x1="5" y1="8" x2="11" y2="8" />
351
+ <line x1="8" y1="5" x2="8" y2="11" />
352
+ </svg>
353
+ Trade
354
+ </button>
355
+ <div className="toolbar-sep" />
356
+ </>
357
+ )}
358
+ <LayoutMenu
359
+ currentName={currentLayoutName}
360
+ currentLayoutId={currentLayoutId}
361
+ autoSave={autoSave}
362
+ onFetchLayouts={onFetchLayouts}
363
+ onSave={onSaveLayout}
364
+ onLoad={onLoadLayout}
365
+ onRename={onRenameLayout}
366
+ onCopy={onCopyLayout}
367
+ onToggleAutoSave={onToggleAutoSave}
368
+ onDelete={onDeleteLayout}
369
+ onOpenInNewTab={onOpenLayoutInNewTab}
370
+ />
371
+ <button
372
+ onClick={onToggleTheme}
373
+ title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
374
+ style={{ fontSize: 14, padding: '4px 8px' }}
375
+ >
376
+ {theme === 'dark' ? '☀' : '☾'}
377
+ </button>
378
+ {/* Screenshot */}
379
+ <div ref={ssRef} style={{ position: 'relative' }}>
380
+ <button
381
+ className={ssOpen ? 'active' : undefined}
382
+ onClick={() => setSsOpen((o) => !o)}
383
+ title="Screenshot"
384
+ style={{ display: 'inline-flex', alignItems: 'center', padding: '4px 7px' }}
385
+ >
386
+ <svg viewBox="0 0 16 16" width="14" height="14" stroke="currentColor" fill="none" strokeWidth="1.5">
387
+ <rect x="2" y="4" width="12" height="9" rx="1" />
388
+ <circle cx="8" cy="8.5" r="2.5" />
389
+ <path d="M6 4l1-2h2l1 2" />
390
+ </svg>
391
+ </button>
392
+ {ssOpen && (
393
+ <div className="ss-dropdown">
394
+ <button className="ss-dropdown-item" onClick={() => { setSsOpen(false); onCopyScreenshot(); }}>
395
+ <svg viewBox="0 0 16 16" width="13" height="13" stroke="currentColor" fill="none" strokeWidth="1.5">
396
+ <rect x="5" y="5" width="9" height="9" rx="1" />
397
+ <path d="M3 11H2a1 1 0 01-1-1V2a1 1 0 011-1h8a1 1 0 011 1v1" />
398
+ </svg>
399
+ Copy to Clipboard
400
+ </button>
401
+ <button className="ss-dropdown-item" onClick={() => { setSsOpen(false); onDownloadScreenshot(); }}>
402
+ <svg viewBox="0 0 16 16" width="13" height="13" stroke="currentColor" fill="none" strokeWidth="1.5" strokeLinecap="round">
403
+ <line x1="8" y1="2" x2="8" y2="11" />
404
+ <polyline points="4,7 8,11 12,7" />
405
+ <line x1="2" y1="14" x2="14" y2="14" />
406
+ </svg>
407
+ Download PNG
408
+ </button>
409
+ </div>
410
+ )}
411
+ </div>
412
+ {/* Fullscreen */}
413
+ <button
414
+ onClick={onFullscreen}
415
+ title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
416
+ style={{ display: 'inline-flex', alignItems: 'center', padding: '4px 7px' }}
417
+ >
418
+ {isFullscreen ? (
419
+ <svg viewBox="0 0 16 16" width="14" height="14" stroke="currentColor" fill="none" strokeWidth="1.5">
420
+ <path d="M6 2v4H2M10 2v4h4M10 14v-4h4M6 14v-4H2" />
421
+ </svg>
422
+ ) : (
423
+ <svg viewBox="0 0 16 16" width="14" height="14" stroke="currentColor" fill="none" strokeWidth="1.5">
424
+ <path d="M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4" />
425
+ </svg>
426
+ )}
427
+ </button>
428
+ </div>
429
+ </div>
430
+ );
431
+ }
package/tsconfig.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "outDir": "./dist",
5
5
  "rootDir": "./src",
6
6
  "composite": true,
7
+ "jsx": "react-jsx",
7
8
  "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
9
  "baseUrl": ".",
9
10
  "paths": {
@@ -18,5 +19,5 @@
18
19
  { "path": "../types" },
19
20
  { "path": "../utils" }
20
21
  ],
21
- "include": ["src/**/*.ts"]
22
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
22
23
  }
package/tsup.config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineConfig } from 'tsup';
2
2
 
3
3
  export default defineConfig({
4
- entry: ['src/index.ts', 'src/internal.ts'],
4
+ entry: ['src/index.ts', 'src/internal.ts', 'src/react/index.ts', 'src/react/internal.ts'],
5
5
  format: ['esm'],
6
6
  dts: false,
7
7
  sourcemap: true,
@@ -10,6 +10,7 @@ export default defineConfig({
10
10
  treeshake: true,
11
11
  // Force .js extension so package.json exports map ("./dist/index.js") resolves correctly
12
12
  outExtension: () => ({ js: '.js' }),
13
- // No bundling of @forgecharts/* siblings consumers share the same workspace
14
- external: ['@forgecharts/types', '@forgecharts/utils'],
13
+ // Only pixi.js and React are true external runtime deps for consumers.
14
+ // @forgecharts/types, utils, shared are bundled in.
15
+ external: ['pixi.js', 'react', 'react-dom'],
15
16
  });