@luxalgo/vela 0.6.17 → 0.6.18
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-BXF7VNNE.js} +177 -104
- 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-D91cU2fF.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
- package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
- 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 +739 -252
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +739 -252
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -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;
|
|
@@ -19844,7 +19885,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19844
19885
|
if (!model) continue;
|
|
19845
19886
|
const paneId = model.paneId ?? "price";
|
|
19846
19887
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19847
|
-
byPane.get(paneId).push({
|
|
19888
|
+
byPane.get(paneId).push({
|
|
19889
|
+
id: r.id,
|
|
19890
|
+
title: model.title || r.title,
|
|
19891
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
19892
|
+
ownScale: model.ownScale === true
|
|
19893
|
+
});
|
|
19848
19894
|
}
|
|
19849
19895
|
const panes = [];
|
|
19850
19896
|
for (const paneId of this.paneOrder) {
|
|
@@ -24477,7 +24523,7 @@ var InputController = class {
|
|
|
24477
24523
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24478
24524
|
if (Number.isNaN(this.cursorX)) return;
|
|
24479
24525
|
if (this.dragging && this.region !== "drawing") return;
|
|
24480
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24526
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24481
24527
|
};
|
|
24482
24528
|
this.onDown = (e) => {
|
|
24483
24529
|
if (e.button === 1) {
|
|
@@ -24509,7 +24555,7 @@ var InputController = class {
|
|
|
24509
24555
|
this.startY = y;
|
|
24510
24556
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24511
24557
|
this.region = "drawing";
|
|
24512
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24558
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24513
24559
|
this.capture(e.pointerId);
|
|
24514
24560
|
return;
|
|
24515
24561
|
}
|
|
@@ -24518,6 +24564,11 @@ var InputController = class {
|
|
|
24518
24564
|
this.capture(e.pointerId);
|
|
24519
24565
|
return;
|
|
24520
24566
|
}
|
|
24567
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24568
|
+
this.region = "drawing";
|
|
24569
|
+
this.capture(e.pointerId);
|
|
24570
|
+
return;
|
|
24571
|
+
}
|
|
24521
24572
|
const vp = this.deps.getCoords().getViewport();
|
|
24522
24573
|
this.deps.apply(vp);
|
|
24523
24574
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24566,7 +24617,7 @@ var InputController = class {
|
|
|
24566
24617
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24567
24618
|
} else if (this.region === "drawing") {
|
|
24568
24619
|
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);
|
|
24620
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24570
24621
|
} else {
|
|
24571
24622
|
const coords = this.deps.getCoords();
|
|
24572
24623
|
const vp = coords.getViewport();
|
|
@@ -24589,7 +24640,7 @@ var InputController = class {
|
|
|
24589
24640
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24590
24641
|
const r = this.regionAt(x, y);
|
|
24591
24642
|
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);
|
|
24643
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24593
24644
|
}
|
|
24594
24645
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24595
24646
|
};
|
|
@@ -24930,12 +24981,12 @@ function defaultTradeMarkersState() {
|
|
|
24930
24981
|
function mergeTradeMarkersState(base, patch) {
|
|
24931
24982
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
24932
24983
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
24933
|
-
const
|
|
24984
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
24934
24985
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
24935
24986
|
return {
|
|
24936
|
-
visible:
|
|
24937
|
-
labels:
|
|
24938
|
-
qty:
|
|
24987
|
+
visible: bool2(p.visible, base.visible),
|
|
24988
|
+
labels: bool2(p.labels, base.labels),
|
|
24989
|
+
qty: bool2(p.qty, base.qty),
|
|
24939
24990
|
colors: {
|
|
24940
24991
|
long: color(c.long, base.colors.long),
|
|
24941
24992
|
short: color(c.short, base.colors.short),
|
|
@@ -28795,6 +28846,18 @@ var DrawingPainter = class {
|
|
|
28795
28846
|
}
|
|
28796
28847
|
this.paintHandles(ctx, pts);
|
|
28797
28848
|
}
|
|
28849
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
28850
|
+
* accent under a thin dashed outline. */
|
|
28851
|
+
paintMarquee(ctx, rect) {
|
|
28852
|
+
ctx.save();
|
|
28853
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
28854
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
28855
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
28856
|
+
ctx.lineWidth = 1;
|
|
28857
|
+
ctx.setLineDash([4, 3]);
|
|
28858
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
28859
|
+
ctx.restore();
|
|
28860
|
+
}
|
|
28798
28861
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
28799
28862
|
paintSnapRing(ctx, x, y, theme) {
|
|
28800
28863
|
ctx.setLineDash([]);
|
|
@@ -30263,6 +30326,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
30263
30326
|
}
|
|
30264
30327
|
return null;
|
|
30265
30328
|
}
|
|
30329
|
+
function deletableSelection(selected, drawings) {
|
|
30330
|
+
const ids = [...selected];
|
|
30331
|
+
if (ids.length < 2) return ids;
|
|
30332
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30333
|
+
}
|
|
30334
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30335
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30336
|
+
return hit.locked ? [] : [hit.id];
|
|
30337
|
+
}
|
|
30266
30338
|
|
|
30267
30339
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30268
30340
|
var DRAG_SLOP3 = 3;
|
|
@@ -30366,10 +30438,32 @@ var DrawingInteraction = class {
|
|
|
30366
30438
|
activeDragId() {
|
|
30367
30439
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30368
30440
|
}
|
|
30441
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30442
|
+
pressedId() {
|
|
30443
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30444
|
+
}
|
|
30445
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30446
|
+
dragClones() {
|
|
30447
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30448
|
+
}
|
|
30449
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30450
|
+
marqueeRect() {
|
|
30451
|
+
if (this.state.kind !== "marquee") return null;
|
|
30452
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30453
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30454
|
+
}
|
|
30455
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30456
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30457
|
+
beginMarquee(x, y) {
|
|
30458
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30459
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30460
|
+
return true;
|
|
30461
|
+
}
|
|
30369
30462
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30370
30463
|
* `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
|
-
|
|
30464
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30465
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30466
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
30373
30467
|
const proj = this.deps.projector();
|
|
30374
30468
|
if (this.state.kind === "placing") {
|
|
30375
30469
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30397,7 +30491,50 @@ var DrawingInteraction = class {
|
|
|
30397
30491
|
return;
|
|
30398
30492
|
}
|
|
30399
30493
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30400
|
-
|
|
30494
|
+
const riders = [];
|
|
30495
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30496
|
+
for (const id of this.deps.selectedIds()) {
|
|
30497
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30498
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30499
|
+
}
|
|
30500
|
+
}
|
|
30501
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30502
|
+
}
|
|
30503
|
+
}
|
|
30504
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30505
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30506
|
+
bodyDelta(paneId, x, y) {
|
|
30507
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30508
|
+
const proj = this.deps.projector();
|
|
30509
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30510
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30511
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30512
|
+
}
|
|
30513
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30514
|
+
dragSources(d) {
|
|
30515
|
+
if (this.state.kind !== "pressed") return [];
|
|
30516
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30517
|
+
for (const r of this.state.riders) {
|
|
30518
|
+
const rd = this.byId(r.id);
|
|
30519
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30520
|
+
}
|
|
30521
|
+
return out;
|
|
30522
|
+
}
|
|
30523
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30524
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30525
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30526
|
+
dragBody(d, x, y) {
|
|
30527
|
+
if (this.state.kind !== "pressed") return;
|
|
30528
|
+
this.snapAt = null;
|
|
30529
|
+
if (this.state.mod && !this.state.clones) {
|
|
30530
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30531
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30532
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30533
|
+
});
|
|
30534
|
+
}
|
|
30535
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30536
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30537
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30401
30538
|
}
|
|
30402
30539
|
}
|
|
30403
30540
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30421,12 +30558,20 @@ var DrawingInteraction = class {
|
|
|
30421
30558
|
this.deps.changed();
|
|
30422
30559
|
return;
|
|
30423
30560
|
}
|
|
30561
|
+
if (this.state.kind === "marquee") {
|
|
30562
|
+
this.state.x1 = x;
|
|
30563
|
+
this.state.y1 = y;
|
|
30564
|
+
this.deps.changed();
|
|
30565
|
+
return;
|
|
30566
|
+
}
|
|
30424
30567
|
if (this.state.kind !== "pressed") return;
|
|
30425
30568
|
const d = this.byId(this.state.id);
|
|
30426
30569
|
if (!d || d.locked) return;
|
|
30427
30570
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
|
|
30428
30571
|
this.state.moved = true;
|
|
30429
|
-
if (this.state.handle
|
|
30572
|
+
if (this.state.handle < 0) {
|
|
30573
|
+
this.dragBody(d, x, y);
|
|
30574
|
+
} else {
|
|
30430
30575
|
const a = d.anchors[this.state.handle];
|
|
30431
30576
|
if (a) {
|
|
30432
30577
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30434,12 +30579,6 @@ var DrawingInteraction = class {
|
|
|
30434
30579
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30435
30580
|
}
|
|
30436
30581
|
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
30582
|
}
|
|
30444
30583
|
this.deps.changed();
|
|
30445
30584
|
}
|
|
@@ -30459,16 +30598,46 @@ var DrawingInteraction = class {
|
|
|
30459
30598
|
}
|
|
30460
30599
|
return;
|
|
30461
30600
|
}
|
|
30601
|
+
if (this.state.kind === "marquee") {
|
|
30602
|
+
const rect = this.marqueeRect();
|
|
30603
|
+
this.state = { kind: "idle" };
|
|
30604
|
+
if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
|
|
30605
|
+
this.deps.changed();
|
|
30606
|
+
return;
|
|
30607
|
+
}
|
|
30462
30608
|
if (this.state.kind !== "pressed") return;
|
|
30463
|
-
const { id, moved } = this.state;
|
|
30609
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30464
30610
|
this.state = { kind: "idle" };
|
|
30465
30611
|
const d = this.byId(id);
|
|
30466
|
-
if (moved) {
|
|
30467
|
-
if (
|
|
30468
|
-
this.deps.
|
|
30469
|
-
|
|
30470
|
-
this.deps.openSettings(id, x, y);
|
|
30612
|
+
if (!moved) {
|
|
30613
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30614
|
+
else this.deps.openSettings(id, x, y);
|
|
30615
|
+
return;
|
|
30471
30616
|
}
|
|
30617
|
+
if (clones) {
|
|
30618
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30619
|
+
} else if (d) {
|
|
30620
|
+
const docs = [d.serialize()];
|
|
30621
|
+
for (const r of riders) {
|
|
30622
|
+
const rd = this.byId(r.id);
|
|
30623
|
+
if (rd) docs.push(rd.serialize());
|
|
30624
|
+
}
|
|
30625
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30626
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30627
|
+
}
|
|
30628
|
+
this.deps.changed();
|
|
30629
|
+
}
|
|
30630
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30631
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30632
|
+
selectWithin(rect) {
|
|
30633
|
+
const proj = this.deps.projector();
|
|
30634
|
+
const ids = [...this.deps.selectedIds()];
|
|
30635
|
+
for (const d of this.deps.drawings()) {
|
|
30636
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30637
|
+
const b = d.bounds(proj);
|
|
30638
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30639
|
+
}
|
|
30640
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30472
30641
|
}
|
|
30473
30642
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30474
30643
|
cancel() {
|
|
@@ -30481,8 +30650,15 @@ var DrawingInteraction = class {
|
|
|
30481
30650
|
return true;
|
|
30482
30651
|
}
|
|
30483
30652
|
if (this.state.kind === "pressed") {
|
|
30484
|
-
|
|
30485
|
-
|
|
30653
|
+
if (this.state.moved && !this.state.clones) {
|
|
30654
|
+
const d = this.byId(this.state.id);
|
|
30655
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30656
|
+
}
|
|
30657
|
+
this.state = { kind: "idle" };
|
|
30658
|
+
this.deps.changed();
|
|
30659
|
+
return true;
|
|
30660
|
+
}
|
|
30661
|
+
if (this.state.kind === "marquee") {
|
|
30486
30662
|
this.state = { kind: "idle" };
|
|
30487
30663
|
this.deps.changed();
|
|
30488
30664
|
return true;
|
|
@@ -30597,6 +30773,9 @@ var DrawingInteraction = class {
|
|
|
30597
30773
|
function cloneAnchors(d) {
|
|
30598
30774
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30599
30775
|
}
|
|
30776
|
+
function rectsIntersect(a, b) {
|
|
30777
|
+
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;
|
|
30778
|
+
}
|
|
30600
30779
|
function applyFree(anchor, pt, free) {
|
|
30601
30780
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
30602
30781
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -30982,10 +31161,27 @@ function btn(label, primary, onClick) {
|
|
|
30982
31161
|
}
|
|
30983
31162
|
|
|
30984
31163
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31164
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31165
|
+
function sharedPaths(drawings) {
|
|
31166
|
+
const [first, ...rest] = drawings;
|
|
31167
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
31168
|
+
for (const d of rest) {
|
|
31169
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31170
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31171
|
+
}
|
|
31172
|
+
return paths;
|
|
31173
|
+
}
|
|
31174
|
+
function commonValue(drawings, read) {
|
|
31175
|
+
const first = read(drawings[0]);
|
|
31176
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
31177
|
+
}
|
|
31178
|
+
function distinctValues(drawings, read) {
|
|
31179
|
+
return [...new Set(drawings.map(read))];
|
|
31180
|
+
}
|
|
30985
31181
|
var BTN = 30;
|
|
30986
31182
|
var ICON = 21;
|
|
30987
31183
|
var STYLE_ID23 = "vela-drawing-popup-styles";
|
|
30988
|
-
var STYLE_REV = "
|
|
31184
|
+
var STYLE_REV = "4";
|
|
30989
31185
|
function ensureStyles5() {
|
|
30990
31186
|
if (typeof document === "undefined") return;
|
|
30991
31187
|
const existing = document.getElementById(STYLE_ID23);
|
|
@@ -30997,6 +31193,8 @@ function ensureStyles5() {
|
|
|
30997
31193
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
30998
31194
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
30999
31195
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31196
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31197
|
+
.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
31198
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
31001
31199
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
31002
31200
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -31028,7 +31226,7 @@ var DrawingSettingsPopup = class {
|
|
|
31028
31226
|
if (this.isOwnChrome(node)) return;
|
|
31029
31227
|
const cb = this.onClose;
|
|
31030
31228
|
this.close();
|
|
31031
|
-
cb?.();
|
|
31229
|
+
cb?.(e);
|
|
31032
31230
|
};
|
|
31033
31231
|
this.theme = theme;
|
|
31034
31232
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31053,16 +31251,27 @@ var DrawingSettingsPopup = class {
|
|
|
31053
31251
|
contains(node) {
|
|
31054
31252
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31055
31253
|
}
|
|
31056
|
-
/** Open the quick toolbar for `
|
|
31057
|
-
* `
|
|
31058
|
-
|
|
31254
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31255
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31256
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31257
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31258
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31259
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31260
|
+
* modifier press (multi-select) from a plain one. */
|
|
31261
|
+
open(drawings, anchor, actions, onClose) {
|
|
31059
31262
|
this.close();
|
|
31263
|
+
const drawing = drawings[0];
|
|
31264
|
+
if (!drawing) return;
|
|
31060
31265
|
ensureStyles5();
|
|
31061
31266
|
this.onClose = onClose ?? null;
|
|
31062
31267
|
const t = this.theme;
|
|
31268
|
+
const multi = drawings.length > 1;
|
|
31063
31269
|
const schema = drawing.schema();
|
|
31064
|
-
const paths =
|
|
31065
|
-
const
|
|
31270
|
+
const paths = sharedPaths(drawings);
|
|
31271
|
+
const common = (read) => commonValue(drawings, read);
|
|
31272
|
+
const every = (pred) => drawings.every(pred);
|
|
31273
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31274
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31066
31275
|
const el = document.createElement("div");
|
|
31067
31276
|
el.className = "vela-dpop";
|
|
31068
31277
|
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 +31285,84 @@ var DrawingSettingsPopup = class {
|
|
|
31076
31285
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31077
31286
|
bar.appendChild(this.dragHandle());
|
|
31078
31287
|
if (paths.has("glyph")) {
|
|
31079
|
-
const cur =
|
|
31288
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31080
31289
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31081
31290
|
}
|
|
31082
31291
|
if (paths.has("size")) {
|
|
31083
|
-
const sz =
|
|
31292
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31084
31293
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31085
31294
|
}
|
|
31086
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31295
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31296
|
+
const mag = (d) => d;
|
|
31087
31297
|
const options = this.lowerTimeframeOptions();
|
|
31088
31298
|
if (options.length > 0) {
|
|
31089
31299
|
bar.appendChild(
|
|
31090
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31300
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31091
31301
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31092
31302
|
labelInTrigger: true
|
|
31093
31303
|
})
|
|
31094
31304
|
);
|
|
31095
31305
|
}
|
|
31096
|
-
bar.appendChild(
|
|
31097
|
-
bar.appendChild(
|
|
31306
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31307
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31098
31308
|
}
|
|
31099
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31309
|
+
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
31310
|
if (paths.has("style.lineWidth")) {
|
|
31101
|
-
const
|
|
31102
|
-
|
|
31103
|
-
|
|
31311
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31312
|
+
const wf = widthField2(drawing);
|
|
31313
|
+
const width = common((d) => d.style.lineWidth);
|
|
31314
|
+
const markers = every((d) => {
|
|
31315
|
+
const f = widthField2(d);
|
|
31316
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31317
|
+
});
|
|
31318
|
+
if (markers && wf?.kind === "number") {
|
|
31319
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31104
31320
|
} else {
|
|
31105
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31321
|
+
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
31322
|
}
|
|
31107
31323
|
}
|
|
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(
|
|
31324
|
+
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 }));
|
|
31325
|
+
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
31326
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31111
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31327
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31112
31328
|
if (isPosition) {
|
|
31113
|
-
const pos =
|
|
31114
|
-
bar.appendChild(
|
|
31115
|
-
bar.appendChild(
|
|
31329
|
+
const pos = (d) => d;
|
|
31330
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31331
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31116
31332
|
}
|
|
31117
|
-
const
|
|
31118
|
-
if (paths.has("reg.midColor") &&
|
|
31333
|
+
const regOf = (d) => d.reg;
|
|
31334
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31335
|
+
const reg = (d) => regOf(d);
|
|
31119
31336
|
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") &&
|
|
31337
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31338
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31339
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31340
|
+
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 }));
|
|
31341
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31342
|
+
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 }));
|
|
31343
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31344
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31345
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31346
|
+
}
|
|
31347
|
+
const vwapOf = (d) => d.vwap;
|
|
31348
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31349
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31132
31350
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31133
31351
|
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 }));
|
|
31352
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31353
|
+
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
31354
|
bar.appendChild(
|
|
31137
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31355
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31138
31356
|
label: (v) => `${v}\u03C3`,
|
|
31139
31357
|
labelInTrigger: true
|
|
31140
31358
|
})
|
|
31141
31359
|
);
|
|
31142
|
-
bar.appendChild(
|
|
31143
|
-
bar.appendChild(
|
|
31144
|
-
bar.appendChild(
|
|
31360
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31361
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31362
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31145
31363
|
}
|
|
31146
31364
|
if (paths.has("maxCurvature")) {
|
|
31147
|
-
const cur =
|
|
31365
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31148
31366
|
bar.appendChild(
|
|
31149
31367
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31150
31368
|
label: (v) => `n=${v}`,
|
|
@@ -31153,7 +31371,7 @@ var DrawingSettingsPopup = class {
|
|
|
31153
31371
|
);
|
|
31154
31372
|
}
|
|
31155
31373
|
if (paths.has("mach")) {
|
|
31156
|
-
const cur =
|
|
31374
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31157
31375
|
bar.appendChild(
|
|
31158
31376
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31159
31377
|
label: (v) => `M=${v}`,
|
|
@@ -31162,7 +31380,7 @@ var DrawingSettingsPopup = class {
|
|
|
31162
31380
|
);
|
|
31163
31381
|
}
|
|
31164
31382
|
if (paths.has("waveCount")) {
|
|
31165
|
-
const cur =
|
|
31383
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31166
31384
|
bar.appendChild(
|
|
31167
31385
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31168
31386
|
label: (v) => `${v}`,
|
|
@@ -31170,26 +31388,27 @@ var DrawingSettingsPopup = class {
|
|
|
31170
31388
|
})
|
|
31171
31389
|
);
|
|
31172
31390
|
}
|
|
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.
|
|
31391
|
+
const flags = (d) => d;
|
|
31392
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31393
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31394
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31395
|
+
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 }));
|
|
31396
|
+
const textField = !multi && paths.has("text.value");
|
|
31397
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31398
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31399
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31181
31400
|
const editableLevels = drawing.editableLevels();
|
|
31182
|
-
if (editableLevels && !(
|
|
31183
|
-
const fib =
|
|
31401
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31402
|
+
const fib = (d) => d;
|
|
31184
31403
|
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 }));
|
|
31404
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31405
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31187
31406
|
}
|
|
31188
31407
|
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,
|
|
31408
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31409
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31410
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31411
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31193
31412
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31194
31413
|
del.style.color = "var(--vela-danger)";
|
|
31195
31414
|
bar.appendChild(del);
|
|
@@ -31375,7 +31594,7 @@ var DrawingSettingsPopup = class {
|
|
|
31375
31594
|
});
|
|
31376
31595
|
return b;
|
|
31377
31596
|
}
|
|
31378
|
-
/** The trailing kebab overflow — z-order actions
|
|
31597
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31379
31598
|
kebabButton(actions) {
|
|
31380
31599
|
const b = this.base("More");
|
|
31381
31600
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31387,6 +31606,7 @@ var DrawingSettingsPopup = class {
|
|
|
31387
31606
|
return;
|
|
31388
31607
|
}
|
|
31389
31608
|
this.openActionMenu(b, [
|
|
31609
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31390
31610
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31391
31611
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31392
31612
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31543,26 +31763,30 @@ var DrawingSettingsPopup = class {
|
|
|
31543
31763
|
this.tipEl?.remove();
|
|
31544
31764
|
this.tipEl = null;
|
|
31545
31765
|
}
|
|
31546
|
-
|
|
31766
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
31767
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
31768
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
31769
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31547
31770
|
const b = this.base(tip);
|
|
31548
|
-
b.innerHTML = sized(icon2);
|
|
31549
31771
|
const set = (on2) => {
|
|
31550
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
31772
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
31773
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31551
31774
|
};
|
|
31552
31775
|
set(active);
|
|
31553
31776
|
let on = active;
|
|
31554
31777
|
b.addEventListener("click", () => {
|
|
31555
|
-
on = !on;
|
|
31778
|
+
on = on === MIXED ? true : !on;
|
|
31556
31779
|
set(on);
|
|
31557
31780
|
onChange(on);
|
|
31558
31781
|
});
|
|
31559
31782
|
return b;
|
|
31560
31783
|
}
|
|
31561
31784
|
/** 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.
|
|
31785
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
31786
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31563
31787
|
widthInput(tip, value, min, max, step, onChange) {
|
|
31564
31788
|
const ni = new NumberInput({
|
|
31565
|
-
value,
|
|
31789
|
+
value: value === MIXED ? min : value,
|
|
31566
31790
|
min,
|
|
31567
31791
|
max,
|
|
31568
31792
|
step,
|
|
@@ -31574,13 +31798,18 @@ var DrawingSettingsPopup = class {
|
|
|
31574
31798
|
title: tip,
|
|
31575
31799
|
onChange
|
|
31576
31800
|
});
|
|
31801
|
+
if (value === MIXED) {
|
|
31802
|
+
ni.input.value = "";
|
|
31803
|
+
ni.input.placeholder = "\u2014";
|
|
31804
|
+
}
|
|
31577
31805
|
ni.el.dataset.tip = tip;
|
|
31578
31806
|
trapChartKeys(ni.el);
|
|
31579
31807
|
return ni.el;
|
|
31580
31808
|
}
|
|
31581
31809
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31582
31810
|
* 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).
|
|
31811
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31812
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31584
31813
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31585
31814
|
const b = this.base(tip);
|
|
31586
31815
|
b.style.width = "auto";
|
|
@@ -31590,14 +31819,20 @@ var DrawingSettingsPopup = class {
|
|
|
31590
31819
|
let cur = current;
|
|
31591
31820
|
const paint = (v) => {
|
|
31592
31821
|
b.replaceChildren();
|
|
31593
|
-
|
|
31822
|
+
if (v === MIXED) {
|
|
31823
|
+
const dash = document.createElement("span");
|
|
31824
|
+
dash.textContent = "\u2014";
|
|
31825
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
31826
|
+
b.appendChild(dash);
|
|
31827
|
+
}
|
|
31828
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31594
31829
|
if (glyph) {
|
|
31595
31830
|
const ic = document.createElement("span");
|
|
31596
31831
|
ic.style.cssText = "display:flex;";
|
|
31597
31832
|
ic.innerHTML = sized(glyph);
|
|
31598
31833
|
b.appendChild(ic);
|
|
31599
31834
|
}
|
|
31600
|
-
if (opts.label && opts.labelInTrigger) {
|
|
31835
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31601
31836
|
const tx = document.createElement("span");
|
|
31602
31837
|
tx.textContent = opts.label(v);
|
|
31603
31838
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -31667,8 +31902,10 @@ var DrawingSettingsPopup = class {
|
|
|
31667
31902
|
}
|
|
31668
31903
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
31669
31904
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
31670
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
31671
|
-
|
|
31905
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
31906
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
31907
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
31908
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
31672
31909
|
const b = document.createElement("button");
|
|
31673
31910
|
b.type = "button";
|
|
31674
31911
|
b.dataset.tip = tip;
|
|
@@ -31678,13 +31915,14 @@ var DrawingSettingsPopup = class {
|
|
|
31678
31915
|
ic.style.cssText = "display:flex;";
|
|
31679
31916
|
ic.innerHTML = sized(icon2, iconSize);
|
|
31680
31917
|
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};`;
|
|
31918
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
31682
31919
|
b.append(ic, bar);
|
|
31683
31920
|
let cur = color;
|
|
31684
31921
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
31685
31922
|
b.addEventListener("click", (e) => {
|
|
31686
31923
|
e.stopPropagation();
|
|
31687
|
-
|
|
31924
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
31925
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
31688
31926
|
cur = v;
|
|
31689
31927
|
bar.style.background = v;
|
|
31690
31928
|
onChange(v);
|
|
@@ -31693,20 +31931,42 @@ var DrawingSettingsPopup = class {
|
|
|
31693
31931
|
return b;
|
|
31694
31932
|
}
|
|
31695
31933
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
31696
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
31934
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
31697
31935
|
if (this.colorOwner === anchor) {
|
|
31698
31936
|
this.closeColorPopover();
|
|
31699
31937
|
return;
|
|
31700
31938
|
}
|
|
31701
|
-
this.openColorPopover(anchor, color, onChange);
|
|
31939
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
31702
31940
|
}
|
|
31703
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31704
|
-
|
|
31941
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31942
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
31943
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
31705
31944
|
const t = this.theme;
|
|
31706
31945
|
this.colorPop = this.hostFloat(anchor, {
|
|
31707
31946
|
zIndex: 25,
|
|
31708
31947
|
padding: "10px",
|
|
31709
|
-
fill: (el) => {
|
|
31948
|
+
fill: (el, pop) => {
|
|
31949
|
+
if (used.length > 1) {
|
|
31950
|
+
const row = document.createElement("div");
|
|
31951
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
31952
|
+
const lbl = document.createElement("span");
|
|
31953
|
+
lbl.textContent = "In use";
|
|
31954
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
31955
|
+
row.appendChild(lbl);
|
|
31956
|
+
for (const c of used) {
|
|
31957
|
+
const sw = document.createElement("button");
|
|
31958
|
+
sw.type = "button";
|
|
31959
|
+
sw.dataset.tip = c;
|
|
31960
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
31961
|
+
sw.addEventListener("click", (e) => {
|
|
31962
|
+
e.stopPropagation();
|
|
31963
|
+
pop.hide();
|
|
31964
|
+
onChange(c);
|
|
31965
|
+
});
|
|
31966
|
+
row.appendChild(sw);
|
|
31967
|
+
}
|
|
31968
|
+
el.appendChild(row);
|
|
31969
|
+
}
|
|
31710
31970
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
31711
31971
|
}
|
|
31712
31972
|
});
|
|
@@ -31727,6 +31987,7 @@ var TYPE_ICON = icon("type");
|
|
|
31727
31987
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
31728
31988
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
31729
31989
|
var LOCK_ICON = icon("lock");
|
|
31990
|
+
var UNLOCK_ICON = icon("unlock");
|
|
31730
31991
|
var FRONT_ICON = icon("bring-front");
|
|
31731
31992
|
var BACK_ICON = icon("send-back");
|
|
31732
31993
|
var TRASH_ICON = icon("trash");
|
|
@@ -31734,6 +31995,7 @@ var BOLD_ICON = icon("bold");
|
|
|
31734
31995
|
var ITALIC_ICON = icon("italic");
|
|
31735
31996
|
var GRIP_ICON = icon("grip");
|
|
31736
31997
|
var KEBAB_ICON = icon("kebab");
|
|
31998
|
+
var CLONE_ICON = icon("clone");
|
|
31737
31999
|
var RESET_ICON = icon("reset");
|
|
31738
32000
|
var GEAR_ICON = icon("gear");
|
|
31739
32001
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -31742,6 +32004,12 @@ var BANDS_ICON = icon("bands");
|
|
|
31742
32004
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
31743
32005
|
var SONIC_ICON2 = icon("sonic");
|
|
31744
32006
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32007
|
+
function stripes(colors) {
|
|
32008
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32009
|
+
const step = 100 / bands.length;
|
|
32010
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32011
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32012
|
+
}
|
|
31745
32013
|
function lineIcon(width, style) {
|
|
31746
32014
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
31747
32015
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -32025,6 +32293,20 @@ var UserDrawingController = class {
|
|
|
32025
32293
|
// reported exactly when it actually changes).
|
|
32026
32294
|
this.textEditor = null;
|
|
32027
32295
|
this.painter = new DrawingPainter();
|
|
32296
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
32297
|
+
* shrinking selection does with it. */
|
|
32298
|
+
this.popupMulti = false;
|
|
32299
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
32300
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
32301
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
32302
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
32303
|
+
* whose bar just closed must stay selected. */
|
|
32304
|
+
this.onPopupDismissed = (e) => {
|
|
32305
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
32306
|
+
const pressed = this.interaction.pressedId();
|
|
32307
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
32308
|
+
this.clearSelection();
|
|
32309
|
+
};
|
|
32028
32310
|
this.ctx = canvas.getContext("2d");
|
|
32029
32311
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32030
32312
|
this.toolbar = new DrawingToolbar(
|
|
@@ -32201,6 +32483,12 @@ var UserDrawingController = class {
|
|
|
32201
32483
|
}
|
|
32202
32484
|
setSelection(ids) {
|
|
32203
32485
|
this.selectedIds = new Set(ids);
|
|
32486
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
32487
|
+
else if (this.popupMulti) {
|
|
32488
|
+
this.popupMulti = false;
|
|
32489
|
+
this.popup.close();
|
|
32490
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
32491
|
+
}
|
|
32204
32492
|
this.render();
|
|
32205
32493
|
}
|
|
32206
32494
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -32246,12 +32534,16 @@ var UserDrawingController = class {
|
|
|
32246
32534
|
});
|
|
32247
32535
|
}
|
|
32248
32536
|
/** 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
|
-
|
|
32537
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
32538
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
32539
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
32540
|
+
deleteAt(x, y, withSelection = false) {
|
|
32251
32541
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
32252
|
-
if (!hit
|
|
32542
|
+
if (!hit) return false;
|
|
32543
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
32544
|
+
if (ids.length === 0) return false;
|
|
32253
32545
|
this.popup.close();
|
|
32254
|
-
this.emit({ kind: "delete", ids
|
|
32546
|
+
this.emit({ kind: "delete", ids });
|
|
32255
32547
|
return true;
|
|
32256
32548
|
}
|
|
32257
32549
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -32277,7 +32569,14 @@ var UserDrawingController = class {
|
|
|
32277
32569
|
this.render();
|
|
32278
32570
|
}
|
|
32279
32571
|
}
|
|
32280
|
-
|
|
32572
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
32573
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
32574
|
+
beginMarqueeAt(x, y) {
|
|
32575
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
32576
|
+
this.render();
|
|
32577
|
+
return true;
|
|
32578
|
+
}
|
|
32579
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32281
32580
|
if (this.eraserMode) {
|
|
32282
32581
|
this.erasing = true;
|
|
32283
32582
|
this.deleteAt(x, y);
|
|
@@ -32297,9 +32596,9 @@ var UserDrawingController = class {
|
|
|
32297
32596
|
return;
|
|
32298
32597
|
}
|
|
32299
32598
|
}
|
|
32300
|
-
this.interaction.down(x, y, snap, shift2);
|
|
32599
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
32301
32600
|
}
|
|
32302
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
32601
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32303
32602
|
if (this.eraserMode) {
|
|
32304
32603
|
if (this.erasing) this.deleteAt(x, y);
|
|
32305
32604
|
return;
|
|
@@ -32311,12 +32610,14 @@ var UserDrawingController = class {
|
|
|
32311
32610
|
return;
|
|
32312
32611
|
}
|
|
32313
32612
|
this.interaction.move(x, y, snap, shift2);
|
|
32314
|
-
this.updateHover(x, y);
|
|
32613
|
+
this.updateHover(x, y, mod);
|
|
32315
32614
|
}
|
|
32316
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
32317
|
-
|
|
32615
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
32616
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
32617
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
32618
|
+
updateHover(x, y, mod = false) {
|
|
32318
32619
|
let id = null;
|
|
32319
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32620
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32320
32621
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
32321
32622
|
}
|
|
32322
32623
|
if (id !== this.hoveredId) {
|
|
@@ -32336,7 +32637,12 @@ var UserDrawingController = class {
|
|
|
32336
32637
|
this.render();
|
|
32337
32638
|
return;
|
|
32338
32639
|
}
|
|
32640
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
32339
32641
|
this.interaction.up(x, y);
|
|
32642
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
32643
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
32644
|
+
else this.openSettingsById(dragged, x, y);
|
|
32645
|
+
}
|
|
32340
32646
|
}
|
|
32341
32647
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
32342
32648
|
toggleMeasure() {
|
|
@@ -32601,7 +32907,7 @@ var UserDrawingController = class {
|
|
|
32601
32907
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
32602
32908
|
break;
|
|
32603
32909
|
case "delete": {
|
|
32604
|
-
const ids = this.selectedIds.size ? this.
|
|
32910
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
32605
32911
|
if (ids.length) {
|
|
32606
32912
|
this.popup.close();
|
|
32607
32913
|
this.emit({ kind: "delete", ids });
|
|
@@ -32617,6 +32923,9 @@ var UserDrawingController = class {
|
|
|
32617
32923
|
selectionIds() {
|
|
32618
32924
|
return [...this.selectedIds];
|
|
32619
32925
|
}
|
|
32926
|
+
deletableSelection() {
|
|
32927
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
32928
|
+
}
|
|
32620
32929
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
32621
32930
|
nudgeSelection(dx, dy) {
|
|
32622
32931
|
const proj = this.deps.projector();
|
|
@@ -32725,6 +33034,8 @@ var UserDrawingController = class {
|
|
|
32725
33034
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
32726
33035
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32727
33036
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33037
|
+
const clones = this.interaction.dragClones();
|
|
33038
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
32728
33039
|
this.layoutTextEditor();
|
|
32729
33040
|
const ghost = this.interaction.ghost();
|
|
32730
33041
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -32740,6 +33051,8 @@ var UserDrawingController = class {
|
|
|
32740
33051
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
32741
33052
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
32742
33053
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
33054
|
+
const marquee = this.interaction.marqueeRect();
|
|
33055
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
32743
33056
|
}
|
|
32744
33057
|
destroy() {
|
|
32745
33058
|
this.closeTextEditor();
|
|
@@ -32757,15 +33070,80 @@ var UserDrawingController = class {
|
|
|
32757
33070
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
32758
33071
|
this.render();
|
|
32759
33072
|
}
|
|
33073
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
33074
|
+
emitEdits(drawings) {
|
|
33075
|
+
const docs = drawings.map((d) => d.serialize());
|
|
33076
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
33077
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
33078
|
+
}
|
|
33079
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
33080
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
33081
|
+
openSettingsForSelection(ids) {
|
|
33082
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
33083
|
+
const drawings = live();
|
|
33084
|
+
if (drawings.length < 2) return;
|
|
33085
|
+
const proj = this.deps.projector();
|
|
33086
|
+
let anchor = null;
|
|
33087
|
+
for (const d of drawings) {
|
|
33088
|
+
const b = d.bounds(proj);
|
|
33089
|
+
if (!b) continue;
|
|
33090
|
+
if (!anchor) anchor = { ...b };
|
|
33091
|
+
else {
|
|
33092
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
33093
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
33094
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
33095
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
33096
|
+
anchor.w = right - anchor.x;
|
|
33097
|
+
anchor.h = bottom - anchor.y;
|
|
33098
|
+
}
|
|
33099
|
+
}
|
|
33100
|
+
this.popupMulti = true;
|
|
33101
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
33102
|
+
this.popup.open(drawings, anchor, {
|
|
33103
|
+
resolve: () => live()[0] ?? null,
|
|
33104
|
+
patch: (p) => {
|
|
33105
|
+
const ds = live();
|
|
33106
|
+
for (const d of ds) d.applySettings(p);
|
|
33107
|
+
this.render();
|
|
33108
|
+
this.emitEdits(ds);
|
|
33109
|
+
},
|
|
33110
|
+
setLocked: (v) => {
|
|
33111
|
+
const ds = live();
|
|
33112
|
+
for (const d of ds) d.locked = v;
|
|
33113
|
+
this.emitEdits(ds);
|
|
33114
|
+
},
|
|
33115
|
+
reorder: (to) => {
|
|
33116
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
33117
|
+
},
|
|
33118
|
+
duplicate: () => {
|
|
33119
|
+
this.popup.close();
|
|
33120
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
33121
|
+
},
|
|
33122
|
+
resetSettings: () => {
|
|
33123
|
+
const ds = live();
|
|
33124
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
33125
|
+
this.render();
|
|
33126
|
+
this.emitEdits(ds);
|
|
33127
|
+
this.openSettingsForSelection(ids);
|
|
33128
|
+
},
|
|
33129
|
+
remove: () => {
|
|
33130
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
33131
|
+
if (ids2.length === 0) return;
|
|
33132
|
+
this.popup.close();
|
|
33133
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
33134
|
+
}
|
|
33135
|
+
}, this.onPopupDismissed);
|
|
33136
|
+
}
|
|
32760
33137
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
32761
33138
|
openSettingsById(id, _x, _y) {
|
|
32762
33139
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
32763
33140
|
if (!drawing) return;
|
|
32764
33141
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
33142
|
+
this.popupMulti = false;
|
|
32765
33143
|
this.emit({ kind: "select", ids: [id] });
|
|
32766
33144
|
this.emit({ kind: "settings", id });
|
|
32767
33145
|
const anchor = drawing.bounds(this.deps.projector());
|
|
32768
|
-
this.popup.open(drawing, anchor, {
|
|
33146
|
+
this.popup.open([drawing], anchor, {
|
|
32769
33147
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
32770
33148
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
32771
33149
|
resolve: () => live() ?? null,
|
|
@@ -32783,6 +33161,11 @@ var UserDrawingController = class {
|
|
|
32783
33161
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32784
33162
|
},
|
|
32785
33163
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
33164
|
+
duplicate: () => {
|
|
33165
|
+
this.finishTextEditor(true);
|
|
33166
|
+
this.popup.close();
|
|
33167
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
33168
|
+
},
|
|
32786
33169
|
resetSettings: () => {
|
|
32787
33170
|
const d = live();
|
|
32788
33171
|
if (!d) return;
|
|
@@ -32805,7 +33188,13 @@ var UserDrawingController = class {
|
|
|
32805
33188
|
this.popup.close();
|
|
32806
33189
|
this.emit({ kind: "delete", ids: [id] });
|
|
32807
33190
|
}
|
|
32808
|
-
},
|
|
33191
|
+
}, this.onPopupDismissed);
|
|
33192
|
+
}
|
|
33193
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
33194
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
33195
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
33196
|
+
deselect() {
|
|
33197
|
+
this.clearSelection();
|
|
32809
33198
|
}
|
|
32810
33199
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
32811
33200
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -32831,6 +33220,13 @@ var UserDrawingController = class {
|
|
|
32831
33220
|
}, 0);
|
|
32832
33221
|
return;
|
|
32833
33222
|
}
|
|
33223
|
+
if (i.kind === "clone") {
|
|
33224
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
33225
|
+
this.intentCb?.(i);
|
|
33226
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
33227
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
33228
|
+
return;
|
|
33229
|
+
}
|
|
32834
33230
|
this.intentCb?.(i);
|
|
32835
33231
|
}
|
|
32836
33232
|
};
|
|
@@ -35006,7 +35402,10 @@ var NativeRenderer = class {
|
|
|
35006
35402
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35007
35403
|
fling: (v) => this.fling(v),
|
|
35008
35404
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35009
|
-
onClick: (x) =>
|
|
35405
|
+
onClick: (x) => {
|
|
35406
|
+
this.userDrawings?.deselect();
|
|
35407
|
+
this.handleClick(x);
|
|
35408
|
+
},
|
|
35010
35409
|
onAxisLongPress: (axis, x, y) => {
|
|
35011
35410
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
35012
35411
|
},
|
|
@@ -35024,11 +35423,13 @@ var NativeRenderer = class {
|
|
|
35024
35423
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
35025
35424
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
35026
35425
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
35027
|
-
|
|
35426
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
35427
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
35428
|
+
// middle-click: a selected hit takes the whole selection
|
|
35028
35429
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
35029
35430
|
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),
|
|
35431
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35432
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35032
35433
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35033
35434
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
35034
35435
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -37362,6 +37763,10 @@ function str3(inputs, key, fallback) {
|
|
|
37362
37763
|
const v = inputs[key];
|
|
37363
37764
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
37364
37765
|
}
|
|
37766
|
+
function bool(inputs, key, fallback) {
|
|
37767
|
+
const v = inputs[key];
|
|
37768
|
+
return typeof v === "boolean" ? v : fallback;
|
|
37769
|
+
}
|
|
37365
37770
|
function toPoints(bars, values, colors) {
|
|
37366
37771
|
const pts = new Array(bars.length);
|
|
37367
37772
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -38893,10 +39298,6 @@ var aroon = {
|
|
|
38893
39298
|
plots: [
|
|
38894
39299
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
38895
39300
|
{ 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
39301
|
]
|
|
38901
39302
|
};
|
|
38902
39303
|
}
|
|
@@ -39394,8 +39795,10 @@ var volumeSpecs = [
|
|
|
39394
39795
|
var DAY_MS2 = 864e5;
|
|
39395
39796
|
function periodKey(anchor, time) {
|
|
39396
39797
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
39397
|
-
if (anchor === "Month") {
|
|
39798
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
39398
39799
|
const d = new Date(time);
|
|
39800
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
39801
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
39399
39802
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
39400
39803
|
}
|
|
39401
39804
|
return Math.floor(time / DAY_MS2);
|
|
@@ -39447,40 +39850,86 @@ var parabolicSar = {
|
|
|
39447
39850
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
39448
39851
|
}
|
|
39449
39852
|
};
|
|
39853
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
39854
|
+
var VWAP_STYLE = "Style";
|
|
39855
|
+
var VWAP_BANDS = [
|
|
39856
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
39857
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
39858
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
39859
|
+
];
|
|
39450
39860
|
var vwap = {
|
|
39451
39861
|
type: "vwap",
|
|
39452
39862
|
title: "Volume Weighted Average Price",
|
|
39453
39863
|
shortTitle: "VWAP",
|
|
39454
39864
|
overlay: true,
|
|
39455
39865
|
inputs: [
|
|
39456
|
-
{ key: "anchor", title: "
|
|
39457
|
-
|
|
39458
|
-
|
|
39866
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
39867
|
+
sourceInput("HLC3"),
|
|
39868
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
39869
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39870
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
39871
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
39872
|
+
]),
|
|
39873
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
39874
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
39875
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39876
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
39877
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
39878
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
39879
|
+
])
|
|
39459
39880
|
],
|
|
39460
39881
|
compute: (bars, inputs) => {
|
|
39461
39882
|
const anchor = str3(inputs, "anchor", "Day");
|
|
39462
|
-
const
|
|
39463
|
-
const
|
|
39883
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
39884
|
+
const n = bars.length;
|
|
39885
|
+
const values = new Array(n).fill(Number.NaN);
|
|
39886
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
39887
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
39888
|
+
color: str3(inputs, b.color, b.defColor),
|
|
39889
|
+
fill: bool(inputs, b.fill, true),
|
|
39890
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
39891
|
+
up: new Array(n).fill(Number.NaN),
|
|
39892
|
+
down: new Array(n).fill(Number.NaN)
|
|
39893
|
+
}));
|
|
39464
39894
|
let period = Number.NaN;
|
|
39465
39895
|
let cumPV = 0;
|
|
39896
|
+
let cumPV2 = 0;
|
|
39466
39897
|
let cumV = 0;
|
|
39467
|
-
for (let i = 0; i <
|
|
39898
|
+
for (let i = 0; i < n; i++) {
|
|
39468
39899
|
const b = bars[i];
|
|
39469
39900
|
const key = periodKey(anchor, b.time);
|
|
39470
39901
|
if (key !== period) {
|
|
39471
39902
|
period = key;
|
|
39472
39903
|
cumPV = 0;
|
|
39904
|
+
cumPV2 = 0;
|
|
39473
39905
|
cumV = 0;
|
|
39474
39906
|
}
|
|
39475
39907
|
const v = b.volume;
|
|
39476
39908
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
39477
|
-
const tp =
|
|
39909
|
+
const tp = src[i];
|
|
39478
39910
|
cumPV += tp * v;
|
|
39911
|
+
cumPV2 += tp * tp * v;
|
|
39479
39912
|
cumV += v;
|
|
39480
39913
|
}
|
|
39481
|
-
if (cumV
|
|
39482
|
-
|
|
39483
|
-
|
|
39914
|
+
if (cumV <= 0) continue;
|
|
39915
|
+
const mean = cumPV / cumV;
|
|
39916
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
39917
|
+
values[i] = mean;
|
|
39918
|
+
for (const band of bands) {
|
|
39919
|
+
band.up[i] = mean + band.mult * sd;
|
|
39920
|
+
band.down[i] = mean - band.mult * sd;
|
|
39921
|
+
}
|
|
39922
|
+
}
|
|
39923
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
39924
|
+
const fills = [];
|
|
39925
|
+
bands.forEach((band, i) => {
|
|
39926
|
+
plots.push(
|
|
39927
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
39928
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
39929
|
+
);
|
|
39930
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
39931
|
+
});
|
|
39932
|
+
return { plots, bands: fills };
|
|
39484
39933
|
}
|
|
39485
39934
|
};
|
|
39486
39935
|
var pivotPoints = {
|
|
@@ -39994,6 +40443,24 @@ function resolveElement(container) {
|
|
|
39994
40443
|
function segmentVisibility(parts, chartHidden = false) {
|
|
39995
40444
|
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
39996
40445
|
}
|
|
40446
|
+
var STATUSLINE_LADDER = [
|
|
40447
|
+
{ stacked: false, level: "full" },
|
|
40448
|
+
{ stacked: true, level: "full" },
|
|
40449
|
+
{ stacked: true, level: "compact" },
|
|
40450
|
+
{ stacked: true, level: "minimal" }
|
|
40451
|
+
];
|
|
40452
|
+
function readoutCells(level, readout) {
|
|
40453
|
+
if (readout === "value") return [{ key: "close", label: "" }];
|
|
40454
|
+
if (level === "full") {
|
|
40455
|
+
return [
|
|
40456
|
+
{ key: "open", label: "O" },
|
|
40457
|
+
{ key: "high", label: "H" },
|
|
40458
|
+
{ key: "low", label: "L" },
|
|
40459
|
+
{ key: "close", label: "C" }
|
|
40460
|
+
];
|
|
40461
|
+
}
|
|
40462
|
+
return [{ key: "close", label: level === "compact" ? "C" : "" }];
|
|
40463
|
+
}
|
|
39997
40464
|
function statuslineMenuItems(parts, chartVisible) {
|
|
39998
40465
|
return [
|
|
39999
40466
|
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
@@ -40022,6 +40489,11 @@ function fmtChange(open2, close) {
|
|
|
40022
40489
|
const sign = diff >= 0 ? "+" : "";
|
|
40023
40490
|
return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
|
|
40024
40491
|
}
|
|
40492
|
+
function fmtChangePct(open2, close) {
|
|
40493
|
+
if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
|
|
40494
|
+
const pct = (close - open2) / open2 * 100;
|
|
40495
|
+
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
|
40496
|
+
}
|
|
40025
40497
|
|
|
40026
40498
|
// src/widget/statusline.ts
|
|
40027
40499
|
var STYLE_ID24 = "vela-widget-statusline";
|
|
@@ -40038,6 +40510,11 @@ var CSS21 = `
|
|
|
40038
40510
|
display: flex;
|
|
40039
40511
|
align-items: baseline;
|
|
40040
40512
|
gap: var(--vela-space-2);
|
|
40513
|
+
/* The chip never grows past the plot: fit() measures overflow against this width to
|
|
40514
|
+
* walk the layout ladder (stack, then shed values), so overflow:hidden only guards
|
|
40515
|
+
* the transient between a resize and the next measure. */
|
|
40516
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40517
|
+
overflow: hidden;
|
|
40041
40518
|
color: var(--vela-fg);
|
|
40042
40519
|
font-size: var(--vela-font-size-md);
|
|
40043
40520
|
/* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
|
|
@@ -40059,6 +40536,31 @@ var CSS21 = `
|
|
|
40059
40536
|
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
40060
40537
|
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
40061
40538
|
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
40539
|
+
/* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
|
|
40540
|
+
* value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
|
|
40541
|
+
* side by side on one line; the STACKED layouts (fit() decides, see the ladder in
|
|
40542
|
+
* statusline-model) turn the chip into a column so the values drop under the symbol. */
|
|
40543
|
+
.vela-statusline .vela-sl-row {
|
|
40544
|
+
display: flex;
|
|
40545
|
+
align-items: baseline;
|
|
40546
|
+
gap: var(--vela-space-2);
|
|
40547
|
+
white-space: nowrap;
|
|
40548
|
+
}
|
|
40549
|
+
.vela-statusline.vela-sl-stacked {
|
|
40550
|
+
flex-direction: column;
|
|
40551
|
+
align-items: flex-start;
|
|
40552
|
+
/* The same air between the two rows as between the values row and the legend row
|
|
40553
|
+
* that follows the chip (see the stacked legend shift below). */
|
|
40554
|
+
row-gap: var(--vela-space-1);
|
|
40555
|
+
}
|
|
40556
|
+
/* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
|
|
40557
|
+
* never does, so its overflow is what fit() measures to keep descending the ladder. */
|
|
40558
|
+
.vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
|
|
40559
|
+
.vela-statusline.vela-sl-stacked .vela-sl-meta {
|
|
40560
|
+
min-width: 0;
|
|
40561
|
+
overflow: hidden;
|
|
40562
|
+
text-overflow: ellipsis;
|
|
40563
|
+
}
|
|
40062
40564
|
.vela-statusline .vela-sl-avatar {
|
|
40063
40565
|
width: 18px;
|
|
40064
40566
|
height: 18px;
|
|
@@ -40110,59 +40612,13 @@ var CSS21 = `
|
|
|
40110
40612
|
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
40111
40613
|
* chart on the page, including statusline-less ones. */
|
|
40112
40614
|
.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); }
|
|
40615
|
+
/* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
|
|
40616
|
+
* host whenever the ladder lands on a stacked layout. */
|
|
40617
|
+
.vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40618
|
+
/* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
|
|
40619
|
+
* of the readout fits, exactly as on a narrow desktop chart. */
|
|
40620
|
+
[data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
|
|
40621
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
|
|
40166
40622
|
`;
|
|
40167
40623
|
function baseOfTicker(ticker) {
|
|
40168
40624
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -40233,6 +40689,8 @@ var Statusline = class {
|
|
|
40233
40689
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
40234
40690
|
this.fitMode = false;
|
|
40235
40691
|
this.fitRO = null;
|
|
40692
|
+
/** The ladder rung currently applied — see {@link fit}. */
|
|
40693
|
+
this.layout = { stacked: false, level: "full" };
|
|
40236
40694
|
this.onContextMenu = (e) => {
|
|
40237
40695
|
if (!this.menu || !this.menuHooks) return;
|
|
40238
40696
|
e.preventDefault();
|
|
@@ -40279,8 +40737,18 @@ var Statusline = class {
|
|
|
40279
40737
|
this.menuHooks?.setChartVisible(true);
|
|
40280
40738
|
this.setChartHidden(false);
|
|
40281
40739
|
});
|
|
40282
|
-
this.
|
|
40740
|
+
this.identityRow = doc.createElement("span");
|
|
40741
|
+
this.identityRow.className = "vela-sl-row vela-sl-identity";
|
|
40742
|
+
this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
|
|
40743
|
+
this.valuesRow = doc.createElement("span");
|
|
40744
|
+
this.valuesRow.className = "vela-sl-row vela-sl-values";
|
|
40745
|
+
this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
|
|
40746
|
+
this.el.append(this.identityRow, this.valuesRow);
|
|
40283
40747
|
host.appendChild(this.el);
|
|
40748
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40749
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40750
|
+
this.fitRO.observe(host);
|
|
40751
|
+
}
|
|
40284
40752
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
40285
40753
|
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
40286
40754
|
this.setMarketStatus("open");
|
|
@@ -40296,27 +40764,15 @@ var Statusline = class {
|
|
|
40296
40764
|
this.fit();
|
|
40297
40765
|
}
|
|
40298
40766
|
/**
|
|
40299
|
-
* Multi-chart cells:
|
|
40300
|
-
*
|
|
40301
|
-
*
|
|
40302
|
-
* market badge; the logo + ticker always stay.
|
|
40767
|
+
* Multi-chart cells: the same width ladder as a single chart, plus a last resort for
|
|
40768
|
+
* cells too narrow for even its bottom rung — whatever still overflows is hidden
|
|
40769
|
+
* outright rather than clipped mid-glyph, least important first: the values, the
|
|
40770
|
+
* venue/timeframe meta, then the market badge; the logo + ticker always stay.
|
|
40303
40771
|
*/
|
|
40304
40772
|
setFitMode(on) {
|
|
40305
40773
|
if (on === this.fitMode) return;
|
|
40306
40774
|
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
|
-
}
|
|
40775
|
+
this.fit();
|
|
40320
40776
|
}
|
|
40321
40777
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
40322
40778
|
syncParts() {
|
|
@@ -40328,22 +40784,42 @@ var Statusline = class {
|
|
|
40328
40784
|
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
40329
40785
|
this.changeEl.style.display = seg.change ? "" : "none";
|
|
40330
40786
|
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
40787
|
+
this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
|
|
40788
|
+
}
|
|
40789
|
+
/** Apply one ladder rung: the stacked/inline shape and the readout's level. */
|
|
40790
|
+
applyLayout(layout) {
|
|
40791
|
+
this.layout = layout;
|
|
40792
|
+
this.el.classList.toggle("vela-sl-stacked", layout.stacked);
|
|
40793
|
+
this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
|
|
40794
|
+
this.renderValues();
|
|
40331
40795
|
}
|
|
40332
|
-
/**
|
|
40796
|
+
/**
|
|
40797
|
+
* Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
|
|
40798
|
+
* max-width: the full one-line readout, then the values stacked under the symbol
|
|
40799
|
+
* line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
|
|
40800
|
+
* still overflows, whole segments hide, least important first. Without layout
|
|
40801
|
+
* (detached host, node tests) nothing overflows, so the widest rung stays.
|
|
40802
|
+
*/
|
|
40333
40803
|
fit() {
|
|
40334
|
-
if (!this.fitMode) return;
|
|
40335
40804
|
this.syncParts();
|
|
40336
40805
|
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
40806
|
+
const overflows = () => this.el.scrollWidth > this.el.clientWidth;
|
|
40807
|
+
const hasValues = seg.ohlc || seg.change;
|
|
40808
|
+
for (const rung of STATUSLINE_LADDER) {
|
|
40809
|
+
this.applyLayout(rung);
|
|
40810
|
+
if (!hasValues || !overflows()) break;
|
|
40811
|
+
}
|
|
40812
|
+
if (!this.fitMode) return;
|
|
40337
40813
|
const order = [
|
|
40338
|
-
[this.
|
|
40339
|
-
[this.changeEl, seg.change],
|
|
40814
|
+
[this.valuesRow, hasValues],
|
|
40340
40815
|
[this.metaEl, seg.meta],
|
|
40341
40816
|
[this.marketEl, seg.market]
|
|
40342
40817
|
];
|
|
40343
40818
|
for (const [el, shown] of order) {
|
|
40344
|
-
if (
|
|
40819
|
+
if (!overflows()) break;
|
|
40345
40820
|
if (shown) el.style.display = "none";
|
|
40346
40821
|
}
|
|
40822
|
+
if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
|
|
40347
40823
|
}
|
|
40348
40824
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
40349
40825
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -40456,7 +40932,7 @@ var Statusline = class {
|
|
|
40456
40932
|
this.marketTip.destroy();
|
|
40457
40933
|
this.eyeTip.destroy();
|
|
40458
40934
|
this.marketBubble.destroy();
|
|
40459
|
-
this.host.classList.remove("vela-has-statusline", "vela-sl-
|
|
40935
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
|
|
40460
40936
|
this.el.remove();
|
|
40461
40937
|
}
|
|
40462
40938
|
detach() {
|
|
@@ -40465,6 +40941,10 @@ var Statusline = class {
|
|
|
40465
40941
|
}
|
|
40466
40942
|
render() {
|
|
40467
40943
|
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
40944
|
+
this.fit();
|
|
40945
|
+
}
|
|
40946
|
+
/** Write the value readout for the current bar at the current ladder level. */
|
|
40947
|
+
renderValues() {
|
|
40468
40948
|
const bar = this.hoverBar ?? this.lastBar;
|
|
40469
40949
|
if (!bar) {
|
|
40470
40950
|
this.ohlcEl.replaceChildren();
|
|
@@ -40484,12 +40964,11 @@ var Statusline = class {
|
|
|
40484
40964
|
s.appendChild(b);
|
|
40485
40965
|
return s;
|
|
40486
40966
|
};
|
|
40487
|
-
|
|
40488
|
-
|
|
40489
|
-
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
40967
|
+
const { level } = this.layout;
|
|
40968
|
+
this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
|
|
40969
|
+
this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
|
|
40490
40970
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
40491
40971
|
this.changeEl.style.color = ink;
|
|
40492
|
-
this.fit();
|
|
40493
40972
|
}
|
|
40494
40973
|
};
|
|
40495
40974
|
|
|
@@ -42629,6 +43108,17 @@ function nextAutoCellId(taken) {
|
|
|
42629
43108
|
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
42630
43109
|
}
|
|
42631
43110
|
}
|
|
43111
|
+
function resolveTyping(key, open2) {
|
|
43112
|
+
if (open2.dialogs > 0) {
|
|
43113
|
+
if (key.length !== 1) return null;
|
|
43114
|
+
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
43115
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
43116
|
+
return null;
|
|
43117
|
+
}
|
|
43118
|
+
if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
43119
|
+
if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
43120
|
+
return null;
|
|
43121
|
+
}
|
|
42632
43122
|
var VelaWorkspace = class {
|
|
42633
43123
|
constructor(container, opts = {}) {
|
|
42634
43124
|
this.events = new TypedEventBus();
|
|
@@ -44262,17 +44752,14 @@ var VelaWorkspace = class {
|
|
|
44262
44752
|
}
|
|
44263
44753
|
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
44264
44754
|
routeTyping(ev) {
|
|
44265
|
-
if (this.destroyed
|
|
44755
|
+
if (this.destroyed) return;
|
|
44266
44756
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
44267
44757
|
if (isEditableTarget(ev)) return;
|
|
44268
|
-
const
|
|
44269
|
-
if (
|
|
44270
|
-
|
|
44271
|
-
|
|
44272
|
-
|
|
44273
|
-
ev.preventDefault();
|
|
44274
|
-
this.tfQuick.open(key);
|
|
44275
|
-
}
|
|
44758
|
+
const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
|
|
44759
|
+
if (!hit) return;
|
|
44760
|
+
ev.preventDefault();
|
|
44761
|
+
if (hit.target === "symbol") this.symbolPicker.type(hit.text);
|
|
44762
|
+
else this.tfQuick.type(hit.text);
|
|
44276
44763
|
}
|
|
44277
44764
|
trackDialog(open2) {
|
|
44278
44765
|
this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
|