@luxalgo/vela 0.6.17 → 0.6.19
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-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
- package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
- package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +563 -149
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
- package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
- package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- 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/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
- package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +563 -149
- package/dist/vela.global.min.js +53 -51
- package/dist/widget.cjs +814 -268
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +814 -268
- package/dist/workspace.d.cts +14 -5
- package/dist/workspace.d.ts +14 -5
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -5039,15 +5039,15 @@ function candleOverrideFor(style, bags) {
|
|
|
5039
5039
|
if (!hasOwnCandlePaint(style)) return null;
|
|
5040
5040
|
const bag = bags[style] ?? {};
|
|
5041
5041
|
const color = (v) => isColor(v) ? v : null;
|
|
5042
|
-
const
|
|
5042
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
5043
5043
|
return {
|
|
5044
5044
|
upColor: color(bag.candleUpColor),
|
|
5045
5045
|
downColor: color(bag.candleDownColor),
|
|
5046
|
-
bodyVisible:
|
|
5047
|
-
borderVisible:
|
|
5046
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
5047
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
5048
5048
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
5049
5049
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
5050
|
-
wickVisible:
|
|
5050
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
5051
5051
|
wickUpColor: color(bag.candleWickUpColor),
|
|
5052
5052
|
wickDownColor: color(bag.candleWickDownColor)
|
|
5053
5053
|
};
|
|
@@ -12034,6 +12034,9 @@ function paneLabel(pane, fallbackTitle) {
|
|
|
12034
12034
|
function indicatorLabel(i, fallbackTitle) {
|
|
12035
12035
|
return i.title || fallbackTitle(i.id) || i.id;
|
|
12036
12036
|
}
|
|
12037
|
+
function indicatorRowLabel(i, fallbackTitle) {
|
|
12038
|
+
return i.shorttitle || indicatorLabel(i, fallbackTitle);
|
|
12039
|
+
}
|
|
12037
12040
|
function paneRows(pane) {
|
|
12038
12041
|
return pane.items.flatMap((it) => it.kind === "row" ? [it.row] : []);
|
|
12039
12042
|
}
|
|
@@ -12165,7 +12168,7 @@ function paneItems(snap, pane, units) {
|
|
|
12165
12168
|
const indicatorRow = (i) => ({
|
|
12166
12169
|
kind: "indicator",
|
|
12167
12170
|
id: i.id,
|
|
12168
|
-
label:
|
|
12171
|
+
label: indicatorRowLabel(i, snap.handleTitle),
|
|
12169
12172
|
visible: snap.indicatorVisible(i.id),
|
|
12170
12173
|
ownScale: i.ownScale
|
|
12171
12174
|
});
|
|
@@ -12578,9 +12581,13 @@ var CSS6 = `
|
|
|
12578
12581
|
.vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
|
|
12579
12582
|
|
|
12580
12583
|
/* \u2500\u2500 drawing groups \u2500\u2500 */
|
|
12581
|
-
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block.
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
+
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
|
|
12585
|
+
border of its own \u2014 units stack flush, so consecutive highlighted rows read as one block; the
|
|
12586
|
+
drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
|
|
12587
|
+
.vela-ot-unit { border-radius: 6px; }
|
|
12588
|
+
/* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
|
|
12589
|
+
.vela-ot-unit:has(> .vela-ot-row[data-picked]) + .vela-ot-unit > .vela-ot-row[data-picked] { border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
12590
|
+
.vela-ot-unit:has(+ .vela-ot-unit > .vela-ot-row[data-picked]) > .vela-ot-row[data-picked] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
12584
12591
|
.vela-ot-row[data-row-kind='group'] > .vela-icon { width: 12px; font-size: 11px; }
|
|
12585
12592
|
/* A member sits indented under its group's header. */
|
|
12586
12593
|
.vela-ot-subrow { padding-left: 26px; }
|
|
@@ -12637,8 +12644,10 @@ var CSS6 = `
|
|
|
12637
12644
|
.vela-ot-gap[data-drop] { height: 4px; background: var(--vela-fg-bright); }
|
|
12638
12645
|
.vela-ot-gap[data-drop]::before { display: none; }
|
|
12639
12646
|
/* A whole container accepts the drop: merging into a pane, or a pane with no drawings yet.
|
|
12640
|
-
The pane block's transparent border reserves the room for this outline
|
|
12647
|
+
The pane block's transparent border reserves the room for this outline; a group unit has no
|
|
12648
|
+
border to color, so it draws the same line as an inset outline (no layout footprint). */
|
|
12641
12649
|
.vela-ot [data-drop='target'] { border-color: var(--vela-fg-bright); background: var(--vela-hover); }
|
|
12650
|
+
.vela-ot-unit[data-drop='target'] { outline: 1px solid var(--vela-fg-bright); outline-offset: -1px; }
|
|
12642
12651
|
/* Where a reorder would insert. */
|
|
12643
12652
|
.vela-ot [data-drop='before'] { box-shadow: inset 0 2px 0 var(--vela-fg-bright); }
|
|
12644
12653
|
.vela-ot [data-drop='after'] { box-shadow: inset 0 -2px 0 var(--vela-fg-bright); }
|
|
@@ -12701,7 +12710,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
12701
12710
|
super(host, "Object tree", "vela-ot");
|
|
12702
12711
|
this.iconFor = iconFor;
|
|
12703
12712
|
this.chart = null;
|
|
12704
|
-
|
|
12713
|
+
/** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
|
|
12714
|
+
this.selectedDrawings = /* @__PURE__ */ new Set();
|
|
12705
12715
|
this.symbolName = "";
|
|
12706
12716
|
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
12707
12717
|
this.symbolRaw = "";
|
|
@@ -12752,12 +12762,9 @@ var ObjectTree = class extends SidePanel {
|
|
|
12752
12762
|
this.unsubs.push(chart.on(ev, () => this.refresh()));
|
|
12753
12763
|
}
|
|
12754
12764
|
this.unsubs.push(
|
|
12755
|
-
chart.on("drawing:selected", ({
|
|
12756
|
-
this.
|
|
12757
|
-
if (!this.syncingSelection)
|
|
12758
|
-
this.picked.clear();
|
|
12759
|
-
if (id) this.picked.add(id);
|
|
12760
|
-
}
|
|
12765
|
+
chart.on("drawing:selected", ({ ids }) => {
|
|
12766
|
+
this.selectedDrawings = new Set(ids);
|
|
12767
|
+
if (!this.syncingSelection) this.picked = new Set(ids);
|
|
12761
12768
|
this.refresh();
|
|
12762
12769
|
})
|
|
12763
12770
|
);
|
|
@@ -13105,7 +13112,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
13105
13112
|
el.dataset.drag = "1";
|
|
13106
13113
|
el.title = "Drag to restack, or onto another pane to move it there";
|
|
13107
13114
|
if (inGroup) el.classList.add("vela-ot-subrow");
|
|
13108
|
-
if (d.id
|
|
13115
|
+
if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
|
|
13109
13116
|
if (this.picked.has(d.id)) el.dataset.picked = "1";
|
|
13110
13117
|
el.addEventListener("click", (e) => this.onDrawClick(e, d.id));
|
|
13111
13118
|
return el;
|
|
@@ -14269,6 +14276,20 @@ var SymbolPicker = class {
|
|
|
14269
14276
|
this.seed = initialQuery;
|
|
14270
14277
|
this.dialog.show();
|
|
14271
14278
|
}
|
|
14279
|
+
get isOpen() {
|
|
14280
|
+
return this.dialog.open;
|
|
14281
|
+
}
|
|
14282
|
+
/** Text typed at the picker that never reached its field: closed, it opens seeded
|
|
14283
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14284
|
+
* fast typist's next key lands in that tick), it extends the query. */
|
|
14285
|
+
type(text) {
|
|
14286
|
+
if (!this.isOpen) {
|
|
14287
|
+
this.open(text);
|
|
14288
|
+
return;
|
|
14289
|
+
}
|
|
14290
|
+
this.input.value = (this.input.value + text).toUpperCase();
|
|
14291
|
+
this.refresh();
|
|
14292
|
+
}
|
|
14272
14293
|
close() {
|
|
14273
14294
|
this.dialog.hide();
|
|
14274
14295
|
}
|
|
@@ -14667,6 +14688,20 @@ var TimeframeQuick = class {
|
|
|
14667
14688
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
14668
14689
|
}, 0);
|
|
14669
14690
|
}
|
|
14691
|
+
get isOpen() {
|
|
14692
|
+
return this.dialog.open;
|
|
14693
|
+
}
|
|
14694
|
+
/** Text typed at the entry that never reached its field: closed, it opens seeded
|
|
14695
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14696
|
+
* fast typist's next key lands in that tick), it extends the entry. */
|
|
14697
|
+
type(text) {
|
|
14698
|
+
if (!this.isOpen) {
|
|
14699
|
+
this.open(text);
|
|
14700
|
+
return;
|
|
14701
|
+
}
|
|
14702
|
+
this.input.value += text;
|
|
14703
|
+
this.renderHint();
|
|
14704
|
+
}
|
|
14670
14705
|
close() {
|
|
14671
14706
|
this.dialog.hide();
|
|
14672
14707
|
}
|
|
@@ -16737,15 +16772,18 @@ var DrawingController = class {
|
|
|
16737
16772
|
next = [...ids];
|
|
16738
16773
|
}
|
|
16739
16774
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
16775
|
+
this.announceSelection();
|
|
16776
|
+
}
|
|
16777
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
16778
|
+
announceSelection() {
|
|
16740
16779
|
this.port?.setSelection(this.selectedIds);
|
|
16741
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16780
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
16742
16781
|
}
|
|
16743
16782
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
16744
16783
|
restoreSnapshot(doc) {
|
|
16745
16784
|
this.store.load(doc);
|
|
16746
16785
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
16747
|
-
this.
|
|
16748
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16786
|
+
this.announceSelection();
|
|
16749
16787
|
}
|
|
16750
16788
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
16751
16789
|
deleteMany(ids) {
|
|
@@ -16878,6 +16916,9 @@ var DrawingController = class {
|
|
|
16878
16916
|
case "duplicate":
|
|
16879
16917
|
this.duplicate(i.ids);
|
|
16880
16918
|
break;
|
|
16919
|
+
case "clone":
|
|
16920
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
16921
|
+
break;
|
|
16881
16922
|
case "copy":
|
|
16882
16923
|
this.copy(i.ids);
|
|
16883
16924
|
break;
|
|
@@ -17063,9 +17104,10 @@ async function resolveIndicators(config, fetchImpl = fetch) {
|
|
|
17063
17104
|
return out;
|
|
17064
17105
|
}
|
|
17065
17106
|
var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
|
|
17107
|
+
var ledgerNativeType = (e) => typeof e === "string" ? e : e.type;
|
|
17066
17108
|
function indicatorLedger(i) {
|
|
17067
17109
|
const natives = [...i.present];
|
|
17068
|
-
if (i.volumePending && !natives.
|
|
17110
|
+
if (i.volumePending && !natives.some((e) => ledgerNativeType(e) === "volume")) natives.push("volume");
|
|
17069
17111
|
return {
|
|
17070
17112
|
manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
|
|
17071
17113
|
natives
|
|
@@ -18020,18 +18062,28 @@ function sanitizeCell(raw) {
|
|
|
18020
18062
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
18021
18063
|
const ind = c.indicators;
|
|
18022
18064
|
if (ind != null && typeof ind === "object") {
|
|
18065
|
+
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
18023
18066
|
const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
|
|
18024
18067
|
if (typeof n === "string") return [n];
|
|
18025
18068
|
if (n != null && typeof n === "object" && typeof n.name === "string") {
|
|
18026
18069
|
const e = n;
|
|
18027
|
-
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
18028
18070
|
const inputs = bag(e.inputs);
|
|
18029
18071
|
const props = bag(e.props);
|
|
18030
|
-
|
|
18072
|
+
const hidden = e.hidden === true;
|
|
18073
|
+
return [inputs || props || hidden ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {}, ...hidden ? { hidden } : {} } : e.name];
|
|
18074
|
+
}
|
|
18075
|
+
return [];
|
|
18076
|
+
}) : [];
|
|
18077
|
+
const natives = Array.isArray(ind.natives) ? ind.natives.flatMap((n) => {
|
|
18078
|
+
if (typeof n === "string") return [n];
|
|
18079
|
+
if (n != null && typeof n === "object" && typeof n.type === "string") {
|
|
18080
|
+
const e = n;
|
|
18081
|
+
const inputs = bag(e.inputs);
|
|
18082
|
+
const hidden = e.hidden === true;
|
|
18083
|
+
return [inputs || hidden ? { type: e.type, ...inputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : e.type];
|
|
18031
18084
|
}
|
|
18032
18085
|
return [];
|
|
18033
18086
|
}) : [];
|
|
18034
|
-
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
18035
18087
|
out.indicators = { manifest, natives };
|
|
18036
18088
|
}
|
|
18037
18089
|
const ext = sanitizeExt(c.ext);
|
|
@@ -19844,7 +19896,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19844
19896
|
if (!model) continue;
|
|
19845
19897
|
const paneId = model.paneId ?? "price";
|
|
19846
19898
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19847
|
-
byPane.get(paneId).push({
|
|
19899
|
+
byPane.get(paneId).push({
|
|
19900
|
+
id: r.id,
|
|
19901
|
+
title: model.title || r.title,
|
|
19902
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
19903
|
+
ownScale: model.ownScale === true
|
|
19904
|
+
});
|
|
19848
19905
|
}
|
|
19849
19906
|
const panes = [];
|
|
19850
19907
|
for (const paneId of this.paneOrder) {
|
|
@@ -24477,7 +24534,7 @@ var InputController = class {
|
|
|
24477
24534
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24478
24535
|
if (Number.isNaN(this.cursorX)) return;
|
|
24479
24536
|
if (this.dragging && this.region !== "drawing") return;
|
|
24480
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24537
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24481
24538
|
};
|
|
24482
24539
|
this.onDown = (e) => {
|
|
24483
24540
|
if (e.button === 1) {
|
|
@@ -24509,7 +24566,7 @@ var InputController = class {
|
|
|
24509
24566
|
this.startY = y;
|
|
24510
24567
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24511
24568
|
this.region = "drawing";
|
|
24512
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24569
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24513
24570
|
this.capture(e.pointerId);
|
|
24514
24571
|
return;
|
|
24515
24572
|
}
|
|
@@ -24518,6 +24575,11 @@ var InputController = class {
|
|
|
24518
24575
|
this.capture(e.pointerId);
|
|
24519
24576
|
return;
|
|
24520
24577
|
}
|
|
24578
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24579
|
+
this.region = "drawing";
|
|
24580
|
+
this.capture(e.pointerId);
|
|
24581
|
+
return;
|
|
24582
|
+
}
|
|
24521
24583
|
const vp = this.deps.getCoords().getViewport();
|
|
24522
24584
|
this.deps.apply(vp);
|
|
24523
24585
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24566,7 +24628,7 @@ var InputController = class {
|
|
|
24566
24628
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24567
24629
|
} else if (this.region === "drawing") {
|
|
24568
24630
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
24569
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24631
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24570
24632
|
} else {
|
|
24571
24633
|
const coords = this.deps.getCoords();
|
|
24572
24634
|
const vp = coords.getViewport();
|
|
@@ -24589,7 +24651,7 @@ var InputController = class {
|
|
|
24589
24651
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24590
24652
|
const r = this.regionAt(x, y);
|
|
24591
24653
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
24592
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24654
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24593
24655
|
}
|
|
24594
24656
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24595
24657
|
};
|
|
@@ -24930,12 +24992,12 @@ function defaultTradeMarkersState() {
|
|
|
24930
24992
|
function mergeTradeMarkersState(base, patch) {
|
|
24931
24993
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
24932
24994
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
24933
|
-
const
|
|
24995
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
24934
24996
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
24935
24997
|
return {
|
|
24936
|
-
visible:
|
|
24937
|
-
labels:
|
|
24938
|
-
qty:
|
|
24998
|
+
visible: bool2(p.visible, base.visible),
|
|
24999
|
+
labels: bool2(p.labels, base.labels),
|
|
25000
|
+
qty: bool2(p.qty, base.qty),
|
|
24939
25001
|
colors: {
|
|
24940
25002
|
long: color(c.long, base.colors.long),
|
|
24941
25003
|
short: color(c.short, base.colors.short),
|
|
@@ -28795,6 +28857,18 @@ var DrawingPainter = class {
|
|
|
28795
28857
|
}
|
|
28796
28858
|
this.paintHandles(ctx, pts);
|
|
28797
28859
|
}
|
|
28860
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
28861
|
+
* accent under a thin dashed outline. */
|
|
28862
|
+
paintMarquee(ctx, rect) {
|
|
28863
|
+
ctx.save();
|
|
28864
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
28865
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
28866
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
28867
|
+
ctx.lineWidth = 1;
|
|
28868
|
+
ctx.setLineDash([4, 3]);
|
|
28869
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
28870
|
+
ctx.restore();
|
|
28871
|
+
}
|
|
28798
28872
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
28799
28873
|
paintSnapRing(ctx, x, y, theme) {
|
|
28800
28874
|
ctx.setLineDash([]);
|
|
@@ -30263,6 +30337,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
30263
30337
|
}
|
|
30264
30338
|
return null;
|
|
30265
30339
|
}
|
|
30340
|
+
function deletableSelection(selected, drawings) {
|
|
30341
|
+
const ids = [...selected];
|
|
30342
|
+
if (ids.length < 2) return ids;
|
|
30343
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30344
|
+
}
|
|
30345
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30346
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30347
|
+
return hit.locked ? [] : [hit.id];
|
|
30348
|
+
}
|
|
30266
30349
|
|
|
30267
30350
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30268
30351
|
var DRAG_SLOP3 = 3;
|
|
@@ -30366,10 +30449,32 @@ var DrawingInteraction = class {
|
|
|
30366
30449
|
activeDragId() {
|
|
30367
30450
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30368
30451
|
}
|
|
30452
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30453
|
+
pressedId() {
|
|
30454
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30455
|
+
}
|
|
30456
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30457
|
+
dragClones() {
|
|
30458
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30459
|
+
}
|
|
30460
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30461
|
+
marqueeRect() {
|
|
30462
|
+
if (this.state.kind !== "marquee") return null;
|
|
30463
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30464
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30465
|
+
}
|
|
30466
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30467
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30468
|
+
beginMarquee(x, y) {
|
|
30469
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30470
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30471
|
+
return true;
|
|
30472
|
+
}
|
|
30369
30473
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30370
30474
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
30371
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
30372
|
-
|
|
30475
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30476
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30477
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
30373
30478
|
const proj = this.deps.projector();
|
|
30374
30479
|
if (this.state.kind === "placing") {
|
|
30375
30480
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30397,7 +30502,50 @@ var DrawingInteraction = class {
|
|
|
30397
30502
|
return;
|
|
30398
30503
|
}
|
|
30399
30504
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30400
|
-
|
|
30505
|
+
const riders = [];
|
|
30506
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30507
|
+
for (const id of this.deps.selectedIds()) {
|
|
30508
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30509
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30510
|
+
}
|
|
30511
|
+
}
|
|
30512
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30513
|
+
}
|
|
30514
|
+
}
|
|
30515
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30516
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30517
|
+
bodyDelta(paneId, x, y) {
|
|
30518
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30519
|
+
const proj = this.deps.projector();
|
|
30520
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30521
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30522
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30523
|
+
}
|
|
30524
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30525
|
+
dragSources(d) {
|
|
30526
|
+
if (this.state.kind !== "pressed") return [];
|
|
30527
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30528
|
+
for (const r of this.state.riders) {
|
|
30529
|
+
const rd = this.byId(r.id);
|
|
30530
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30531
|
+
}
|
|
30532
|
+
return out;
|
|
30533
|
+
}
|
|
30534
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30535
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30536
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30537
|
+
dragBody(d, x, y) {
|
|
30538
|
+
if (this.state.kind !== "pressed") return;
|
|
30539
|
+
this.snapAt = null;
|
|
30540
|
+
if (this.state.mod && !this.state.clones) {
|
|
30541
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30542
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30543
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30544
|
+
});
|
|
30545
|
+
}
|
|
30546
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30547
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30548
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30401
30549
|
}
|
|
30402
30550
|
}
|
|
30403
30551
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30421,12 +30569,20 @@ var DrawingInteraction = class {
|
|
|
30421
30569
|
this.deps.changed();
|
|
30422
30570
|
return;
|
|
30423
30571
|
}
|
|
30572
|
+
if (this.state.kind === "marquee") {
|
|
30573
|
+
this.state.x1 = x;
|
|
30574
|
+
this.state.y1 = y;
|
|
30575
|
+
this.deps.changed();
|
|
30576
|
+
return;
|
|
30577
|
+
}
|
|
30424
30578
|
if (this.state.kind !== "pressed") return;
|
|
30425
30579
|
const d = this.byId(this.state.id);
|
|
30426
30580
|
if (!d || d.locked) return;
|
|
30427
30581
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
|
|
30428
30582
|
this.state.moved = true;
|
|
30429
|
-
if (this.state.handle
|
|
30583
|
+
if (this.state.handle < 0) {
|
|
30584
|
+
this.dragBody(d, x, y);
|
|
30585
|
+
} else {
|
|
30430
30586
|
const a = d.anchors[this.state.handle];
|
|
30431
30587
|
if (a) {
|
|
30432
30588
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30434,12 +30590,6 @@ var DrawingInteraction = class {
|
|
|
30434
30590
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30435
30591
|
}
|
|
30436
30592
|
d.constrainHandleDrag(this.state.handle);
|
|
30437
|
-
} else {
|
|
30438
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
30439
|
-
this.snapAt = null;
|
|
30440
|
-
const dt = pt.time - this.state.grab.time;
|
|
30441
|
-
const dp = pt.price - this.state.grab.price;
|
|
30442
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
30443
30593
|
}
|
|
30444
30594
|
this.deps.changed();
|
|
30445
30595
|
}
|
|
@@ -30459,16 +30609,46 @@ var DrawingInteraction = class {
|
|
|
30459
30609
|
}
|
|
30460
30610
|
return;
|
|
30461
30611
|
}
|
|
30612
|
+
if (this.state.kind === "marquee") {
|
|
30613
|
+
const rect = this.marqueeRect();
|
|
30614
|
+
this.state = { kind: "idle" };
|
|
30615
|
+
if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
|
|
30616
|
+
this.deps.changed();
|
|
30617
|
+
return;
|
|
30618
|
+
}
|
|
30462
30619
|
if (this.state.kind !== "pressed") return;
|
|
30463
|
-
const { id, moved } = this.state;
|
|
30620
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30464
30621
|
this.state = { kind: "idle" };
|
|
30465
30622
|
const d = this.byId(id);
|
|
30466
|
-
if (moved) {
|
|
30467
|
-
if (
|
|
30468
|
-
this.deps.
|
|
30469
|
-
|
|
30470
|
-
|
|
30623
|
+
if (!moved) {
|
|
30624
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30625
|
+
else this.deps.openSettings(id, x, y);
|
|
30626
|
+
return;
|
|
30627
|
+
}
|
|
30628
|
+
if (clones) {
|
|
30629
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30630
|
+
} else if (d) {
|
|
30631
|
+
const docs = [d.serialize()];
|
|
30632
|
+
for (const r of riders) {
|
|
30633
|
+
const rd = this.byId(r.id);
|
|
30634
|
+
if (rd) docs.push(rd.serialize());
|
|
30635
|
+
}
|
|
30636
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30637
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30471
30638
|
}
|
|
30639
|
+
this.deps.changed();
|
|
30640
|
+
}
|
|
30641
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30642
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30643
|
+
selectWithin(rect) {
|
|
30644
|
+
const proj = this.deps.projector();
|
|
30645
|
+
const ids = [...this.deps.selectedIds()];
|
|
30646
|
+
for (const d of this.deps.drawings()) {
|
|
30647
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30648
|
+
const b = d.bounds(proj);
|
|
30649
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30650
|
+
}
|
|
30651
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30472
30652
|
}
|
|
30473
30653
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30474
30654
|
cancel() {
|
|
@@ -30481,8 +30661,15 @@ var DrawingInteraction = class {
|
|
|
30481
30661
|
return true;
|
|
30482
30662
|
}
|
|
30483
30663
|
if (this.state.kind === "pressed") {
|
|
30484
|
-
|
|
30485
|
-
|
|
30664
|
+
if (this.state.moved && !this.state.clones) {
|
|
30665
|
+
const d = this.byId(this.state.id);
|
|
30666
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30667
|
+
}
|
|
30668
|
+
this.state = { kind: "idle" };
|
|
30669
|
+
this.deps.changed();
|
|
30670
|
+
return true;
|
|
30671
|
+
}
|
|
30672
|
+
if (this.state.kind === "marquee") {
|
|
30486
30673
|
this.state = { kind: "idle" };
|
|
30487
30674
|
this.deps.changed();
|
|
30488
30675
|
return true;
|
|
@@ -30597,6 +30784,9 @@ var DrawingInteraction = class {
|
|
|
30597
30784
|
function cloneAnchors(d) {
|
|
30598
30785
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30599
30786
|
}
|
|
30787
|
+
function rectsIntersect(a, b) {
|
|
30788
|
+
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
|
|
30789
|
+
}
|
|
30600
30790
|
function applyFree(anchor, pt, free) {
|
|
30601
30791
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
30602
30792
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -30982,10 +31172,27 @@ function btn(label, primary, onClick) {
|
|
|
30982
31172
|
}
|
|
30983
31173
|
|
|
30984
31174
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31175
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31176
|
+
function sharedPaths(drawings) {
|
|
31177
|
+
const [first, ...rest] = drawings;
|
|
31178
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
31179
|
+
for (const d of rest) {
|
|
31180
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31181
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31182
|
+
}
|
|
31183
|
+
return paths;
|
|
31184
|
+
}
|
|
31185
|
+
function commonValue(drawings, read) {
|
|
31186
|
+
const first = read(drawings[0]);
|
|
31187
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
31188
|
+
}
|
|
31189
|
+
function distinctValues(drawings, read) {
|
|
31190
|
+
return [...new Set(drawings.map(read))];
|
|
31191
|
+
}
|
|
30985
31192
|
var BTN = 30;
|
|
30986
31193
|
var ICON = 21;
|
|
30987
31194
|
var STYLE_ID23 = "vela-drawing-popup-styles";
|
|
30988
|
-
var STYLE_REV = "
|
|
31195
|
+
var STYLE_REV = "4";
|
|
30989
31196
|
function ensureStyles5() {
|
|
30990
31197
|
if (typeof document === "undefined") return;
|
|
30991
31198
|
const existing = document.getElementById(STYLE_ID23);
|
|
@@ -30997,6 +31204,8 @@ function ensureStyles5() {
|
|
|
30997
31204
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
30998
31205
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
30999
31206
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31207
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31208
|
+
.vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
|
|
31000
31209
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
31001
31210
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
31002
31211
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -31028,7 +31237,7 @@ var DrawingSettingsPopup = class {
|
|
|
31028
31237
|
if (this.isOwnChrome(node)) return;
|
|
31029
31238
|
const cb = this.onClose;
|
|
31030
31239
|
this.close();
|
|
31031
|
-
cb?.();
|
|
31240
|
+
cb?.(e);
|
|
31032
31241
|
};
|
|
31033
31242
|
this.theme = theme;
|
|
31034
31243
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31053,16 +31262,27 @@ var DrawingSettingsPopup = class {
|
|
|
31053
31262
|
contains(node) {
|
|
31054
31263
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31055
31264
|
}
|
|
31056
|
-
/** Open the quick toolbar for `
|
|
31057
|
-
* `
|
|
31058
|
-
|
|
31265
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31266
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31267
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31268
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31269
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31270
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31271
|
+
* modifier press (multi-select) from a plain one. */
|
|
31272
|
+
open(drawings, anchor, actions, onClose) {
|
|
31059
31273
|
this.close();
|
|
31274
|
+
const drawing = drawings[0];
|
|
31275
|
+
if (!drawing) return;
|
|
31060
31276
|
ensureStyles5();
|
|
31061
31277
|
this.onClose = onClose ?? null;
|
|
31062
31278
|
const t = this.theme;
|
|
31279
|
+
const multi = drawings.length > 1;
|
|
31063
31280
|
const schema = drawing.schema();
|
|
31064
|
-
const paths =
|
|
31065
|
-
const
|
|
31281
|
+
const paths = sharedPaths(drawings);
|
|
31282
|
+
const common = (read) => commonValue(drawings, read);
|
|
31283
|
+
const every = (pred) => drawings.every(pred);
|
|
31284
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31285
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31066
31286
|
const el = document.createElement("div");
|
|
31067
31287
|
el.className = "vela-dpop";
|
|
31068
31288
|
el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
|
|
@@ -31076,75 +31296,84 @@ var DrawingSettingsPopup = class {
|
|
|
31076
31296
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31077
31297
|
bar.appendChild(this.dragHandle());
|
|
31078
31298
|
if (paths.has("glyph")) {
|
|
31079
|
-
const cur =
|
|
31299
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31080
31300
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31081
31301
|
}
|
|
31082
31302
|
if (paths.has("size")) {
|
|
31083
|
-
const sz =
|
|
31303
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31084
31304
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31085
31305
|
}
|
|
31086
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31306
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31307
|
+
const mag = (d) => d;
|
|
31087
31308
|
const options = this.lowerTimeframeOptions();
|
|
31088
31309
|
if (options.length > 0) {
|
|
31089
31310
|
bar.appendChild(
|
|
31090
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31311
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31091
31312
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31092
31313
|
labelInTrigger: true
|
|
31093
31314
|
})
|
|
31094
31315
|
);
|
|
31095
31316
|
}
|
|
31096
|
-
bar.appendChild(
|
|
31097
|
-
bar.appendChild(
|
|
31317
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31318
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31098
31319
|
}
|
|
31099
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31320
|
+
if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
|
|
31100
31321
|
if (paths.has("style.lineWidth")) {
|
|
31101
|
-
const
|
|
31102
|
-
|
|
31103
|
-
|
|
31322
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31323
|
+
const wf = widthField2(drawing);
|
|
31324
|
+
const width = common((d) => d.style.lineWidth);
|
|
31325
|
+
const markers = every((d) => {
|
|
31326
|
+
const f = widthField2(d);
|
|
31327
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31328
|
+
});
|
|
31329
|
+
if (markers && wf?.kind === "number") {
|
|
31330
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31104
31331
|
} else {
|
|
31105
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31332
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
31106
31333
|
}
|
|
31107
31334
|
}
|
|
31108
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
31109
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
31335
|
+
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
31336
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
31110
31337
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31111
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31338
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31112
31339
|
if (isPosition) {
|
|
31113
|
-
const pos =
|
|
31114
|
-
bar.appendChild(
|
|
31115
|
-
bar.appendChild(
|
|
31340
|
+
const pos = (d) => d;
|
|
31341
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31342
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31116
31343
|
}
|
|
31117
|
-
const
|
|
31118
|
-
if (paths.has("reg.midColor") &&
|
|
31344
|
+
const regOf = (d) => d.reg;
|
|
31345
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31346
|
+
const reg = (d) => regOf(d);
|
|
31119
31347
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31120
|
-
bar.appendChild(
|
|
31121
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31122
|
-
bar.appendChild(
|
|
31123
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31124
|
-
bar.appendChild(
|
|
31125
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31126
|
-
bar.appendChild(
|
|
31127
|
-
bar.appendChild(
|
|
31128
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
31129
|
-
}
|
|
31130
|
-
const
|
|
31131
|
-
if (paths.has("vwap.midColor") &&
|
|
31348
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31349
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31350
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31351
|
+
bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31352
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31353
|
+
bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31354
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31355
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31356
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31357
|
+
}
|
|
31358
|
+
const vwapOf = (d) => d.vwap;
|
|
31359
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31360
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31132
31361
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31133
31362
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
31134
|
-
bar.appendChild(
|
|
31135
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31363
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31364
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31136
31365
|
bar.appendChild(
|
|
31137
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31366
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31138
31367
|
label: (v) => `${v}\u03C3`,
|
|
31139
31368
|
labelInTrigger: true
|
|
31140
31369
|
})
|
|
31141
31370
|
);
|
|
31142
|
-
bar.appendChild(
|
|
31143
|
-
bar.appendChild(
|
|
31144
|
-
bar.appendChild(
|
|
31371
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31372
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31373
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31145
31374
|
}
|
|
31146
31375
|
if (paths.has("maxCurvature")) {
|
|
31147
|
-
const cur =
|
|
31376
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31148
31377
|
bar.appendChild(
|
|
31149
31378
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31150
31379
|
label: (v) => `n=${v}`,
|
|
@@ -31153,7 +31382,7 @@ var DrawingSettingsPopup = class {
|
|
|
31153
31382
|
);
|
|
31154
31383
|
}
|
|
31155
31384
|
if (paths.has("mach")) {
|
|
31156
|
-
const cur =
|
|
31385
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31157
31386
|
bar.appendChild(
|
|
31158
31387
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31159
31388
|
label: (v) => `M=${v}`,
|
|
@@ -31162,7 +31391,7 @@ var DrawingSettingsPopup = class {
|
|
|
31162
31391
|
);
|
|
31163
31392
|
}
|
|
31164
31393
|
if (paths.has("waveCount")) {
|
|
31165
|
-
const cur =
|
|
31394
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31166
31395
|
bar.appendChild(
|
|
31167
31396
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31168
31397
|
label: (v) => `${v}`,
|
|
@@ -31170,26 +31399,27 @@ var DrawingSettingsPopup = class {
|
|
|
31170
31399
|
})
|
|
31171
31400
|
);
|
|
31172
31401
|
}
|
|
31173
|
-
const
|
|
31174
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
31175
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
31176
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
31177
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
31178
|
-
|
|
31179
|
-
if (paths.has("text.
|
|
31180
|
-
if (paths.has("text.
|
|
31402
|
+
const flags = (d) => d;
|
|
31403
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31404
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31405
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31406
|
+
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
|
|
31407
|
+
const textField = !multi && paths.has("text.value");
|
|
31408
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31409
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31410
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31181
31411
|
const editableLevels = drawing.editableLevels();
|
|
31182
|
-
if (editableLevels && !(
|
|
31183
|
-
const fib =
|
|
31412
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31413
|
+
const fib = (d) => d;
|
|
31184
31414
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
31185
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31186
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31415
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31416
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31187
31417
|
}
|
|
31188
31418
|
bar.appendChild(this.divider());
|
|
31189
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31190
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31191
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31192
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
31419
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31420
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31421
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31422
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31193
31423
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31194
31424
|
del.style.color = "var(--vela-danger)";
|
|
31195
31425
|
bar.appendChild(del);
|
|
@@ -31375,7 +31605,7 @@ var DrawingSettingsPopup = class {
|
|
|
31375
31605
|
});
|
|
31376
31606
|
return b;
|
|
31377
31607
|
}
|
|
31378
|
-
/** The trailing kebab overflow — z-order actions
|
|
31608
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31379
31609
|
kebabButton(actions) {
|
|
31380
31610
|
const b = this.base("More");
|
|
31381
31611
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31387,6 +31617,7 @@ var DrawingSettingsPopup = class {
|
|
|
31387
31617
|
return;
|
|
31388
31618
|
}
|
|
31389
31619
|
this.openActionMenu(b, [
|
|
31620
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31390
31621
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31391
31622
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31392
31623
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31543,26 +31774,30 @@ var DrawingSettingsPopup = class {
|
|
|
31543
31774
|
this.tipEl?.remove();
|
|
31544
31775
|
this.tipEl = null;
|
|
31545
31776
|
}
|
|
31546
|
-
|
|
31777
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
31778
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
31779
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
31780
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31547
31781
|
const b = this.base(tip);
|
|
31548
|
-
b.innerHTML = sized(icon2);
|
|
31549
31782
|
const set = (on2) => {
|
|
31550
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
31783
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
31784
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31551
31785
|
};
|
|
31552
31786
|
set(active);
|
|
31553
31787
|
let on = active;
|
|
31554
31788
|
b.addEventListener("click", () => {
|
|
31555
|
-
on = !on;
|
|
31789
|
+
on = on === MIXED ? true : !on;
|
|
31556
31790
|
set(on);
|
|
31557
31791
|
onChange(on);
|
|
31558
31792
|
});
|
|
31559
31793
|
return b;
|
|
31560
31794
|
}
|
|
31561
31795
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
31562
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
31796
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
31797
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31563
31798
|
widthInput(tip, value, min, max, step, onChange) {
|
|
31564
31799
|
const ni = new NumberInput({
|
|
31565
|
-
value,
|
|
31800
|
+
value: value === MIXED ? min : value,
|
|
31566
31801
|
min,
|
|
31567
31802
|
max,
|
|
31568
31803
|
step,
|
|
@@ -31574,13 +31809,18 @@ var DrawingSettingsPopup = class {
|
|
|
31574
31809
|
title: tip,
|
|
31575
31810
|
onChange
|
|
31576
31811
|
});
|
|
31812
|
+
if (value === MIXED) {
|
|
31813
|
+
ni.input.value = "";
|
|
31814
|
+
ni.input.placeholder = "\u2014";
|
|
31815
|
+
}
|
|
31577
31816
|
ni.el.dataset.tip = tip;
|
|
31578
31817
|
trapChartKeys(ni.el);
|
|
31579
31818
|
return ni.el;
|
|
31580
31819
|
}
|
|
31581
31820
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31582
31821
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
31583
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31822
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31823
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31584
31824
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31585
31825
|
const b = this.base(tip);
|
|
31586
31826
|
b.style.width = "auto";
|
|
@@ -31590,14 +31830,20 @@ var DrawingSettingsPopup = class {
|
|
|
31590
31830
|
let cur = current;
|
|
31591
31831
|
const paint = (v) => {
|
|
31592
31832
|
b.replaceChildren();
|
|
31593
|
-
|
|
31833
|
+
if (v === MIXED) {
|
|
31834
|
+
const dash = document.createElement("span");
|
|
31835
|
+
dash.textContent = "\u2014";
|
|
31836
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
31837
|
+
b.appendChild(dash);
|
|
31838
|
+
}
|
|
31839
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31594
31840
|
if (glyph) {
|
|
31595
31841
|
const ic = document.createElement("span");
|
|
31596
31842
|
ic.style.cssText = "display:flex;";
|
|
31597
31843
|
ic.innerHTML = sized(glyph);
|
|
31598
31844
|
b.appendChild(ic);
|
|
31599
31845
|
}
|
|
31600
|
-
if (opts.label && opts.labelInTrigger) {
|
|
31846
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31601
31847
|
const tx = document.createElement("span");
|
|
31602
31848
|
tx.textContent = opts.label(v);
|
|
31603
31849
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -31667,8 +31913,10 @@ var DrawingSettingsPopup = class {
|
|
|
31667
31913
|
}
|
|
31668
31914
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
31669
31915
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
31670
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
31671
|
-
|
|
31916
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
31917
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
31918
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
31919
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
31672
31920
|
const b = document.createElement("button");
|
|
31673
31921
|
b.type = "button";
|
|
31674
31922
|
b.dataset.tip = tip;
|
|
@@ -31678,13 +31926,14 @@ var DrawingSettingsPopup = class {
|
|
|
31678
31926
|
ic.style.cssText = "display:flex;";
|
|
31679
31927
|
ic.innerHTML = sized(icon2, iconSize);
|
|
31680
31928
|
const bar = document.createElement("span");
|
|
31681
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
31929
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
31682
31930
|
b.append(ic, bar);
|
|
31683
31931
|
let cur = color;
|
|
31684
31932
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
31685
31933
|
b.addEventListener("click", (e) => {
|
|
31686
31934
|
e.stopPropagation();
|
|
31687
|
-
|
|
31935
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
31936
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
31688
31937
|
cur = v;
|
|
31689
31938
|
bar.style.background = v;
|
|
31690
31939
|
onChange(v);
|
|
@@ -31693,20 +31942,42 @@ var DrawingSettingsPopup = class {
|
|
|
31693
31942
|
return b;
|
|
31694
31943
|
}
|
|
31695
31944
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
31696
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
31945
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
31697
31946
|
if (this.colorOwner === anchor) {
|
|
31698
31947
|
this.closeColorPopover();
|
|
31699
31948
|
return;
|
|
31700
31949
|
}
|
|
31701
|
-
this.openColorPopover(anchor, color, onChange);
|
|
31950
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
31702
31951
|
}
|
|
31703
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31704
|
-
|
|
31952
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31953
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
31954
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
31705
31955
|
const t = this.theme;
|
|
31706
31956
|
this.colorPop = this.hostFloat(anchor, {
|
|
31707
31957
|
zIndex: 25,
|
|
31708
31958
|
padding: "10px",
|
|
31709
|
-
fill: (el) => {
|
|
31959
|
+
fill: (el, pop) => {
|
|
31960
|
+
if (used.length > 1) {
|
|
31961
|
+
const row = document.createElement("div");
|
|
31962
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
31963
|
+
const lbl = document.createElement("span");
|
|
31964
|
+
lbl.textContent = "In use";
|
|
31965
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
31966
|
+
row.appendChild(lbl);
|
|
31967
|
+
for (const c of used) {
|
|
31968
|
+
const sw = document.createElement("button");
|
|
31969
|
+
sw.type = "button";
|
|
31970
|
+
sw.dataset.tip = c;
|
|
31971
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
31972
|
+
sw.addEventListener("click", (e) => {
|
|
31973
|
+
e.stopPropagation();
|
|
31974
|
+
pop.hide();
|
|
31975
|
+
onChange(c);
|
|
31976
|
+
});
|
|
31977
|
+
row.appendChild(sw);
|
|
31978
|
+
}
|
|
31979
|
+
el.appendChild(row);
|
|
31980
|
+
}
|
|
31710
31981
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
31711
31982
|
}
|
|
31712
31983
|
});
|
|
@@ -31727,6 +31998,7 @@ var TYPE_ICON = icon("type");
|
|
|
31727
31998
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
31728
31999
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
31729
32000
|
var LOCK_ICON = icon("lock");
|
|
32001
|
+
var UNLOCK_ICON = icon("unlock");
|
|
31730
32002
|
var FRONT_ICON = icon("bring-front");
|
|
31731
32003
|
var BACK_ICON = icon("send-back");
|
|
31732
32004
|
var TRASH_ICON = icon("trash");
|
|
@@ -31734,6 +32006,7 @@ var BOLD_ICON = icon("bold");
|
|
|
31734
32006
|
var ITALIC_ICON = icon("italic");
|
|
31735
32007
|
var GRIP_ICON = icon("grip");
|
|
31736
32008
|
var KEBAB_ICON = icon("kebab");
|
|
32009
|
+
var CLONE_ICON = icon("clone");
|
|
31737
32010
|
var RESET_ICON = icon("reset");
|
|
31738
32011
|
var GEAR_ICON = icon("gear");
|
|
31739
32012
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -31742,6 +32015,12 @@ var BANDS_ICON = icon("bands");
|
|
|
31742
32015
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
31743
32016
|
var SONIC_ICON2 = icon("sonic");
|
|
31744
32017
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32018
|
+
function stripes(colors) {
|
|
32019
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32020
|
+
const step = 100 / bands.length;
|
|
32021
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32022
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32023
|
+
}
|
|
31745
32024
|
function lineIcon(width, style) {
|
|
31746
32025
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
31747
32026
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -32025,6 +32304,20 @@ var UserDrawingController = class {
|
|
|
32025
32304
|
// reported exactly when it actually changes).
|
|
32026
32305
|
this.textEditor = null;
|
|
32027
32306
|
this.painter = new DrawingPainter();
|
|
32307
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
32308
|
+
* shrinking selection does with it. */
|
|
32309
|
+
this.popupMulti = false;
|
|
32310
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
32311
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
32312
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
32313
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
32314
|
+
* whose bar just closed must stay selected. */
|
|
32315
|
+
this.onPopupDismissed = (e) => {
|
|
32316
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
32317
|
+
const pressed = this.interaction.pressedId();
|
|
32318
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
32319
|
+
this.clearSelection();
|
|
32320
|
+
};
|
|
32028
32321
|
this.ctx = canvas.getContext("2d");
|
|
32029
32322
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32030
32323
|
this.toolbar = new DrawingToolbar(
|
|
@@ -32201,6 +32494,12 @@ var UserDrawingController = class {
|
|
|
32201
32494
|
}
|
|
32202
32495
|
setSelection(ids) {
|
|
32203
32496
|
this.selectedIds = new Set(ids);
|
|
32497
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
32498
|
+
else if (this.popupMulti) {
|
|
32499
|
+
this.popupMulti = false;
|
|
32500
|
+
this.popup.close();
|
|
32501
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
32502
|
+
}
|
|
32204
32503
|
this.render();
|
|
32205
32504
|
}
|
|
32206
32505
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -32246,12 +32545,16 @@ var UserDrawingController = class {
|
|
|
32246
32545
|
});
|
|
32247
32546
|
}
|
|
32248
32547
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32249
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
32250
|
-
|
|
32548
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
32549
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
32550
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
32551
|
+
deleteAt(x, y, withSelection = false) {
|
|
32251
32552
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
32252
|
-
if (!hit
|
|
32553
|
+
if (!hit) return false;
|
|
32554
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
32555
|
+
if (ids.length === 0) return false;
|
|
32253
32556
|
this.popup.close();
|
|
32254
|
-
this.emit({ kind: "delete", ids
|
|
32557
|
+
this.emit({ kind: "delete", ids });
|
|
32255
32558
|
return true;
|
|
32256
32559
|
}
|
|
32257
32560
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -32277,7 +32580,14 @@ var UserDrawingController = class {
|
|
|
32277
32580
|
this.render();
|
|
32278
32581
|
}
|
|
32279
32582
|
}
|
|
32280
|
-
|
|
32583
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
32584
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
32585
|
+
beginMarqueeAt(x, y) {
|
|
32586
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
32587
|
+
this.render();
|
|
32588
|
+
return true;
|
|
32589
|
+
}
|
|
32590
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32281
32591
|
if (this.eraserMode) {
|
|
32282
32592
|
this.erasing = true;
|
|
32283
32593
|
this.deleteAt(x, y);
|
|
@@ -32297,9 +32607,9 @@ var UserDrawingController = class {
|
|
|
32297
32607
|
return;
|
|
32298
32608
|
}
|
|
32299
32609
|
}
|
|
32300
|
-
this.interaction.down(x, y, snap, shift2);
|
|
32610
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
32301
32611
|
}
|
|
32302
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
32612
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32303
32613
|
if (this.eraserMode) {
|
|
32304
32614
|
if (this.erasing) this.deleteAt(x, y);
|
|
32305
32615
|
return;
|
|
@@ -32311,12 +32621,14 @@ var UserDrawingController = class {
|
|
|
32311
32621
|
return;
|
|
32312
32622
|
}
|
|
32313
32623
|
this.interaction.move(x, y, snap, shift2);
|
|
32314
|
-
this.updateHover(x, y);
|
|
32624
|
+
this.updateHover(x, y, mod);
|
|
32315
32625
|
}
|
|
32316
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
32317
|
-
|
|
32626
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
32627
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
32628
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
32629
|
+
updateHover(x, y, mod = false) {
|
|
32318
32630
|
let id = null;
|
|
32319
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32631
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32320
32632
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
32321
32633
|
}
|
|
32322
32634
|
if (id !== this.hoveredId) {
|
|
@@ -32336,7 +32648,12 @@ var UserDrawingController = class {
|
|
|
32336
32648
|
this.render();
|
|
32337
32649
|
return;
|
|
32338
32650
|
}
|
|
32651
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
32339
32652
|
this.interaction.up(x, y);
|
|
32653
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
32654
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
32655
|
+
else this.openSettingsById(dragged, x, y);
|
|
32656
|
+
}
|
|
32340
32657
|
}
|
|
32341
32658
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
32342
32659
|
toggleMeasure() {
|
|
@@ -32601,7 +32918,7 @@ var UserDrawingController = class {
|
|
|
32601
32918
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
32602
32919
|
break;
|
|
32603
32920
|
case "delete": {
|
|
32604
|
-
const ids = this.selectedIds.size ? this.
|
|
32921
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
32605
32922
|
if (ids.length) {
|
|
32606
32923
|
this.popup.close();
|
|
32607
32924
|
this.emit({ kind: "delete", ids });
|
|
@@ -32617,6 +32934,9 @@ var UserDrawingController = class {
|
|
|
32617
32934
|
selectionIds() {
|
|
32618
32935
|
return [...this.selectedIds];
|
|
32619
32936
|
}
|
|
32937
|
+
deletableSelection() {
|
|
32938
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
32939
|
+
}
|
|
32620
32940
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
32621
32941
|
nudgeSelection(dx, dy) {
|
|
32622
32942
|
const proj = this.deps.projector();
|
|
@@ -32725,6 +33045,8 @@ var UserDrawingController = class {
|
|
|
32725
33045
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
32726
33046
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32727
33047
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33048
|
+
const clones = this.interaction.dragClones();
|
|
33049
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
32728
33050
|
this.layoutTextEditor();
|
|
32729
33051
|
const ghost = this.interaction.ghost();
|
|
32730
33052
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -32740,6 +33062,8 @@ var UserDrawingController = class {
|
|
|
32740
33062
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
32741
33063
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
32742
33064
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
33065
|
+
const marquee = this.interaction.marqueeRect();
|
|
33066
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
32743
33067
|
}
|
|
32744
33068
|
destroy() {
|
|
32745
33069
|
this.closeTextEditor();
|
|
@@ -32757,15 +33081,80 @@ var UserDrawingController = class {
|
|
|
32757
33081
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
32758
33082
|
this.render();
|
|
32759
33083
|
}
|
|
33084
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
33085
|
+
emitEdits(drawings) {
|
|
33086
|
+
const docs = drawings.map((d) => d.serialize());
|
|
33087
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
33088
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
33089
|
+
}
|
|
33090
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
33091
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
33092
|
+
openSettingsForSelection(ids) {
|
|
33093
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
33094
|
+
const drawings = live();
|
|
33095
|
+
if (drawings.length < 2) return;
|
|
33096
|
+
const proj = this.deps.projector();
|
|
33097
|
+
let anchor = null;
|
|
33098
|
+
for (const d of drawings) {
|
|
33099
|
+
const b = d.bounds(proj);
|
|
33100
|
+
if (!b) continue;
|
|
33101
|
+
if (!anchor) anchor = { ...b };
|
|
33102
|
+
else {
|
|
33103
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
33104
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
33105
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
33106
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
33107
|
+
anchor.w = right - anchor.x;
|
|
33108
|
+
anchor.h = bottom - anchor.y;
|
|
33109
|
+
}
|
|
33110
|
+
}
|
|
33111
|
+
this.popupMulti = true;
|
|
33112
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
33113
|
+
this.popup.open(drawings, anchor, {
|
|
33114
|
+
resolve: () => live()[0] ?? null,
|
|
33115
|
+
patch: (p) => {
|
|
33116
|
+
const ds = live();
|
|
33117
|
+
for (const d of ds) d.applySettings(p);
|
|
33118
|
+
this.render();
|
|
33119
|
+
this.emitEdits(ds);
|
|
33120
|
+
},
|
|
33121
|
+
setLocked: (v) => {
|
|
33122
|
+
const ds = live();
|
|
33123
|
+
for (const d of ds) d.locked = v;
|
|
33124
|
+
this.emitEdits(ds);
|
|
33125
|
+
},
|
|
33126
|
+
reorder: (to) => {
|
|
33127
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
33128
|
+
},
|
|
33129
|
+
duplicate: () => {
|
|
33130
|
+
this.popup.close();
|
|
33131
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
33132
|
+
},
|
|
33133
|
+
resetSettings: () => {
|
|
33134
|
+
const ds = live();
|
|
33135
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
33136
|
+
this.render();
|
|
33137
|
+
this.emitEdits(ds);
|
|
33138
|
+
this.openSettingsForSelection(ids);
|
|
33139
|
+
},
|
|
33140
|
+
remove: () => {
|
|
33141
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
33142
|
+
if (ids2.length === 0) return;
|
|
33143
|
+
this.popup.close();
|
|
33144
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
33145
|
+
}
|
|
33146
|
+
}, this.onPopupDismissed);
|
|
33147
|
+
}
|
|
32760
33148
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
32761
33149
|
openSettingsById(id, _x, _y) {
|
|
32762
33150
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
32763
33151
|
if (!drawing) return;
|
|
32764
33152
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
33153
|
+
this.popupMulti = false;
|
|
32765
33154
|
this.emit({ kind: "select", ids: [id] });
|
|
32766
33155
|
this.emit({ kind: "settings", id });
|
|
32767
33156
|
const anchor = drawing.bounds(this.deps.projector());
|
|
32768
|
-
this.popup.open(drawing, anchor, {
|
|
33157
|
+
this.popup.open([drawing], anchor, {
|
|
32769
33158
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
32770
33159
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
32771
33160
|
resolve: () => live() ?? null,
|
|
@@ -32783,6 +33172,11 @@ var UserDrawingController = class {
|
|
|
32783
33172
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32784
33173
|
},
|
|
32785
33174
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
33175
|
+
duplicate: () => {
|
|
33176
|
+
this.finishTextEditor(true);
|
|
33177
|
+
this.popup.close();
|
|
33178
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
33179
|
+
},
|
|
32786
33180
|
resetSettings: () => {
|
|
32787
33181
|
const d = live();
|
|
32788
33182
|
if (!d) return;
|
|
@@ -32805,7 +33199,13 @@ var UserDrawingController = class {
|
|
|
32805
33199
|
this.popup.close();
|
|
32806
33200
|
this.emit({ kind: "delete", ids: [id] });
|
|
32807
33201
|
}
|
|
32808
|
-
},
|
|
33202
|
+
}, this.onPopupDismissed);
|
|
33203
|
+
}
|
|
33204
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
33205
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
33206
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
33207
|
+
deselect() {
|
|
33208
|
+
this.clearSelection();
|
|
32809
33209
|
}
|
|
32810
33210
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
32811
33211
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -32831,6 +33231,13 @@ var UserDrawingController = class {
|
|
|
32831
33231
|
}, 0);
|
|
32832
33232
|
return;
|
|
32833
33233
|
}
|
|
33234
|
+
if (i.kind === "clone") {
|
|
33235
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
33236
|
+
this.intentCb?.(i);
|
|
33237
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
33238
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
33239
|
+
return;
|
|
33240
|
+
}
|
|
32834
33241
|
this.intentCb?.(i);
|
|
32835
33242
|
}
|
|
32836
33243
|
};
|
|
@@ -35006,7 +35413,10 @@ var NativeRenderer = class {
|
|
|
35006
35413
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35007
35414
|
fling: (v) => this.fling(v),
|
|
35008
35415
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35009
|
-
onClick: (x) =>
|
|
35416
|
+
onClick: (x) => {
|
|
35417
|
+
this.userDrawings?.deselect();
|
|
35418
|
+
this.handleClick(x);
|
|
35419
|
+
},
|
|
35010
35420
|
onAxisLongPress: (axis, x, y) => {
|
|
35011
35421
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
35012
35422
|
},
|
|
@@ -35024,11 +35434,13 @@ var NativeRenderer = class {
|
|
|
35024
35434
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
35025
35435
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
35026
35436
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
35027
|
-
|
|
35437
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
35438
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
35439
|
+
// middle-click: a selected hit takes the whole selection
|
|
35028
35440
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
35029
35441
|
drawingsSnapMode: () => this.snapMode,
|
|
35030
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
35031
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
35442
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35443
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35032
35444
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35033
35445
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
35034
35446
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -37362,6 +37774,10 @@ function str3(inputs, key, fallback) {
|
|
|
37362
37774
|
const v = inputs[key];
|
|
37363
37775
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
37364
37776
|
}
|
|
37777
|
+
function bool(inputs, key, fallback) {
|
|
37778
|
+
const v = inputs[key];
|
|
37779
|
+
return typeof v === "boolean" ? v : fallback;
|
|
37780
|
+
}
|
|
37365
37781
|
function toPoints(bars, values, colors) {
|
|
37366
37782
|
const pts = new Array(bars.length);
|
|
37367
37783
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -38893,10 +39309,6 @@ var aroon = {
|
|
|
38893
39309
|
plots: [
|
|
38894
39310
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
38895
39311
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
38896
|
-
],
|
|
38897
|
-
levels: [
|
|
38898
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
38899
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
38900
39312
|
]
|
|
38901
39313
|
};
|
|
38902
39314
|
}
|
|
@@ -39394,8 +39806,10 @@ var volumeSpecs = [
|
|
|
39394
39806
|
var DAY_MS2 = 864e5;
|
|
39395
39807
|
function periodKey(anchor, time) {
|
|
39396
39808
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
39397
|
-
if (anchor === "Month") {
|
|
39809
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
39398
39810
|
const d = new Date(time);
|
|
39811
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
39812
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
39399
39813
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
39400
39814
|
}
|
|
39401
39815
|
return Math.floor(time / DAY_MS2);
|
|
@@ -39447,40 +39861,86 @@ var parabolicSar = {
|
|
|
39447
39861
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
39448
39862
|
}
|
|
39449
39863
|
};
|
|
39864
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
39865
|
+
var VWAP_STYLE = "Style";
|
|
39866
|
+
var VWAP_BANDS = [
|
|
39867
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
39868
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
39869
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
39870
|
+
];
|
|
39450
39871
|
var vwap = {
|
|
39451
39872
|
type: "vwap",
|
|
39452
39873
|
title: "Volume Weighted Average Price",
|
|
39453
39874
|
shortTitle: "VWAP",
|
|
39454
39875
|
overlay: true,
|
|
39455
39876
|
inputs: [
|
|
39456
|
-
{ key: "anchor", title: "
|
|
39457
|
-
|
|
39458
|
-
|
|
39877
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
39878
|
+
sourceInput("HLC3"),
|
|
39879
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
39880
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39881
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
39882
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
39883
|
+
]),
|
|
39884
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
39885
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
39886
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39887
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
39888
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
39889
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
39890
|
+
])
|
|
39459
39891
|
],
|
|
39460
39892
|
compute: (bars, inputs) => {
|
|
39461
39893
|
const anchor = str3(inputs, "anchor", "Day");
|
|
39462
|
-
const
|
|
39463
|
-
const
|
|
39894
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
39895
|
+
const n = bars.length;
|
|
39896
|
+
const values = new Array(n).fill(Number.NaN);
|
|
39897
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
39898
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
39899
|
+
color: str3(inputs, b.color, b.defColor),
|
|
39900
|
+
fill: bool(inputs, b.fill, true),
|
|
39901
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
39902
|
+
up: new Array(n).fill(Number.NaN),
|
|
39903
|
+
down: new Array(n).fill(Number.NaN)
|
|
39904
|
+
}));
|
|
39464
39905
|
let period = Number.NaN;
|
|
39465
39906
|
let cumPV = 0;
|
|
39907
|
+
let cumPV2 = 0;
|
|
39466
39908
|
let cumV = 0;
|
|
39467
|
-
for (let i = 0; i <
|
|
39909
|
+
for (let i = 0; i < n; i++) {
|
|
39468
39910
|
const b = bars[i];
|
|
39469
39911
|
const key = periodKey(anchor, b.time);
|
|
39470
39912
|
if (key !== period) {
|
|
39471
39913
|
period = key;
|
|
39472
39914
|
cumPV = 0;
|
|
39915
|
+
cumPV2 = 0;
|
|
39473
39916
|
cumV = 0;
|
|
39474
39917
|
}
|
|
39475
39918
|
const v = b.volume;
|
|
39476
39919
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
39477
|
-
const tp =
|
|
39920
|
+
const tp = src[i];
|
|
39478
39921
|
cumPV += tp * v;
|
|
39922
|
+
cumPV2 += tp * tp * v;
|
|
39479
39923
|
cumV += v;
|
|
39480
39924
|
}
|
|
39481
|
-
if (cumV
|
|
39482
|
-
|
|
39483
|
-
|
|
39925
|
+
if (cumV <= 0) continue;
|
|
39926
|
+
const mean = cumPV / cumV;
|
|
39927
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
39928
|
+
values[i] = mean;
|
|
39929
|
+
for (const band of bands) {
|
|
39930
|
+
band.up[i] = mean + band.mult * sd;
|
|
39931
|
+
band.down[i] = mean - band.mult * sd;
|
|
39932
|
+
}
|
|
39933
|
+
}
|
|
39934
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
39935
|
+
const fills = [];
|
|
39936
|
+
bands.forEach((band, i) => {
|
|
39937
|
+
plots.push(
|
|
39938
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
39939
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
39940
|
+
);
|
|
39941
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
39942
|
+
});
|
|
39943
|
+
return { plots, bands: fills };
|
|
39484
39944
|
}
|
|
39485
39945
|
};
|
|
39486
39946
|
var pivotPoints = {
|
|
@@ -39994,6 +40454,24 @@ function resolveElement(container) {
|
|
|
39994
40454
|
function segmentVisibility(parts, chartHidden = false) {
|
|
39995
40455
|
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
39996
40456
|
}
|
|
40457
|
+
var STATUSLINE_LADDER = [
|
|
40458
|
+
{ stacked: false, level: "full" },
|
|
40459
|
+
{ stacked: true, level: "full" },
|
|
40460
|
+
{ stacked: true, level: "compact" },
|
|
40461
|
+
{ stacked: true, level: "minimal" }
|
|
40462
|
+
];
|
|
40463
|
+
function readoutCells(level, readout) {
|
|
40464
|
+
if (readout === "value") return [{ key: "close", label: "" }];
|
|
40465
|
+
if (level === "full") {
|
|
40466
|
+
return [
|
|
40467
|
+
{ key: "open", label: "O" },
|
|
40468
|
+
{ key: "high", label: "H" },
|
|
40469
|
+
{ key: "low", label: "L" },
|
|
40470
|
+
{ key: "close", label: "C" }
|
|
40471
|
+
];
|
|
40472
|
+
}
|
|
40473
|
+
return [{ key: "close", label: level === "compact" ? "C" : "" }];
|
|
40474
|
+
}
|
|
39997
40475
|
function statuslineMenuItems(parts, chartVisible) {
|
|
39998
40476
|
return [
|
|
39999
40477
|
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
@@ -40022,6 +40500,11 @@ function fmtChange(open2, close) {
|
|
|
40022
40500
|
const sign = diff >= 0 ? "+" : "";
|
|
40023
40501
|
return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
|
|
40024
40502
|
}
|
|
40503
|
+
function fmtChangePct(open2, close) {
|
|
40504
|
+
if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
|
|
40505
|
+
const pct = (close - open2) / open2 * 100;
|
|
40506
|
+
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
|
40507
|
+
}
|
|
40025
40508
|
|
|
40026
40509
|
// src/widget/statusline.ts
|
|
40027
40510
|
var STYLE_ID24 = "vela-widget-statusline";
|
|
@@ -40038,6 +40521,11 @@ var CSS21 = `
|
|
|
40038
40521
|
display: flex;
|
|
40039
40522
|
align-items: baseline;
|
|
40040
40523
|
gap: var(--vela-space-2);
|
|
40524
|
+
/* The chip never grows past the plot: fit() measures overflow against this width to
|
|
40525
|
+
* walk the layout ladder (stack, then shed values), so overflow:hidden only guards
|
|
40526
|
+
* the transient between a resize and the next measure. */
|
|
40527
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40528
|
+
overflow: hidden;
|
|
40041
40529
|
color: var(--vela-fg);
|
|
40042
40530
|
font-size: var(--vela-font-size-md);
|
|
40043
40531
|
/* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
|
|
@@ -40059,6 +40547,31 @@ var CSS21 = `
|
|
|
40059
40547
|
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
40060
40548
|
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
40061
40549
|
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
40550
|
+
/* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
|
|
40551
|
+
* value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
|
|
40552
|
+
* side by side on one line; the STACKED layouts (fit() decides, see the ladder in
|
|
40553
|
+
* statusline-model) turn the chip into a column so the values drop under the symbol. */
|
|
40554
|
+
.vela-statusline .vela-sl-row {
|
|
40555
|
+
display: flex;
|
|
40556
|
+
align-items: baseline;
|
|
40557
|
+
gap: var(--vela-space-2);
|
|
40558
|
+
white-space: nowrap;
|
|
40559
|
+
}
|
|
40560
|
+
.vela-statusline.vela-sl-stacked {
|
|
40561
|
+
flex-direction: column;
|
|
40562
|
+
align-items: flex-start;
|
|
40563
|
+
/* The same air between the two rows as between the values row and the legend row
|
|
40564
|
+
* that follows the chip (see the stacked legend shift below). */
|
|
40565
|
+
row-gap: var(--vela-space-1);
|
|
40566
|
+
}
|
|
40567
|
+
/* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
|
|
40568
|
+
* never does, so its overflow is what fit() measures to keep descending the ladder. */
|
|
40569
|
+
.vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
|
|
40570
|
+
.vela-statusline.vela-sl-stacked .vela-sl-meta {
|
|
40571
|
+
min-width: 0;
|
|
40572
|
+
overflow: hidden;
|
|
40573
|
+
text-overflow: ellipsis;
|
|
40574
|
+
}
|
|
40062
40575
|
.vela-statusline .vela-sl-avatar {
|
|
40063
40576
|
width: 18px;
|
|
40064
40577
|
height: 18px;
|
|
@@ -40110,59 +40623,13 @@ var CSS21 = `
|
|
|
40110
40623
|
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
40111
40624
|
* chart on the page, including statusline-less ones. */
|
|
40112
40625
|
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40113
|
-
/*
|
|
40114
|
-
*
|
|
40115
|
-
|
|
40116
|
-
|
|
40117
|
-
*
|
|
40118
|
-
|
|
40119
|
-
[data-layout='mobile'] .vela-statusline {
|
|
40120
|
-
display: grid;
|
|
40121
|
-
grid-template-columns: auto auto minmax(0, auto) auto;
|
|
40122
|
-
justify-content: start;
|
|
40123
|
-
align-items: center;
|
|
40124
|
-
row-gap: 1px;
|
|
40125
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40126
|
-
font-size: var(--vela-font-size-sm);
|
|
40127
|
-
}
|
|
40128
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-symbol {
|
|
40129
|
-
font-size: var(--vela-font-size-md);
|
|
40130
|
-
line-height: 1;
|
|
40131
|
-
}
|
|
40132
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-meta {
|
|
40133
|
-
min-width: 0;
|
|
40134
|
-
overflow: hidden;
|
|
40135
|
-
text-overflow: ellipsis;
|
|
40136
|
-
white-space: nowrap;
|
|
40137
|
-
line-height: 1;
|
|
40138
|
-
}
|
|
40139
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-avatar,
|
|
40140
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-market { align-self: center; }
|
|
40141
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-ohlc { display: none !important; }
|
|
40142
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-change {
|
|
40143
|
-
/* Second row, under the text column \u2014 the avatar keeps the first column. */
|
|
40144
|
-
grid-column: 2 / -1;
|
|
40145
|
-
font-size: var(--vela-font-size-sm);
|
|
40146
|
-
line-height: 1.2;
|
|
40147
|
-
}
|
|
40148
|
-
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40149
|
-
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
40150
|
-
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
40151
|
-
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
40152
|
-
* Placed after the mobile block on purpose: same specificity, later wins. */
|
|
40153
|
-
.vela-statusline.vela-sl-fit {
|
|
40154
|
-
display: flex; /* undo the mobile grid \u2014 fit mode is one flex row again */
|
|
40155
|
-
flex-wrap: nowrap;
|
|
40156
|
-
align-items: center;
|
|
40157
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40158
|
-
overflow: hidden;
|
|
40159
|
-
}
|
|
40160
|
-
.vela-statusline.vela-sl-fit .vela-sl-change {
|
|
40161
|
-
flex-basis: auto;
|
|
40162
|
-
padding-left: 0;
|
|
40163
|
-
}
|
|
40164
|
-
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
40165
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40626
|
+
/* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
|
|
40627
|
+
* host whenever the ladder lands on a stacked layout. */
|
|
40628
|
+
.vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40629
|
+
/* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
|
|
40630
|
+
* of the readout fits, exactly as on a narrow desktop chart. */
|
|
40631
|
+
[data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
|
|
40632
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
|
|
40166
40633
|
`;
|
|
40167
40634
|
function baseOfTicker(ticker) {
|
|
40168
40635
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -40233,6 +40700,8 @@ var Statusline = class {
|
|
|
40233
40700
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
40234
40701
|
this.fitMode = false;
|
|
40235
40702
|
this.fitRO = null;
|
|
40703
|
+
/** The ladder rung currently applied — see {@link fit}. */
|
|
40704
|
+
this.layout = { stacked: false, level: "full" };
|
|
40236
40705
|
this.onContextMenu = (e) => {
|
|
40237
40706
|
if (!this.menu || !this.menuHooks) return;
|
|
40238
40707
|
e.preventDefault();
|
|
@@ -40279,8 +40748,18 @@ var Statusline = class {
|
|
|
40279
40748
|
this.menuHooks?.setChartVisible(true);
|
|
40280
40749
|
this.setChartHidden(false);
|
|
40281
40750
|
});
|
|
40282
|
-
this.
|
|
40751
|
+
this.identityRow = doc.createElement("span");
|
|
40752
|
+
this.identityRow.className = "vela-sl-row vela-sl-identity";
|
|
40753
|
+
this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
|
|
40754
|
+
this.valuesRow = doc.createElement("span");
|
|
40755
|
+
this.valuesRow.className = "vela-sl-row vela-sl-values";
|
|
40756
|
+
this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
|
|
40757
|
+
this.el.append(this.identityRow, this.valuesRow);
|
|
40283
40758
|
host.appendChild(this.el);
|
|
40759
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40760
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40761
|
+
this.fitRO.observe(host);
|
|
40762
|
+
}
|
|
40284
40763
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
40285
40764
|
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
40286
40765
|
this.setMarketStatus("open");
|
|
@@ -40296,27 +40775,15 @@ var Statusline = class {
|
|
|
40296
40775
|
this.fit();
|
|
40297
40776
|
}
|
|
40298
40777
|
/**
|
|
40299
|
-
* Multi-chart cells:
|
|
40300
|
-
*
|
|
40301
|
-
*
|
|
40302
|
-
* market badge; the logo + ticker always stay.
|
|
40778
|
+
* Multi-chart cells: the same width ladder as a single chart, plus a last resort for
|
|
40779
|
+
* cells too narrow for even its bottom rung — whatever still overflows is hidden
|
|
40780
|
+
* outright rather than clipped mid-glyph, least important first: the values, the
|
|
40781
|
+
* venue/timeframe meta, then the market badge; the logo + ticker always stay.
|
|
40303
40782
|
*/
|
|
40304
40783
|
setFitMode(on) {
|
|
40305
40784
|
if (on === this.fitMode) return;
|
|
40306
40785
|
this.fitMode = on;
|
|
40307
|
-
this.
|
|
40308
|
-
this.host.classList.toggle("vela-sl-fit-host", on);
|
|
40309
|
-
if (on) {
|
|
40310
|
-
if (typeof ResizeObserver !== "undefined" && !this.fitRO) {
|
|
40311
|
-
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40312
|
-
this.fitRO.observe(this.host);
|
|
40313
|
-
}
|
|
40314
|
-
this.fit();
|
|
40315
|
-
} else {
|
|
40316
|
-
this.fitRO?.disconnect();
|
|
40317
|
-
this.fitRO = null;
|
|
40318
|
-
this.syncParts();
|
|
40319
|
-
}
|
|
40786
|
+
this.fit();
|
|
40320
40787
|
}
|
|
40321
40788
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
40322
40789
|
syncParts() {
|
|
@@ -40328,22 +40795,42 @@ var Statusline = class {
|
|
|
40328
40795
|
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
40329
40796
|
this.changeEl.style.display = seg.change ? "" : "none";
|
|
40330
40797
|
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
40798
|
+
this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
|
|
40799
|
+
}
|
|
40800
|
+
/** Apply one ladder rung: the stacked/inline shape and the readout's level. */
|
|
40801
|
+
applyLayout(layout) {
|
|
40802
|
+
this.layout = layout;
|
|
40803
|
+
this.el.classList.toggle("vela-sl-stacked", layout.stacked);
|
|
40804
|
+
this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
|
|
40805
|
+
this.renderValues();
|
|
40331
40806
|
}
|
|
40332
|
-
/**
|
|
40807
|
+
/**
|
|
40808
|
+
* Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
|
|
40809
|
+
* max-width: the full one-line readout, then the values stacked under the symbol
|
|
40810
|
+
* line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
|
|
40811
|
+
* still overflows, whole segments hide, least important first. Without layout
|
|
40812
|
+
* (detached host, node tests) nothing overflows, so the widest rung stays.
|
|
40813
|
+
*/
|
|
40333
40814
|
fit() {
|
|
40334
|
-
if (!this.fitMode) return;
|
|
40335
40815
|
this.syncParts();
|
|
40336
40816
|
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
40817
|
+
const overflows = () => this.el.scrollWidth > this.el.clientWidth;
|
|
40818
|
+
const hasValues = seg.ohlc || seg.change;
|
|
40819
|
+
for (const rung of STATUSLINE_LADDER) {
|
|
40820
|
+
this.applyLayout(rung);
|
|
40821
|
+
if (!hasValues || !overflows()) break;
|
|
40822
|
+
}
|
|
40823
|
+
if (!this.fitMode) return;
|
|
40337
40824
|
const order = [
|
|
40338
|
-
[this.
|
|
40339
|
-
[this.changeEl, seg.change],
|
|
40825
|
+
[this.valuesRow, hasValues],
|
|
40340
40826
|
[this.metaEl, seg.meta],
|
|
40341
40827
|
[this.marketEl, seg.market]
|
|
40342
40828
|
];
|
|
40343
40829
|
for (const [el, shown] of order) {
|
|
40344
|
-
if (
|
|
40830
|
+
if (!overflows()) break;
|
|
40345
40831
|
if (shown) el.style.display = "none";
|
|
40346
40832
|
}
|
|
40833
|
+
if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
|
|
40347
40834
|
}
|
|
40348
40835
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
40349
40836
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -40456,7 +40943,7 @@ var Statusline = class {
|
|
|
40456
40943
|
this.marketTip.destroy();
|
|
40457
40944
|
this.eyeTip.destroy();
|
|
40458
40945
|
this.marketBubble.destroy();
|
|
40459
|
-
this.host.classList.remove("vela-has-statusline", "vela-sl-
|
|
40946
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
|
|
40460
40947
|
this.el.remove();
|
|
40461
40948
|
}
|
|
40462
40949
|
detach() {
|
|
@@ -40465,6 +40952,10 @@ var Statusline = class {
|
|
|
40465
40952
|
}
|
|
40466
40953
|
render() {
|
|
40467
40954
|
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
40955
|
+
this.fit();
|
|
40956
|
+
}
|
|
40957
|
+
/** Write the value readout for the current bar at the current ladder level. */
|
|
40958
|
+
renderValues() {
|
|
40468
40959
|
const bar = this.hoverBar ?? this.lastBar;
|
|
40469
40960
|
if (!bar) {
|
|
40470
40961
|
this.ohlcEl.replaceChildren();
|
|
@@ -40484,12 +40975,11 @@ var Statusline = class {
|
|
|
40484
40975
|
s.appendChild(b);
|
|
40485
40976
|
return s;
|
|
40486
40977
|
};
|
|
40487
|
-
|
|
40488
|
-
|
|
40489
|
-
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
40978
|
+
const { level } = this.layout;
|
|
40979
|
+
this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
|
|
40980
|
+
this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
|
|
40490
40981
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
40491
40982
|
this.changeEl.style.color = ink;
|
|
40492
|
-
this.fit();
|
|
40493
40983
|
}
|
|
40494
40984
|
};
|
|
40495
40985
|
|
|
@@ -41257,6 +41747,11 @@ function cellDrawings(opt) {
|
|
|
41257
41747
|
if (opt === true || opt == null) return { toolbar: false };
|
|
41258
41748
|
return { ...opt, toolbar: false };
|
|
41259
41749
|
}
|
|
41750
|
+
function nativeLedgerEntry(handle) {
|
|
41751
|
+
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
41752
|
+
const hidden = !handle.visible;
|
|
41753
|
+
return inputs || hidden ? { type: handle.nativeType, ...inputs ? { inputs } : {}, ...hidden ? { hidden: true } : {} } : handle.nativeType;
|
|
41754
|
+
}
|
|
41260
41755
|
function instanceDeltas(handle) {
|
|
41261
41756
|
if (!handle) return void 0;
|
|
41262
41757
|
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
@@ -41379,10 +41874,13 @@ var ChartCell = class {
|
|
|
41379
41874
|
if (seed.rendererConfig != null) this.inner.renderer.applyConfig(seed.rendererConfig);
|
|
41380
41875
|
if (seed.drawings != null) this.inner.drawings.fromJSON(seed.drawings);
|
|
41381
41876
|
if (seed.indicators) {
|
|
41382
|
-
for (const
|
|
41877
|
+
for (const entry of seed.indicators.natives) {
|
|
41878
|
+
const handle = this.inner.addNativeIndicator(ledgerNativeType(entry));
|
|
41879
|
+
this.applyNativeLedgerEntry(handle, entry);
|
|
41880
|
+
}
|
|
41383
41881
|
this.pendingManifestNames = [...seed.indicators.manifest];
|
|
41384
41882
|
}
|
|
41385
|
-
this.volumeIntent = seed.indicators ? seed.indicators.natives.
|
|
41883
|
+
this.volumeIntent = seed.indicators ? seed.indicators.natives.some((e) => ledgerNativeType(e) === "volume") : deps.volume;
|
|
41386
41884
|
this.extState = { ...seed.ext ?? {} };
|
|
41387
41885
|
this.inner.on("load:end", () => {
|
|
41388
41886
|
this.volumeMayBePending = false;
|
|
@@ -41454,6 +41952,7 @@ var ChartCell = class {
|
|
|
41454
41952
|
this.refreshNativeCatalog();
|
|
41455
41953
|
});
|
|
41456
41954
|
this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
|
|
41955
|
+
this.inner.on("indicator:visibility", () => this.deps.onStateDirty());
|
|
41457
41956
|
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
41458
41957
|
if (this.destroyed) return;
|
|
41459
41958
|
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
@@ -41898,7 +42397,7 @@ var ChartCell = class {
|
|
|
41898
42397
|
if (list.length === 0) return;
|
|
41899
42398
|
for (const led of this.pendingManifestNames) {
|
|
41900
42399
|
const entry = list.find((e) => e.name === ledgerEntryName(led));
|
|
41901
|
-
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
|
|
42400
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props, hidden: led.hidden } : {} });
|
|
41902
42401
|
}
|
|
41903
42402
|
this.pendingManifestNames = null;
|
|
41904
42403
|
return;
|
|
@@ -41917,23 +42416,35 @@ var ChartCell = class {
|
|
|
41917
42416
|
applyIndicatorLedger(led) {
|
|
41918
42417
|
const chart = this.inner;
|
|
41919
42418
|
if (!chart) return;
|
|
41920
|
-
this.volumeIntent = led.natives.
|
|
42419
|
+
this.volumeIntent = led.natives.some((e) => ledgerNativeType(e) === "volume");
|
|
41921
42420
|
this.history.silently(() => {
|
|
41922
42421
|
const owed = /* @__PURE__ */ new Map();
|
|
41923
|
-
for (const
|
|
42422
|
+
for (const e of led.natives) {
|
|
42423
|
+
const type = ledgerNativeType(e);
|
|
42424
|
+
const queue = owed.get(type);
|
|
42425
|
+
if (queue) queue.push(e);
|
|
42426
|
+
else owed.set(type, [e]);
|
|
42427
|
+
}
|
|
41924
42428
|
for (const h of this.nativeHandles()) {
|
|
41925
|
-
const
|
|
41926
|
-
|
|
41927
|
-
if (n > 0) owed.set(type, n - 1);
|
|
42429
|
+
const entry = owed.get(h.nativeType)?.shift();
|
|
42430
|
+
if (entry !== void 0) this.applyNativeLedgerEntry(h, entry);
|
|
41928
42431
|
else h.remove();
|
|
41929
42432
|
}
|
|
41930
|
-
for (const
|
|
42433
|
+
for (const queue of owed.values()) {
|
|
42434
|
+
for (const e of queue) {
|
|
42435
|
+
const h = chart.addNativeIndicator(ledgerNativeType(e));
|
|
42436
|
+
this.applyNativeLedgerEntry(h, e);
|
|
42437
|
+
}
|
|
42438
|
+
}
|
|
41931
42439
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
41932
42440
|
if (this.manifest.length > 0) {
|
|
41933
42441
|
for (const item of led.manifest) {
|
|
41934
42442
|
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
41935
42443
|
if (entry)
|
|
41936
|
-
this.addManifestInstance(entry, {
|
|
42444
|
+
this.addManifestInstance(entry, {
|
|
42445
|
+
record: false,
|
|
42446
|
+
...typeof item === "object" ? { inputs: item.inputs, props: item.props, hidden: item.hidden } : {}
|
|
42447
|
+
});
|
|
41937
42448
|
}
|
|
41938
42449
|
this.pendingManifestNames = null;
|
|
41939
42450
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -42000,6 +42511,7 @@ var ChartCell = class {
|
|
|
42000
42511
|
if (this.destroyed) return;
|
|
42001
42512
|
const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
|
|
42002
42513
|
const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
|
|
42514
|
+
if (opts.hidden) it.handle?.setVisible(false);
|
|
42003
42515
|
this.instances.push(it);
|
|
42004
42516
|
this.deps.onIndicatorsChanged(this.id);
|
|
42005
42517
|
if (opts.record === false) return;
|
|
@@ -42043,6 +42555,29 @@ var ChartCell = class {
|
|
|
42043
42555
|
/** Add a native indicator. A multi-instance type gets a fresh instance every time; a
|
|
42044
42556
|
* single-instance type already on the chart hands back its existing one — nothing
|
|
42045
42557
|
* changed, so nothing enters the undo timeline. */
|
|
42558
|
+
/**
|
|
42559
|
+
* Converge one native handle to a restored ledger entry: every schema key gets the
|
|
42560
|
+
* entry's value or its declaration default (the DOCUMENT is the truth — a live
|
|
42561
|
+
* tweak must not survive a restore that says otherwise), and visibility follows
|
|
42562
|
+
* the `hidden` flag. Skips the input write when nothing differs — `setInputs`
|
|
42563
|
+
* re-runs the indicator, and a no-op restore must not flicker every native.
|
|
42564
|
+
*/
|
|
42565
|
+
applyNativeLedgerEntry(handle, entry) {
|
|
42566
|
+
const wanted = typeof entry === "string" ? void 0 : entry.inputs;
|
|
42567
|
+
if (handle.inputs.length > 0) {
|
|
42568
|
+
const current = handle.inputValues();
|
|
42569
|
+
const next = {};
|
|
42570
|
+
for (const schema of handle.inputs) {
|
|
42571
|
+
const target = wanted?.[schema.key] ?? schema.defval;
|
|
42572
|
+
if (JSON.stringify(current[schema.key]) !== JSON.stringify(target)) next[schema.key] = target;
|
|
42573
|
+
}
|
|
42574
|
+
if (Object.keys(next).length > 0) handle.setInputs(next);
|
|
42575
|
+
} else if (wanted) {
|
|
42576
|
+
handle.setInputs(wanted);
|
|
42577
|
+
}
|
|
42578
|
+
const visible = !(typeof entry === "object" && entry.hidden);
|
|
42579
|
+
if (handle.visible !== visible) handle.setVisible(visible);
|
|
42580
|
+
}
|
|
42046
42581
|
addNative(type) {
|
|
42047
42582
|
const chart = this.inner;
|
|
42048
42583
|
if (!chart) return;
|
|
@@ -42206,10 +42741,13 @@ var ChartCell = class {
|
|
|
42206
42741
|
// (`ctx.addIndicator`) stay out: their names would never resolve against the
|
|
42207
42742
|
// manifest — their plugin persists them via the `ext` seam instead.
|
|
42208
42743
|
indicators: indicatorLedger({
|
|
42209
|
-
|
|
42744
|
+
// Live handles carry what the registry read cannot: the input DELTAS and
|
|
42745
|
+
// the hidden flag, so native settings/visibility survive the round-trip.
|
|
42746
|
+
present: this.inner ? this.nativeHandles().map((h) => nativeLedgerEntry(h)) : [],
|
|
42210
42747
|
instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
|
|
42211
42748
|
const d = it.handle ? instanceDeltas(it.handle) : it.values;
|
|
42212
|
-
|
|
42749
|
+
const hidden = it.handle ? !it.handle.visible : false;
|
|
42750
|
+
return d || hidden ? { name: it.entry.name, ...d ?? {}, ...hidden ? { hidden: true } : {} } : it.entry.name;
|
|
42213
42751
|
}),
|
|
42214
42752
|
pendingManifest: this.pendingManifestNames,
|
|
42215
42753
|
manifestSettled: this.deps.manifestSettled(),
|
|
@@ -42629,6 +43167,17 @@ function nextAutoCellId(taken) {
|
|
|
42629
43167
|
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
42630
43168
|
}
|
|
42631
43169
|
}
|
|
43170
|
+
function resolveTyping(key, open2) {
|
|
43171
|
+
if (open2.dialogs > 0) {
|
|
43172
|
+
if (key.length !== 1) return null;
|
|
43173
|
+
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
43174
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
43175
|
+
return null;
|
|
43176
|
+
}
|
|
43177
|
+
if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
43178
|
+
if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
43179
|
+
return null;
|
|
43180
|
+
}
|
|
42632
43181
|
var VelaWorkspace = class {
|
|
42633
43182
|
constructor(container, opts = {}) {
|
|
42634
43183
|
this.events = new TypedEventBus();
|
|
@@ -44262,17 +44811,14 @@ var VelaWorkspace = class {
|
|
|
44262
44811
|
}
|
|
44263
44812
|
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
44264
44813
|
routeTyping(ev) {
|
|
44265
|
-
if (this.destroyed
|
|
44814
|
+
if (this.destroyed) return;
|
|
44266
44815
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
44267
44816
|
if (isEditableTarget(ev)) return;
|
|
44268
|
-
const
|
|
44269
|
-
if (
|
|
44270
|
-
|
|
44271
|
-
|
|
44272
|
-
|
|
44273
|
-
ev.preventDefault();
|
|
44274
|
-
this.tfQuick.open(key);
|
|
44275
|
-
}
|
|
44817
|
+
const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
|
|
44818
|
+
if (!hit) return;
|
|
44819
|
+
ev.preventDefault();
|
|
44820
|
+
if (hit.target === "symbol") this.symbolPicker.type(hit.text);
|
|
44821
|
+
else this.tfQuick.type(hit.text);
|
|
44276
44822
|
}
|
|
44277
44823
|
trackDialog(open2) {
|
|
44278
44824
|
this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
|