react-klinecharts-ui 1.0.0 → 1.2.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-JD4NBJ5F.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-JD4NBJ5F.js';
3
- import { createContext, useContext, useReducer, useRef, useEffect, useCallback, useMemo, useState, useId } from 'react';
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';
4
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":
@@ -102,13 +127,16 @@ function KlinechartsUIProvider({
102
127
  defaultPeriod,
103
128
  defaultTheme = "light",
104
129
  defaultTimezone = "Asia/Shanghai",
105
- defaultMainIndicators = ["MA"],
106
- 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,
107
134
  defaultLocale = "en-US",
108
135
  periods,
109
136
  styles,
110
137
  registerExtensions: shouldRegister = true,
111
138
  overlays: extraOverlays,
139
+ storage: storageOptions,
112
140
  children,
113
141
  onStateChange,
114
142
  onSymbolChange,
@@ -119,6 +147,11 @@ function KlinechartsUIProvider({
119
147
  onSubIndicatorsChange,
120
148
  onSettingsChange
121
149
  }) {
150
+ const resolvedStorage = useMemo(
151
+ () => storageOptions ? resolveStorage(storageOptions) : null,
152
+ // eslint-disable-next-line react-hooks/exhaustive-deps
153
+ []
154
+ );
122
155
  const [state, dispatch] = useReducer(
123
156
  reducer,
124
157
  {
@@ -131,41 +164,61 @@ function KlinechartsUIProvider({
131
164
  defaultSubIndicators,
132
165
  defaultLocale,
133
166
  periods,
134
- styles
135
- },
136
- (opts) => ({
137
- chart: null,
138
- datafeed: opts.datafeed,
139
- symbol: opts.defaultSymbol ?? null,
140
- period: opts.defaultPeriod ?? (opts.periods ?? DEFAULT_PERIODS)[0],
141
- theme: opts.defaultTheme ?? "light",
142
- timezone: opts.defaultTimezone ?? "Asia/Shanghai",
143
- isLoading: false,
144
- locale: opts.defaultLocale ?? "en-US",
145
- periods: opts.periods ?? DEFAULT_PERIODS,
146
- mainIndicators: opts.defaultMainIndicators ?? ["MA"],
147
- subIndicators: (opts.defaultSubIndicators ?? ["VOL"]).reduce(
148
- (acc, name) => ({ ...acc, [name]: "" }),
149
- {}
150
- ),
151
- indicatorAxes: {},
152
- indicatorVisibility: {},
153
- alerts: [],
154
- measure: { isActive: false, fromPoint: null, result: null },
155
- replay: {
156
- isReplaying: false,
157
- isPaused: false,
158
- speed: 1,
159
- barIndex: 0,
160
- totalBars: 0
161
- },
162
- styles: opts.styles,
163
- screenshotUrl: null
164
- })
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
+ }
165
218
  );
166
219
  const fullscreenContainerRef = useRef(null);
167
220
  const undoRedoListenerRef = useRef(null);
168
- const alertTriggeredListenerRef = useRef(null);
221
+ const alertTriggeredListenersRef = useRef(/* @__PURE__ */ new Set());
169
222
  const replayIntervalRef = useRef(null);
170
223
  const replaySavedDataRef = useRef([]);
171
224
  const replayIndexRef = useRef(0);
@@ -243,23 +296,53 @@ function KlinechartsUIProvider({
243
296
  const chart = state.chart;
244
297
  if (!chart || !hasAlerts) return;
245
298
  alertPrevCloseRef.current = null;
299
+ const prevValueByAlert = /* @__PURE__ */ new Map();
246
300
  const interval = setInterval(() => {
247
301
  const dataList = chart.getDataList();
248
302
  if (!dataList || dataList.length === 0) return;
249
- const currentClose = dataList[dataList.length - 1].close;
250
- const prevClose = alertPrevCloseRef.current;
251
- alertPrevCloseRef.current = currentClose;
252
- 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
+ };
253
324
  const alerts = stateRef.current.alerts;
254
325
  const triggeredIds = [];
255
326
  for (const alert of alerts) {
256
327
  if (alert.triggered) continue;
257
- const crossedUp = prevClose < alert.price && currentClose >= alert.price;
258
- const crossedDown = prevClose > alert.price && currentClose <= alert.price;
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;
259
341
  const shouldTrigger = alert.condition === "crossing_up" ? crossedUp : alert.condition === "crossing_down" ? crossedDown : crossedUp || crossedDown;
260
342
  if (shouldTrigger) {
261
343
  triggeredIds.push(alert.id);
262
- alertTriggeredListenerRef.current?.({ ...alert, triggered: true });
344
+ const fired = { ...alert, triggered: true };
345
+ alertTriggeredListenersRef.current.forEach((cb) => cb(fired));
263
346
  }
264
347
  }
265
348
  if (triggeredIds.length > 0) {
@@ -274,6 +357,36 @@ function KlinechartsUIProvider({
274
357
  useEffect(() => {
275
358
  alertPrevCloseRef.current = null;
276
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
+ ]);
277
390
  useEffect(() => {
278
391
  return () => {
279
392
  if (replayIntervalRef.current !== null) {
@@ -289,13 +402,14 @@ function KlinechartsUIProvider({
289
402
  onSettingsChange: onSettingsChangeRef.current,
290
403
  fullscreenContainerRef,
291
404
  undoRedoListenerRef,
292
- alertTriggeredListenerRef,
405
+ alertTriggeredListenersRef,
293
406
  replayIntervalRef,
294
407
  replaySavedDataRef,
295
- replayIndexRef
408
+ replayIndexRef,
409
+ storage: resolvedStorage
296
410
  }),
297
411
  // eslint-disable-next-line react-hooks/exhaustive-deps
298
- [enhancedDispatch]
412
+ [enhancedDispatch, resolvedStorage]
299
413
  );
300
414
  return /* @__PURE__ */ jsx(KlinechartsUIStateContext.Provider, { value: state, children: /* @__PURE__ */ jsx(KlinechartsUIDispatchContext.Provider, { value: dispatchValue, children }) });
301
415
  }
@@ -1244,6 +1358,25 @@ var DRAWING_CATEGORIES = [
1244
1358
 
1245
1359
  // src/hooks/useDrawingTools.ts
1246
1360
  var DRAWING_GROUP_ID = "drawing_tools";
1361
+ var DRAWING_NAME_TO_LOCALE_KEY = new Map(
1362
+ DRAWING_CATEGORIES.flatMap(
1363
+ (cat) => cat.tools.map((tool) => [tool.name, tool.localeKey])
1364
+ )
1365
+ );
1366
+ function drawingLabel(name) {
1367
+ return DRAWING_NAME_TO_LOCALE_KEY.get(name) ?? name;
1368
+ }
1369
+ function overlaysEqual(a, b) {
1370
+ if (a.length !== b.length) return false;
1371
+ for (let i = 0; i < a.length; i++) {
1372
+ const x = a[i];
1373
+ const y = b[i];
1374
+ if (x.id !== y.id || x.name !== y.name || x.paneId !== y.paneId || x.locked !== y.locked || x.visible !== y.visible) {
1375
+ return false;
1376
+ }
1377
+ }
1378
+ return true;
1379
+ }
1247
1380
  function useDrawingTools() {
1248
1381
  const { state } = useKlinechartsUI();
1249
1382
  const { undoRedoListenerRef } = useKlinechartsUIDispatch();
@@ -1264,6 +1397,34 @@ function useDrawingTools() {
1264
1397
  isVisibleRef.current = isVisible;
1265
1398
  magnetModeRef.current = magnetMode;
1266
1399
  });
1400
+ const [overlays2, setOverlays] = useState([]);
1401
+ const refreshOverlays = useCallback(() => {
1402
+ if (!state.chart) {
1403
+ setOverlays((prev) => prev.length === 0 ? prev : []);
1404
+ return;
1405
+ }
1406
+ const list = state.chart.getOverlays({ groupId: DRAWING_GROUP_ID });
1407
+ const next = list.map((o) => ({
1408
+ id: o.id,
1409
+ name: o.name,
1410
+ paneId: o.paneId,
1411
+ locked: !!o.lock,
1412
+ visible: o.visible !== false
1413
+ // klinecharts default = true
1414
+ }));
1415
+ setOverlays((prev) => overlaysEqual(prev, next) ? prev : next);
1416
+ }, [state.chart]);
1417
+ const refreshOverlaysRef = useRef(() => {
1418
+ });
1419
+ useEffect(() => {
1420
+ refreshOverlaysRef.current = refreshOverlays;
1421
+ });
1422
+ useEffect(() => {
1423
+ if (!state.chart) return;
1424
+ queueMicrotask(() => refreshOverlaysRef.current());
1425
+ const interval = setInterval(() => refreshOverlaysRef.current(), 1e3);
1426
+ return () => clearInterval(interval);
1427
+ }, [state.chart, refreshOverlays]);
1267
1428
  const categories = useMemo(
1268
1429
  () => DRAWING_CATEGORIES.map((cat) => ({
1269
1430
  key: cat.key,
@@ -1299,6 +1460,7 @@ function useDrawingTools() {
1299
1460
  }
1300
1461
  }
1301
1462
  });
1463
+ refreshOverlaysRef.current();
1302
1464
  if (autoRetriggerRef.current && activeToolRef.current === name) {
1303
1465
  requestAnimationFrame(() => {
1304
1466
  createOverlayForToolRef.current(name);
@@ -1325,49 +1487,52 @@ function useDrawingTools() {
1325
1487
  const setMagnetMode = useCallback(
1326
1488
  (mode) => {
1327
1489
  setMagnetModeState(mode);
1328
- const overlays2 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1329
- if (overlays2) {
1490
+ const overlays3 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1491
+ if (overlays3) {
1330
1492
  const overlayMode = mode === "strong" ? "strong_magnet" : mode === "weak" ? "weak_magnet" : "normal";
1331
- overlays2.forEach((overlay) => {
1493
+ overlays3.forEach((overlay) => {
1332
1494
  state.chart?.overrideOverlay({
1333
1495
  id: overlay.id,
1334
1496
  mode: overlayMode
1335
1497
  });
1336
1498
  });
1337
1499
  }
1500
+ refreshOverlaysRef.current();
1338
1501
  },
1339
1502
  [state.chart]
1340
1503
  );
1341
1504
  const toggleLock = useCallback(() => {
1342
1505
  const newLocked = !isLocked;
1343
1506
  setIsLocked(newLocked);
1344
- const overlays2 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1345
- if (overlays2) {
1346
- overlays2.forEach((overlay) => {
1507
+ const overlays3 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1508
+ if (overlays3) {
1509
+ overlays3.forEach((overlay) => {
1347
1510
  state.chart?.overrideOverlay({
1348
1511
  id: overlay.id,
1349
1512
  lock: newLocked
1350
1513
  });
1351
1514
  });
1352
1515
  }
1516
+ refreshOverlaysRef.current();
1353
1517
  }, [state.chart, isLocked]);
1354
1518
  const toggleVisibility = useCallback(() => {
1355
1519
  const newVisible = !isVisible;
1356
1520
  setIsVisible(newVisible);
1357
- const overlays2 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1358
- if (overlays2) {
1359
- overlays2.forEach((overlay) => {
1521
+ const overlays3 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1522
+ if (overlays3) {
1523
+ overlays3.forEach((overlay) => {
1360
1524
  state.chart?.overrideOverlay({
1361
1525
  id: overlay.id,
1362
1526
  visible: newVisible
1363
1527
  });
1364
1528
  });
1365
1529
  }
1530
+ refreshOverlaysRef.current();
1366
1531
  }, [state.chart, isVisible]);
1367
1532
  const removeAllDrawings = useCallback(() => {
1368
- const overlays2 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1369
- if (overlays2 && overlays2.length > 0) {
1370
- const snapshot = overlays2.map((o) => ({
1533
+ const overlays3 = state.chart?.getOverlays({ groupId: DRAWING_GROUP_ID });
1534
+ if (overlays3 && overlays3.length > 0) {
1535
+ const snapshot = overlays3.map((o) => ({
1371
1536
  name: o.name,
1372
1537
  id: o.id,
1373
1538
  points: o.points,
@@ -1381,7 +1546,29 @@ function useDrawingTools() {
1381
1546
  }
1382
1547
  state.chart?.removeOverlay({ groupId: DRAWING_GROUP_ID });
1383
1548
  setActiveTool(null);
1549
+ refreshOverlaysRef.current();
1384
1550
  }, [state.chart, undoRedoListenerRef]);
1551
+ const removeDrawing = useCallback(
1552
+ (id) => {
1553
+ state.chart?.removeOverlay({ id, groupId: DRAWING_GROUP_ID });
1554
+ refreshOverlaysRef.current();
1555
+ },
1556
+ [state.chart]
1557
+ );
1558
+ const setDrawingVisible = useCallback(
1559
+ (id, visible) => {
1560
+ state.chart?.overrideOverlay({ id, visible });
1561
+ refreshOverlaysRef.current();
1562
+ },
1563
+ [state.chart]
1564
+ );
1565
+ const setDrawingLocked = useCallback(
1566
+ (id, locked) => {
1567
+ state.chart?.overrideOverlay({ id, lock: locked });
1568
+ refreshOverlaysRef.current();
1569
+ },
1570
+ [state.chart]
1571
+ );
1385
1572
  return {
1386
1573
  categories,
1387
1574
  activeTool,
@@ -1389,13 +1576,17 @@ function useDrawingTools() {
1389
1576
  isLocked,
1390
1577
  isVisible,
1391
1578
  autoRetrigger,
1579
+ overlays: overlays2,
1392
1580
  selectTool,
1393
1581
  clearActiveTool,
1394
1582
  setMagnetMode,
1395
1583
  toggleLock,
1396
1584
  toggleVisibility,
1397
1585
  removeAllDrawings,
1398
- setAutoRetrigger
1586
+ setAutoRetrigger,
1587
+ removeDrawing,
1588
+ setDrawingVisible,
1589
+ setDrawingLocked
1399
1590
  };
1400
1591
  }
1401
1592
 
@@ -1437,7 +1628,17 @@ var defaultSettings = {
1437
1628
  };
1438
1629
  function useKlinechartsUISettings() {
1439
1630
  const { state, onSettingsChange } = useKlinechartsUI();
1440
- const [settings, setSettings] = useState(defaultSettings);
1631
+ const { storage } = useKlinechartsUIDispatch();
1632
+ const [settings, setSettings] = useState(() => {
1633
+ if (storage && storage.persists("settings")) {
1634
+ try {
1635
+ const raw = storage.adapter.getItem(storage.key("settings"));
1636
+ if (raw) return { ...defaultSettings, ...JSON.parse(raw) };
1637
+ } catch {
1638
+ }
1639
+ }
1640
+ return defaultSettings;
1641
+ });
1441
1642
  const isInitialMount = useRef(true);
1442
1643
  useEffect(() => {
1443
1644
  if (isInitialMount.current) {
@@ -1446,6 +1647,14 @@ function useKlinechartsUISettings() {
1446
1647
  }
1447
1648
  onSettingsChange?.({ ...settings });
1448
1649
  }, [settings, onSettingsChange]);
1650
+ useEffect(() => {
1651
+ if (!storage || !storage.persists("settings")) return;
1652
+ if (isInitialMount.current) return;
1653
+ try {
1654
+ storage.adapter.setItem(storage.key("settings"), JSON.stringify(settings));
1655
+ } catch {
1656
+ }
1657
+ }, [settings, storage]);
1449
1658
  const candleTypes = useMemo(
1450
1659
  () => CANDLE_TYPES.map((ct) => ({
1451
1660
  key: ct.key,
@@ -2788,10 +2997,10 @@ function useCrosshair() {
2788
2997
  var alertCounter = 0;
2789
2998
  function useAlerts() {
2790
2999
  const { state, dispatch } = useKlinechartsUI();
2791
- const { alertTriggeredListenerRef } = useKlinechartsUIDispatch();
3000
+ const { alertTriggeredListenersRef } = useKlinechartsUIDispatch();
2792
3001
  const alerts = state.alerts;
2793
3002
  const addAlert = useCallback(
2794
- (price, condition, message, extendData) => {
3003
+ (price, condition, message, extendData, target) => {
2795
3004
  const id = `alert_${++alertCounter}`;
2796
3005
  const precision = state.chart?.getSymbol()?.pricePrecision ?? 2;
2797
3006
  const resolvedExtendData = {
@@ -2804,7 +3013,10 @@ function useAlerts() {
2804
3013
  condition,
2805
3014
  message,
2806
3015
  triggered: false,
2807
- extendData: resolvedExtendData
3016
+ extendData: resolvedExtendData,
3017
+ // Persist the target only when it's an indicator alert; omit for the
3018
+ // default price target so existing serialized alerts stay compatible.
3019
+ ...target && target.type === "indicator" ? { target } : {}
2808
3020
  };
2809
3021
  dispatch({ type: "ADD_ALERT", alert });
2810
3022
  if (state.chart) {
@@ -2835,9 +3047,13 @@ function useAlerts() {
2835
3047
  }, [state.chart, dispatch]);
2836
3048
  const onAlertTriggered = useCallback(
2837
3049
  (callback) => {
2838
- alertTriggeredListenerRef.current = callback;
3050
+ const set = alertTriggeredListenersRef.current;
3051
+ set.add(callback);
3052
+ return () => {
3053
+ set.delete(callback);
3054
+ };
2839
3055
  },
2840
- [alertTriggeredListenerRef]
3056
+ [alertTriggeredListenersRef]
2841
3057
  );
2842
3058
  return {
2843
3059
  alerts,
@@ -3441,71 +3657,161 @@ function useAnnotations() {
3441
3657
  clearAnnotations
3442
3658
  };
3443
3659
  }
3660
+ var WorkspaceContext = createContext(null);
3661
+ function useWorkspace() {
3662
+ const ctx = useContext(WorkspaceContext);
3663
+ if (!ctx) {
3664
+ throw new Error("useWorkspace must be used within a <WorkspaceProvider>");
3665
+ }
3666
+ return ctx;
3667
+ }
3444
3668
 
3445
- // src/utils/createDataLoader.ts
3446
- function createDataLoader(datafeed, dispatch) {
3447
- let oldestTimestamp = null;
3448
- let currentGen = 0;
3449
- return {
3450
- getBars: async (params) => {
3669
+ // src/workspace/types.ts
3670
+ var DEFAULT_SYNC_CONFIG = {
3671
+ crosshair: true,
3672
+ scroll: true,
3673
+ zoom: true,
3674
+ symbol: true,
3675
+ period: true
3676
+ };
3677
+ function reducer2(state, action) {
3678
+ switch (action.type) {
3679
+ case "SET_CELLS":
3680
+ return { ...state, cells: action.cells };
3681
+ case "ADD_CELL":
3682
+ return { ...state, cells: [...state.cells, action.cell] };
3683
+ case "REMOVE_CELL": {
3684
+ const cells = state.cells.filter((c) => c.id !== action.id);
3685
+ return {
3686
+ cells,
3687
+ activeCellId: state.activeCellId === action.id ? cells[0]?.id ?? null : state.activeCellId
3688
+ };
3689
+ }
3690
+ case "SET_CELL_SYMBOL":
3691
+ return {
3692
+ ...state,
3693
+ cells: state.cells.map(
3694
+ (c) => c.id === action.id ? { ...c, symbol: action.symbol } : c
3695
+ )
3696
+ };
3697
+ case "SET_CELL_PERIOD":
3698
+ return {
3699
+ ...state,
3700
+ cells: state.cells.map(
3701
+ (c) => c.id === action.id ? { ...c, period: action.period } : c
3702
+ )
3703
+ };
3704
+ case "SET_ACTIVE_CELL":
3705
+ return { ...state, activeCellId: action.id };
3706
+ default:
3707
+ return state;
3708
+ }
3709
+ }
3710
+ function WorkspaceProvider({
3711
+ defaultCells,
3712
+ sync,
3713
+ children
3714
+ }) {
3715
+ const [state, dispatch] = useReducer(reducer2, defaultCells, (cells) => ({
3716
+ cells,
3717
+ activeCellId: cells[0]?.id ?? null
3718
+ }));
3719
+ const chartsRef = useRef(/* @__PURE__ */ new Map());
3720
+ const broadcastingRef = useRef(false);
3721
+ const resolvedSync = useMemo(
3722
+ () => ({ ...DEFAULT_SYNC_CONFIG, ...sync }),
3723
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3724
+ []
3725
+ );
3726
+ const value = useMemo(
3727
+ () => ({
3728
+ state,
3729
+ dispatch,
3730
+ chartsRef,
3731
+ broadcastingRef,
3732
+ sync: resolvedSync
3733
+ }),
3734
+ [state, resolvedSync]
3735
+ );
3736
+ return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children });
3737
+ }
3738
+ function useChartSync({ cellId }) {
3739
+ const { chartsRef, broadcastingRef, sync, dispatch: workspaceDispatch } = useWorkspace();
3740
+ const { state } = useKlinechartsUI();
3741
+ const chart = state.chart;
3742
+ useEffect(() => {
3743
+ if (!chart) return;
3744
+ chartsRef.current.set(cellId, chart);
3745
+ return () => {
3746
+ chartsRef.current.delete(cellId);
3747
+ };
3748
+ }, [chart, cellId, chartsRef]);
3749
+ useEffect(() => {
3750
+ if (!chart) return;
3751
+ const broadcast = (fn) => {
3752
+ if (broadcastingRef.current) return;
3753
+ broadcastingRef.current = true;
3451
3754
  try {
3452
- dispatch({ type: "SET_LOADING", isLoading: true });
3453
- if (params.type === "init") {
3454
- oldestTimestamp = null;
3455
- const gen = ++currentGen;
3456
- const data = await datafeed.getHistoryKLineData(
3457
- params.symbol,
3458
- { ...params.period, label: "" },
3459
- 0,
3460
- Date.now()
3461
- );
3462
- if (gen !== currentGen) return;
3463
- if (data.length > 0) {
3464
- oldestTimestamp = data[0].timestamp;
3465
- }
3466
- params.callback(data, {
3467
- forward: data.length > 0,
3468
- backward: false
3469
- });
3470
- } else if (params.type === "forward" && oldestTimestamp !== null) {
3471
- const gen = currentGen;
3472
- const data = await datafeed.getHistoryKLineData(
3473
- params.symbol,
3474
- { ...params.period, label: "" },
3475
- 0,
3476
- oldestTimestamp - 1
3477
- );
3478
- if (gen !== currentGen) return;
3479
- if (data.length > 0) {
3480
- oldestTimestamp = data[0].timestamp;
3755
+ chartsRef.current.forEach((target, id) => {
3756
+ if (id !== cellId) {
3757
+ try {
3758
+ fn(target);
3759
+ } catch {
3760
+ }
3481
3761
  }
3482
- params.callback(data, {
3483
- forward: data.length > 0,
3484
- backward: false
3485
- });
3486
- } else if (params.type === "backward") {
3487
- params.callback([], { forward: false, backward: false });
3488
- }
3489
- } catch (error) {
3490
- console.error("Failed to load chart data:", error);
3491
- params.callback([], { forward: false, backward: false });
3762
+ });
3492
3763
  } finally {
3493
- dispatch({ type: "SET_LOADING", isLoading: false });
3764
+ broadcastingRef.current = false;
3494
3765
  }
3495
- },
3496
- subscribeBar: (params) => {
3497
- datafeed.subscribe(
3498
- params.symbol,
3499
- { ...params.period, label: "" },
3500
- (klineData) => params.callback(klineData)
3766
+ };
3767
+ const onCrosshairChange = (data) => {
3768
+ if (!sync.crosshair) return;
3769
+ const crosshair = data;
3770
+ if (!crosshair) return;
3771
+ broadcast((target) => target.executeAction("onCrosshairChange", crosshair));
3772
+ };
3773
+ const onScroll = () => {
3774
+ if (!sync.scroll) return;
3775
+ let range = null;
3776
+ try {
3777
+ range = chart.getVisibleRange();
3778
+ } catch {
3779
+ return;
3780
+ }
3781
+ if (!range || range.realTo == null) return;
3782
+ broadcast(
3783
+ (target) => target.scrollToTimestamp(range.realTo, 0)
3501
3784
  );
3502
- },
3503
- unsubscribeBar: (params) => {
3504
- datafeed.unsubscribe(params.symbol, { ...params.period, label: "" });
3785
+ };
3786
+ const onZoom = () => {
3787
+ if (!sync.zoom) return;
3788
+ let bar = 0;
3789
+ try {
3790
+ bar = chart.getBarSpace().bar;
3791
+ } catch {
3792
+ return;
3793
+ }
3794
+ broadcast((target) => target.setBarSpace(bar));
3795
+ };
3796
+ chart.subscribeAction("onCrosshairChange", onCrosshairChange);
3797
+ chart.subscribeAction("onScroll", onScroll);
3798
+ chart.subscribeAction("onZoom", onZoom);
3799
+ return () => {
3800
+ chart.unsubscribeAction("onCrosshairChange", onCrosshairChange);
3801
+ chart.unsubscribeAction("onScroll", onScroll);
3802
+ chart.unsubscribeAction("onZoom", onZoom);
3803
+ };
3804
+ }, [chart, cellId, chartsRef, broadcastingRef, sync.crosshair, sync.scroll, sync.zoom]);
3805
+ useEffect(() => {
3806
+ if (state.symbol) {
3807
+ workspaceDispatch({ type: "SET_CELL_SYMBOL", id: cellId, symbol: state.symbol });
3505
3808
  }
3506
- };
3809
+ }, [state.symbol]);
3810
+ useEffect(() => {
3811
+ workspaceDispatch({ type: "SET_CELL_PERIOD", id: cellId, period: state.period });
3812
+ }, [state.period]);
3507
3813
  }
3508
3814
 
3509
- 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 };
3815
+ 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, drawingLabel, 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 };
3510
3816
  //# sourceMappingURL=index.js.map
3511
3817
  //# sourceMappingURL=index.js.map