react-klinecharts-ui 0.6.0 → 1.1.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/dist/index.js CHANGED
@@ -1,31 +1,12 @@
1
- import { registerExtensions, TA_default, ensureAlertLineRegistered } from './chunk-MVD3ILJT.js';
2
- export { TA_default as TA, abcd_default as abcd, alertLine_default as alertLine, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, depthOverlay_default as depthOverlay, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-MVD3ILJT.js';
3
- import { createContext, useContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
4
- import { registerOverlay, registerHotkey, getHotkey, getSupportedHotkeys, registerIndicator } from 'react-klinecharts';
1
+ import { registerExtensions, TA_default, ensureAlertLineRegistered } from './chunk-RIT2LJQT.js';
2
+ export { TA_default as TA, abcd_default as abcd, alertLine_default as alertLine, anyWaves_default as anyWaves, arrow_default as arrow, bollTv_default as bollTv, brush_default as brush, cci_default as cci, circle_default as circle, depthOverlay_default as depthOverlay, eightWaves_default as eightWaves, elliottWave_default as elliottWave, fibRetracement_default as fibRetracement, fibonacciCircle_default as fibonacciCircle, fibonacciExtension_default as fibonacciExtension, fibonacciSegment_default as fibonacciSegment, fibonacciSpeedResistanceFan_default as fibonacciSpeedResistanceFan, fibonacciSpiral_default as fibonacciSpiral, fiveWaves_default as fiveWaves, gannBox_default as gannBox, gannFan_default as gannFan, hma_default as hma, ichimoku_default as ichimoku, indicators, longPosition_default as longPosition, maRibbon_default as maRibbon, macdTv_default as macdTv, measure_default as measure, orderLine_default as orderLine, overlays, parallelChannel_default as parallelChannel, parallelogram_default as parallelogram, pivotPoints_default as pivotPoints, ray_default as ray, rect_default as rect, registerExtensions, rsiTv_default as rsiTv, shortPosition_default as shortPosition, stochastic_default as stochastic, superTrend_default as superTrend, threeWaves_default as threeWaves, triangle_default as triangle, vwap_default as vwap, xabcd_default as xabcd } from './chunk-RIT2LJQT.js';
3
+ import { KlinechartsUIStateContext, KlinechartsUIDispatchContext, useKlinechartsUI, useKlinechartsUIDispatch } from './chunk-O7E57I66.js';
4
+ export { KlinechartsUIDispatchContext, KlinechartsUIStateContext, createDataLoader, useKlinechartsUI } from './chunk-O7E57I66.js';
5
+ import './chunk-PZ5AY32C.js';
6
+ import { createContext, useMemo, useReducer, useRef, useEffect, useCallback, useState, useId, useContext } from 'react';
7
+ import { registerOverlay, registerHotkey, getHotkey, getSupportedHotkeys, registerIndicator } from 'klinecharts';
5
8
  import { jsx } from 'react/jsx-runtime';
6
9
 
7
- var KlinechartsUIStateContext = createContext(null);
8
- var KlinechartsUIDispatchContext = createContext(null);
9
- function useKlinechartsUIDispatch() {
10
- const ctx = useContext(KlinechartsUIDispatchContext);
11
- if (!ctx) {
12
- throw new Error(
13
- "useKlinechartsUI must be used within a <KlinechartsUIProvider>"
14
- );
15
- }
16
- return ctx;
17
- }
18
- function useKlinechartsUI() {
19
- const state = useContext(KlinechartsUIStateContext);
20
- const dispatch = useContext(KlinechartsUIDispatchContext);
21
- if (!state || !dispatch) {
22
- throw new Error(
23
- "useKlinechartsUI must be used within a <KlinechartsUIProvider>"
24
- );
25
- }
26
- return { state, ...dispatch };
27
- }
28
-
29
10
  // src/data/periods.ts
30
11
  var DEFAULT_PERIODS = [
31
12
  { span: 1, type: "minute", label: "1m" },
@@ -39,6 +20,50 @@ var DEFAULT_PERIODS = [
39
20
  { span: 1, type: "month", label: "M" },
40
21
  { span: 1, type: "year", label: "Y" }
41
22
  ];
23
+
24
+ // src/storage/types.ts
25
+ var DEFAULT_STORAGE_NAMESPACES = [
26
+ "alerts",
27
+ "settings",
28
+ "indicators"
29
+ ];
30
+ var DEFAULT_STORAGE_KEY_PREFIX = "rkui:";
31
+
32
+ // src/storage/createDefaultStorage.ts
33
+ var NOOP_ADAPTER = {
34
+ getItem: () => null,
35
+ setItem: () => {
36
+ },
37
+ removeItem: () => {
38
+ }
39
+ };
40
+ function createDefaultStorage() {
41
+ if (typeof localStorage === "undefined") return NOOP_ADAPTER;
42
+ try {
43
+ const probe = localStorage.getItem("__rkui_probe__");
44
+ return probe === null ? localStorage : localStorage;
45
+ } catch {
46
+ return NOOP_ADAPTER;
47
+ }
48
+ }
49
+ function resolveStorage(options) {
50
+ const adapter = options?.adapter ?? createDefaultStorage();
51
+ const keyPrefix = options?.keyPrefix ?? DEFAULT_STORAGE_KEY_PREFIX;
52
+ const namespaces = new Set(
53
+ options?.namespaces ?? DEFAULT_STORAGE_NAMESPACES
54
+ );
55
+ return {
56
+ adapter,
57
+ namespaces,
58
+ keyPrefix,
59
+ key(namespace) {
60
+ return `${keyPrefix}${namespace}`;
61
+ },
62
+ persists(namespace) {
63
+ return namespaces.has(namespace);
64
+ }
65
+ };
66
+ }
42
67
  function reducer(state, action) {
43
68
  switch (action.type) {
44
69
  case "SET_CHART":
@@ -63,6 +88,25 @@ function reducer(state, action) {
63
88
  return { ...state, indicatorVisibility: action.visibility };
64
89
  case "SET_ALERTS":
65
90
  return { ...state, alerts: action.alerts };
91
+ case "ADD_ALERT":
92
+ return { ...state, alerts: [...state.alerts, action.alert] };
93
+ case "REMOVE_ALERT":
94
+ return {
95
+ ...state,
96
+ alerts: state.alerts.filter((a) => a.id !== action.id)
97
+ };
98
+ case "CLEAR_ALERTS":
99
+ return { ...state, alerts: [] };
100
+ case "MARK_ALERT_TRIGGERED": {
101
+ if (action.ids.length === 0) return state;
102
+ const triggered = new Set(action.ids);
103
+ return {
104
+ ...state,
105
+ alerts: state.alerts.map(
106
+ (a) => triggered.has(a.id) ? { ...a, triggered: true } : a
107
+ )
108
+ };
109
+ }
66
110
  case "SET_MEASURE":
67
111
  return { ...state, measure: { ...state.measure, ...action.measure } };
68
112
  case "SET_REPLAY":
@@ -83,13 +127,16 @@ function KlinechartsUIProvider({
83
127
  defaultPeriod,
84
128
  defaultTheme = "light",
85
129
  defaultTimezone = "Asia/Shanghai",
86
- defaultMainIndicators = ["MA"],
87
- defaultSubIndicators = ["VOL"],
130
+ // NOTE: no destructuring default here — the init function must distinguish
131
+ // "consumer passed default*" (wins) from "use stored value" (hydrate).
132
+ defaultMainIndicators,
133
+ defaultSubIndicators,
88
134
  defaultLocale = "en-US",
89
135
  periods,
90
136
  styles,
91
137
  registerExtensions: shouldRegister = true,
92
138
  overlays: extraOverlays,
139
+ storage: storageOptions,
93
140
  children,
94
141
  onStateChange,
95
142
  onSymbolChange,
@@ -100,6 +147,11 @@ function KlinechartsUIProvider({
100
147
  onSubIndicatorsChange,
101
148
  onSettingsChange
102
149
  }) {
150
+ const resolvedStorage = useMemo(
151
+ () => storageOptions ? resolveStorage(storageOptions) : null,
152
+ // eslint-disable-next-line react-hooks/exhaustive-deps
153
+ []
154
+ );
103
155
  const [state, dispatch] = useReducer(
104
156
  reducer,
105
157
  {
@@ -112,47 +164,66 @@ function KlinechartsUIProvider({
112
164
  defaultSubIndicators,
113
165
  defaultLocale,
114
166
  periods,
115
- styles
116
- },
117
- (opts) => ({
118
- chart: null,
119
- datafeed: opts.datafeed,
120
- symbol: opts.defaultSymbol ?? null,
121
- period: opts.defaultPeriod ?? (opts.periods ?? DEFAULT_PERIODS)[0],
122
- theme: opts.defaultTheme ?? "light",
123
- timezone: opts.defaultTimezone ?? "Asia/Shanghai",
124
- isLoading: false,
125
- locale: opts.defaultLocale ?? "en-US",
126
- periods: opts.periods ?? DEFAULT_PERIODS,
127
- mainIndicators: opts.defaultMainIndicators ?? ["MA"],
128
- subIndicators: (opts.defaultSubIndicators ?? ["VOL"]).reduce(
129
- (acc, name) => ({ ...acc, [name]: "" }),
130
- {}
131
- ),
132
- indicatorAxes: {},
133
- indicatorVisibility: {},
134
- alerts: [],
135
- measure: { isActive: false, fromPoint: null, result: null },
136
- replay: {
137
- isReplaying: false,
138
- isPaused: false,
139
- speed: 1,
140
- barIndex: 0,
141
- totalBars: 0
142
- },
143
- styles: opts.styles,
144
- screenshotUrl: null
145
- })
167
+ styles,
168
+ storage: resolvedStorage
169
+ },
170
+ (opts) => {
171
+ const s = opts.storage;
172
+ const read = (ns, fallback) => {
173
+ if (!s || !s.persists(ns)) return fallback;
174
+ try {
175
+ const raw = s.adapter.getItem(s.key(ns));
176
+ return raw ? JSON.parse(raw) : fallback;
177
+ } catch {
178
+ return fallback;
179
+ }
180
+ };
181
+ const storedIndicators = read("indicators", {});
182
+ return {
183
+ chart: null,
184
+ datafeed: opts.datafeed,
185
+ symbol: opts.defaultSymbol ?? null,
186
+ period: opts.defaultPeriod ?? (opts.periods ?? DEFAULT_PERIODS)[0],
187
+ theme: opts.defaultTheme ?? "light",
188
+ timezone: opts.defaultTimezone ?? "Asia/Shanghai",
189
+ isLoading: false,
190
+ locale: opts.defaultLocale ?? "en-US",
191
+ periods: opts.periods ?? DEFAULT_PERIODS,
192
+ // Persistence semantics: STORED values win over `default*` props —
193
+ // the user's saved configuration should survive reload even when the
194
+ // app passes defaults. Only when storage is empty/unconfigured do the
195
+ // `default*` props apply, falling back to the built-ins last.
196
+ // (Settings live in useKlinechartsUISettings useState and hydrate
197
+ // separately via the dispatch-context `storage`.)
198
+ mainIndicators: storedIndicators.main ?? opts.defaultMainIndicators ?? ["MA"],
199
+ subIndicators: storedIndicators.sub ?? (opts.defaultSubIndicators ? opts.defaultSubIndicators.reduce(
200
+ (acc, name) => ({ ...acc, [name]: "" }),
201
+ {}
202
+ ) : { VOL: "" }),
203
+ indicatorAxes: storedIndicators.axes ?? {},
204
+ indicatorVisibility: storedIndicators.visibility ?? {},
205
+ alerts: read("alerts", []),
206
+ measure: { isActive: false, fromPoint: null, result: null },
207
+ replay: {
208
+ isReplaying: false,
209
+ isPaused: false,
210
+ speed: 1,
211
+ barIndex: 0,
212
+ totalBars: 0
213
+ },
214
+ styles: opts.styles,
215
+ screenshotUrl: null
216
+ };
217
+ }
146
218
  );
147
219
  const fullscreenContainerRef = useRef(null);
148
220
  const undoRedoListenerRef = useRef(null);
149
- const alertTriggeredListenerRef = useRef(null);
221
+ const alertTriggeredListenersRef = useRef(/* @__PURE__ */ new Set());
150
222
  const replayIntervalRef = useRef(null);
151
223
  const replaySavedDataRef = useRef([]);
152
224
  const replayIndexRef = useRef(0);
153
225
  const alertPrevCloseRef = useRef(null);
154
226
  const stateRef = useRef(state);
155
- stateRef.current = state;
156
227
  const callbacksRef = useRef({
157
228
  onStateChange,
158
229
  onSymbolChange,
@@ -162,15 +233,18 @@ function KlinechartsUIProvider({
162
233
  onMainIndicatorsChange,
163
234
  onSubIndicatorsChange
164
235
  });
165
- callbacksRef.current = {
166
- onStateChange,
167
- onSymbolChange,
168
- onPeriodChange,
169
- onThemeChange,
170
- onTimezoneChange,
171
- onMainIndicatorsChange,
172
- onSubIndicatorsChange
173
- };
236
+ useEffect(() => {
237
+ stateRef.current = state;
238
+ callbacksRef.current = {
239
+ onStateChange,
240
+ onSymbolChange,
241
+ onPeriodChange,
242
+ onThemeChange,
243
+ onTimezoneChange,
244
+ onMainIndicatorsChange,
245
+ onSubIndicatorsChange
246
+ };
247
+ });
174
248
  const enhancedDispatch = useCallback((action) => {
175
249
  const prevState = stateRef.current;
176
250
  const newState = reducer(prevState, action);
@@ -200,6 +274,12 @@ function KlinechartsUIProvider({
200
274
  }
201
275
  }, []);
202
276
  const extraOverlaysRef = useRef(extraOverlays);
277
+ const datafeedRef = useRef(datafeed);
278
+ const onSettingsChangeRef = useRef(onSettingsChange);
279
+ useEffect(() => {
280
+ datafeedRef.current = datafeed;
281
+ onSettingsChangeRef.current = onSettingsChange;
282
+ });
203
283
  useEffect(() => {
204
284
  if (shouldRegister) {
205
285
  registerExtensions();
@@ -216,34 +296,97 @@ function KlinechartsUIProvider({
216
296
  const chart = state.chart;
217
297
  if (!chart || !hasAlerts) return;
218
298
  alertPrevCloseRef.current = null;
299
+ const prevValueByAlert = /* @__PURE__ */ new Map();
219
300
  const interval = setInterval(() => {
220
301
  const dataList = chart.getDataList();
221
302
  if (!dataList || dataList.length === 0) return;
222
- const currentClose = dataList[dataList.length - 1].close;
223
- const prevClose = alertPrevCloseRef.current;
224
- alertPrevCloseRef.current = currentClose;
225
- if (prevClose === null) return;
303
+ const lastIdx = dataList.length - 1;
304
+ const indicatorCache = /* @__PURE__ */ new Map();
305
+ const readIndicatorValue = (indicatorId, figureKey) => {
306
+ const cacheKey = `${indicatorId}:${figureKey}`;
307
+ if (indicatorCache.has(cacheKey)) return indicatorCache.get(cacheKey);
308
+ let value = null;
309
+ try {
310
+ const indicators2 = chart.getIndicators({ id: indicatorId });
311
+ const ind = indicators2?.[0];
312
+ const result = ind?.result;
313
+ const last = Array.isArray(result) ? result[lastIdx] : null;
314
+ if (last && typeof last === "object") {
315
+ const v = last[figureKey];
316
+ value = typeof v === "number" ? v : null;
317
+ }
318
+ } catch {
319
+ value = null;
320
+ }
321
+ indicatorCache.set(cacheKey, value);
322
+ return value;
323
+ };
226
324
  const alerts = stateRef.current.alerts;
227
- let changed = false;
228
- const next = alerts.map((alert) => {
229
- if (alert.triggered) return alert;
230
- const crossedUp = prevClose < alert.price && currentClose >= alert.price;
231
- const crossedDown = prevClose > alert.price && currentClose <= alert.price;
325
+ const triggeredIds = [];
326
+ for (const alert of alerts) {
327
+ if (alert.triggered) continue;
328
+ const target = alert.target ?? { type: "price" };
329
+ let currentValue;
330
+ if (target.type === "indicator") {
331
+ currentValue = readIndicatorValue(target.indicatorId, target.figureKey);
332
+ } else {
333
+ currentValue = dataList[lastIdx].close;
334
+ }
335
+ if (currentValue === null || !Number.isFinite(currentValue)) continue;
336
+ const prevValue = prevValueByAlert.get(alert.id) ?? null;
337
+ prevValueByAlert.set(alert.id, currentValue);
338
+ if (prevValue === null) continue;
339
+ const crossedUp = prevValue < alert.price && currentValue >= alert.price;
340
+ const crossedDown = prevValue > alert.price && currentValue <= alert.price;
232
341
  const shouldTrigger = alert.condition === "crossing_up" ? crossedUp : alert.condition === "crossing_down" ? crossedDown : crossedUp || crossedDown;
233
342
  if (shouldTrigger) {
234
- changed = true;
235
- const triggered = { ...alert, triggered: true };
236
- alertTriggeredListenerRef.current?.(triggered);
237
- return triggered;
343
+ triggeredIds.push(alert.id);
344
+ const fired = { ...alert, triggered: true };
345
+ alertTriggeredListenersRef.current.forEach((cb) => cb(fired));
238
346
  }
239
- return alert;
240
- });
241
- if (changed) {
242
- enhancedDispatch({ type: "SET_ALERTS", alerts: next });
347
+ }
348
+ if (triggeredIds.length > 0) {
349
+ enhancedDispatch({
350
+ type: "MARK_ALERT_TRIGGERED",
351
+ ids: triggeredIds
352
+ });
243
353
  }
244
354
  }, 1e3);
245
355
  return () => clearInterval(interval);
246
356
  }, [state.chart, hasAlerts, enhancedDispatch]);
357
+ useEffect(() => {
358
+ alertPrevCloseRef.current = null;
359
+ }, [state.symbol, state.period]);
360
+ const writeNs = useCallback(
361
+ (ns, value) => {
362
+ if (!resolvedStorage || !resolvedStorage.persists(ns)) return;
363
+ try {
364
+ resolvedStorage.adapter.setItem(
365
+ resolvedStorage.key(ns),
366
+ JSON.stringify(value)
367
+ );
368
+ } catch {
369
+ }
370
+ },
371
+ [resolvedStorage]
372
+ );
373
+ useEffect(() => {
374
+ writeNs("alerts", state.alerts);
375
+ }, [state.alerts, writeNs]);
376
+ useEffect(() => {
377
+ writeNs("indicators", {
378
+ main: state.mainIndicators,
379
+ sub: state.subIndicators,
380
+ axes: state.indicatorAxes,
381
+ visibility: state.indicatorVisibility
382
+ });
383
+ }, [
384
+ state.mainIndicators,
385
+ state.subIndicators,
386
+ state.indicatorAxes,
387
+ state.indicatorVisibility,
388
+ writeNs
389
+ ]);
247
390
  useEffect(() => {
248
391
  return () => {
249
392
  if (replayIntervalRef.current !== null) {
@@ -255,16 +398,18 @@ function KlinechartsUIProvider({
255
398
  const dispatchValue = useMemo(
256
399
  () => ({
257
400
  dispatch: enhancedDispatch,
258
- datafeed,
259
- onSettingsChange,
401
+ datafeed: datafeedRef.current,
402
+ onSettingsChange: onSettingsChangeRef.current,
260
403
  fullscreenContainerRef,
261
404
  undoRedoListenerRef,
262
- alertTriggeredListenerRef,
405
+ alertTriggeredListenersRef,
263
406
  replayIntervalRef,
264
407
  replaySavedDataRef,
265
- replayIndexRef
408
+ replayIndexRef,
409
+ storage: resolvedStorage
266
410
  }),
267
- [enhancedDispatch, datafeed, onSettingsChange]
411
+ // eslint-disable-next-line react-hooks/exhaustive-deps
412
+ [enhancedDispatch, resolvedStorage]
268
413
  );
269
414
  return /* @__PURE__ */ jsx(KlinechartsUIStateContext.Provider, { value: state, children: /* @__PURE__ */ jsx(KlinechartsUIDispatchContext.Provider, { value: dispatchValue, children }) });
270
415
  }
@@ -1222,15 +1367,17 @@ function useDrawingTools() {
1222
1367
  const [isVisible, setIsVisible] = useState(true);
1223
1368
  const [autoRetrigger, setAutoRetrigger] = useState(true);
1224
1369
  const activeToolRef = useRef(activeTool);
1225
- activeToolRef.current = activeTool;
1226
1370
  const autoRetriggerRef = useRef(autoRetrigger);
1227
- autoRetriggerRef.current = autoRetrigger;
1228
1371
  const isLockedRef = useRef(isLocked);
1229
- isLockedRef.current = isLocked;
1230
1372
  const isVisibleRef = useRef(isVisible);
1231
- isVisibleRef.current = isVisible;
1232
1373
  const magnetModeRef = useRef(magnetMode);
1233
- magnetModeRef.current = magnetMode;
1374
+ useEffect(() => {
1375
+ activeToolRef.current = activeTool;
1376
+ autoRetriggerRef.current = autoRetrigger;
1377
+ isLockedRef.current = isLocked;
1378
+ isVisibleRef.current = isVisible;
1379
+ magnetModeRef.current = magnetMode;
1380
+ });
1234
1381
  const categories = useMemo(
1235
1382
  () => DRAWING_CATEGORIES.map((cat) => ({
1236
1383
  key: cat.key,
@@ -1241,6 +1388,8 @@ function useDrawingTools() {
1241
1388
  })),
1242
1389
  []
1243
1390
  );
1391
+ const createOverlayForToolRef = useRef(() => {
1392
+ });
1244
1393
  const createOverlayForTool = useCallback(
1245
1394
  (name) => {
1246
1395
  const mode = magnetModeRef.current === "strong" ? "strong_magnet" : magnetModeRef.current === "weak" ? "weak_magnet" : "normal";
@@ -1266,7 +1415,7 @@ function useDrawingTools() {
1266
1415
  });
1267
1416
  if (autoRetriggerRef.current && activeToolRef.current === name) {
1268
1417
  requestAnimationFrame(() => {
1269
- createOverlayForTool(name);
1418
+ createOverlayForToolRef.current(name);
1270
1419
  });
1271
1420
  }
1272
1421
  }
@@ -1274,6 +1423,9 @@ function useDrawingTools() {
1274
1423
  },
1275
1424
  [state.chart, undoRedoListenerRef]
1276
1425
  );
1426
+ useEffect(() => {
1427
+ createOverlayForToolRef.current = createOverlayForTool;
1428
+ });
1277
1429
  const selectTool = useCallback(
1278
1430
  (name) => {
1279
1431
  setActiveTool(name);
@@ -1399,7 +1551,17 @@ var defaultSettings = {
1399
1551
  };
1400
1552
  function useKlinechartsUISettings() {
1401
1553
  const { state, onSettingsChange } = useKlinechartsUI();
1402
- const [settings, setSettings] = useState(defaultSettings);
1554
+ const { storage } = useKlinechartsUIDispatch();
1555
+ const [settings, setSettings] = useState(() => {
1556
+ if (storage && storage.persists("settings")) {
1557
+ try {
1558
+ const raw = storage.adapter.getItem(storage.key("settings"));
1559
+ if (raw) return { ...defaultSettings, ...JSON.parse(raw) };
1560
+ } catch {
1561
+ }
1562
+ }
1563
+ return defaultSettings;
1564
+ });
1403
1565
  const isInitialMount = useRef(true);
1404
1566
  useEffect(() => {
1405
1567
  if (isInitialMount.current) {
@@ -1408,6 +1570,14 @@ function useKlinechartsUISettings() {
1408
1570
  }
1409
1571
  onSettingsChange?.({ ...settings });
1410
1572
  }, [settings, onSettingsChange]);
1573
+ useEffect(() => {
1574
+ if (!storage || !storage.persists("settings")) return;
1575
+ if (isInitialMount.current) return;
1576
+ try {
1577
+ storage.adapter.setItem(storage.key("settings"), JSON.stringify(settings));
1578
+ } catch {
1579
+ }
1580
+ }, [settings, storage]);
1411
1581
  const candleTypes = useMemo(
1412
1582
  () => CANDLE_TYPES.map((ct) => ({
1413
1583
  key: ct.key,
@@ -1730,22 +1900,28 @@ function useFullscreen() {
1730
1900
  const enter = useCallback(() => {
1731
1901
  const el = fullscreenContainerRef.current;
1732
1902
  if (!el) return;
1903
+ let result;
1733
1904
  if (el.requestFullscreen) {
1734
- el.requestFullscreen();
1905
+ result = el.requestFullscreen();
1735
1906
  } else if ("webkitRequestFullscreen" in el) {
1736
1907
  el.webkitRequestFullscreen();
1737
1908
  } else if ("msRequestFullscreen" in el) {
1738
1909
  el.msRequestFullscreen();
1739
1910
  }
1911
+ result?.catch(() => {
1912
+ });
1740
1913
  }, [fullscreenContainerRef]);
1741
1914
  const exit = useCallback(() => {
1915
+ let result;
1742
1916
  if (document.exitFullscreen) {
1743
- document.exitFullscreen();
1917
+ result = document.exitFullscreen();
1744
1918
  } else if ("webkitExitFullscreen" in document) {
1745
1919
  document.webkitExitFullscreen();
1746
1920
  } else if ("msExitFullscreen" in document) {
1747
1921
  document.msExitFullscreen();
1748
1922
  }
1923
+ result?.catch(() => {
1924
+ });
1749
1925
  }, []);
1750
1926
  const toggle = useCallback(() => {
1751
1927
  if (isFullscreen) {
@@ -1776,6 +1952,7 @@ function useFullscreen() {
1776
1952
  function useOrderLines() {
1777
1953
  const { state } = useKlinechartsUI();
1778
1954
  const callbacksRef = useRef(/* @__PURE__ */ new Map());
1955
+ const ownedIdsRef = useRef(/* @__PURE__ */ new Set());
1779
1956
  const createOrderLine = useCallback(
1780
1957
  (options) => {
1781
1958
  if (!state.chart) return null;
@@ -1806,6 +1983,7 @@ function useOrderLines() {
1806
1983
  }
1807
1984
  }
1808
1985
  });
1986
+ ownedIdsRef.current.add(id);
1809
1987
  return id;
1810
1988
  },
1811
1989
  [state.chart]
@@ -1837,12 +2015,28 @@ function useOrderLines() {
1837
2015
  (id) => {
1838
2016
  state.chart?.removeOverlay({ id });
1839
2017
  callbacksRef.current.delete(id);
2018
+ ownedIdsRef.current.delete(id);
1840
2019
  },
1841
2020
  [state.chart]
1842
2021
  );
1843
2022
  const removeAllOrderLines = useCallback(() => {
1844
2023
  state.chart?.removeOverlay({ name: "orderLine" });
1845
2024
  callbacksRef.current.clear();
2025
+ ownedIdsRef.current.clear();
2026
+ }, [state.chart]);
2027
+ useEffect(() => {
2028
+ const chart = state.chart;
2029
+ const owned = ownedIdsRef.current;
2030
+ return () => {
2031
+ owned.forEach((id) => {
2032
+ try {
2033
+ chart?.removeOverlay({ id });
2034
+ } catch {
2035
+ }
2036
+ });
2037
+ owned.clear();
2038
+ callbacksRef.current.clear();
2039
+ };
1846
2040
  }, [state.chart]);
1847
2041
  return {
1848
2042
  createOrderLine,
@@ -1860,6 +2054,14 @@ function useUndoRedo() {
1860
2054
  const isProcessingRef = useRef(false);
1861
2055
  const canUndo = undoStack.length > 0;
1862
2056
  const canRedo = redoStack.length > 0;
2057
+ const undoStackRef = useRef([]);
2058
+ const redoStackRef = useRef([]);
2059
+ useEffect(() => {
2060
+ undoStackRef.current = undoStack;
2061
+ }, [undoStack]);
2062
+ useEffect(() => {
2063
+ redoStackRef.current = redoStack;
2064
+ }, [redoStack]);
1863
2065
  const pushAction = useCallback((action) => {
1864
2066
  if (isProcessingRef.current) return;
1865
2067
  setUndoStack((prev) => [...prev, action]);
@@ -1876,10 +2078,11 @@ function useUndoRedo() {
1876
2078
  setRedoStack([]);
1877
2079
  }, []);
1878
2080
  const undo = useCallback(() => {
1879
- if (undoStack.length === 0 || !state.chart || isProcessingRef.current)
2081
+ const stack = undoStackRef.current;
2082
+ if (stack.length === 0 || !state.chart || isProcessingRef.current)
1880
2083
  return;
1881
2084
  isProcessingRef.current = true;
1882
- const action = undoStack[undoStack.length - 1];
2085
+ const action = stack[stack.length - 1];
1883
2086
  setUndoStack((prev) => prev.slice(0, -1));
1884
2087
  try {
1885
2088
  switch (action.type) {
@@ -2000,12 +2203,13 @@ function useUndoRedo() {
2000
2203
  } finally {
2001
2204
  isProcessingRef.current = false;
2002
2205
  }
2003
- }, [undoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2206
+ }, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2004
2207
  const redo = useCallback(() => {
2005
- if (redoStack.length === 0 || !state.chart || isProcessingRef.current)
2208
+ const stack = redoStackRef.current;
2209
+ if (stack.length === 0 || !state.chart || isProcessingRef.current)
2006
2210
  return;
2007
2211
  isProcessingRef.current = true;
2008
- const action = redoStack[redoStack.length - 1];
2212
+ const action = stack[stack.length - 1];
2009
2213
  setRedoStack((prev) => prev.slice(0, -1));
2010
2214
  try {
2011
2215
  switch (action.type) {
@@ -2130,7 +2334,7 @@ function useUndoRedo() {
2130
2334
  } finally {
2131
2335
  isProcessingRef.current = false;
2132
2336
  }
2133
- }, [redoStack, state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2337
+ }, [state.chart, state.mainIndicators, state.subIndicators, state.indicatorAxes, dispatch]);
2134
2338
  useEffect(() => {
2135
2339
  const handleKeyDown = (e) => {
2136
2340
  const isCtrlOrMeta = e.ctrlKey || e.metaKey;
@@ -2168,6 +2372,7 @@ function generateId() {
2168
2372
  );
2169
2373
  }
2170
2374
  function getLayoutIds() {
2375
+ if (typeof localStorage === "undefined") return [];
2171
2376
  try {
2172
2377
  const raw = localStorage.getItem(INDEX_KEY);
2173
2378
  return raw ? JSON.parse(raw) : [];
@@ -2176,6 +2381,7 @@ function getLayoutIds() {
2176
2381
  }
2177
2382
  }
2178
2383
  function getEntry(id) {
2384
+ if (typeof localStorage === "undefined") return null;
2179
2385
  try {
2180
2386
  const raw = localStorage.getItem(STORAGE_KEY_PREFIX + id);
2181
2387
  return raw ? JSON.parse(raw) : null;
@@ -2185,35 +2391,31 @@ function getEntry(id) {
2185
2391
  }
2186
2392
  function useLayoutManager() {
2187
2393
  const { state, dispatch } = useKlinechartsUI();
2188
- const [layouts, setLayouts] = useState([]);
2394
+ const [layouts, setLayouts] = useState(
2395
+ () => getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
2396
+ );
2189
2397
  const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
2190
2398
  const autoSaveTimerRef = useRef(null);
2191
2399
  const autoSaveIdRef = useRef(null);
2192
2400
  const refreshLayouts = useCallback(() => {
2193
- const entries = getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null);
2194
- setLayouts(entries);
2401
+ setLayouts(
2402
+ getLayoutIds().map((id) => getEntry(id)).filter((e) => e !== null)
2403
+ );
2195
2404
  }, []);
2196
- useEffect(() => {
2197
- refreshLayouts();
2198
- }, [refreshLayouts]);
2199
2405
  const serializeState = useCallback(() => {
2200
2406
  const chart = state.chart;
2201
2407
  if (!chart) return null;
2202
2408
  const indicators2 = [];
2203
- const indicatorMap = chart.getIndicatorByPaneId?.();
2204
- if (indicatorMap) {
2205
- indicatorMap.forEach((paneIndicators, paneId) => {
2206
- paneIndicators.forEach((indicator) => {
2207
- const yAxisId = state.indicatorAxes[indicator.id];
2208
- indicators2.push({
2209
- paneId,
2210
- name: indicator.name,
2211
- calcParams: indicator.calcParams,
2212
- visible: indicator.visible,
2213
- ...indicator.styles ? { styles: indicator.styles } : {},
2214
- ...yAxisId ? { yAxisId } : {}
2215
- });
2216
- });
2409
+ const allIndicators = chart.getIndicators();
2410
+ for (const indicator of allIndicators) {
2411
+ const yAxisId = state.indicatorAxes[indicator.id];
2412
+ indicators2.push({
2413
+ paneId: indicator.paneId,
2414
+ name: indicator.name,
2415
+ calcParams: indicator.calcParams,
2416
+ visible: indicator.visible,
2417
+ ...indicator.styles ? { styles: indicator.styles } : {},
2418
+ ...yAxisId ? { yAxisId } : {}
2217
2419
  });
2218
2420
  }
2219
2421
  const drawings = [];
@@ -2277,13 +2479,8 @@ function useLayoutManager() {
2277
2479
  if (chartState.version !== STATE_VERSION) return false;
2278
2480
  const chart = state.chart;
2279
2481
  chart.removeOverlay();
2280
- const indicatorMap = chart.getIndicatorByPaneId?.();
2281
- if (indicatorMap) {
2282
- indicatorMap.forEach((_paneIndicators, paneId) => {
2283
- _paneIndicators.forEach((_, indicatorName) => {
2284
- chart.removeIndicator({ paneId, name: indicatorName });
2285
- });
2286
- });
2482
+ for (const indicator of chart.getIndicators()) {
2483
+ chart.removeIndicator({ id: indicator.id });
2287
2484
  }
2288
2485
  const newMainIndicators = [];
2289
2486
  const newSubIndicators = {};
@@ -2301,7 +2498,12 @@ function useLayoutManager() {
2301
2498
  visible: ind.visible
2302
2499
  },
2303
2500
  {
2304
- isStack: ind.paneId !== "candle_pane",
2501
+ // Main indicators stack OVER the candle series on the candle
2502
+ // pane (isStack: true), exactly as useIndicators does on the
2503
+ // add path. The previous `ind.paneId !== "candle_pane"` inverted
2504
+ // this, so loading a layout with main indicators replaced the
2505
+ // candles instead of overlaying them.
2506
+ isStack: isMain,
2305
2507
  pane: { id: ind.paneId },
2306
2508
  ...ind.yAxisId ? { yAxis: { id: ind.yAxisId } } : {}
2307
2509
  }
@@ -2501,8 +2703,10 @@ function useScriptEditor() {
2501
2703
  const [status, setStatus] = useState("");
2502
2704
  const [isRunning, setIsRunning] = useState(false);
2503
2705
  const activeIdRef = useRef(null);
2706
+ const [activeName, setActiveName] = useState(null);
2504
2707
  const activeNameRef = useRef(null);
2505
- const hasActiveScript = activeNameRef.current !== null;
2708
+ activeNameRef.current = activeName;
2709
+ const hasActiveScript = activeName !== null;
2506
2710
  const runScript = useCallback(() => {
2507
2711
  const chart = state.chart;
2508
2712
  if (!chart) {
@@ -2523,19 +2727,26 @@ function useScriptEditor() {
2523
2727
  `"use strict";
2524
2728
  ${code}`
2525
2729
  ];
2526
- const fn = new Function(...allArgs);
2730
+ const compiledFn = new Function(...allArgs);
2527
2731
  const shadowValues = SHADOW_KEYS.map(() => void 0);
2528
- const result = fn(...shadowValues, TA_default, dataList, parsedParams);
2529
- if (!Array.isArray(result)) {
2732
+ const sampleResult = compiledFn(
2733
+ ...shadowValues,
2734
+ TA_default,
2735
+ dataList,
2736
+ parsedParams
2737
+ );
2738
+ if (!Array.isArray(sampleResult)) {
2530
2739
  throw new Error("Script must return an Array.");
2531
2740
  }
2532
- const sample = result.find((v) => v !== null && v !== void 0);
2741
+ const sample = sampleResult.find(
2742
+ (v) => v !== null && v !== void 0
2743
+ );
2533
2744
  const seriesKeys = sample ? Object.keys(sample) : ["value"];
2534
2745
  if (seriesKeys.length === 0) {
2535
2746
  throw new Error("Returned objects have no keys.");
2536
2747
  }
2537
2748
  scriptCounter++;
2538
- const indicatorName = `_custom_script_${scriptCounter}`;
2749
+ const indicatorName = `_custom_script_active`;
2539
2750
  const figures = seriesKeys.map((key, i) => ({
2540
2751
  key,
2541
2752
  title: `${key}: `,
@@ -2547,7 +2758,7 @@ ${code}`
2547
2758
  shortName: (scriptName.trim() || `Script #${scriptCounter}`) + (placement === "main" ? " (Main)" : " (Sub)"),
2548
2759
  calcParams: parsedParams,
2549
2760
  figures,
2550
- calc: () => result,
2761
+ calc: (liveDataList) => compiledFn(...shadowValues, TA_default, liveDataList, parsedParams),
2551
2762
  extendData: {
2552
2763
  isCustomScript: true,
2553
2764
  code,
@@ -2573,7 +2784,7 @@ ${code}`
2573
2784
  paneId = chart.createIndicator({ name: indicatorName });
2574
2785
  }
2575
2786
  activeIdRef.current = typeof paneId === "string" ? paneId : null;
2576
- activeNameRef.current = indicatorName;
2787
+ setActiveName(indicatorName);
2577
2788
  const title = scriptName.trim() || `Script #${scriptCounter}`;
2578
2789
  setStatus(
2579
2790
  `${title} applied to ${placement === "main" ? "Main Chart" : "Sub Pane"} \u2014 ${seriesKeys.length} series: ${seriesKeys.join(", ")}`
@@ -2595,7 +2806,7 @@ ${code}`
2595
2806
  } catch {
2596
2807
  }
2597
2808
  activeIdRef.current = null;
2598
- activeNameRef.current = null;
2809
+ setActiveName(null);
2599
2810
  setStatus("Indicator removed.");
2600
2811
  }
2601
2812
  }, [state.chart]);
@@ -2709,10 +2920,10 @@ function useCrosshair() {
2709
2920
  var alertCounter = 0;
2710
2921
  function useAlerts() {
2711
2922
  const { state, dispatch } = useKlinechartsUI();
2712
- const { alertTriggeredListenerRef } = useKlinechartsUIDispatch();
2923
+ const { alertTriggeredListenersRef } = useKlinechartsUIDispatch();
2713
2924
  const alerts = state.alerts;
2714
2925
  const addAlert = useCallback(
2715
- (price, condition, message, extendData) => {
2926
+ (price, condition, message, extendData, target) => {
2716
2927
  const id = `alert_${++alertCounter}`;
2717
2928
  const precision = state.chart?.getSymbol()?.pricePrecision ?? 2;
2718
2929
  const resolvedExtendData = {
@@ -2725,9 +2936,12 @@ function useAlerts() {
2725
2936
  condition,
2726
2937
  message,
2727
2938
  triggered: false,
2728
- extendData: resolvedExtendData
2939
+ extendData: resolvedExtendData,
2940
+ // Persist the target only when it's an indicator alert; omit for the
2941
+ // default price target so existing serialized alerts stay compatible.
2942
+ ...target && target.type === "indicator" ? { target } : {}
2729
2943
  };
2730
- dispatch({ type: "SET_ALERTS", alerts: [...state.alerts, alert] });
2944
+ dispatch({ type: "ADD_ALERT", alert });
2731
2945
  if (state.chart) {
2732
2946
  ensureAlertLineRegistered();
2733
2947
  state.chart.createOverlay({
@@ -2741,29 +2955,28 @@ function useAlerts() {
2741
2955
  }
2742
2956
  return id;
2743
2957
  },
2744
- [state.chart, state.alerts, dispatch]
2958
+ [state.chart, dispatch]
2745
2959
  );
2746
2960
  const removeAlert = useCallback(
2747
2961
  (id) => {
2748
- dispatch({
2749
- type: "SET_ALERTS",
2750
- alerts: state.alerts.filter((a) => a.id !== id)
2751
- });
2962
+ dispatch({ type: "REMOVE_ALERT", id });
2752
2963
  state.chart?.removeOverlay({ id });
2753
2964
  },
2754
- [state.chart, state.alerts, dispatch]
2965
+ [state.chart, dispatch]
2755
2966
  );
2756
2967
  const clearAlerts = useCallback(() => {
2757
- for (const alert of state.alerts) {
2758
- state.chart?.removeOverlay({ id: alert.id });
2759
- }
2760
- dispatch({ type: "SET_ALERTS", alerts: [] });
2761
- }, [state.chart, state.alerts, dispatch]);
2968
+ state.chart?.removeOverlay({ groupId: "price_alerts" });
2969
+ dispatch({ type: "CLEAR_ALERTS" });
2970
+ }, [state.chart, dispatch]);
2762
2971
  const onAlertTriggered = useCallback(
2763
2972
  (callback) => {
2764
- alertTriggeredListenerRef.current = callback;
2973
+ const set = alertTriggeredListenersRef.current;
2974
+ set.add(callback);
2975
+ return () => {
2976
+ set.delete(callback);
2977
+ };
2765
2978
  },
2766
- [alertTriggeredListenerRef]
2979
+ [alertTriggeredListenersRef]
2767
2980
  );
2768
2981
  return {
2769
2982
  alerts,
@@ -2942,6 +3155,7 @@ function useReplay() {
2942
3155
  );
2943
3156
  const startReplay = useCallback(() => {
2944
3157
  if (!state.chart) return;
3158
+ if (isReplaying) return;
2945
3159
  const dataList = state.chart.getDataList();
2946
3160
  if (!dataList || dataList.length === 0) return;
2947
3161
  replaySavedDataRef.current = [...dataList];
@@ -2957,7 +3171,12 @@ function useReplay() {
2957
3171
  });
2958
3172
  state.chart.clearData?.();
2959
3173
  startInterval(speed);
2960
- }, [state.chart, speed, startInterval, replaySavedDataRef, replayIndexRef, dispatch]);
3174
+ }, [state.chart, state.symbol, state.period, speed, startInterval, isReplaying, replaySavedDataRef, replayIndexRef, dispatch]);
3175
+ useEffect(() => {
3176
+ if (isReplaying) {
3177
+ stopReplay();
3178
+ }
3179
+ }, [state.symbol, state.period]);
2961
3180
  const stopReplay = useCallback(() => {
2962
3181
  if (!state.chart) return;
2963
3182
  clearInterval_();
@@ -3053,24 +3272,29 @@ var DEFAULT_COLORS = [
3053
3272
  "#ff5722",
3054
3273
  "#8bc34a"
3055
3274
  ];
3056
- var colorIndex = 0;
3057
- var compareCounter = 0;
3058
3275
  function useCompare() {
3059
3276
  const { state, datafeed } = useKlinechartsUI();
3060
3277
  const [symbols, setSymbols] = useState([]);
3061
3278
  const indicatorsRef = useRef(/* @__PURE__ */ new Map());
3279
+ const instanceSalt = useId().replace(/[^a-zA-Z0-9]/g, "");
3062
3280
  const addSymbol = useCallback(
3063
3281
  async (ticker, color) => {
3064
3282
  if (!state.chart || !datafeed) return;
3065
3283
  if (indicatorsRef.current.has(ticker)) return;
3066
- const assignedColor = color ?? DEFAULT_COLORS[colorIndex++ % DEFAULT_COLORS.length];
3284
+ const assignedColor = color ?? DEFAULT_COLORS[symbols.length % DEFAULT_COLORS.length];
3067
3285
  const mainDataList = state.chart.getDataList();
3068
3286
  if (!mainDataList || mainDataList.length === 0) return;
3069
3287
  const from = mainDataList[0].timestamp;
3070
3288
  const to = mainDataList[mainDataList.length - 1].timestamp;
3289
+ const mainSymbol = state.symbol;
3290
+ const symbol = {
3291
+ ticker,
3292
+ pricePrecision: mainSymbol?.pricePrecision ?? 2,
3293
+ volumePrecision: mainSymbol?.volumePrecision ?? 8
3294
+ };
3071
3295
  const compareData = await datafeed.getHistoryKLineData(
3072
- { ticker },
3073
- { ...state.period, label: "" },
3296
+ symbol,
3297
+ state.period,
3074
3298
  from,
3075
3299
  to
3076
3300
  );
@@ -3089,13 +3313,7 @@ function useCompare() {
3089
3313
  }
3090
3314
  if (basePrice === null || basePrice === 0) return;
3091
3315
  const bp = basePrice;
3092
- const normalizedData = mainDataList.map((mainBar) => {
3093
- const close = compareMap.get(mainBar.timestamp);
3094
- return {
3095
- pct: close != null ? (close - bp) / bp * 100 : NaN
3096
- };
3097
- });
3098
- const indicatorName = `__cmp_${++compareCounter}_${ticker}`;
3316
+ const indicatorName = `__cmp_${instanceSalt}_${ticker}`;
3099
3317
  registerIndicator({
3100
3318
  name: indicatorName,
3101
3319
  shortName: ticker,
@@ -3107,7 +3325,16 @@ function useCompare() {
3107
3325
  styles: () => ({ color: assignedColor })
3108
3326
  }
3109
3327
  ],
3110
- calc: () => normalizedData
3328
+ calc: (dataList) => {
3329
+ let lastPct = null;
3330
+ return dataList.map((bar) => {
3331
+ const close = compareMap.get(bar.timestamp);
3332
+ if (close != null) {
3333
+ lastPct = (close - bp) / bp * 100;
3334
+ }
3335
+ return { pct: lastPct };
3336
+ });
3337
+ }
3111
3338
  });
3112
3339
  const paneId = state.chart.createIndicator(
3113
3340
  { name: indicatorName },
@@ -3125,7 +3352,7 @@ function useCompare() {
3125
3352
  ];
3126
3353
  });
3127
3354
  },
3128
- [state.chart, state.period, datafeed]
3355
+ [state.chart, state.symbol, state.period, datafeed, symbols.length, instanceSalt]
3129
3356
  );
3130
3357
  const removeSymbol = useCallback(
3131
3358
  (ticker) => {
@@ -3335,13 +3562,11 @@ function useAnnotations() {
3335
3562
  [state.chart]
3336
3563
  );
3337
3564
  const clearAnnotations = useCallback(() => {
3338
- setAnnotations((prev) => {
3339
- for (const annotation of prev) {
3340
- state.chart?.removeOverlay({ id: annotation.id });
3341
- }
3342
- return [];
3343
- });
3344
- }, [state.chart]);
3565
+ for (const annotation of annotations) {
3566
+ state.chart?.removeOverlay({ id: annotation.id });
3567
+ }
3568
+ setAnnotations([]);
3569
+ }, [state.chart, annotations]);
3345
3570
  useEffect(() => {
3346
3571
  return () => {
3347
3572
  state.chart?.removeOverlay({ groupId: "annotations" });
@@ -3355,71 +3580,161 @@ function useAnnotations() {
3355
3580
  clearAnnotations
3356
3581
  };
3357
3582
  }
3583
+ var WorkspaceContext = createContext(null);
3584
+ function useWorkspace() {
3585
+ const ctx = useContext(WorkspaceContext);
3586
+ if (!ctx) {
3587
+ throw new Error("useWorkspace must be used within a <WorkspaceProvider>");
3588
+ }
3589
+ return ctx;
3590
+ }
3358
3591
 
3359
- // src/utils/createDataLoader.ts
3360
- function createDataLoader(datafeed, dispatch) {
3361
- let oldestTimestamp = null;
3362
- let currentGen = 0;
3363
- return {
3364
- getBars: async (params) => {
3592
+ // src/workspace/types.ts
3593
+ var DEFAULT_SYNC_CONFIG = {
3594
+ crosshair: true,
3595
+ scroll: true,
3596
+ zoom: true,
3597
+ symbol: true,
3598
+ period: true
3599
+ };
3600
+ function reducer2(state, action) {
3601
+ switch (action.type) {
3602
+ case "SET_CELLS":
3603
+ return { ...state, cells: action.cells };
3604
+ case "ADD_CELL":
3605
+ return { ...state, cells: [...state.cells, action.cell] };
3606
+ case "REMOVE_CELL": {
3607
+ const cells = state.cells.filter((c) => c.id !== action.id);
3608
+ return {
3609
+ cells,
3610
+ activeCellId: state.activeCellId === action.id ? cells[0]?.id ?? null : state.activeCellId
3611
+ };
3612
+ }
3613
+ case "SET_CELL_SYMBOL":
3614
+ return {
3615
+ ...state,
3616
+ cells: state.cells.map(
3617
+ (c) => c.id === action.id ? { ...c, symbol: action.symbol } : c
3618
+ )
3619
+ };
3620
+ case "SET_CELL_PERIOD":
3621
+ return {
3622
+ ...state,
3623
+ cells: state.cells.map(
3624
+ (c) => c.id === action.id ? { ...c, period: action.period } : c
3625
+ )
3626
+ };
3627
+ case "SET_ACTIVE_CELL":
3628
+ return { ...state, activeCellId: action.id };
3629
+ default:
3630
+ return state;
3631
+ }
3632
+ }
3633
+ function WorkspaceProvider({
3634
+ defaultCells,
3635
+ sync,
3636
+ children
3637
+ }) {
3638
+ const [state, dispatch] = useReducer(reducer2, defaultCells, (cells) => ({
3639
+ cells,
3640
+ activeCellId: cells[0]?.id ?? null
3641
+ }));
3642
+ const chartsRef = useRef(/* @__PURE__ */ new Map());
3643
+ const broadcastingRef = useRef(false);
3644
+ const resolvedSync = useMemo(
3645
+ () => ({ ...DEFAULT_SYNC_CONFIG, ...sync }),
3646
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3647
+ []
3648
+ );
3649
+ const value = useMemo(
3650
+ () => ({
3651
+ state,
3652
+ dispatch,
3653
+ chartsRef,
3654
+ broadcastingRef,
3655
+ sync: resolvedSync
3656
+ }),
3657
+ [state, resolvedSync]
3658
+ );
3659
+ return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children });
3660
+ }
3661
+ function useChartSync({ cellId }) {
3662
+ const { chartsRef, broadcastingRef, sync, dispatch: workspaceDispatch } = useWorkspace();
3663
+ const { state } = useKlinechartsUI();
3664
+ const chart = state.chart;
3665
+ useEffect(() => {
3666
+ if (!chart) return;
3667
+ chartsRef.current.set(cellId, chart);
3668
+ return () => {
3669
+ chartsRef.current.delete(cellId);
3670
+ };
3671
+ }, [chart, cellId, chartsRef]);
3672
+ useEffect(() => {
3673
+ if (!chart) return;
3674
+ const broadcast = (fn) => {
3675
+ if (broadcastingRef.current) return;
3676
+ broadcastingRef.current = true;
3365
3677
  try {
3366
- dispatch({ type: "SET_LOADING", isLoading: true });
3367
- if (params.type === "init") {
3368
- oldestTimestamp = null;
3369
- const gen = ++currentGen;
3370
- const data = await datafeed.getHistoryKLineData(
3371
- params.symbol,
3372
- { ...params.period, label: "" },
3373
- 0,
3374
- Date.now()
3375
- );
3376
- if (gen !== currentGen) return;
3377
- if (data.length > 0) {
3378
- oldestTimestamp = data[0].timestamp;
3379
- }
3380
- params.callback(data, {
3381
- forward: data.length > 0,
3382
- backward: false
3383
- });
3384
- } else if (params.type === "forward" && oldestTimestamp !== null) {
3385
- const gen = currentGen;
3386
- const data = await datafeed.getHistoryKLineData(
3387
- params.symbol,
3388
- { ...params.period, label: "" },
3389
- 0,
3390
- oldestTimestamp - 1
3391
- );
3392
- if (gen !== currentGen) return;
3393
- if (data.length > 0) {
3394
- oldestTimestamp = data[0].timestamp;
3678
+ chartsRef.current.forEach((target, id) => {
3679
+ if (id !== cellId) {
3680
+ try {
3681
+ fn(target);
3682
+ } catch {
3683
+ }
3395
3684
  }
3396
- params.callback(data, {
3397
- forward: data.length > 0,
3398
- backward: false
3399
- });
3400
- } else if (params.type === "backward") {
3401
- params.callback([], { forward: false, backward: false });
3402
- }
3403
- } catch (error) {
3404
- console.error("Failed to load chart data:", error);
3405
- params.callback([], { forward: false, backward: false });
3685
+ });
3406
3686
  } finally {
3407
- dispatch({ type: "SET_LOADING", isLoading: false });
3687
+ broadcastingRef.current = false;
3408
3688
  }
3409
- },
3410
- subscribeBar: (params) => {
3411
- datafeed.subscribe(
3412
- params.symbol,
3413
- { ...params.period, label: "" },
3414
- (klineData) => params.callback(klineData)
3689
+ };
3690
+ const onCrosshairChange = (data) => {
3691
+ if (!sync.crosshair) return;
3692
+ const crosshair = data;
3693
+ if (!crosshair) return;
3694
+ broadcast((target) => target.executeAction("onCrosshairChange", crosshair));
3695
+ };
3696
+ const onScroll = () => {
3697
+ if (!sync.scroll) return;
3698
+ let range = null;
3699
+ try {
3700
+ range = chart.getVisibleRange();
3701
+ } catch {
3702
+ return;
3703
+ }
3704
+ if (!range || range.realTo == null) return;
3705
+ broadcast(
3706
+ (target) => target.scrollToTimestamp(range.realTo, 0)
3415
3707
  );
3416
- },
3417
- unsubscribeBar: (params) => {
3418
- datafeed.unsubscribe(params.symbol, { ...params.period, label: "" });
3708
+ };
3709
+ const onZoom = () => {
3710
+ if (!sync.zoom) return;
3711
+ let bar = 0;
3712
+ try {
3713
+ bar = chart.getBarSpace().bar;
3714
+ } catch {
3715
+ return;
3716
+ }
3717
+ broadcast((target) => target.setBarSpace(bar));
3718
+ };
3719
+ chart.subscribeAction("onCrosshairChange", onCrosshairChange);
3720
+ chart.subscribeAction("onScroll", onScroll);
3721
+ chart.subscribeAction("onZoom", onZoom);
3722
+ return () => {
3723
+ chart.unsubscribeAction("onCrosshairChange", onCrosshairChange);
3724
+ chart.unsubscribeAction("onScroll", onScroll);
3725
+ chart.unsubscribeAction("onZoom", onZoom);
3726
+ };
3727
+ }, [chart, cellId, chartsRef, broadcastingRef, sync.crosshair, sync.scroll, sync.zoom]);
3728
+ useEffect(() => {
3729
+ if (state.symbol) {
3730
+ workspaceDispatch({ type: "SET_CELL_SYMBOL", id: cellId, symbol: state.symbol });
3419
3731
  }
3420
- };
3732
+ }, [state.symbol]);
3733
+ useEffect(() => {
3734
+ workspaceDispatch({ type: "SET_CELL_PERIOD", id: cellId, period: state.period });
3735
+ }, [state.period]);
3421
3736
  }
3422
3737
 
3423
- export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIDispatchContext, KlinechartsUIProvider, KlinechartsUIStateContext, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, YAXIS_POSITIONS, createDataLoader, useAlerts, useAnnotations, useChartAxes, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useHotkeys, useIndicators, useKlinechartsUI, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist };
3738
+ export { CANDLE_TYPES, COMPARE_RULES, DEFAULT_PERIODS, DEFAULT_STORAGE_KEY_PREFIX, DEFAULT_STORAGE_NAMESPACES, DEFAULT_SYNC_CONFIG, DRAWING_CATEGORIES, INDICATOR_PARAMS, KlinechartsUIProvider, MAIN_INDICATORS, PRICE_AXIS_TYPES, SUB_INDICATORS, TIMEZONES, TOOLTIP_SHOW_RULES, WorkspaceProvider, YAXIS_POSITIONS, createDefaultStorage, resolveStorage, useAlerts, useAnnotations, useChartAxes, useChartSync, useCompare, useCrosshair, useDataExport, useDrawingTools, useFullscreen, useHotkeys, useIndicators, useKlinechartsUILoading, useKlinechartsUISettings, useKlinechartsUITheme, useLayoutManager, useMeasure, useOrderLines, usePeriods, useReplay, useScreenshot, useScriptEditor, useSymbolSearch, useTimezone, useUndoRedo, useWatchlist, useWorkspace };
3424
3739
  //# sourceMappingURL=index.js.map
3425
3740
  //# sourceMappingURL=index.js.map