@luxalgo/vela 0.5.0 → 0.5.2
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/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
- package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
- package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
- package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
- package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
- package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
- package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
- package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
- package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
- package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
- package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
- package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
- package/dist/index.cjs +2010 -397
- package/dist/index.d.cts +83 -25
- package/dist/index.d.ts +83 -25
- package/dist/index.js +4 -4
- package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
- package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
- package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
- package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
- package/dist/plugin.cjs +58 -7
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/ui.cjs +342 -9
- package/dist/ui.d.cts +64 -2
- package/dist/ui.d.ts +64 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +2010 -397
- package/dist/vela.global.min.js +115 -34
- package/dist/widget.cjs +5987 -2242
- package/dist/widget.d.cts +89 -17
- package/dist/widget.d.ts +89 -17
- package/dist/widget.js +169 -18
- package/dist/workspace.cjs +5627 -1587
- package/dist/workspace.d.cts +106 -12
- package/dist/workspace.d.ts +106 -12
- package/dist/workspace.js +494 -52
- package/package.json +1 -1
package/dist/workspace.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { WidgetHistory, prefixedSymbol, Watermark, Statusline, ChartContextMenu, statuslineInkOf, indicatorLedger, Glider, localStorageAdapter, decodeState, SymbolPicker, IndicatorPicker, TimeframeQuick, Topbar, PanelDock, ObjectTree, DataWindow, Toast, Bottombar, toolShortcutHints, resolveIndicators, sanitizeState, encodeState, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp } from './chunk-
|
|
2
|
-
export { decodeState, encodeState, sanitizeState } from './chunk-
|
|
3
|
-
import { parseSymbol, Vela, normalizeTimezone, TypedEventBus, MultiProviderFeed, resolveTheme, createCustomMark, createAttributionMark, DrawingToolbar, defaultToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-
|
|
4
|
-
import { resolveEngines, legendActionsProviderFor, rendererDefaults, widgetAttachments } from './chunk-
|
|
5
|
-
import { applyPlotOverlayTokens, ensureUIHost, KeymapManager, Menu, isEditableTarget } from './chunk-
|
|
6
|
-
import './chunk-
|
|
7
|
-
import { registerIcon, svg16, injectStyles } from './chunk-
|
|
1
|
+
import { WidgetHistory, prefixedSymbol, Watermark, Statusline, ChartContextMenu, statuslineInkOf, indicatorLedger, Glider, localStorageAdapter, decodeState, SymbolPicker, IndicatorPicker, TimeframeQuick, Topbar, PanelDock, ObjectTree, DataWindow, Toast, Bottombar, MobileBar, DrawingPill, LayoutModeController, toolShortcutHints, resolveIndicators, sanitizeState, encodeState, TimeframeDrawer, RANGE_PRESETS, DrawingsDrawer, MoreDrawer, priceStyleIcon, priceStyleLabel, TimezoneDrawer, PriceScaleDrawer, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp } from './chunk-Y7TK4ZWK.js';
|
|
2
|
+
export { decodeState, encodeState, sanitizeState } from './chunk-Y7TK4ZWK.js';
|
|
3
|
+
import { parseSymbol, Vela, normalizeTimezone, TypedEventBus, MultiProviderFeed, resolveTheme, createCustomMark, createAttributionMark, DrawingToolbar, defaultToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs, priceStyleIds } from './chunk-RRFGWZNJ.js';
|
|
4
|
+
import { resolveEngines, legendActionsProviderFor, rendererDefaults, widgetActions, widgetAttachments } from './chunk-B5TIKQJ5.js';
|
|
5
|
+
import { applyPlotOverlayTokens, ensureUIHost, KeymapManager, Menu, isEditableTarget } from './chunk-P556H6EA.js';
|
|
6
|
+
import './chunk-ATPU5CI6.js';
|
|
7
|
+
import { registerIcon, svg16, injectStyles } from './chunk-LJLZR44Y.js';
|
|
8
8
|
|
|
9
9
|
// src/workspace/sync.ts
|
|
10
10
|
function syncTargets(originId, setting, cellIds) {
|
|
@@ -81,6 +81,8 @@ var ChartCell = class {
|
|
|
81
81
|
this.pendingRange = null;
|
|
82
82
|
/** Indicator titles (this cell's in-chart legend rows) shown. */
|
|
83
83
|
this.indicatorTitlesOn = true;
|
|
84
|
+
/** Plot values beside this cell's legend titles shown. */
|
|
85
|
+
this.indicatorValuesOn = true;
|
|
84
86
|
this.destroyed = false;
|
|
85
87
|
this.appTheme = deps.theme;
|
|
86
88
|
const symbol = prefixedSymbol(seed);
|
|
@@ -145,10 +147,14 @@ var ChartCell = class {
|
|
|
145
147
|
this.inner.on("load:end", () => {
|
|
146
148
|
this.volumeMayBePending = false;
|
|
147
149
|
});
|
|
150
|
+
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
151
|
+
this.inner.on("load:end", () => this.watermark?.setLoading(false));
|
|
148
152
|
const tz = deps.timezone();
|
|
149
153
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
150
154
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
151
155
|
if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
|
|
156
|
+
this.indicatorValuesOn = seed.indicatorValues ?? true;
|
|
157
|
+
if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
|
|
152
158
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
153
159
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
154
160
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
@@ -256,6 +262,12 @@ var ChartCell = class {
|
|
|
256
262
|
label: "Titles",
|
|
257
263
|
get: () => this.indicatorTitlesOn,
|
|
258
264
|
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
kind: "toggle",
|
|
268
|
+
label: "Values",
|
|
269
|
+
get: () => this.indicatorValuesOn,
|
|
270
|
+
set: (v) => this.setIndicatorValuesVisible(v)
|
|
259
271
|
}
|
|
260
272
|
]
|
|
261
273
|
},
|
|
@@ -289,6 +301,12 @@ var ChartCell = class {
|
|
|
289
301
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
290
302
|
this.deps.onStateDirty();
|
|
291
303
|
}
|
|
304
|
+
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
305
|
+
setIndicatorValuesVisible(visible) {
|
|
306
|
+
this.indicatorValuesOn = visible;
|
|
307
|
+
this.inner?.renderer.set("indicatorValues", visible);
|
|
308
|
+
this.deps.onStateDirty();
|
|
309
|
+
}
|
|
292
310
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
293
311
|
* identity is what endures; the chart dies with the cell). */
|
|
294
312
|
get chart() {
|
|
@@ -333,6 +351,11 @@ var ChartCell = class {
|
|
|
333
351
|
if (!this.statusline || !this.inner) return;
|
|
334
352
|
this.statusline.setDirectionColors(...statuslineInkOf(this.inner.renderer, this.priceStyle));
|
|
335
353
|
}
|
|
354
|
+
/** Multi-cell grids keep the status line on one row and hide what doesn't fit —
|
|
355
|
+
* the workspace flips this with the layout (see Statusline.setFitMode). */
|
|
356
|
+
setStatuslineFit(on) {
|
|
357
|
+
this.statusline?.setFitMode(on);
|
|
358
|
+
}
|
|
336
359
|
/** The workspace shell keeps the app theme; the cell host's tokens re-derive from
|
|
337
360
|
* the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
|
|
338
361
|
syncPlotOverlayTokens() {
|
|
@@ -521,6 +544,7 @@ var ChartCell = class {
|
|
|
521
544
|
priceStyle: this.priceStyle,
|
|
522
545
|
watermark: this.watermarkOn,
|
|
523
546
|
indicatorTitles: this.indicatorTitlesOn,
|
|
547
|
+
indicatorValues: this.indicatorValuesOn,
|
|
524
548
|
rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
|
|
525
549
|
drawings: this.inner ? this.inner.drawings.toJSON() : void 0,
|
|
526
550
|
// Natives from the chart's SYNC registry read — an async catalog mirror here
|
|
@@ -598,6 +622,49 @@ function registerBuiltinLayouts() {
|
|
|
598
622
|
registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
|
|
599
623
|
registerLayout({ id: "8", label: "8 grid", cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
|
|
600
624
|
}
|
|
625
|
+
var GRID_PICKER_MAX = 4;
|
|
626
|
+
var GRID_BUILTIN_IDS = {
|
|
627
|
+
"1x1": "1",
|
|
628
|
+
"1x2": "2h",
|
|
629
|
+
"2x1": "2v",
|
|
630
|
+
"2x2": "4",
|
|
631
|
+
"2x4": "8"
|
|
632
|
+
};
|
|
633
|
+
var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
|
|
634
|
+
function layoutForGrid(rows, cols) {
|
|
635
|
+
const r = clampTrack(rows);
|
|
636
|
+
const c = clampTrack(cols);
|
|
637
|
+
const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
|
|
638
|
+
const registered = builtin ? registry.get(builtin) : void 0;
|
|
639
|
+
if (registered) return registered;
|
|
640
|
+
return {
|
|
641
|
+
id: `g${r}x${c}`,
|
|
642
|
+
// Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
|
|
643
|
+
label: `${c} \xD7 ${r} grid`,
|
|
644
|
+
cols: Array.from({ length: c }, () => 1),
|
|
645
|
+
rows: Array.from({ length: r }, () => 1),
|
|
646
|
+
cells: slots(r * c)
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
var GRID_ID_RE = /^g([1-4])x([1-4])$/;
|
|
650
|
+
function ensureLayout(id) {
|
|
651
|
+
const registered = registry.get(id);
|
|
652
|
+
if (registered) return registered;
|
|
653
|
+
const g = GRID_ID_RE.exec(id);
|
|
654
|
+
if (g) return layoutForGrid(Number(g[1]), Number(g[2]));
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
function layoutShape(def) {
|
|
658
|
+
if (!def.areas && def.rows.length <= GRID_PICKER_MAX && def.cols.length <= GRID_PICKER_MAX && def.cells.length === def.rows.length * def.cols.length) {
|
|
659
|
+
return { rows: def.rows.length, cols: def.cols.length };
|
|
660
|
+
}
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
function occupancyGrid(def) {
|
|
664
|
+
if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
|
|
665
|
+
const cols = def.cols.length;
|
|
666
|
+
return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
|
|
667
|
+
}
|
|
601
668
|
function gridStyles(def, trackSizes) {
|
|
602
669
|
const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
|
|
603
670
|
const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
|
|
@@ -617,6 +684,15 @@ function activeAfterLayout(current, cellIds) {
|
|
|
617
684
|
if (current != null && cellIds.includes(current)) return current;
|
|
618
685
|
return cellIds[0] ?? null;
|
|
619
686
|
}
|
|
687
|
+
function orderAfterLayout(order, slots2, active) {
|
|
688
|
+
const next = [...order];
|
|
689
|
+
if (active == null || slots2 <= 0) return next;
|
|
690
|
+
const idx = next.indexOf(active);
|
|
691
|
+
if (idx < 0 || idx < slots2) return next;
|
|
692
|
+
next.splice(idx, 1);
|
|
693
|
+
next.splice(slots2 - 1, 0, active);
|
|
694
|
+
return next;
|
|
695
|
+
}
|
|
620
696
|
|
|
621
697
|
// src/workspace/splitters.ts
|
|
622
698
|
var MIN_TRACK_FRAC = 0.1;
|
|
@@ -649,6 +725,27 @@ function trackOffsets(weights, sizePx, gapPx) {
|
|
|
649
725
|
}
|
|
650
726
|
return out;
|
|
651
727
|
}
|
|
728
|
+
function seamSegments(grid, axis, index) {
|
|
729
|
+
const count = axis === "cols" ? grid.length : grid[0]?.length ?? 0;
|
|
730
|
+
const isSeam = (k) => axis === "cols" ? grid[k]?.[index] !== grid[k]?.[index + 1] : grid[index]?.[k] !== grid[index + 1]?.[k];
|
|
731
|
+
const out = [];
|
|
732
|
+
for (let k = 0; k < count; k += 1) {
|
|
733
|
+
if (!isSeam(k)) continue;
|
|
734
|
+
const last = out[out.length - 1];
|
|
735
|
+
if (last && last[1] === k - 1) last[1] = k;
|
|
736
|
+
else out.push([k, k]);
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
function segmentSpanPx(weights, sizePx, gapPx, from, to) {
|
|
741
|
+
const total = weights.reduce((s, w) => s + w, 0);
|
|
742
|
+
if (total <= 0) return { start: 0, end: 0 };
|
|
743
|
+
const content = sizePx - gapPx * (weights.length - 1);
|
|
744
|
+
const startOf = (k) => weights.slice(0, k).reduce((s, w) => s + w / total * content, 0) + gapPx * k;
|
|
745
|
+
const start = from === 0 ? 0 : startOf(from) - gapPx / 2;
|
|
746
|
+
const end = to === weights.length - 1 ? sizePx : startOf(to + 1) - gapPx / 2;
|
|
747
|
+
return { start, end };
|
|
748
|
+
}
|
|
652
749
|
var HIT_PX = 8;
|
|
653
750
|
var SplitterLayer = class {
|
|
654
751
|
constructor(container, deps) {
|
|
@@ -657,23 +754,36 @@ var SplitterLayer = class {
|
|
|
657
754
|
this.strips = [];
|
|
658
755
|
this.drag = null;
|
|
659
756
|
}
|
|
660
|
-
/** Rebuild + reposition the strips for the current tracks/size. */
|
|
757
|
+
/** Rebuild + reposition the strips for the current tracks/size/occupancy. */
|
|
661
758
|
layout() {
|
|
662
759
|
const { cols, rows } = this.deps.tracks();
|
|
760
|
+
const grid = this.deps.grid();
|
|
663
761
|
const rect = this.container.getBoundingClientRect();
|
|
664
762
|
const gap = this.deps.gapPx();
|
|
665
|
-
const
|
|
666
|
-
while (this.strips.length > wanted) this.strips.pop().remove();
|
|
667
|
-
while (this.strips.length < wanted) this.strips.push(this.makeStrip());
|
|
668
|
-
let k = 0;
|
|
763
|
+
const placements = [];
|
|
669
764
|
for (const [i, x] of trackOffsets(cols, rect.width, gap).entries()) {
|
|
670
|
-
|
|
671
|
-
|
|
765
|
+
for (const [from, to] of seamSegments(grid, "cols", i)) {
|
|
766
|
+
const { start, end } = segmentSpanPx(rows, rect.height, gap, from, to);
|
|
767
|
+
placements.push({
|
|
768
|
+
axis: "cols",
|
|
769
|
+
index: i,
|
|
770
|
+
css: `left:${Math.round(x - HIT_PX / 2)}px;top:${Math.round(start)}px;width:${HIT_PX}px;height:${Math.round(end - start)}px;cursor:col-resize;`
|
|
771
|
+
});
|
|
772
|
+
}
|
|
672
773
|
}
|
|
673
774
|
for (const [i, y] of trackOffsets(rows, rect.height, gap).entries()) {
|
|
674
|
-
|
|
675
|
-
|
|
775
|
+
for (const [from, to] of seamSegments(grid, "rows", i)) {
|
|
776
|
+
const { start, end } = segmentSpanPx(cols, rect.width, gap, from, to);
|
|
777
|
+
placements.push({
|
|
778
|
+
axis: "rows",
|
|
779
|
+
index: i,
|
|
780
|
+
css: `left:${Math.round(start)}px;top:${Math.round(y - HIT_PX / 2)}px;width:${Math.round(end - start)}px;height:${HIT_PX}px;cursor:row-resize;`
|
|
781
|
+
});
|
|
782
|
+
}
|
|
676
783
|
}
|
|
784
|
+
while (this.strips.length > placements.length) this.strips.pop().remove();
|
|
785
|
+
while (this.strips.length < placements.length) this.strips.push(this.makeStrip());
|
|
786
|
+
for (const [k, p] of placements.entries()) this.place(this.strips[k], p.axis, p.index, p.css);
|
|
677
787
|
}
|
|
678
788
|
destroy() {
|
|
679
789
|
for (const s of this.strips.splice(0)) s.remove();
|
|
@@ -747,8 +857,10 @@ var CSS = `
|
|
|
747
857
|
.vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
|
|
748
858
|
.vela-cell { background: var(--vela-bg); position: relative; }
|
|
749
859
|
/* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
|
|
750
|
-
outline on the cell is painted under them) \u2014 inert to the pointer.
|
|
751
|
-
|
|
860
|
+
outline on the cell is painted under them) \u2014 inert to the pointer. Scoped to
|
|
861
|
+
multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
|
|
862
|
+
and ringing the only chart would just be noise. */
|
|
863
|
+
.vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
|
|
752
864
|
content: '';
|
|
753
865
|
position: absolute;
|
|
754
866
|
inset: 0;
|
|
@@ -756,7 +868,15 @@ var CSS = `
|
|
|
756
868
|
pointer-events: none;
|
|
757
869
|
z-index: 10;
|
|
758
870
|
}
|
|
759
|
-
|
|
871
|
+
/* Splitter hover mirrors the in-chart pane separator hover (CrosshairRenderer):
|
|
872
|
+
a soft band over the whole grab target + a solid 2px line on the seam center. */
|
|
873
|
+
.vela-ws-splitter:hover { background: var(--vela-separator-hover-band); }
|
|
874
|
+
.vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
|
|
875
|
+
.vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
|
|
876
|
+
.vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
|
|
877
|
+
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
878
|
+
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
879
|
+
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
760
880
|
`;
|
|
761
881
|
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
762
882
|
function declaredOrder(cells) {
|
|
@@ -807,6 +927,15 @@ var VelaWorkspace = class {
|
|
|
807
927
|
/** Re-entrance guard around one propagation tick: followers' synchronous echoes
|
|
808
928
|
* (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
|
|
809
929
|
this.syncBusy = false;
|
|
930
|
+
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
931
|
+
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
932
|
+
this.drawingSyncBusy = false;
|
|
933
|
+
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
934
|
+
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
935
|
+
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
936
|
+
* toggle-off (propagation freezes while the setting is off; re-enabling resumes
|
|
937
|
+
* edit/delete for these pairs). Cleared on reload / `applyState`. */
|
|
938
|
+
this.drawingLinks = /* @__PURE__ */ new Map();
|
|
810
939
|
this.manifest = [];
|
|
811
940
|
/** The shared manifest can no longer change instance sets — resolved, or no
|
|
812
941
|
* `indicators` option so nothing ever will. Gates the cells' ledger fallback. */
|
|
@@ -814,6 +943,13 @@ var VelaWorkspace = class {
|
|
|
814
943
|
this.openDialogs = 0;
|
|
815
944
|
this.alerts = [];
|
|
816
945
|
this.alertsMenu = null;
|
|
946
|
+
this.tfDrawer = null;
|
|
947
|
+
this.drawingsDrawer = null;
|
|
948
|
+
this.moreDrawer = null;
|
|
949
|
+
this.timezoneDrawer = null;
|
|
950
|
+
this.priceScaleDrawer = null;
|
|
951
|
+
/** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
|
|
952
|
+
this.priceScalePressY = 0;
|
|
817
953
|
this.attachmentDisposers = /* @__PURE__ */ new Map();
|
|
818
954
|
/** The single grid-wide attribution mark — re-inked on a live theme swap. */
|
|
819
955
|
this.attributionMark = null;
|
|
@@ -848,10 +984,10 @@ var VelaWorkspace = class {
|
|
|
848
984
|
this.timezone = boot?.timezone ?? opts.timezone ?? "Etc/UTC";
|
|
849
985
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
850
986
|
const sync = boot?.sync ?? opts.sync;
|
|
851
|
-
for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
|
|
987
|
+
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
|
|
852
988
|
this.applySyncSetting(kind, sync?.[kind]);
|
|
853
989
|
}
|
|
854
|
-
this.def = this.resolveLayout(boot?.layout &&
|
|
990
|
+
this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
|
|
855
991
|
if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
|
|
856
992
|
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
857
993
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
@@ -874,14 +1010,18 @@ var VelaWorkspace = class {
|
|
|
874
1010
|
}
|
|
875
1011
|
});
|
|
876
1012
|
this.symbolPicker.setSource(() => this.feed.symbols());
|
|
877
|
-
this.indicatorPicker = new IndicatorPicker({
|
|
1013
|
+
this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
|
|
878
1014
|
host: this.root,
|
|
879
1015
|
library: () => this.active.libraryRows(),
|
|
880
1016
|
onChart: () => this.active.onChartRows(),
|
|
881
1017
|
onAdd: (i) => this.active.addFromLibrary(i),
|
|
882
1018
|
onRemove: (i) => this.active.removeFromChart(i),
|
|
883
|
-
onOpenChange: (open) =>
|
|
884
|
-
|
|
1019
|
+
onOpenChange: (open) => {
|
|
1020
|
+
if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
|
|
1021
|
+
this.trackDialog(open);
|
|
1022
|
+
}
|
|
1023
|
+
}) : null;
|
|
1024
|
+
const picker = this.indicatorPicker;
|
|
885
1025
|
this.tfQuick = new TimeframeQuick({
|
|
886
1026
|
host: this.root,
|
|
887
1027
|
onApply: (tf) => this.setActiveTimeframe(tf),
|
|
@@ -890,7 +1030,7 @@ var VelaWorkspace = class {
|
|
|
890
1030
|
this.topbar = new Topbar(this.root, {
|
|
891
1031
|
symbol: "",
|
|
892
1032
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
893
|
-
onIndicatorsClick: () =>
|
|
1033
|
+
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
894
1034
|
onUndoClick: () => this.active.history.undo(),
|
|
895
1035
|
onRedoClick: () => this.active.history.redo(),
|
|
896
1036
|
onScreenshotClick: () => this.active.downloadScreenshot(),
|
|
@@ -902,14 +1042,23 @@ var VelaWorkspace = class {
|
|
|
902
1042
|
onPriceStyle: (style) => this.active.setPriceStyle(style),
|
|
903
1043
|
layout: {
|
|
904
1044
|
current: this.def.id,
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1045
|
+
// The picker composes dynamic layouts on its grid canvas; registered
|
|
1046
|
+
// presets the canvas cannot express (bespoke plugin areas) list as rows.
|
|
1047
|
+
shape: () => layoutShape(this.def),
|
|
1048
|
+
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
|
|
1049
|
+
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
1050
|
+
onSelectPreset: (id) => this.setLayout(id),
|
|
1051
|
+
// The SYNC switches reflect the simple all-cells form; flipping one
|
|
1052
|
+
// OVERRIDES a host-set group record with plain on/off (groups stay an
|
|
1053
|
+
// API-only shape).
|
|
1054
|
+
syncs: () => [
|
|
1055
|
+
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
1056
|
+
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
1057
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
1058
|
+
],
|
|
1059
|
+
onToggleSync: (id) => {
|
|
1060
|
+
const kind = id;
|
|
1061
|
+
this.sync.set(kind, this.syncOpts[kind] ? false : true);
|
|
913
1062
|
}
|
|
914
1063
|
},
|
|
915
1064
|
getContext: () => this.context()
|
|
@@ -972,10 +1121,19 @@ var VelaWorkspace = class {
|
|
|
972
1121
|
this.active.chart.drawings.setStayMode(on);
|
|
973
1122
|
this.refocusActive();
|
|
974
1123
|
},
|
|
975
|
-
{
|
|
1124
|
+
{
|
|
1125
|
+
dock: "static",
|
|
1126
|
+
// Drawings sync is a WORKSPACE link (same model as the layout
|
|
1127
|
+
// dropdown's switches) — the bar only hosts its toggle.
|
|
1128
|
+
onDrawingsSync: (on) => {
|
|
1129
|
+
this.sync.set("drawings", on);
|
|
1130
|
+
this.refocusActive();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
976
1133
|
) : null;
|
|
977
1134
|
this.drawToolbar?.setDefinition(defaultToolbar());
|
|
978
1135
|
this.drawToolbar?.setVisible(true);
|
|
1136
|
+
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
979
1137
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
980
1138
|
timezone: this.timezone,
|
|
981
1139
|
onRange: (preset) => {
|
|
@@ -985,9 +1143,24 @@ var VelaWorkspace = class {
|
|
|
985
1143
|
onTimezone: (zone) => this.setTimezone(zone),
|
|
986
1144
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
987
1145
|
}) : null;
|
|
1146
|
+
this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
|
|
1147
|
+
symbol: "",
|
|
1148
|
+
timeframe: "60",
|
|
1149
|
+
onSymbolClick: () => this.symbolPicker.open(),
|
|
1150
|
+
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
1151
|
+
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
1152
|
+
getContext: () => this.context(),
|
|
1153
|
+
onDrawingsClick: () => this.openDrawingsDrawer(),
|
|
1154
|
+
onMoreClick: () => this.openMoreDrawer(),
|
|
1155
|
+
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
1156
|
+
}) : null;
|
|
1157
|
+
this.drawingPill = new DrawingPill(this.gridEl);
|
|
988
1158
|
hostEl.appendChild(this.root);
|
|
1159
|
+
this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
|
|
1160
|
+
this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
|
|
989
1161
|
this.splitters = new SplitterLayer(this.gridEl, {
|
|
990
1162
|
tracks: () => this.currentTracks(),
|
|
1163
|
+
grid: () => occupancyGrid(this.def),
|
|
991
1164
|
apply: (axis, weights) => this.applyTracks(axis, weights),
|
|
992
1165
|
reset: (axis) => this.applyTracks(axis, evenTracks(this.currentTracks()[axis].length)),
|
|
993
1166
|
gapPx: () => GAP_PX
|
|
@@ -1005,6 +1178,7 @@ var VelaWorkspace = class {
|
|
|
1005
1178
|
this.cellBackend = this.backendFor(this.def);
|
|
1006
1179
|
this.applyGrid();
|
|
1007
1180
|
this.buildCells();
|
|
1181
|
+
this.syncCellPresentation();
|
|
1008
1182
|
this.setActiveCell(bootActive != null && this.cellsById.has(bootActive) ? bootActive : this.order[0] ?? null);
|
|
1009
1183
|
if (opts.autofocus) this.refocusActive();
|
|
1010
1184
|
if (opts.indicators !== void 0) {
|
|
@@ -1087,6 +1261,7 @@ var VelaWorkspace = class {
|
|
|
1087
1261
|
refreshActions() {
|
|
1088
1262
|
this.mountAttachments();
|
|
1089
1263
|
this.topbar.renderActions();
|
|
1264
|
+
this.mobileBar?.renderActions();
|
|
1090
1265
|
this.dock.refresh();
|
|
1091
1266
|
for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
1092
1267
|
}
|
|
@@ -1135,7 +1310,7 @@ var VelaWorkspace = class {
|
|
|
1135
1310
|
}
|
|
1136
1311
|
if (st.favorites) this.favs = [...st.favorites];
|
|
1137
1312
|
this.dock.applyState(st.panels);
|
|
1138
|
-
for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
1313
|
+
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
1139
1314
|
this.trackSizes.clear();
|
|
1140
1315
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
1141
1316
|
for (const [id, cell] of [...this.cellsById]) {
|
|
@@ -1144,13 +1319,15 @@ var VelaWorkspace = class {
|
|
|
1144
1319
|
this.events.emit("cell:destroyed", { id });
|
|
1145
1320
|
}
|
|
1146
1321
|
this.pool.clear();
|
|
1322
|
+
this.drawingLinks.clear();
|
|
1147
1323
|
for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
|
|
1148
1324
|
this.order = st.charts.map((c) => c.id);
|
|
1149
|
-
const def =
|
|
1325
|
+
const def = ensureLayout(st.layout);
|
|
1150
1326
|
if (def) this.def = def;
|
|
1151
1327
|
this.cellBackend = this.backendFor(this.def);
|
|
1152
1328
|
this.applyGrid();
|
|
1153
1329
|
this.buildCells();
|
|
1330
|
+
this.syncCellPresentation();
|
|
1154
1331
|
this.topbar.setLayout(this.def.id);
|
|
1155
1332
|
const nextActive = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
1156
1333
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -1197,6 +1374,7 @@ var VelaWorkspace = class {
|
|
|
1197
1374
|
const next = this.resolveLayout(layout);
|
|
1198
1375
|
const nextBackend = this.backendFor(next);
|
|
1199
1376
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
1377
|
+
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
1200
1378
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
1201
1379
|
for (const [id, cell] of [...this.cellsById]) {
|
|
1202
1380
|
if (!keep.has(id) || rebuildAll) {
|
|
@@ -1210,6 +1388,7 @@ var VelaWorkspace = class {
|
|
|
1210
1388
|
this.cellBackend = nextBackend;
|
|
1211
1389
|
this.applyGrid();
|
|
1212
1390
|
this.buildCells();
|
|
1391
|
+
this.syncCellPresentation();
|
|
1213
1392
|
this.topbar.setLayout(next.id);
|
|
1214
1393
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
1215
1394
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -1245,11 +1424,19 @@ var VelaWorkspace = class {
|
|
|
1245
1424
|
this.drawToolbar?.destroy();
|
|
1246
1425
|
this.topbar.destroy();
|
|
1247
1426
|
this.bottombar?.destroy();
|
|
1427
|
+
this.mobileBar?.destroy();
|
|
1428
|
+
this.drawingPill.destroy();
|
|
1429
|
+
this.tfDrawer?.destroy();
|
|
1430
|
+
this.drawingsDrawer?.destroy();
|
|
1431
|
+
this.moreDrawer?.destroy();
|
|
1432
|
+
this.timezoneDrawer?.destroy();
|
|
1433
|
+
this.priceScaleDrawer?.destroy();
|
|
1434
|
+
this.layoutCtl.destroy();
|
|
1248
1435
|
this.dock.destroy();
|
|
1249
1436
|
this.objectTree.destroy();
|
|
1250
1437
|
this.dataWindow.destroy();
|
|
1251
1438
|
this.symbolPicker.destroy();
|
|
1252
|
-
this.indicatorPicker
|
|
1439
|
+
this.indicatorPicker?.destroy();
|
|
1253
1440
|
this.tfQuick.destroy();
|
|
1254
1441
|
this.shortcutsHelp?.destroy();
|
|
1255
1442
|
this.toast.destroy();
|
|
@@ -1270,6 +1457,10 @@ var VelaWorkspace = class {
|
|
|
1270
1457
|
this.topbar.setPriceStyle(cell.priceStyle);
|
|
1271
1458
|
this.topbar.setIndicatorCount(cell.indicatorCount);
|
|
1272
1459
|
this.topbar.renderActions();
|
|
1460
|
+
this.mobileBar?.renderActions();
|
|
1461
|
+
this.mobileBar?.setSymbol(cell.symbol);
|
|
1462
|
+
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
1463
|
+
this.drawingPill.onChart(cell.chart);
|
|
1273
1464
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
1274
1465
|
this.historyUnsub?.();
|
|
1275
1466
|
this.historyUnsub = cell.history.onChange(pushHistory);
|
|
@@ -1277,7 +1468,7 @@ var VelaWorkspace = class {
|
|
|
1277
1468
|
this.objectTree.setSymbol(cell.symbol);
|
|
1278
1469
|
this.dock.onChart(cell.chart);
|
|
1279
1470
|
this.bottombar?.setActiveRange(cell.activeRangeId);
|
|
1280
|
-
this.indicatorPicker
|
|
1471
|
+
this.indicatorPicker?.sync();
|
|
1281
1472
|
this.glider.stop();
|
|
1282
1473
|
const d = cell.chart.drawings;
|
|
1283
1474
|
if (d.getTool() !== this.globalTool) d.setTool(this.globalTool);
|
|
@@ -1320,7 +1511,7 @@ var VelaWorkspace = class {
|
|
|
1320
1511
|
// ── internals ───────────────────────────────────────────────
|
|
1321
1512
|
resolveLayout(layout) {
|
|
1322
1513
|
if (typeof layout !== "string") return layout;
|
|
1323
|
-
const def =
|
|
1514
|
+
const def = ensureLayout(layout);
|
|
1324
1515
|
if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" \u2014 register it with registerLayout().`);
|
|
1325
1516
|
return def;
|
|
1326
1517
|
}
|
|
@@ -1348,6 +1539,8 @@ var VelaWorkspace = class {
|
|
|
1348
1539
|
* `order[i]` — the identity/position decoupling in one line. */
|
|
1349
1540
|
applyGrid() {
|
|
1350
1541
|
const { container, perCell } = gridStyles(this.def, this.trackSizes.get(this.def.id));
|
|
1542
|
+
if (this.def.cells.length > 1) this.gridEl.dataset.multi = "1";
|
|
1543
|
+
else delete this.gridEl.dataset.multi;
|
|
1351
1544
|
this.gridEl.style.gridTemplateColumns = container.gridTemplateColumns ?? "";
|
|
1352
1545
|
this.gridEl.style.gridTemplateRows = container.gridTemplateRows ?? "";
|
|
1353
1546
|
this.gridEl.style.gridTemplateAreas = container.gridTemplateAreas ?? "";
|
|
@@ -1395,7 +1588,9 @@ var VelaWorkspace = class {
|
|
|
1395
1588
|
});
|
|
1396
1589
|
cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
1397
1590
|
this.cellsById.set(id, cell);
|
|
1591
|
+
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
1398
1592
|
this.wireCell(cell);
|
|
1593
|
+
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
1399
1594
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
1400
1595
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
1401
1596
|
this.events.emit("cell:created", { id });
|
|
@@ -1425,10 +1620,20 @@ var VelaWorkspace = class {
|
|
|
1425
1620
|
this.drawToolbar?.setFavorites(favorites);
|
|
1426
1621
|
this.markStateDirty();
|
|
1427
1622
|
});
|
|
1428
|
-
chart.renderer.onCrosshairMove((e) => this.propagateCrosshair(cell.id, e.time));
|
|
1429
|
-
chart.on("drawing:created", () =>
|
|
1430
|
-
|
|
1431
|
-
|
|
1623
|
+
chart.renderer.onCrosshairMove((e) => this.propagateCrosshair(cell.id, e.time, e.paneKind === "price" ? e.price : null));
|
|
1624
|
+
chart.on("drawing:created", ({ id }) => {
|
|
1625
|
+
this.propagateDrawing(cell.id, id);
|
|
1626
|
+
this.markStateDirty();
|
|
1627
|
+
});
|
|
1628
|
+
chart.on("drawing:draft", ({ doc }) => this.propagateDraft(cell.id, doc));
|
|
1629
|
+
chart.on("drawing:edited", ({ id }) => {
|
|
1630
|
+
this.propagateDrawingEdit(cell.id, id);
|
|
1631
|
+
this.markStateDirty();
|
|
1632
|
+
});
|
|
1633
|
+
chart.on("drawing:removed", ({ id }) => {
|
|
1634
|
+
this.propagateDrawingRemoval(cell.id, id);
|
|
1635
|
+
this.markStateDirty();
|
|
1636
|
+
});
|
|
1432
1637
|
chart.on("drawing:tool", ({ type }) => {
|
|
1433
1638
|
if (cell.id !== this.activeId) return;
|
|
1434
1639
|
this.globalTool = type;
|
|
@@ -1451,11 +1656,22 @@ var VelaWorkspace = class {
|
|
|
1451
1656
|
});
|
|
1452
1657
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
1453
1658
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
1659
|
+
chart.renderer.onAxisLongPress((e) => {
|
|
1660
|
+
if (this.layoutCtl.current !== "mobile") return;
|
|
1661
|
+
if (e.axis === "time") this.openTimezoneDrawer();
|
|
1662
|
+
else this.openPriceScaleDrawer(e.y);
|
|
1663
|
+
});
|
|
1454
1664
|
}
|
|
1455
1665
|
// ── sync links ──────────────────────────────────────────────
|
|
1456
1666
|
applySyncSetting(kind, setting, align = false) {
|
|
1457
1667
|
if (setting == null || setting === false) delete this.syncOpts[kind];
|
|
1458
1668
|
else this.syncOpts[kind] = setting;
|
|
1669
|
+
if (kind === "drawings") {
|
|
1670
|
+
for (const cell of this.cellsById.values()) cell.chart.drawings.setExternalGhost(null);
|
|
1671
|
+
this.drawToolbar?.setDrawingsSyncMode(!!setting);
|
|
1672
|
+
this.markStateDirty();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1459
1675
|
if (kind === "crosshair") {
|
|
1460
1676
|
for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
|
|
1461
1677
|
if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
|
|
@@ -1465,6 +1681,7 @@ var VelaWorkspace = class {
|
|
|
1465
1681
|
this.markStateDirty();
|
|
1466
1682
|
return;
|
|
1467
1683
|
}
|
|
1684
|
+
if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
|
|
1468
1685
|
this.markStateDirty();
|
|
1469
1686
|
if (align && setting && this.activeId) {
|
|
1470
1687
|
if (kind === "viewport") {
|
|
@@ -1477,17 +1694,24 @@ var VelaWorkspace = class {
|
|
|
1477
1694
|
}
|
|
1478
1695
|
/**
|
|
1479
1696
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
1480
|
-
* crosshairs (`renderer.setExternalCrosshair`).
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1697
|
+
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
1698
|
+
* along ONLY to followers showing the same ticker — price scales are comparable
|
|
1699
|
+
* there, while an origin's price painted on another market's pane would be noise.
|
|
1700
|
+
* Leaving the origin propagates `null` (the event already carries it) and clears
|
|
1701
|
+
* every ghost. No busy guard needed: an external crosshair never re-emits
|
|
1702
|
+
* `onCrosshairMove` — the flow is one-way by port contract, so no echo loop can
|
|
1703
|
+
* exist.
|
|
1484
1704
|
*/
|
|
1485
|
-
propagateCrosshair(originId, time) {
|
|
1705
|
+
propagateCrosshair(originId, time, price = null) {
|
|
1486
1706
|
if (this.destroyed) return;
|
|
1487
1707
|
const setting = this.syncOpts.crosshair;
|
|
1488
1708
|
if (!setting) return;
|
|
1709
|
+
const originTicker = parseSymbol(this.cellsById.get(originId)?.symbol ?? "").ticker;
|
|
1489
1710
|
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1490
|
-
this.cellsById.get(id)
|
|
1711
|
+
const follower = this.cellsById.get(id);
|
|
1712
|
+
if (!follower) continue;
|
|
1713
|
+
const comparable = originTicker !== "" && parseSymbol(follower.symbol).ticker === originTicker;
|
|
1714
|
+
follower.chart.renderer.setExternalCrosshair(time, comparable ? price : null);
|
|
1491
1715
|
}
|
|
1492
1716
|
}
|
|
1493
1717
|
/**
|
|
@@ -1516,6 +1740,101 @@ var VelaWorkspace = class {
|
|
|
1516
1740
|
this.syncBusy = false;
|
|
1517
1741
|
}
|
|
1518
1742
|
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Copy a freshly created drawing onto the origin's same-group followers (fresh ids
|
|
1745
|
+
* through `drawings.add` — anchors are time+price, so the copy lands at the same
|
|
1746
|
+
* spot whatever the follower shows) and register the whole set as LINKED: edits
|
|
1747
|
+
* and removals of any member follow while the link is on ({@link drawingLinks}).
|
|
1748
|
+
* The busy guard stops the copies' own `drawing:created` events from fanning out.
|
|
1749
|
+
*/
|
|
1750
|
+
propagateDrawing(originId, drawingId) {
|
|
1751
|
+
if (this.drawingSyncBusy || this.destroyed) return;
|
|
1752
|
+
const setting = this.syncOpts.drawings;
|
|
1753
|
+
if (!setting) return;
|
|
1754
|
+
const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
|
|
1755
|
+
if (!doc) return;
|
|
1756
|
+
this.drawingSyncBusy = true;
|
|
1757
|
+
try {
|
|
1758
|
+
const group = /* @__PURE__ */ new Map([[originId, drawingId]]);
|
|
1759
|
+
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1760
|
+
const copy = this.cellsById.get(id)?.chart.drawings.add(doc.type, {
|
|
1761
|
+
paneId: doc.paneId,
|
|
1762
|
+
anchors: doc.anchors,
|
|
1763
|
+
style: doc.style,
|
|
1764
|
+
text: doc.text,
|
|
1765
|
+
props: doc.props
|
|
1766
|
+
});
|
|
1767
|
+
if (copy) group.set(id, copy.id);
|
|
1768
|
+
}
|
|
1769
|
+
if (group.size > 1) {
|
|
1770
|
+
for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
|
|
1771
|
+
}
|
|
1772
|
+
} finally {
|
|
1773
|
+
this.drawingSyncBusy = false;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/** Mirror an in-progress placement (its current ghost, `null` = placement ended)
|
|
1777
|
+
* onto the origin's same-group followers — the live half of the drawings link;
|
|
1778
|
+
* the created copy replaces the ghosts when the placement completes. One-way by
|
|
1779
|
+
* contract (an external ghost never re-emits drafts), so no busy guard needed. */
|
|
1780
|
+
propagateDraft(originId, doc) {
|
|
1781
|
+
if (this.destroyed) return;
|
|
1782
|
+
const setting = this.syncOpts.drawings;
|
|
1783
|
+
if (!setting) return;
|
|
1784
|
+
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1785
|
+
this.cellsById.get(id)?.chart.drawings.setExternalGhost(doc);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
/** Push a linked drawing's edited CONTENT (anchors, style, text, per-type props)
|
|
1789
|
+
* onto its same-group peers — any member propagates, not just the original. */
|
|
1790
|
+
propagateDrawingEdit(originId, drawingId) {
|
|
1791
|
+
if (this.drawingSyncBusy || this.destroyed) return;
|
|
1792
|
+
const setting = this.syncOpts.drawings;
|
|
1793
|
+
if (!setting) return;
|
|
1794
|
+
const group = this.drawingLinks.get(`${originId}\0${drawingId}`);
|
|
1795
|
+
if (!group) return;
|
|
1796
|
+
const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
|
|
1797
|
+
if (!doc) return;
|
|
1798
|
+
this.drawingSyncBusy = true;
|
|
1799
|
+
try {
|
|
1800
|
+
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1801
|
+
const peerId = group.get(id);
|
|
1802
|
+
if (peerId == null) continue;
|
|
1803
|
+
this.cellsById.get(id)?.chart.drawings.update(peerId, {
|
|
1804
|
+
anchors: doc.anchors,
|
|
1805
|
+
style: doc.style,
|
|
1806
|
+
text: doc.text,
|
|
1807
|
+
props: doc.props
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
} finally {
|
|
1811
|
+
this.drawingSyncBusy = false;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/** Remove a linked drawing's peers with it. The removed member always leaves its
|
|
1815
|
+
* link group (whatever the setting); peers are only deleted while the link is on.
|
|
1816
|
+
* A propagated peer removal re-enters here under the busy guard and just cleans
|
|
1817
|
+
* its own link key. */
|
|
1818
|
+
propagateDrawingRemoval(originId, drawingId) {
|
|
1819
|
+
if (this.destroyed) return;
|
|
1820
|
+
const key = `${originId}\0${drawingId}`;
|
|
1821
|
+
const group = this.drawingLinks.get(key);
|
|
1822
|
+
if (!group) return;
|
|
1823
|
+
this.drawingLinks.delete(key);
|
|
1824
|
+
group.delete(originId);
|
|
1825
|
+
if (this.drawingSyncBusy) return;
|
|
1826
|
+
const setting = this.syncOpts.drawings;
|
|
1827
|
+
if (!setting) return;
|
|
1828
|
+
this.drawingSyncBusy = true;
|
|
1829
|
+
try {
|
|
1830
|
+
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1831
|
+
const peerId = group.get(id);
|
|
1832
|
+
if (peerId != null) this.cellsById.get(id)?.chart.drawings.remove(peerId);
|
|
1833
|
+
}
|
|
1834
|
+
} finally {
|
|
1835
|
+
this.drawingSyncBusy = false;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1519
1838
|
/**
|
|
1520
1839
|
* Push an origin cell's symbol/timeframe onto its same-group followers (fired from
|
|
1521
1840
|
* `market:changed`). Convergence comes from IDEMPOTENCE, not the guard: a follower's
|
|
@@ -1547,6 +1866,8 @@ var VelaWorkspace = class {
|
|
|
1547
1866
|
if (!cell) return;
|
|
1548
1867
|
this.topbar.setSymbol(cell.symbol);
|
|
1549
1868
|
this.topbar.setTimeframe(cell.timeframe);
|
|
1869
|
+
this.mobileBar?.setSymbol(cell.symbol);
|
|
1870
|
+
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
1550
1871
|
this.objectTree.setSymbol(cell.symbol);
|
|
1551
1872
|
}
|
|
1552
1873
|
/** Trigger ② — a cell's indicator ledger changed: count + picker only if active. */
|
|
@@ -1556,13 +1877,132 @@ var VelaWorkspace = class {
|
|
|
1556
1877
|
const cell = this.cellsById.get(id);
|
|
1557
1878
|
if (!cell) return;
|
|
1558
1879
|
this.topbar.setIndicatorCount(cell.indicatorCount);
|
|
1559
|
-
this.indicatorPicker
|
|
1880
|
+
this.indicatorPicker?.sync();
|
|
1560
1881
|
}
|
|
1561
1882
|
/** Timeframe changes routed from the topbar menu / quick entry (chip state follows). */
|
|
1562
1883
|
setActiveTimeframe(tf) {
|
|
1563
1884
|
this.bottombar?.setActiveRange(null);
|
|
1564
1885
|
this.active.setTimeframe(tf);
|
|
1565
1886
|
}
|
|
1887
|
+
/** The mode flipped (container resized across the breakpoint, or a coarse-pointer
|
|
1888
|
+
* change). Close the open surfaces — a desktop card must not linger over the
|
|
1889
|
+
* mobile chrome (and vice versa) — and re-present every cell's own chrome. */
|
|
1890
|
+
onLayoutModeChange(mode) {
|
|
1891
|
+
this.symbolPicker.close();
|
|
1892
|
+
this.indicatorPicker?.close();
|
|
1893
|
+
this.tfDrawer?.close();
|
|
1894
|
+
this.drawingsDrawer?.close();
|
|
1895
|
+
this.moreDrawer?.close();
|
|
1896
|
+
this.timezoneDrawer?.close();
|
|
1897
|
+
this.priceScaleDrawer?.close();
|
|
1898
|
+
this.alertsMenu?.destroy();
|
|
1899
|
+
this.alertsMenu = null;
|
|
1900
|
+
for (const cell of this.cellsById.values()) {
|
|
1901
|
+
cell.chart.renderer.closeDialogs();
|
|
1902
|
+
cell.chart.renderer.setLayoutMode(mode);
|
|
1903
|
+
}
|
|
1904
|
+
this.syncCellPresentation();
|
|
1905
|
+
}
|
|
1906
|
+
/** Push the layout-shape-dependent chrome onto every cell: multi-cell grids keep
|
|
1907
|
+
* the status line on one row (segments that don't fit hide), and on MOBILE their
|
|
1908
|
+
* legends' fold chip routes to the object tree instead of unfolding in place —
|
|
1909
|
+
* per-indicator controls live there (the legend rows have no room in a grid cell). */
|
|
1910
|
+
syncCellPresentation() {
|
|
1911
|
+
const multi = this.def.cells.length > 1;
|
|
1912
|
+
const overview = multi && this.layoutCtl.current === "mobile" ? () => this.dock.toggle("objects", true) : null;
|
|
1913
|
+
for (const cell of this.cellsById.values()) {
|
|
1914
|
+
cell.setStatuslineFit(multi);
|
|
1915
|
+
cell.chart.renderer.setLegendOverviewAction(overview);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
// ── mobile drawers (built on first open; every read is live and hits the ACTIVE cell) ──
|
|
1919
|
+
openTimeframeDrawer() {
|
|
1920
|
+
this.tfDrawer ?? (this.tfDrawer = new TimeframeDrawer({
|
|
1921
|
+
host: this.root,
|
|
1922
|
+
timeframes: this.opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
1923
|
+
ranges: RANGE_PRESETS,
|
|
1924
|
+
currentTimeframe: () => this.active.timeframe,
|
|
1925
|
+
activeRange: () => this.active.activeRangeId,
|
|
1926
|
+
onTimeframe: (tf) => this.setActiveTimeframe(tf),
|
|
1927
|
+
onRange: (preset) => {
|
|
1928
|
+
this.active.applyRange(preset);
|
|
1929
|
+
this.bottombar?.setActiveRange(preset.id);
|
|
1930
|
+
},
|
|
1931
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
1932
|
+
}));
|
|
1933
|
+
this.tfDrawer.open();
|
|
1934
|
+
}
|
|
1935
|
+
openDrawingsDrawer() {
|
|
1936
|
+
this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
|
|
1937
|
+
host: this.root,
|
|
1938
|
+
toolbar: () => defaultToolbar(),
|
|
1939
|
+
// the shared static toolbar's definition (see constructor)
|
|
1940
|
+
currentTool: () => this.active.chart.drawings.getTool(),
|
|
1941
|
+
isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
|
|
1942
|
+
onFavorite: (type, on) => this.active.chart.drawings.setFavorite(type, on),
|
|
1943
|
+
onSelect: (type) => this.active.chart.drawings.setTool(type),
|
|
1944
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
1945
|
+
}));
|
|
1946
|
+
this.drawingsDrawer.open();
|
|
1947
|
+
}
|
|
1948
|
+
openMoreDrawer() {
|
|
1949
|
+
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
1950
|
+
host: this.root,
|
|
1951
|
+
onUndo: () => this.active.history.undo(),
|
|
1952
|
+
onRedo: () => this.active.history.redo(),
|
|
1953
|
+
onScreenshot: () => this.active.downloadScreenshot(),
|
|
1954
|
+
canUndo: () => this.active.history.canUndo,
|
|
1955
|
+
canRedo: () => this.active.history.canRedo,
|
|
1956
|
+
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
1957
|
+
priceStyle: () => this.active.priceStyle,
|
|
1958
|
+
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
1959
|
+
panels: () => [...this.dock.list()],
|
|
1960
|
+
onTogglePanel: (id) => this.dock.toggle(id),
|
|
1961
|
+
alerts: () => this.alerts.map((a) => ({ title: `[${a.cellId} \xB7 ${a.symbol}] ${a.title}`, message: a.message, time: a.time })),
|
|
1962
|
+
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
1963
|
+
// lands in the drawer, or every left action would appear twice.
|
|
1964
|
+
actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
|
|
1965
|
+
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
1966
|
+
// non-canvas presets and the sync switches — relocated into the kebab
|
|
1967
|
+
// drawer (the topbar is hidden on mobile). Same reads as the topbar block.
|
|
1968
|
+
layout: {
|
|
1969
|
+
shape: () => layoutShape(this.def),
|
|
1970
|
+
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
1971
|
+
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
1972
|
+
onSelectPreset: (id) => this.setLayout(id),
|
|
1973
|
+
syncs: () => [
|
|
1974
|
+
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
1975
|
+
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
1976
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
1977
|
+
],
|
|
1978
|
+
onToggleSync: (id) => {
|
|
1979
|
+
const kind = id;
|
|
1980
|
+
this.sync.set(kind, this.syncOpts[kind] ? false : true);
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
1984
|
+
}));
|
|
1985
|
+
this.moreDrawer.open();
|
|
1986
|
+
}
|
|
1987
|
+
openTimezoneDrawer() {
|
|
1988
|
+
this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
|
|
1989
|
+
host: this.root,
|
|
1990
|
+
timezone: () => this.timezone,
|
|
1991
|
+
onTimezone: (zone) => this.setTimezone(zone),
|
|
1992
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
1993
|
+
}));
|
|
1994
|
+
this.timezoneDrawer.open();
|
|
1995
|
+
}
|
|
1996
|
+
openPriceScaleDrawer(y) {
|
|
1997
|
+
this.priceScalePressY = y;
|
|
1998
|
+
this.priceScaleDrawer ?? (this.priceScaleDrawer = new PriceScaleDrawer({
|
|
1999
|
+
host: this.root,
|
|
2000
|
+
chart: () => this.activeId ? this.cellsById.get(this.activeId)?.chart ?? null : null,
|
|
2001
|
+
pressY: () => this.priceScalePressY,
|
|
2002
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
2003
|
+
}));
|
|
2004
|
+
this.priceScaleDrawer.open();
|
|
2005
|
+
}
|
|
1566
2006
|
/** The aggregated alerts bell — entries carry their cell; selecting one activates it. */
|
|
1567
2007
|
openAlertsMenu(anchor) {
|
|
1568
2008
|
this.alertsMenu?.destroy();
|
|
@@ -1634,7 +2074,9 @@ var VelaWorkspace = class {
|
|
|
1634
2074
|
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
1635
2075
|
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
|
|
1636
2076
|
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
|
|
1637
|
-
|
|
2077
|
+
if (this.indicatorPicker) {
|
|
2078
|
+
this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
|
|
2079
|
+
}
|
|
1638
2080
|
this.keymap.register({
|
|
1639
2081
|
id: "help.shortcuts",
|
|
1640
2082
|
keys: "?",
|
|
@@ -1691,4 +2133,4 @@ var VelaWorkspace = class {
|
|
|
1691
2133
|
}
|
|
1692
2134
|
};
|
|
1693
2135
|
|
|
1694
|
-
export { ChartCell, VelaWorkspace, activeAfterLayout, evenTracks, gridStyles, layoutDefinition, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };
|
|
2136
|
+
export { ChartCell, GRID_PICKER_MAX, VelaWorkspace, activeAfterLayout, ensureLayout, evenTracks, gridStyles, layoutDefinition, layoutForGrid, layoutShape, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };
|