@forgecharts/sdk 1.5.82 → 1.5.83

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.
Files changed (53) hide show
  1. package/dist/api/EventBus.d.ts +1 -1
  2. package/dist/api/{TChart.d.ts → ForgeCharts.d.ts} +38 -4
  3. package/dist/api/ForgeCharts.d.ts.map +1 -0
  4. package/dist/api/createChart.d.ts +4 -4
  5. package/dist/api/createChart.d.ts.map +1 -1
  6. package/dist/core/Chart.d.ts +5 -5
  7. package/dist/core/Chart.d.ts.map +1 -1
  8. package/dist/core/Series.d.ts +14 -0
  9. package/dist/core/Series.d.ts.map +1 -1
  10. package/dist/core/__tests__/seriesCutoff.test.d.ts +2 -0
  11. package/dist/core/__tests__/seriesCutoff.test.d.ts.map +1 -0
  12. package/dist/datafeed/DatafeedConnector.d.ts +1 -1
  13. package/dist/index.d.ts +3 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +432 -27
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal.d.ts +1 -1
  18. package/dist/internal.js +432 -27
  19. package/dist/internal.js.map +1 -1
  20. package/dist/licensing/packageFeatures.d.ts.map +1 -1
  21. package/dist/node.d.ts +1 -1
  22. package/dist/node.d.ts.map +1 -1
  23. package/dist/pixi/PixiCandlestickRenderer.d.ts +4 -0
  24. package/dist/pixi/PixiCandlestickRenderer.d.ts.map +1 -1
  25. package/dist/pixi/PixiChart.d.ts +12 -2
  26. package/dist/pixi/PixiChart.d.ts.map +1 -1
  27. package/dist/pixi/PixiHistogramRenderer.d.ts +2 -0
  28. package/dist/pixi/PixiHistogramRenderer.d.ts.map +1 -1
  29. package/dist/pixi/PixiLineRenderer.d.ts +2 -0
  30. package/dist/pixi/PixiLineRenderer.d.ts.map +1 -1
  31. package/dist/react/canvas/ChartCanvas.d.ts +7 -2
  32. package/dist/react/canvas/ChartCanvas.d.ts.map +1 -1
  33. package/dist/react/hooks/useUserPackage.d.ts.map +1 -1
  34. package/dist/react/index.js +719 -63
  35. package/dist/react/index.js.map +1 -1
  36. package/dist/react/internal.js +726 -83
  37. package/dist/react/internal.js.map +1 -1
  38. package/dist/react/shell/AlertsDrawer.d.ts +11 -0
  39. package/dist/react/shell/AlertsDrawer.d.ts.map +1 -0
  40. package/dist/react/shell/ManagedAppShell.d.ts +1 -1
  41. package/dist/react/shell/ManagedAppShell.d.ts.map +1 -1
  42. package/dist/react/workspace/ChartWorkspace.d.ts +10 -1
  43. package/dist/react/workspace/ChartWorkspace.d.ts.map +1 -1
  44. package/dist/react/workspace/LayoutMenu.d.ts +3 -1
  45. package/dist/react/workspace/LayoutMenu.d.ts.map +1 -1
  46. package/dist/react/workspace/toolbars/TopToolbar.d.ts +7 -1
  47. package/dist/react/workspace/toolbars/TopToolbar.d.ts.map +1 -1
  48. package/dist/types/ChartBackend.d.ts +15 -4
  49. package/dist/types/ChartBackend.d.ts.map +1 -1
  50. package/dist/types/ISeries.d.ts +5 -0
  51. package/dist/types/ISeries.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/dist/api/TChart.d.ts.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import React11, { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback, createContext, useMemo, useContext, useReducer } from 'react';
2
- import { TextStyle, Application, Container, Graphics, Text, FillGradient, CanvasTextMetrics } from 'pixi.js';
2
+ import { TextStyle, Application, Graphics, Container, Text, FillGradient, CanvasTextMetrics } from 'pixi.js';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import ReactDOM, { createPortal } from 'react-dom';
5
5
 
@@ -1410,6 +1410,7 @@ var Series = class _Series {
1410
1410
  // ─── ISeries implementation ──────────────────────────────────────────────────
1411
1411
  setData(data) {
1412
1412
  this._data = [...data].sort((a, b) => a.time - b.time);
1413
+ this._cutoffView = null;
1413
1414
  this.onDataChanged?.();
1414
1415
  }
1415
1416
  /**
@@ -1449,11 +1450,47 @@ var Series = class _Series {
1449
1450
  this._data = this._data.slice(this._data.length - 1e5);
1450
1451
  }
1451
1452
  }
1453
+ this._cutoffView = null;
1452
1454
  this.onDataChanged?.();
1453
1455
  }
1454
- data() {
1456
+ /**
1457
+ * Bar-replay display cutoff. When set, data() returns only bars at or
1458
+ * before this time while the FULL tape keeps flowing underneath — the
1459
+ * datafeed keeps writing live bars via setData/update, and everything
1460
+ * downstream (renderers, indicators, crosshair, last-price) naturally sees
1461
+ * the truncated past. Replay is not a mode the pipeline knows about; it is
1462
+ * this clamp plus a controller that moves it.
1463
+ */
1464
+ _cutoffTime = null;
1465
+ _cutoffView = null;
1466
+ setCutoffTime(time) {
1467
+ this._cutoffTime = time;
1468
+ this._cutoffView = null;
1469
+ this.onDataChanged?.();
1470
+ }
1471
+ cutoffTime() {
1472
+ return this._cutoffTime;
1473
+ }
1474
+ /** The complete tape, ignoring any replay cutoff. Datafeed-side reads only. */
1475
+ fullData() {
1455
1476
  return this._data;
1456
1477
  }
1478
+ data() {
1479
+ if (this._cutoffTime === null) return this._data;
1480
+ const len = this._data.length;
1481
+ const cached = this._cutoffView;
1482
+ if (cached && cached.len === len && cached.cutoff === this._cutoffTime) return cached.view;
1483
+ let lo = 0;
1484
+ let hi = len;
1485
+ while (lo < hi) {
1486
+ const mid = lo + hi >>> 1;
1487
+ if (this._data[mid].time <= this._cutoffTime) lo = mid + 1;
1488
+ else hi = mid;
1489
+ }
1490
+ const view = this._data.slice(0, lo);
1491
+ this._cutoffView = { len, cutoff: this._cutoffTime, view };
1492
+ return view;
1493
+ }
1457
1494
  applyOptions(options) {
1458
1495
  this._options = { ...this._options, ...options };
1459
1496
  }
@@ -4730,14 +4767,14 @@ var Chart = class {
4730
4767
  _crosshairEnabled = true;
4731
4768
  _magnetMode = "snap-xy";
4732
4769
  // ── Trading overlay ───────────────────────────────────────────────────────────
4733
- /** Current set of order lines supplied by TChart for canvas rendering. */
4770
+ /** Current set of order lines supplied by ForgeCharts for canvas rendering. */
4734
4771
  _tradingOrders = [];
4735
4772
  /** Called whenever a drawing is committed (created or updated). */
4736
4773
  onDrawingCommitted;
4737
4774
  /** Fired when an EXISTING drawing is modified via drag (handle or body move). */
4738
4775
  onDrawingEdited;
4739
4776
  _drawingEditDirty = false;
4740
- /** Ctrl+Z / Ctrl+Y — drawing history is owned by TChart. */
4777
+ /** Ctrl+Z / Ctrl+Y — drawing history is owned by ForgeCharts. */
4741
4778
  onUndoRequest;
4742
4779
  onRedoRequest;
4743
4780
  /** Called whenever a drawing is deleted. */
@@ -4753,7 +4790,7 @@ var Chart = class {
4753
4790
  /** Called when the user drags a TP/SL pill off an entry line to create a bracket leg. */
4754
4791
  onTpSlCreate;
4755
4792
  /**
4756
- * Called after every pan or zoom interaction so external code (e.g. TChart)
4793
+ * Called after every pan or zoom interaction so external code (e.g. ForgeCharts)
4757
4794
  * can check whether the visible time range now extends past the oldest
4758
4795
  * loaded bar and trigger a lazy history fetch.
4759
4796
  */
@@ -5256,11 +5293,11 @@ var Chart = class {
5256
5293
  }
5257
5294
  ctx.restore();
5258
5295
  }
5259
- /** Provides the overlay renderer with the latest order list from TChart. */
5296
+ /** Provides the overlay renderer with the latest order list from ForgeCharts. */
5260
5297
  setTradingOrders(orders) {
5261
5298
  this._tradingOrders = orders;
5262
5299
  }
5263
- /** Provides the overlay renderer with the latest position list from TChart. */
5300
+ /** Provides the overlay renderer with the latest position list from ForgeCharts. */
5264
5301
  setTradingPositions(_positions) {
5265
5302
  }
5266
5303
  /** Sets contract switch markers for continuous futures roll dates. */
@@ -6077,9 +6114,17 @@ var PixiCandlestickRenderer = class {
6077
6114
  this._gfx = new Graphics();
6078
6115
  layer.addChild(this._gfx);
6079
6116
  }
6117
+ /** Clear this renderer's graphics — used when another style takes over. */
6118
+ clear() {
6119
+ this._gfx.clear();
6120
+ }
6080
6121
  draw(bars, transform, opts) {
6081
6122
  const gfx = this._gfx;
6082
6123
  gfx.clear();
6124
+ if (opts?.type === "bar") {
6125
+ this._drawBars(bars, transform, opts);
6126
+ return;
6127
+ }
6083
6128
  if (bars.length === 0) return;
6084
6129
  const { plotWidth: pw, plotHeight: ph } = transform;
6085
6130
  const upColor = hexToNum(opts && "upColor" in opts ? opts.upColor : void 0, UP_COLOR);
@@ -6210,6 +6255,36 @@ var PixiCandlestickRenderer = class {
6210
6255
  /**
6211
6256
  * Converts OHLCV bars to Heikin-Ashi values before drawing.
6212
6257
  */
6258
+ /** Classic OHLC bars: high-low spine, open tick left, close tick right. */
6259
+ _drawBars(bars, transform, opts) {
6260
+ const gfx = this._gfx;
6261
+ if (bars.length === 0) return;
6262
+ const upColor = hexToNum(opts && "upColor" in opts ? opts.upColor : void 0, UP_COLOR);
6263
+ const downColor = hexToNum(opts && "downColor" in opts ? opts.downColor : void 0, DOWN_COLOR);
6264
+ const { plotWidth: pw } = transform;
6265
+ let barPitch = pw / Math.max(1, bars.length);
6266
+ if (bars.length >= 2) {
6267
+ const dx = Math.abs(transform.timeToX(bars[1].time) - transform.timeToX(bars[0].time));
6268
+ if (dx > 0) barPitch = dx;
6269
+ }
6270
+ const tick = Math.max(1.5, Math.min(barPitch * 0.35, 6));
6271
+ const lineW = barPitch < 3 ? 1 : 1.5;
6272
+ for (const bar of bars) {
6273
+ const x = transform.timeToX(bar.time);
6274
+ if (x < -barPitch || x > pw + barPitch) continue;
6275
+ const yHigh = transform.priceToY(bar.high);
6276
+ const yLow = transform.priceToY(bar.low);
6277
+ const yOpen = transform.priceToY(bar.open);
6278
+ const yClose = transform.priceToY(bar.close);
6279
+ const color = bar.close >= bar.open ? upColor : downColor;
6280
+ gfx.moveTo(x, yHigh).lineTo(x, yLow);
6281
+ if (barPitch >= 3) {
6282
+ gfx.moveTo(x - tick, yOpen).lineTo(x, yOpen);
6283
+ gfx.moveTo(x, yClose).lineTo(x + tick, yClose);
6284
+ }
6285
+ gfx.stroke({ color, width: lineW });
6286
+ }
6287
+ }
6213
6288
  drawHeikinAshi(bars, transform, opts) {
6214
6289
  const first = bars[0];
6215
6290
  const last = bars[bars.length - 1];
@@ -7532,6 +7607,10 @@ var PixiLineRenderer = class {
7532
7607
  this._gradientCache = null;
7533
7608
  this._gfx.destroy(true);
7534
7609
  }
7610
+ /** Clear this renderer's graphics — used when another style takes over. */
7611
+ clear() {
7612
+ this._gfx.clear();
7613
+ }
7535
7614
  draw(bars, transform, options) {
7536
7615
  const gfx = this._gfx;
7537
7616
  gfx.clear();
@@ -7626,6 +7705,10 @@ var PixiHistogramRenderer = class {
7626
7705
  destroy() {
7627
7706
  this._gfx.destroy(true);
7628
7707
  }
7708
+ /** Clear this renderer's graphics — used when another style takes over. */
7709
+ clear() {
7710
+ this._gfx.clear();
7711
+ }
7629
7712
  draw(bars, transform) {
7630
7713
  const gfx = this._gfx;
7631
7714
  gfx.clear();
@@ -8605,7 +8688,7 @@ var PixiChart = class {
8605
8688
  /** Fired when an EXISTING drawing is modified via drag (handle or body move). */
8606
8689
  onDrawingEdited;
8607
8690
  _drawingEditDirty = false;
8608
- /** Ctrl+Z / Ctrl+Y — drawing history is owned by TChart. */
8691
+ /** Ctrl+Z / Ctrl+Y — drawing history is owned by ForgeCharts. */
8609
8692
  onUndoRequest;
8610
8693
  onRedoRequest;
8611
8694
  /** Called whenever a drawing is deleted. */
@@ -8620,7 +8703,7 @@ var PixiChart = class {
8620
8703
  onDrawingContextMenu;
8621
8704
  /** Called when the user double-clicks a drawing. */
8622
8705
  onDrawingDoubleClick;
8623
- /** Called after every pan/zoom so TChart can trigger lazy history loading. */
8706
+ /** Called after every pan/zoom so ForgeCharts can trigger lazy history loading. */
8624
8707
  onViewportChanged;
8625
8708
  constructor(container, options = {}) {
8626
8709
  this._container = container;
@@ -8727,6 +8810,12 @@ var PixiChart = class {
8727
8810
  this.onViewportChanged?.();
8728
8811
  },
8729
8812
  onCrosshairMove: (x, y) => {
8813
+ if (this._replaySelect) {
8814
+ const t = this._cursorBucketTime(x);
8815
+ this._replayHoverTime = t;
8816
+ this._replaySelect.onHover(t);
8817
+ this._drawReplayDim(x);
8818
+ }
8730
8819
  if (!this._crosshairEnabled) return;
8731
8820
  if (this._magnetMode === "off") {
8732
8821
  this._crosshairRenderer.update(x, y, this._cursorBucketTime(x));
@@ -9016,7 +9105,7 @@ var PixiChart = class {
9016
9105
  }).catch(() => {
9017
9106
  });
9018
9107
  }
9019
- // ─── Drawing objects (used by PixiChart directly; TChart uses DrawingManager) ─
9108
+ // ─── Drawing objects (used by PixiChart directly; ForgeCharts uses DrawingManager) ─
9020
9109
  /** @internal Used by PixiChart when DrawingManager pushes an update. */
9021
9110
  setDrawings(drawings) {
9022
9111
  this._drawings = [...drawings];
@@ -9025,6 +9114,47 @@ var PixiChart = class {
9025
9114
  // ─── Rendering ───────────────────────────────────────────────────────────────
9026
9115
  /** Styles whose renderer has already logged a failure this session. */
9027
9116
  _renderErrorLogged = /* @__PURE__ */ new Set();
9117
+ // ── Bar-replay anchor selection ─────────────────────────────────────────
9118
+ _replaySelect = null;
9119
+ _replayHoverTime = null;
9120
+ _replayDimGfx = null;
9121
+ _replayClickHandler = null;
9122
+ setReplaySelect(cb) {
9123
+ this._replaySelect = cb;
9124
+ this._replayHoverTime = null;
9125
+ if (cb) {
9126
+ this._replayClickHandler = () => {
9127
+ if (this._replayHoverTime !== null) this._replaySelect?.onCommit(this._replayHoverTime);
9128
+ };
9129
+ this._app.canvas.addEventListener("click", this._replayClickHandler);
9130
+ this._app.canvas.style.cursor = "crosshair";
9131
+ } else {
9132
+ if (this._replayClickHandler) {
9133
+ this._app.canvas.removeEventListener("click", this._replayClickHandler);
9134
+ this._replayClickHandler = null;
9135
+ }
9136
+ this._app.canvas.style.cursor = "";
9137
+ if (this._replayDimGfx) {
9138
+ this._replayDimGfx.clear();
9139
+ this._replayDimGfx.destroy();
9140
+ this._replayDimGfx = null;
9141
+ }
9142
+ }
9143
+ }
9144
+ /** Translucent veil over the bars a replay-anchor click would remove. */
9145
+ _drawReplayDim(x) {
9146
+ if (!this._replayDimGfx) {
9147
+ this._replayDimGfx = new Graphics();
9148
+ this._layers.get(5 /* Interaction */).addChildAt(this._replayDimGfx, 0);
9149
+ }
9150
+ const gfx = this._replayDimGfx;
9151
+ const { plotWidth, plotHeight } = this._transform;
9152
+ gfx.clear();
9153
+ if (x >= 0 && x < plotWidth) {
9154
+ gfx.rect(x, 0, plotWidth - x, plotHeight).fill({ color: 8421520, alpha: 0.35 });
9155
+ }
9156
+ this._layers.dirty.mark(5 /* Interaction */);
9157
+ }
9028
9158
  _renderFrame() {
9029
9159
  if (!this._ready || this._destroyed) return;
9030
9160
  const t = this._transform;
@@ -9034,6 +9164,9 @@ var PixiChart = class {
9034
9164
  if (dirty.isDirty(1 /* PriceSeries */)) {
9035
9165
  this._autoFitPriceRange();
9036
9166
  dirty.mark(0 /* Background */);
9167
+ this._candleRenderer.clear();
9168
+ this._lineRenderer.clear();
9169
+ this._histogramRenderer.clear();
9037
9170
  for (const series of this._series) {
9038
9171
  const opts = series.options();
9039
9172
  if (opts.visible === false) continue;
@@ -11596,6 +11729,8 @@ var resolver = () => ChartRuntimeResolver.getInstance();
11596
11729
  var canUseChartTypes = () => resolver().canUseChartTypes();
11597
11730
  var canUseExtendedDrawings = () => resolver().canUseExtendedDrawings();
11598
11731
  var canUseMultiPane = () => resolver().canUseMultiPane();
11732
+ var canUseBarReplay = () => resolver().canUseBarReplay();
11733
+ var canRenderOverlays = () => resolver().canRenderOverlays();
11599
11734
  var canUseDraggableOrders = () => resolver().canUseDraggableOrders();
11600
11735
  var canUseBracketOrders = () => resolver().canUseBracketOrders();
11601
11736
  var canUsePositionsOverlay = () => resolver().canUsePositionsOverlay();
@@ -11912,8 +12047,8 @@ var ManagedTradingController = class {
11912
12047
  }
11913
12048
  };
11914
12049
 
11915
- // src/api/TChart.ts
11916
- var TChart = class _TChart {
12050
+ // src/api/ForgeCharts.ts
12051
+ var ForgeCharts = class _ForgeCharts {
11917
12052
  _core;
11918
12053
  _bus;
11919
12054
  _indicators;
@@ -12304,7 +12439,7 @@ var TChart = class _TChart {
12304
12439
  "[ForgeCharts] ForgeScript indicators are not enabled on the active license. Contact your license provider to enable the forgeScript feature."
12305
12440
  );
12306
12441
  }
12307
- let finalConfig = _TChart.OVERLAY_ONLY.has(config.type) && config.overlay !== true ? { ...config, overlay: true } : config;
12442
+ let finalConfig = _ForgeCharts.OVERLAY_ONLY.has(config.type) && config.overlay !== true ? { ...config, overlay: true } : config;
12308
12443
  if (finalConfig.overlay !== true && !canUseMultiPane()) {
12309
12444
  console.warn("[ForgeCharts] multiPane is not enabled on the active license \u2014 indicator added as a price-pane overlay instead.");
12310
12445
  finalConfig = { ...finalConfig, overlay: true };
@@ -12482,7 +12617,7 @@ var TChart = class _TChart {
12482
12617
  */
12483
12618
  startDrawingTool(type) {
12484
12619
  this._assertAlive();
12485
- if (_TChart.EXTENDED_DRAWING_TOOLS.has(type) && !canUseExtendedDrawings()) {
12620
+ if (_ForgeCharts.EXTENDED_DRAWING_TOOLS.has(type) && !canUseExtendedDrawings()) {
12486
12621
  console.warn(`[ForgeCharts] Drawing tool '${type}' requires the extendedDrawings feature, which is not enabled on the active license.`);
12487
12622
  return;
12488
12623
  }
@@ -12520,6 +12655,14 @@ var TChart = class _TChart {
12520
12655
  /** Set contract switch roll markers for continuous futures. */
12521
12656
  setContractSwitches(switches) {
12522
12657
  this._assertAlive();
12658
+ if (!canRenderOverlays()) {
12659
+ if (switches.length > 0) {
12660
+ console.warn("[ForgeCharts] Overlay rendering (overlays) is not enabled on the active plan \u2014 contract switch markers suppressed.");
12661
+ }
12662
+ this._core.setContractSwitches([]);
12663
+ this._core.markDirty();
12664
+ return;
12665
+ }
12523
12666
  this._core.setContractSwitches(switches);
12524
12667
  this._core.markDirty();
12525
12668
  }
@@ -12563,7 +12706,7 @@ var TChart = class _TChart {
12563
12706
  const snap = this._drawings.all().map((d) => ({ ...d, points: [...d.points] }));
12564
12707
  this._drawHistory.splice(this._drawHistIdx + 1);
12565
12708
  this._drawHistory.push(snap);
12566
- if (this._drawHistory.length > _TChart._HIST_MAX) this._drawHistory.shift();
12709
+ if (this._drawHistory.length > _ForgeCharts._HIST_MAX) this._drawHistory.shift();
12567
12710
  this._drawHistIdx = this._drawHistory.length - 1;
12568
12711
  }
12569
12712
  /** Steps drawing state one edit back (Ctrl+Z). */
@@ -12928,6 +13071,158 @@ var TChart = class _TChart {
12928
13071
  }
12929
13072
  return this._unmanagedIngestion;
12930
13073
  }
13074
+ // ─── Bar replay ───────────────────────────────────────────────────────────────
13075
+ //
13076
+ // Replay is a display cutoff on the primary series (Series.setCutoffTime)
13077
+ // plus this controller moving it. The full tape keeps flowing underneath —
13078
+ // live bars land in fullData() and stay hidden until stepped over or the
13079
+ // replay exits — so no datafeed, indicator, or renderer knows replay
13080
+ // exists. Selection (the hover shadow of what a click would remove) is a
13081
+ // backend surface only Pixi implements; ForgeCharts refuses replay elsewhere.
13082
+ /** Playback speeds, in bars per second. */
13083
+ static REPLAY_SPEEDS = [0.5, 1, 2, 5, 10];
13084
+ _replay = {
13085
+ selecting: false,
13086
+ active: false,
13087
+ playing: false,
13088
+ speedIdx: 1,
13089
+ timer: null
13090
+ };
13091
+ _emitReplayState() {
13092
+ this._bus.emit("replayStateChanged", {
13093
+ selecting: this._replay.selecting,
13094
+ active: this._replay.active,
13095
+ playing: this._replay.playing,
13096
+ speed: _ForgeCharts.REPLAY_SPEEDS[this._replay.speedIdx],
13097
+ cutoffTime: this._series.cutoffTime()
13098
+ });
13099
+ }
13100
+ /** Current replay state — the control strip's render source. */
13101
+ replayState() {
13102
+ return {
13103
+ selecting: this._replay.selecting,
13104
+ active: this._replay.active,
13105
+ playing: this._replay.playing,
13106
+ speed: _ForgeCharts.REPLAY_SPEEDS[this._replay.speedIdx],
13107
+ cutoffTime: this._series.cutoffTime()
13108
+ };
13109
+ }
13110
+ /**
13111
+ * Enter replay-anchor selection: the cursor shadows everything a click
13112
+ * would remove; clicking commits that bucket as the replay head. Returns
13113
+ * false (with a warning) when the plan lacks bar_replay or the backend
13114
+ * has no selection surface.
13115
+ */
13116
+ replayEnterSelection() {
13117
+ this._assertAlive();
13118
+ if (!canUseBarReplay()) {
13119
+ console.warn("[ForgeCharts] Bar replay (bar_replay) is not enabled on the active plan.");
13120
+ return false;
13121
+ }
13122
+ if (!this._core.setReplaySelect) {
13123
+ console.warn("[ForgeCharts] Bar replay requires the Pixi render backend.");
13124
+ return false;
13125
+ }
13126
+ this._replayStopTimer();
13127
+ this._replay.selecting = true;
13128
+ this._replay.playing = false;
13129
+ this._core.setReplaySelect({
13130
+ onHover: () => {
13131
+ },
13132
+ onCommit: (time) => this._replayCommit(time)
13133
+ });
13134
+ this._emitReplayState();
13135
+ return true;
13136
+ }
13137
+ /** Leave selection without committing (existing replay, if any, survives). */
13138
+ replayCancelSelection() {
13139
+ this._assertAlive();
13140
+ if (!this._replay.selecting) return;
13141
+ this._replay.selecting = false;
13142
+ this._core.setReplaySelect?.(null);
13143
+ this._emitReplayState();
13144
+ }
13145
+ _replayCommit(time) {
13146
+ this._replay.selecting = false;
13147
+ this._core.setReplaySelect?.(null);
13148
+ this._series.setCutoffTime(time);
13149
+ this._replay.active = true;
13150
+ this._replay.playing = false;
13151
+ this._core.markDirty();
13152
+ this._emitReplayState();
13153
+ }
13154
+ replayPlay() {
13155
+ this._assertAlive();
13156
+ if (!this._replay.active || this._replay.playing) return;
13157
+ this._replay.playing = true;
13158
+ this._replayStartTimer();
13159
+ this._emitReplayState();
13160
+ }
13161
+ replayPause() {
13162
+ this._assertAlive();
13163
+ if (!this._replay.playing) return;
13164
+ this._replay.playing = false;
13165
+ this._replayStopTimer();
13166
+ this._emitReplayState();
13167
+ }
13168
+ /** Advance the replay head n bars. Pauses at the end of the tape. */
13169
+ replayStepForward(n = 1) {
13170
+ this._assertAlive();
13171
+ if (!this._replay.active) return;
13172
+ const full = this._series.fullData();
13173
+ const shown = this._series.data().length;
13174
+ const nextIdx = Math.min(shown + n, full.length) - 1;
13175
+ if (nextIdx < shown) {
13176
+ this.replayPause();
13177
+ return;
13178
+ }
13179
+ this._series.setCutoffTime(full[nextIdx].time);
13180
+ this._core.markDirty();
13181
+ if (nextIdx >= full.length - 1) this.replayPause();
13182
+ this._emitReplayState();
13183
+ }
13184
+ /** Rewind the replay head n bars back in time (default 10). */
13185
+ replayRewind(n = 10) {
13186
+ this._assertAlive();
13187
+ if (!this._replay.active) return;
13188
+ const full = this._series.fullData();
13189
+ const shown = this._series.data().length;
13190
+ const idx = Math.max(shown - 1 - n, 0);
13191
+ if (full.length === 0) return;
13192
+ this._series.setCutoffTime(full[idx].time);
13193
+ this._core.markDirty();
13194
+ this._emitReplayState();
13195
+ }
13196
+ /** Cycle to the next playback speed; restarts the timer when playing. */
13197
+ replayCycleSpeed() {
13198
+ this._assertAlive();
13199
+ this._replay.speedIdx = (this._replay.speedIdx + 1) % _ForgeCharts.REPLAY_SPEEDS.length;
13200
+ if (this._replay.playing) this._replayStartTimer();
13201
+ this._emitReplayState();
13202
+ return _ForgeCharts.REPLAY_SPEEDS[this._replay.speedIdx];
13203
+ }
13204
+ /** Exit replay entirely: the full tape (live head included) returns. */
13205
+ replayExit() {
13206
+ this._assertAlive();
13207
+ this._replayStopTimer();
13208
+ this._replay.selecting = false;
13209
+ this._replay.active = false;
13210
+ this._replay.playing = false;
13211
+ this._core.setReplaySelect?.(null);
13212
+ this._series.setCutoffTime(null);
13213
+ this._core.markDirty();
13214
+ this._core.scrollToEnd();
13215
+ this._emitReplayState();
13216
+ }
13217
+ _replayStartTimer() {
13218
+ this._replayStopTimer();
13219
+ const speed = _ForgeCharts.REPLAY_SPEEDS[this._replay.speedIdx];
13220
+ this._replay.timer = setInterval(() => this.replayStepForward(1), Math.max(1e3 / speed, 16));
13221
+ }
13222
+ _replayStopTimer() {
13223
+ if (this._replay.timer) clearInterval(this._replay.timer);
13224
+ this._replay.timer = null;
13225
+ }
12931
13226
  // ─── Datafeed ─────────────────────────────────────────────────────────────────
12932
13227
  _buildConnector(datafeed) {
12933
13228
  return new DatafeedConnector(datafeed, this._series, {
@@ -18146,7 +18441,7 @@ var DEMONSTRATION_STROKE = "rgba(255, 200, 50, 0.7)";
18146
18441
  var DEMONSTRATION_COLOR = "var(--crosshair-overlay, rgba(255,255,255,0.75))";
18147
18442
 
18148
18443
  // src/version.ts
18149
- var FORGECHARTS_VERSION = "1.5.82" ;
18444
+ var FORGECHARTS_VERSION = "1.5.83" ;
18150
18445
  function applyRuntimeConfig(caps, config) {
18151
18446
  if (!config) return caps;
18152
18447
  const orderEntry = config.enableOrderEntry === false ? false : caps.orderEntry;
@@ -18248,10 +18543,7 @@ function useHostLicense(apiUrl, getAuthToken) {
18248
18543
  function useUserPackage(apiUrl, getAuthToken) {
18249
18544
  useEffect(() => {
18250
18545
  const resolver3 = ChartRuntimeResolver.getInstance();
18251
- if (!apiUrl || !getAuthToken) {
18252
- resolver3.setUserPackageFeatures(null);
18253
- return;
18254
- }
18546
+ if (!apiUrl || !getAuthToken) return;
18255
18547
  let cancelled = false;
18256
18548
  void (async () => {
18257
18549
  try {
@@ -23110,6 +23402,14 @@ var ChartCanvas = forwardRef(
23110
23402
  },
23111
23403
  addIndicator: (config) => chartRef.current?.addIndicator(config) ?? null,
23112
23404
  setSeriesType: (type) => chartRef.current?.setSeriesType(type),
23405
+ toggleReplay: () => {
23406
+ const chart = chartRef.current;
23407
+ if (!chart) return;
23408
+ const st = chart.replayState();
23409
+ if (st.selecting) chart.replayCancelSelection();
23410
+ else if (st.active) chart.replayExit();
23411
+ else chart.replayEnterSelection();
23412
+ },
23113
23413
  removeIndicator: (id) => chartRef.current?.removeIndicator(id),
23114
23414
  moveIndicatorToOverlay: (id) => chartRef.current?.moveIndicatorToOverlay(id),
23115
23415
  moveIndicatorToPane: (id, targetPaneId) => chartRef.current?.moveIndicatorToPane(id, targetPaneId),
@@ -23157,7 +23457,7 @@ var ChartCanvas = forwardRef(
23157
23457
  useEffect(() => {
23158
23458
  const container = priceRef.current;
23159
23459
  if (!container) return;
23160
- const chart = new TChart({
23460
+ const chart = new ForgeCharts({
23161
23461
  container,
23162
23462
  symbol,
23163
23463
  interval: timeframe,
@@ -23171,6 +23471,9 @@ var ChartCanvas = forwardRef(
23171
23471
  for (const config of initialIndicators) {
23172
23472
  chart.addIndicator(config);
23173
23473
  }
23474
+ chart.on("replayStateChanged", (s) => {
23475
+ setReplayState(s.selecting || s.active ? s : null);
23476
+ });
23174
23477
  transformRef.current = chart.getTransform();
23175
23478
  const syncChartState = () => {
23176
23479
  setBars([...chart.getBars()]);
@@ -24130,6 +24433,7 @@ var ChartCanvas = forwardRef(
24130
24433
  selectPointerToolRef.current = handleToolSelect;
24131
24434
  const initialPointerAppliedRef = useRef(false);
24132
24435
  const initialSeriesAppliedRef = useRef(false);
24436
+ const [replayState, setReplayState] = useState(null);
24133
24437
  useEffect(() => {
24134
24438
  if (!initialSeriesAppliedRef.current && initialSeriesType && chartRef.current) {
24135
24439
  initialSeriesAppliedRef.current = true;
@@ -24138,7 +24442,7 @@ var ChartCanvas = forwardRef(
24138
24442
  if (initialPointerAppliedRef.current || !initialPointerTool) return;
24139
24443
  initialPointerAppliedRef.current = true;
24140
24444
  selectPointerToolRef.current(initialPointerTool);
24141
- }, [initialPointerTool]);
24445
+ }, [initialPointerTool, initialSeriesType]);
24142
24446
  const handleMagnetModeChange = useCallback((mode) => {
24143
24447
  setMagnetMode(mode);
24144
24448
  chartRef.current?.setMagnetMode(mode);
@@ -24234,6 +24538,107 @@ var ChartCanvas = forwardRef(
24234
24538
  crosshairXRef.current = null;
24235
24539
  },
24236
24540
  children: [
24541
+ replayState && (() => {
24542
+ const dark = effectiveTheme !== "light";
24543
+ const bg = dark ? "rgba(24,26,34,0.94)" : "rgba(255,255,255,0.96)";
24544
+ const fg = dark ? "#dfe3ee" : "#20242f";
24545
+ const line = dark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.12)";
24546
+ const btn = {
24547
+ background: "transparent",
24548
+ border: "none",
24549
+ color: fg,
24550
+ cursor: "pointer",
24551
+ padding: "5px 9px",
24552
+ borderRadius: 6,
24553
+ fontSize: 12,
24554
+ display: "inline-flex",
24555
+ alignItems: "center",
24556
+ gap: 5,
24557
+ lineHeight: 1
24558
+ };
24559
+ const call = (fn) => () => {
24560
+ if (chartRef.current) fn(chartRef.current);
24561
+ };
24562
+ return /* @__PURE__ */ jsxs("div", { style: {
24563
+ position: "absolute",
24564
+ bottom: 46,
24565
+ left: "50%",
24566
+ transform: "translateX(-50%)",
24567
+ zIndex: 8,
24568
+ display: "flex",
24569
+ alignItems: "center",
24570
+ gap: 2,
24571
+ background: bg,
24572
+ color: fg,
24573
+ border: `1px solid ${line}`,
24574
+ borderRadius: 8,
24575
+ padding: "2px 6px",
24576
+ boxShadow: "0 4px 16px rgba(0,0,0,0.25)",
24577
+ userSelect: "none"
24578
+ }, children: [
24579
+ replayState.selecting ? /* @__PURE__ */ jsx("span", { style: { fontSize: 12, padding: "5px 9px" }, children: "Click a bar to start replay from there" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
24580
+ /* @__PURE__ */ jsx(
24581
+ "button",
24582
+ {
24583
+ style: btn,
24584
+ title: "Pick a new starting bar",
24585
+ onClick: call((c) => c.replayEnterSelection()),
24586
+ children: "\u2506\u2190 Select bar"
24587
+ }
24588
+ ),
24589
+ /* @__PURE__ */ jsx("span", { style: { width: 1, alignSelf: "stretch", background: line, margin: "4px 2px" } }),
24590
+ /* @__PURE__ */ jsx(
24591
+ "button",
24592
+ {
24593
+ style: btn,
24594
+ title: "Rewind 10 bars",
24595
+ onClick: call((c) => c.replayRewind(10)),
24596
+ children: "\u23EA"
24597
+ }
24598
+ ),
24599
+ /* @__PURE__ */ jsx(
24600
+ "button",
24601
+ {
24602
+ style: btn,
24603
+ title: replayState.playing ? "Pause" : "Play",
24604
+ onClick: call((c) => replayState.playing ? c.replayPause() : c.replayPlay()),
24605
+ children: replayState.playing ? "\u23F8" : "\u25B6"
24606
+ }
24607
+ ),
24608
+ /* @__PURE__ */ jsx(
24609
+ "button",
24610
+ {
24611
+ style: btn,
24612
+ title: "Forward one bar",
24613
+ onClick: call((c) => c.replayStepForward(1)),
24614
+ children: "\u23E9"
24615
+ }
24616
+ ),
24617
+ /* @__PURE__ */ jsxs(
24618
+ "button",
24619
+ {
24620
+ style: { ...btn, fontVariantNumeric: "tabular-nums" },
24621
+ title: "Playback speed",
24622
+ onClick: call((c) => c.replayCycleSpeed()),
24623
+ children: [
24624
+ replayState.speed,
24625
+ "x"
24626
+ ]
24627
+ }
24628
+ )
24629
+ ] }),
24630
+ /* @__PURE__ */ jsx("span", { style: { width: 1, alignSelf: "stretch", background: line, margin: "4px 2px" } }),
24631
+ /* @__PURE__ */ jsx(
24632
+ "button",
24633
+ {
24634
+ style: btn,
24635
+ title: replayState.selecting ? "Cancel selection" : "Exit replay",
24636
+ onClick: call((c) => replayState.selecting ? c.replayCancelSelection() : c.replayExit()),
24637
+ children: "\u2715"
24638
+ }
24639
+ )
24640
+ ] });
24641
+ })(),
24237
24642
  /* @__PURE__ */ jsx(
24238
24643
  "div",
24239
24644
  {
@@ -24321,7 +24726,7 @@ var ChartCanvas = forwardRef(
24321
24726
  symbol,
24322
24727
  timeframe,
24323
24728
  bars,
24324
- theme,
24729
+ theme: effectiveTheme,
24325
24730
  indicators,
24326
24731
  overlayColors: OVERLAY_COLORS,
24327
24732
  onRemove: handleRemoveIndicator,
@@ -24762,6 +25167,7 @@ function LayoutMenu({
24762
25167
  currentName,
24763
25168
  currentLayoutId,
24764
25169
  autoSave,
25170
+ saveState,
24765
25171
  onFetchLayouts,
24766
25172
  onSave,
24767
25173
  onLoad,
@@ -24903,7 +25309,9 @@ function LayoutMenu({
24903
25309
  ] }),
24904
25310
  currentName ?? "Layout",
24905
25311
  " \u25BE",
24906
- autoSave && isSaved && /* @__PURE__ */ jsx("span", { className: "layout-autosave-dot", title: "Auto-save enabled" })
25312
+ saveState === "saving" && /* @__PURE__ */ jsx("span", { style: { fontSize: 10.5, opacity: 0.75, fontStyle: "italic" }, children: "Saving\u2026" }),
25313
+ saveState === "saved" && /* @__PURE__ */ jsx("span", { style: { fontSize: 10.5, color: "var(--up)" }, children: "Saved \u2713" }),
25314
+ saveState !== "saving" && saveState !== "saved" && autoSave && isSaved && /* @__PURE__ */ jsx("span", { className: "layout-autosave-dot", title: "Auto-save enabled" })
24907
25315
  ]
24908
25316
  }
24909
25317
  ),
@@ -26275,28 +26683,32 @@ function RollRuleDropdown({
26275
26683
  ))
26276
26684
  ] });
26277
26685
  }
26278
- var SERIES_STYLES = [
26279
- { id: "candlestick", label: "Candles", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "currentColor", children: [
26280
- /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "3", height: "6", rx: "0.5" }),
26281
- /* @__PURE__ */ jsx("line", { x1: "4.5", y1: "2", x2: "4.5", y2: "14", stroke: "currentColor", strokeWidth: "1" }),
26282
- /* @__PURE__ */ jsx("rect", { x: "10", y: "3", width: "3", height: "7", rx: "0.5" }),
26283
- /* @__PURE__ */ jsx("line", { x1: "11.5", y1: "1", x2: "11.5", y2: "13", stroke: "currentColor", strokeWidth: "1" })
26284
- ] }) },
26285
- { id: "heikinashi", label: "Heikin Ashi", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "currentColor", children: [
26286
- /* @__PURE__ */ jsx("rect", { x: "2.5", y: "6", width: "3.4", height: "6", rx: "1.6" }),
26287
- /* @__PURE__ */ jsx("rect", { x: "9.5", y: "3", width: "3.4", height: "6", rx: "1.6" })
26288
- ] }) },
26289
- { id: "line", label: "Line", icon: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", children: /* @__PURE__ */ jsx("polyline", { points: "2,12 6,7 10,10 14,4" }) }) },
26290
- { id: "area", label: "Area", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", children: [
26291
- /* @__PURE__ */ jsx("polyline", { points: "2,12 6,7 10,10 14,4", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }),
26292
- /* @__PURE__ */ jsx("polygon", { points: "2,12 6,7 10,10 14,4 14,14 2,14", fill: "currentColor", opacity: "0.35" })
26293
- ] }) },
26294
- { id: "histogram", label: "Histogram", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "currentColor", children: [
26295
- /* @__PURE__ */ jsx("rect", { x: "2", y: "9", width: "2.6", height: "5" }),
26296
- /* @__PURE__ */ jsx("rect", { x: "6.5", y: "5", width: "2.6", height: "9" }),
26297
- /* @__PURE__ */ jsx("rect", { x: "11", y: "7", width: "2.6", height: "7" })
26298
- ] }) }
26686
+ var SERIES_STYLE_GROUPS = [
26687
+ { label: "Bars", items: [
26688
+ { id: "bar", label: "Bars", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", strokeWidth: "1.4", fill: "none", children: [
26689
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "2", x2: "5", y2: "13" }),
26690
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "5", x2: "5", y2: "5" }),
26691
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "10", x2: "7", y2: "10" }),
26692
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "4", x2: "11", y2: "14" }),
26693
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "7", x2: "11", y2: "7" }),
26694
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "12", x2: "13", y2: "12" })
26695
+ ] }) },
26696
+ { id: "candlestick", label: "Candles", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "currentColor", children: [
26697
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "3", height: "6", rx: "0.5" }),
26698
+ /* @__PURE__ */ jsx("line", { x1: "4.5", y1: "2", x2: "4.5", y2: "14", stroke: "currentColor", strokeWidth: "1" }),
26699
+ /* @__PURE__ */ jsx("rect", { x: "10", y: "3", width: "3", height: "7", rx: "0.5" }),
26700
+ /* @__PURE__ */ jsx("line", { x1: "11.5", y1: "1", x2: "11.5", y2: "13", stroke: "currentColor", strokeWidth: "1" })
26701
+ ] }) }
26702
+ ] },
26703
+ { label: "Lines", items: [
26704
+ { id: "line", label: "Line", icon: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", children: /* @__PURE__ */ jsx("polyline", { points: "2,12 6,7 10,10 14,4" }) }) },
26705
+ { id: "area", label: "Area", icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", children: [
26706
+ /* @__PURE__ */ jsx("polyline", { points: "2,12 6,7 10,10 14,4", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }),
26707
+ /* @__PURE__ */ jsx("polygon", { points: "2,12 6,7 10,10 14,4 14,14 2,14", fill: "currentColor", opacity: "0.35" })
26708
+ ] }) }
26709
+ ] }
26299
26710
  ];
26711
+ var ALL_SERIES_STYLES = SERIES_STYLE_GROUPS.flatMap((g) => g.items);
26300
26712
  function TopToolbar({
26301
26713
  symbol,
26302
26714
  timeframe,
@@ -26310,6 +26722,9 @@ function TopToolbar({
26310
26722
  onFavoritesChange,
26311
26723
  seriesType,
26312
26724
  onSeriesTypeChange,
26725
+ onToggleReplay,
26726
+ onToggleAlerts,
26727
+ alertsOpen,
26313
26728
  onAddIndicator,
26314
26729
  maxIndicators,
26315
26730
  indicatorsOpen,
@@ -26347,7 +26762,8 @@ function TopToolbar({
26347
26762
  showSavedLayouts,
26348
26763
  showCustomTimeframes,
26349
26764
  showDataExport,
26350
- showMultiChartLayout
26765
+ showMultiChartLayout,
26766
+ layoutSaveState
26351
26767
  }) {
26352
26768
  const [tfOpen, setTfOpen] = useState(false);
26353
26769
  const [styleOpen, setStyleOpen] = useState(false);
@@ -26505,27 +26921,60 @@ function TopToolbar({
26505
26921
  onClick: () => setStyleOpen((o) => !o),
26506
26922
  title: "Chart style",
26507
26923
  children: [
26508
- (SERIES_STYLES.find((s) => s.id === seriesType) ?? SERIES_STYLES[0]).icon,
26509
- (SERIES_STYLES.find((s) => s.id === seriesType) ?? SERIES_STYLES[0]).label
26924
+ (ALL_SERIES_STYLES.find((s) => s.id === seriesType) ?? ALL_SERIES_STYLES[1]).icon,
26925
+ (ALL_SERIES_STYLES.find((s) => s.id === seriesType) ?? ALL_SERIES_STYLES[1]).label
26510
26926
  ]
26511
26927
  }
26512
26928
  ),
26513
- styleOpen && /* @__PURE__ */ jsx("div", { className: "tf-dropdown", style: { minWidth: 150 }, children: SERIES_STYLES.map((style) => /* @__PURE__ */ jsx(
26514
- "button",
26515
- {
26516
- className: "tf-dropdown-row",
26517
- style: style.id === seriesType ? { fontWeight: 600 } : void 0,
26518
- onClick: () => {
26519
- onSeriesTypeChange(style.id);
26520
- setStyleOpen(false);
26929
+ styleOpen && /* @__PURE__ */ jsx("div", { className: "tf-dropdown", style: { minWidth: 165 }, children: SERIES_STYLE_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
26930
+ /* @__PURE__ */ jsx("div", { style: {
26931
+ padding: "6px 12px 3px",
26932
+ fontSize: 10.5,
26933
+ fontWeight: 600,
26934
+ letterSpacing: 1,
26935
+ textTransform: "uppercase",
26936
+ opacity: 0.55
26937
+ }, children: group.label }),
26938
+ group.items.map((style) => /* @__PURE__ */ jsxs(
26939
+ "button",
26940
+ {
26941
+ className: "tf-dropdown-row",
26942
+ style: style.id === seriesType ? { fontWeight: 600 } : void 0,
26943
+ onClick: () => {
26944
+ onSeriesTypeChange(style.id);
26945
+ setStyleOpen(false);
26946
+ },
26947
+ children: [
26948
+ /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
26949
+ style.icon,
26950
+ style.label
26951
+ ] }),
26952
+ style.id === seriesType && /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: { opacity: 0.9 }, children: "\u2713" })
26953
+ ]
26521
26954
  },
26522
- children: /* @__PURE__ */ jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
26523
- style.icon,
26524
- style.label
26525
- ] })
26526
- },
26527
- style.id
26528
- )) })
26955
+ style.id
26956
+ ))
26957
+ ] }, group.label)) })
26958
+ ] })
26959
+ ] }),
26960
+ onToggleAlerts !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
26961
+ /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
26962
+ /* @__PURE__ */ jsxs("button", { className: `ind-trigger${alertsOpen ? " active" : ""}`, onClick: onToggleAlerts, title: "Price alerts", children: [
26963
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
26964
+ /* @__PURE__ */ jsx("path", { d: "M8 2.5a4 4 0 0 0-4 4v2.5l-1.4 2.3a.5.5 0 0 0 .43.75h9.94a.5.5 0 0 0 .43-.75L12 9V6.5a4 4 0 0 0-4-4z" }),
26965
+ /* @__PURE__ */ jsx("path", { d: "M6.6 12.5a1.5 1.5 0 0 0 2.8 0" })
26966
+ ] }),
26967
+ "Alerts"
26968
+ ] })
26969
+ ] }),
26970
+ onToggleReplay !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
26971
+ /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
26972
+ /* @__PURE__ */ jsxs("button", { className: "ind-trigger", onClick: onToggleReplay, title: "Bar replay", children: [
26973
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
26974
+ /* @__PURE__ */ jsx("polygon", { points: "6,4 12,8 6,12", fill: "currentColor", stroke: "none" }),
26975
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "3", x2: "3", y2: "13" })
26976
+ ] }),
26977
+ "Replay"
26529
26978
  ] })
26530
26979
  ] }),
26531
26980
  /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
@@ -26594,6 +27043,7 @@ function TopToolbar({
26594
27043
  currentName: currentLayoutName,
26595
27044
  currentLayoutId,
26596
27045
  autoSave,
27046
+ ...layoutSaveState !== void 0 ? { saveState: layoutSaveState } : {},
26597
27047
  onFetchLayouts,
26598
27048
  onSave: onSaveLayout,
26599
27049
  onLoad: onLoadLayout,
@@ -31346,6 +31796,194 @@ function WatchlistDrawer({ onClose, onSelectSymbol, symbolResolver, apiUrl, getA
31346
31796
  }
31347
31797
  );
31348
31798
  }
31799
+ function AlertsDrawer({ apiUrl, getAuthToken, symbol, onClose }) {
31800
+ const [alerts, setAlerts] = useState(null);
31801
+ const [error, setError] = useState("");
31802
+ const [busy, setBusy] = useState(false);
31803
+ const [formSymbol, setFormSymbol] = useState(symbol);
31804
+ const [condition, setCondition] = useState("above");
31805
+ const [priceStr, setPriceStr] = useState("");
31806
+ useEffect(() => {
31807
+ setFormSymbol(symbol);
31808
+ }, [symbol]);
31809
+ const call = useCallback(async (path, init) => {
31810
+ const token = getAuthToken ? await getAuthToken() : "";
31811
+ return fetch(`${apiUrl.replace(/\/$/, "")}/api/alerts${path}`, {
31812
+ ...init,
31813
+ headers: {
31814
+ "Content-Type": "application/json",
31815
+ ...token ? { Authorization: `Bearer ${token}` } : {},
31816
+ ...init?.headers ?? {}
31817
+ }
31818
+ });
31819
+ }, [apiUrl, getAuthToken]);
31820
+ const load = useCallback(() => {
31821
+ call("").then(async (r) => {
31822
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
31823
+ setAlerts(await r.json());
31824
+ }).catch((e) => setError(`Failed to load alerts: ${e instanceof Error ? e.message : e}`));
31825
+ }, [call]);
31826
+ useEffect(() => {
31827
+ load();
31828
+ const t = setInterval(load, 15e3);
31829
+ return () => clearInterval(t);
31830
+ }, [load]);
31831
+ async function create() {
31832
+ const price = parseFloat(priceStr);
31833
+ if (!(price > 0) || !formSymbol.trim()) return;
31834
+ setBusy(true);
31835
+ setError("");
31836
+ try {
31837
+ const r = await call("", {
31838
+ method: "POST",
31839
+ body: JSON.stringify({ symbol: formSymbol.trim(), condition, price })
31840
+ });
31841
+ const d = await r.json().catch(() => ({}));
31842
+ if (!r.ok) throw new Error(d.error ?? `HTTP ${r.status}`);
31843
+ setPriceStr("");
31844
+ load();
31845
+ } catch (e) {
31846
+ setError(e instanceof Error ? e.message : String(e));
31847
+ } finally {
31848
+ setBusy(false);
31849
+ }
31850
+ }
31851
+ async function remove(id) {
31852
+ try {
31853
+ await call(`/${encodeURIComponent(id)}`, { method: "DELETE" });
31854
+ setAlerts((prev) => prev?.filter((a) => a.id !== id) ?? null);
31855
+ } catch {
31856
+ }
31857
+ }
31858
+ return /* @__PURE__ */ jsxs("div", { style: {
31859
+ width: 280,
31860
+ flexShrink: 0,
31861
+ display: "flex",
31862
+ flexDirection: "column",
31863
+ background: "var(--surface)",
31864
+ border: "1px solid var(--border)",
31865
+ borderRadius: 8,
31866
+ overflow: "hidden",
31867
+ color: "var(--text)"
31868
+ }, children: [
31869
+ /* @__PURE__ */ jsxs("div", { style: {
31870
+ display: "flex",
31871
+ alignItems: "center",
31872
+ justifyContent: "space-between",
31873
+ padding: "10px 14px",
31874
+ borderBottom: "1px solid var(--border)"
31875
+ }, children: [
31876
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 13 }, children: "Alerts" }),
31877
+ /* @__PURE__ */ jsx(
31878
+ "button",
31879
+ {
31880
+ onClick: onClose,
31881
+ title: "Close",
31882
+ style: { background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 14 },
31883
+ children: "\u2715"
31884
+ }
31885
+ )
31886
+ ] }),
31887
+ /* @__PURE__ */ jsxs("div", { style: { padding: "10px 14px", borderBottom: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 8 }, children: [
31888
+ /* @__PURE__ */ jsx(
31889
+ "input",
31890
+ {
31891
+ value: formSymbol,
31892
+ onChange: (e) => setFormSymbol(e.target.value),
31893
+ placeholder: "Symbol",
31894
+ style: { fontSize: 12.5, padding: "5px 8px", background: "var(--surface-2)", color: "var(--text)", border: "1px solid var(--border)", borderRadius: 6 }
31895
+ }
31896
+ ),
31897
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
31898
+ /* @__PURE__ */ jsxs(
31899
+ "select",
31900
+ {
31901
+ value: condition,
31902
+ onChange: (e) => setCondition(e.target.value),
31903
+ style: { fontSize: 12.5, padding: "5px 6px", background: "var(--surface-2)", color: "var(--text)", border: "1px solid var(--border)", borderRadius: 6 },
31904
+ children: [
31905
+ /* @__PURE__ */ jsx("option", { value: "above", children: "Crosses above" }),
31906
+ /* @__PURE__ */ jsx("option", { value: "below", children: "Crosses below" })
31907
+ ]
31908
+ }
31909
+ ),
31910
+ /* @__PURE__ */ jsx(
31911
+ "input",
31912
+ {
31913
+ value: priceStr,
31914
+ onChange: (e) => setPriceStr(e.target.value.replace(/[^0-9.]/g, "")),
31915
+ placeholder: "Price",
31916
+ inputMode: "decimal",
31917
+ style: { flex: 1, minWidth: 0, fontSize: 12.5, padding: "5px 8px", background: "var(--surface-2)", color: "var(--text)", border: "1px solid var(--border)", borderRadius: 6, fontVariantNumeric: "tabular-nums" },
31918
+ onKeyDown: (e) => e.key === "Enter" && !busy && create()
31919
+ }
31920
+ )
31921
+ ] }),
31922
+ /* @__PURE__ */ jsx(
31923
+ "button",
31924
+ {
31925
+ onClick: create,
31926
+ disabled: busy || !(parseFloat(priceStr) > 0),
31927
+ style: {
31928
+ fontSize: 12.5,
31929
+ padding: "6px 0",
31930
+ borderRadius: 6,
31931
+ border: "none",
31932
+ cursor: "pointer",
31933
+ background: "var(--primary, #5b5bd6)",
31934
+ color: "#fff",
31935
+ opacity: busy || !(parseFloat(priceStr) > 0) ? 0.55 : 1
31936
+ },
31937
+ children: busy ? "Creating\u2026" : "Create alert"
31938
+ }
31939
+ ),
31940
+ error && /* @__PURE__ */ jsx("div", { style: { fontSize: 11.5, color: "var(--down, #e5484d)" }, children: error })
31941
+ ] }),
31942
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, overflowY: "auto" }, children: [
31943
+ alerts === null && !error && /* @__PURE__ */ jsx("div", { style: { padding: 14, fontSize: 12.5, color: "var(--text-muted)" }, children: "Loading\u2026" }),
31944
+ alerts !== null && alerts.length === 0 && /* @__PURE__ */ jsx("div", { style: { padding: 14, fontSize: 12.5, color: "var(--text-muted)" }, children: "No alerts yet. They fire server-side and email you \u2014 the chart doesn't need to stay open." }),
31945
+ alerts?.map((a) => /* @__PURE__ */ jsxs("div", { style: {
31946
+ display: "flex",
31947
+ alignItems: "center",
31948
+ gap: 8,
31949
+ padding: "8px 14px",
31950
+ borderBottom: "1px solid var(--border)",
31951
+ fontSize: 12.5,
31952
+ opacity: a.status === "triggered" ? 0.75 : 1
31953
+ }, children: [
31954
+ /* @__PURE__ */ jsx("span", { style: {
31955
+ width: 7,
31956
+ height: 7,
31957
+ borderRadius: "50%",
31958
+ flexShrink: 0,
31959
+ background: a.status === "active" ? "var(--up, #22a06b)" : "var(--text-muted)"
31960
+ }, title: a.status }),
31961
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
31962
+ /* @__PURE__ */ jsxs("div", { style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: [
31963
+ /* @__PURE__ */ jsx("strong", { children: a.symbol }),
31964
+ " ",
31965
+ a.condition === "above" ? "\u2265" : "\u2264",
31966
+ " ",
31967
+ /* @__PURE__ */ jsx("span", { style: { fontVariantNumeric: "tabular-nums" }, children: a.price })
31968
+ ] }),
31969
+ a.status === "triggered" && /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, color: "var(--text-muted)" }, children: [
31970
+ "fired at ",
31971
+ a.trigger_price
31972
+ ] })
31973
+ ] }),
31974
+ /* @__PURE__ */ jsx(
31975
+ "button",
31976
+ {
31977
+ onClick: () => remove(a.id),
31978
+ title: "Remove alert",
31979
+ style: { background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 13 },
31980
+ children: "\u2715"
31981
+ }
31982
+ )
31983
+ ] }, a.id))
31984
+ ] })
31985
+ ] });
31986
+ }
31349
31987
  function ChartWorkspace({
31350
31988
  // TabBar
31351
31989
  tabs,
@@ -31396,6 +32034,9 @@ function ChartWorkspace({
31396
32034
  seriesType,
31397
32035
  onSeriesTypeChange,
31398
32036
  showSeriesTypes,
32037
+ onToggleReplay,
32038
+ showReplay,
32039
+ layoutSaveState,
31399
32040
  extraTfGroups,
31400
32041
  // RightToolbar
31401
32042
  watchlistOpen,
@@ -31506,6 +32147,9 @@ function ChartWorkspace({
31506
32147
  const effShowMultiChartLayout = gate(showMultiChartLayout, capabilities.multiChartLayout);
31507
32148
  const effShowMultipleTabs = gate(showMultipleTabs, capabilities.multipleWorkspaces);
31508
32149
  const effShowSeriesTypes = gate(showSeriesTypes, capabilities.chartTypes) && onSeriesTypeChange !== void 0;
32150
+ const effShowReplay = gate(showReplay, capabilities.barReplay) && onToggleReplay !== void 0;
32151
+ const effShowAlerts = gate(void 0, capabilities.alerts) && hostApiUrl !== void 0;
32152
+ const [alertsOpen, setAlertsOpen] = React11.useState(false);
31509
32153
  const tabItems = tabs.map((t) => ({
31510
32154
  id: t.id,
31511
32155
  label: t.label,
@@ -31584,6 +32228,8 @@ function ChartWorkspace({
31584
32228
  onSymbolChange,
31585
32229
  onTimeframeChange,
31586
32230
  ...effShowSeriesTypes ? { seriesType: seriesType ?? "candlestick", onSeriesTypeChange } : {},
32231
+ ...effShowReplay ? { onToggleReplay } : {},
32232
+ ...effShowAlerts ? { onToggleAlerts: () => setAlertsOpen((o) => !o), alertsOpen } : {},
31587
32233
  onAddCustomTimeframe,
31588
32234
  onRemoveCustomTimeframe,
31589
32235
  onFavoritesChange: onFavoriteTfsChange,
@@ -31599,6 +32245,7 @@ function ChartWorkspace({
31599
32245
  currentLayoutName,
31600
32246
  currentLayoutId,
31601
32247
  autoSave,
32248
+ ...layoutSaveState !== void 0 ? { layoutSaveState } : {},
31602
32249
  onFetchLayouts,
31603
32250
  onSaveLayout,
31604
32251
  onLoadLayout,
@@ -31630,6 +32277,15 @@ function ChartWorkspace({
31630
32277
  /* @__PURE__ */ jsx("div", { style: { flex: 1, display: "flex", gap: 4, minHeight: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, display: "flex", gap: 4, minWidth: 0, minHeight: 0 }, children: [
31631
32278
  (dataBlocked || isLicensed === false) && /* @__PURE__ */ jsx(LicenseRequiredOverlay, {}),
31632
32279
  /* @__PURE__ */ jsx("div", { style: { position: "relative", flex: 1, minWidth: 0, minHeight: 0 }, children: chartSlots }),
32280
+ effShowAlerts && alertsOpen && /* @__PURE__ */ jsx(
32281
+ AlertsDrawer,
32282
+ {
32283
+ apiUrl: hostApiUrl,
32284
+ getAuthToken,
32285
+ symbol: activeSymbol ?? "",
32286
+ onClose: () => setAlertsOpen(false)
32287
+ }
32288
+ ),
31633
32289
  autoTrading && !onToggleOrderEntry && effOrderEntryOpen && capabilities.orderEntry && tradingBridge && /* @__PURE__ */ jsx(
31634
32290
  OrderTicket,
31635
32291
  {