@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/widget.cjs
CHANGED
|
@@ -5093,15 +5093,15 @@ function candleOverrideFor(style, bags) {
|
|
|
5093
5093
|
if (!hasOwnCandlePaint(style)) return null;
|
|
5094
5094
|
const bag = bags[style] ?? {};
|
|
5095
5095
|
const color = (v) => isColor(v) ? v : null;
|
|
5096
|
-
const
|
|
5096
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
5097
5097
|
return {
|
|
5098
5098
|
upColor: color(bag.candleUpColor),
|
|
5099
5099
|
downColor: color(bag.candleDownColor),
|
|
5100
|
-
bodyVisible:
|
|
5101
|
-
borderVisible:
|
|
5100
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
5101
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
5102
5102
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
5103
5103
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
5104
|
-
wickVisible:
|
|
5104
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
5105
5105
|
wickUpColor: color(bag.candleWickUpColor),
|
|
5106
5106
|
wickDownColor: color(bag.candleWickDownColor)
|
|
5107
5107
|
};
|
|
@@ -12088,6 +12088,9 @@ function paneLabel(pane, fallbackTitle) {
|
|
|
12088
12088
|
function indicatorLabel(i, fallbackTitle) {
|
|
12089
12089
|
return i.title || fallbackTitle(i.id) || i.id;
|
|
12090
12090
|
}
|
|
12091
|
+
function indicatorRowLabel(i, fallbackTitle) {
|
|
12092
|
+
return i.shorttitle || indicatorLabel(i, fallbackTitle);
|
|
12093
|
+
}
|
|
12091
12094
|
function paneRows(pane) {
|
|
12092
12095
|
return pane.items.flatMap((it) => it.kind === "row" ? [it.row] : []);
|
|
12093
12096
|
}
|
|
@@ -12219,7 +12222,7 @@ function paneItems(snap, pane, units) {
|
|
|
12219
12222
|
const indicatorRow = (i) => ({
|
|
12220
12223
|
kind: "indicator",
|
|
12221
12224
|
id: i.id,
|
|
12222
|
-
label:
|
|
12225
|
+
label: indicatorRowLabel(i, snap.handleTitle),
|
|
12223
12226
|
visible: snap.indicatorVisible(i.id),
|
|
12224
12227
|
ownScale: i.ownScale
|
|
12225
12228
|
});
|
|
@@ -12632,9 +12635,13 @@ var CSS6 = `
|
|
|
12632
12635
|
.vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
|
|
12633
12636
|
|
|
12634
12637
|
/* \u2500\u2500 drawing groups \u2500\u2500 */
|
|
12635
|
-
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block.
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
+
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
|
|
12639
|
+
border of its own \u2014 units stack flush, so consecutive highlighted rows read as one block; the
|
|
12640
|
+
drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
|
|
12641
|
+
.vela-ot-unit { border-radius: 6px; }
|
|
12642
|
+
/* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
|
|
12643
|
+
.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; }
|
|
12644
|
+
.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; }
|
|
12638
12645
|
.vela-ot-row[data-row-kind='group'] > .vela-icon { width: 12px; font-size: 11px; }
|
|
12639
12646
|
/* A member sits indented under its group's header. */
|
|
12640
12647
|
.vela-ot-subrow { padding-left: 26px; }
|
|
@@ -12691,8 +12698,10 @@ var CSS6 = `
|
|
|
12691
12698
|
.vela-ot-gap[data-drop] { height: 4px; background: var(--vela-fg-bright); }
|
|
12692
12699
|
.vela-ot-gap[data-drop]::before { display: none; }
|
|
12693
12700
|
/* A whole container accepts the drop: merging into a pane, or a pane with no drawings yet.
|
|
12694
|
-
The pane block's transparent border reserves the room for this outline
|
|
12701
|
+
The pane block's transparent border reserves the room for this outline; a group unit has no
|
|
12702
|
+
border to color, so it draws the same line as an inset outline (no layout footprint). */
|
|
12695
12703
|
.vela-ot [data-drop='target'] { border-color: var(--vela-fg-bright); background: var(--vela-hover); }
|
|
12704
|
+
.vela-ot-unit[data-drop='target'] { outline: 1px solid var(--vela-fg-bright); outline-offset: -1px; }
|
|
12696
12705
|
/* Where a reorder would insert. */
|
|
12697
12706
|
.vela-ot [data-drop='before'] { box-shadow: inset 0 2px 0 var(--vela-fg-bright); }
|
|
12698
12707
|
.vela-ot [data-drop='after'] { box-shadow: inset 0 -2px 0 var(--vela-fg-bright); }
|
|
@@ -12755,7 +12764,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
12755
12764
|
super(host, "Object tree", "vela-ot");
|
|
12756
12765
|
this.iconFor = iconFor;
|
|
12757
12766
|
this.chart = null;
|
|
12758
|
-
|
|
12767
|
+
/** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
|
|
12768
|
+
this.selectedDrawings = /* @__PURE__ */ new Set();
|
|
12759
12769
|
this.symbolName = "";
|
|
12760
12770
|
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
12761
12771
|
this.symbolRaw = "";
|
|
@@ -12806,12 +12816,9 @@ var ObjectTree = class extends SidePanel {
|
|
|
12806
12816
|
this.unsubs.push(chart.on(ev, () => this.refresh()));
|
|
12807
12817
|
}
|
|
12808
12818
|
this.unsubs.push(
|
|
12809
|
-
chart.on("drawing:selected", ({
|
|
12810
|
-
this.
|
|
12811
|
-
if (!this.syncingSelection)
|
|
12812
|
-
this.picked.clear();
|
|
12813
|
-
if (id) this.picked.add(id);
|
|
12814
|
-
}
|
|
12819
|
+
chart.on("drawing:selected", ({ ids }) => {
|
|
12820
|
+
this.selectedDrawings = new Set(ids);
|
|
12821
|
+
if (!this.syncingSelection) this.picked = new Set(ids);
|
|
12815
12822
|
this.refresh();
|
|
12816
12823
|
})
|
|
12817
12824
|
);
|
|
@@ -13159,7 +13166,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
13159
13166
|
el.dataset.drag = "1";
|
|
13160
13167
|
el.title = "Drag to restack, or onto another pane to move it there";
|
|
13161
13168
|
if (inGroup) el.classList.add("vela-ot-subrow");
|
|
13162
|
-
if (d.id
|
|
13169
|
+
if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
|
|
13163
13170
|
if (this.picked.has(d.id)) el.dataset.picked = "1";
|
|
13164
13171
|
el.addEventListener("click", (e) => this.onDrawClick(e, d.id));
|
|
13165
13172
|
return el;
|
|
@@ -14336,6 +14343,20 @@ var SymbolPicker = class {
|
|
|
14336
14343
|
this.seed = initialQuery;
|
|
14337
14344
|
this.dialog.show();
|
|
14338
14345
|
}
|
|
14346
|
+
get isOpen() {
|
|
14347
|
+
return this.dialog.open;
|
|
14348
|
+
}
|
|
14349
|
+
/** Text typed at the picker that never reached its field: closed, it opens seeded
|
|
14350
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14351
|
+
* fast typist's next key lands in that tick), it extends the query. */
|
|
14352
|
+
type(text) {
|
|
14353
|
+
if (!this.isOpen) {
|
|
14354
|
+
this.open(text);
|
|
14355
|
+
return;
|
|
14356
|
+
}
|
|
14357
|
+
this.input.value = (this.input.value + text).toUpperCase();
|
|
14358
|
+
this.refresh();
|
|
14359
|
+
}
|
|
14339
14360
|
close() {
|
|
14340
14361
|
this.dialog.hide();
|
|
14341
14362
|
}
|
|
@@ -14749,6 +14770,20 @@ var TimeframeQuick = class {
|
|
|
14749
14770
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
14750
14771
|
}, 0);
|
|
14751
14772
|
}
|
|
14773
|
+
get isOpen() {
|
|
14774
|
+
return this.dialog.open;
|
|
14775
|
+
}
|
|
14776
|
+
/** Text typed at the entry that never reached its field: closed, it opens seeded
|
|
14777
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14778
|
+
* fast typist's next key lands in that tick), it extends the entry. */
|
|
14779
|
+
type(text) {
|
|
14780
|
+
if (!this.isOpen) {
|
|
14781
|
+
this.open(text);
|
|
14782
|
+
return;
|
|
14783
|
+
}
|
|
14784
|
+
this.input.value += text;
|
|
14785
|
+
this.renderHint();
|
|
14786
|
+
}
|
|
14752
14787
|
close() {
|
|
14753
14788
|
this.dialog.hide();
|
|
14754
14789
|
}
|
|
@@ -16819,15 +16854,18 @@ var DrawingController = class {
|
|
|
16819
16854
|
next = [...ids];
|
|
16820
16855
|
}
|
|
16821
16856
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
16857
|
+
this.announceSelection();
|
|
16858
|
+
}
|
|
16859
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
16860
|
+
announceSelection() {
|
|
16822
16861
|
this.port?.setSelection(this.selectedIds);
|
|
16823
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16862
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
16824
16863
|
}
|
|
16825
16864
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
16826
16865
|
restoreSnapshot(doc) {
|
|
16827
16866
|
this.store.load(doc);
|
|
16828
16867
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
16829
|
-
this.
|
|
16830
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16868
|
+
this.announceSelection();
|
|
16831
16869
|
}
|
|
16832
16870
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
16833
16871
|
deleteMany(ids) {
|
|
@@ -16960,6 +16998,9 @@ var DrawingController = class {
|
|
|
16960
16998
|
case "duplicate":
|
|
16961
16999
|
this.duplicate(i.ids);
|
|
16962
17000
|
break;
|
|
17001
|
+
case "clone":
|
|
17002
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
17003
|
+
break;
|
|
16963
17004
|
case "copy":
|
|
16964
17005
|
this.copy(i.ids);
|
|
16965
17006
|
break;
|
|
@@ -19912,7 +19953,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19912
19953
|
if (!model) continue;
|
|
19913
19954
|
const paneId = model.paneId ?? "price";
|
|
19914
19955
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19915
|
-
byPane.get(paneId).push({
|
|
19956
|
+
byPane.get(paneId).push({
|
|
19957
|
+
id: r.id,
|
|
19958
|
+
title: model.title || r.title,
|
|
19959
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
19960
|
+
ownScale: model.ownScale === true
|
|
19961
|
+
});
|
|
19916
19962
|
}
|
|
19917
19963
|
const panes = [];
|
|
19918
19964
|
for (const paneId of this.paneOrder) {
|
|
@@ -24545,7 +24591,7 @@ var InputController = class {
|
|
|
24545
24591
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24546
24592
|
if (Number.isNaN(this.cursorX)) return;
|
|
24547
24593
|
if (this.dragging && this.region !== "drawing") return;
|
|
24548
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24594
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24549
24595
|
};
|
|
24550
24596
|
this.onDown = (e) => {
|
|
24551
24597
|
if (e.button === 1) {
|
|
@@ -24577,7 +24623,7 @@ var InputController = class {
|
|
|
24577
24623
|
this.startY = y;
|
|
24578
24624
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24579
24625
|
this.region = "drawing";
|
|
24580
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24626
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24581
24627
|
this.capture(e.pointerId);
|
|
24582
24628
|
return;
|
|
24583
24629
|
}
|
|
@@ -24586,6 +24632,11 @@ var InputController = class {
|
|
|
24586
24632
|
this.capture(e.pointerId);
|
|
24587
24633
|
return;
|
|
24588
24634
|
}
|
|
24635
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24636
|
+
this.region = "drawing";
|
|
24637
|
+
this.capture(e.pointerId);
|
|
24638
|
+
return;
|
|
24639
|
+
}
|
|
24589
24640
|
const vp = this.deps.getCoords().getViewport();
|
|
24590
24641
|
this.deps.apply(vp);
|
|
24591
24642
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24634,7 +24685,7 @@ var InputController = class {
|
|
|
24634
24685
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24635
24686
|
} else if (this.region === "drawing") {
|
|
24636
24687
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
24637
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24688
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24638
24689
|
} else {
|
|
24639
24690
|
const coords = this.deps.getCoords();
|
|
24640
24691
|
const vp = coords.getViewport();
|
|
@@ -24657,7 +24708,7 @@ var InputController = class {
|
|
|
24657
24708
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24658
24709
|
const r = this.regionAt(x, y);
|
|
24659
24710
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
24660
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24711
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24661
24712
|
}
|
|
24662
24713
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24663
24714
|
};
|
|
@@ -24998,12 +25049,12 @@ function defaultTradeMarkersState() {
|
|
|
24998
25049
|
function mergeTradeMarkersState(base, patch) {
|
|
24999
25050
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
25000
25051
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
25001
|
-
const
|
|
25052
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
25002
25053
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
25003
25054
|
return {
|
|
25004
|
-
visible:
|
|
25005
|
-
labels:
|
|
25006
|
-
qty:
|
|
25055
|
+
visible: bool2(p.visible, base.visible),
|
|
25056
|
+
labels: bool2(p.labels, base.labels),
|
|
25057
|
+
qty: bool2(p.qty, base.qty),
|
|
25007
25058
|
colors: {
|
|
25008
25059
|
long: color(c.long, base.colors.long),
|
|
25009
25060
|
short: color(c.short, base.colors.short),
|
|
@@ -28863,6 +28914,18 @@ var DrawingPainter = class {
|
|
|
28863
28914
|
}
|
|
28864
28915
|
this.paintHandles(ctx, pts);
|
|
28865
28916
|
}
|
|
28917
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
28918
|
+
* accent under a thin dashed outline. */
|
|
28919
|
+
paintMarquee(ctx, rect) {
|
|
28920
|
+
ctx.save();
|
|
28921
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
28922
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
28923
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
28924
|
+
ctx.lineWidth = 1;
|
|
28925
|
+
ctx.setLineDash([4, 3]);
|
|
28926
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
28927
|
+
ctx.restore();
|
|
28928
|
+
}
|
|
28866
28929
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
28867
28930
|
paintSnapRing(ctx, x, y, theme) {
|
|
28868
28931
|
ctx.setLineDash([]);
|
|
@@ -30331,6 +30394,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
30331
30394
|
}
|
|
30332
30395
|
return null;
|
|
30333
30396
|
}
|
|
30397
|
+
function deletableSelection(selected, drawings) {
|
|
30398
|
+
const ids = [...selected];
|
|
30399
|
+
if (ids.length < 2) return ids;
|
|
30400
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30401
|
+
}
|
|
30402
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30403
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30404
|
+
return hit.locked ? [] : [hit.id];
|
|
30405
|
+
}
|
|
30334
30406
|
|
|
30335
30407
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30336
30408
|
var DRAG_SLOP3 = 3;
|
|
@@ -30434,10 +30506,32 @@ var DrawingInteraction = class {
|
|
|
30434
30506
|
activeDragId() {
|
|
30435
30507
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30436
30508
|
}
|
|
30509
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30510
|
+
pressedId() {
|
|
30511
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30512
|
+
}
|
|
30513
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30514
|
+
dragClones() {
|
|
30515
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30516
|
+
}
|
|
30517
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30518
|
+
marqueeRect() {
|
|
30519
|
+
if (this.state.kind !== "marquee") return null;
|
|
30520
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30521
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30522
|
+
}
|
|
30523
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30524
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30525
|
+
beginMarquee(x, y) {
|
|
30526
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30527
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30528
|
+
return true;
|
|
30529
|
+
}
|
|
30437
30530
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30438
30531
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
30439
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
30440
|
-
|
|
30532
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30533
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30534
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
30441
30535
|
const proj = this.deps.projector();
|
|
30442
30536
|
if (this.state.kind === "placing") {
|
|
30443
30537
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30465,7 +30559,50 @@ var DrawingInteraction = class {
|
|
|
30465
30559
|
return;
|
|
30466
30560
|
}
|
|
30467
30561
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30468
|
-
|
|
30562
|
+
const riders = [];
|
|
30563
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30564
|
+
for (const id of this.deps.selectedIds()) {
|
|
30565
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30566
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30567
|
+
}
|
|
30568
|
+
}
|
|
30569
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30570
|
+
}
|
|
30571
|
+
}
|
|
30572
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30573
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30574
|
+
bodyDelta(paneId, x, y) {
|
|
30575
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30576
|
+
const proj = this.deps.projector();
|
|
30577
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30578
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30579
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30580
|
+
}
|
|
30581
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30582
|
+
dragSources(d) {
|
|
30583
|
+
if (this.state.kind !== "pressed") return [];
|
|
30584
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30585
|
+
for (const r of this.state.riders) {
|
|
30586
|
+
const rd = this.byId(r.id);
|
|
30587
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30588
|
+
}
|
|
30589
|
+
return out;
|
|
30590
|
+
}
|
|
30591
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30592
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30593
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30594
|
+
dragBody(d, x, y) {
|
|
30595
|
+
if (this.state.kind !== "pressed") return;
|
|
30596
|
+
this.snapAt = null;
|
|
30597
|
+
if (this.state.mod && !this.state.clones) {
|
|
30598
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30599
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30600
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30601
|
+
});
|
|
30602
|
+
}
|
|
30603
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30604
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30605
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30469
30606
|
}
|
|
30470
30607
|
}
|
|
30471
30608
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30489,12 +30626,20 @@ var DrawingInteraction = class {
|
|
|
30489
30626
|
this.deps.changed();
|
|
30490
30627
|
return;
|
|
30491
30628
|
}
|
|
30629
|
+
if (this.state.kind === "marquee") {
|
|
30630
|
+
this.state.x1 = x;
|
|
30631
|
+
this.state.y1 = y;
|
|
30632
|
+
this.deps.changed();
|
|
30633
|
+
return;
|
|
30634
|
+
}
|
|
30492
30635
|
if (this.state.kind !== "pressed") return;
|
|
30493
30636
|
const d = this.byId(this.state.id);
|
|
30494
30637
|
if (!d || d.locked) return;
|
|
30495
30638
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
|
|
30496
30639
|
this.state.moved = true;
|
|
30497
|
-
if (this.state.handle
|
|
30640
|
+
if (this.state.handle < 0) {
|
|
30641
|
+
this.dragBody(d, x, y);
|
|
30642
|
+
} else {
|
|
30498
30643
|
const a = d.anchors[this.state.handle];
|
|
30499
30644
|
if (a) {
|
|
30500
30645
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30502,12 +30647,6 @@ var DrawingInteraction = class {
|
|
|
30502
30647
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30503
30648
|
}
|
|
30504
30649
|
d.constrainHandleDrag(this.state.handle);
|
|
30505
|
-
} else {
|
|
30506
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
30507
|
-
this.snapAt = null;
|
|
30508
|
-
const dt = pt.time - this.state.grab.time;
|
|
30509
|
-
const dp = pt.price - this.state.grab.price;
|
|
30510
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
30511
30650
|
}
|
|
30512
30651
|
this.deps.changed();
|
|
30513
30652
|
}
|
|
@@ -30527,16 +30666,46 @@ var DrawingInteraction = class {
|
|
|
30527
30666
|
}
|
|
30528
30667
|
return;
|
|
30529
30668
|
}
|
|
30669
|
+
if (this.state.kind === "marquee") {
|
|
30670
|
+
const rect = this.marqueeRect();
|
|
30671
|
+
this.state = { kind: "idle" };
|
|
30672
|
+
if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
|
|
30673
|
+
this.deps.changed();
|
|
30674
|
+
return;
|
|
30675
|
+
}
|
|
30530
30676
|
if (this.state.kind !== "pressed") return;
|
|
30531
|
-
const { id, moved } = this.state;
|
|
30677
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30532
30678
|
this.state = { kind: "idle" };
|
|
30533
30679
|
const d = this.byId(id);
|
|
30534
|
-
if (moved) {
|
|
30535
|
-
if (
|
|
30536
|
-
this.deps.
|
|
30537
|
-
|
|
30538
|
-
this.deps.openSettings(id, x, y);
|
|
30680
|
+
if (!moved) {
|
|
30681
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30682
|
+
else this.deps.openSettings(id, x, y);
|
|
30683
|
+
return;
|
|
30539
30684
|
}
|
|
30685
|
+
if (clones) {
|
|
30686
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30687
|
+
} else if (d) {
|
|
30688
|
+
const docs = [d.serialize()];
|
|
30689
|
+
for (const r of riders) {
|
|
30690
|
+
const rd = this.byId(r.id);
|
|
30691
|
+
if (rd) docs.push(rd.serialize());
|
|
30692
|
+
}
|
|
30693
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30694
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30695
|
+
}
|
|
30696
|
+
this.deps.changed();
|
|
30697
|
+
}
|
|
30698
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30699
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30700
|
+
selectWithin(rect) {
|
|
30701
|
+
const proj = this.deps.projector();
|
|
30702
|
+
const ids = [...this.deps.selectedIds()];
|
|
30703
|
+
for (const d of this.deps.drawings()) {
|
|
30704
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30705
|
+
const b = d.bounds(proj);
|
|
30706
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30707
|
+
}
|
|
30708
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30540
30709
|
}
|
|
30541
30710
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30542
30711
|
cancel() {
|
|
@@ -30549,8 +30718,15 @@ var DrawingInteraction = class {
|
|
|
30549
30718
|
return true;
|
|
30550
30719
|
}
|
|
30551
30720
|
if (this.state.kind === "pressed") {
|
|
30552
|
-
|
|
30553
|
-
|
|
30721
|
+
if (this.state.moved && !this.state.clones) {
|
|
30722
|
+
const d = this.byId(this.state.id);
|
|
30723
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30724
|
+
}
|
|
30725
|
+
this.state = { kind: "idle" };
|
|
30726
|
+
this.deps.changed();
|
|
30727
|
+
return true;
|
|
30728
|
+
}
|
|
30729
|
+
if (this.state.kind === "marquee") {
|
|
30554
30730
|
this.state = { kind: "idle" };
|
|
30555
30731
|
this.deps.changed();
|
|
30556
30732
|
return true;
|
|
@@ -30665,6 +30841,9 @@ var DrawingInteraction = class {
|
|
|
30665
30841
|
function cloneAnchors(d) {
|
|
30666
30842
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30667
30843
|
}
|
|
30844
|
+
function rectsIntersect(a, b) {
|
|
30845
|
+
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;
|
|
30846
|
+
}
|
|
30668
30847
|
function applyFree(anchor, pt, free) {
|
|
30669
30848
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
30670
30849
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -31050,10 +31229,27 @@ function btn(label, primary, onClick) {
|
|
|
31050
31229
|
}
|
|
31051
31230
|
|
|
31052
31231
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31232
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31233
|
+
function sharedPaths(drawings) {
|
|
31234
|
+
const [first, ...rest] = drawings;
|
|
31235
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
31236
|
+
for (const d of rest) {
|
|
31237
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31238
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31239
|
+
}
|
|
31240
|
+
return paths;
|
|
31241
|
+
}
|
|
31242
|
+
function commonValue(drawings, read) {
|
|
31243
|
+
const first = read(drawings[0]);
|
|
31244
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
31245
|
+
}
|
|
31246
|
+
function distinctValues(drawings, read) {
|
|
31247
|
+
return [...new Set(drawings.map(read))];
|
|
31248
|
+
}
|
|
31053
31249
|
var BTN = 30;
|
|
31054
31250
|
var ICON = 21;
|
|
31055
31251
|
var STYLE_ID23 = "vela-drawing-popup-styles";
|
|
31056
|
-
var STYLE_REV = "
|
|
31252
|
+
var STYLE_REV = "4";
|
|
31057
31253
|
function ensureStyles5() {
|
|
31058
31254
|
if (typeof document === "undefined") return;
|
|
31059
31255
|
const existing = document.getElementById(STYLE_ID23);
|
|
@@ -31065,6 +31261,8 @@ function ensureStyles5() {
|
|
|
31065
31261
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
31066
31262
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
31067
31263
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31264
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31265
|
+
.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;}
|
|
31068
31266
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
31069
31267
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
31070
31268
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -31096,7 +31294,7 @@ var DrawingSettingsPopup = class {
|
|
|
31096
31294
|
if (this.isOwnChrome(node)) return;
|
|
31097
31295
|
const cb = this.onClose;
|
|
31098
31296
|
this.close();
|
|
31099
|
-
cb?.();
|
|
31297
|
+
cb?.(e);
|
|
31100
31298
|
};
|
|
31101
31299
|
this.theme = theme;
|
|
31102
31300
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31121,16 +31319,27 @@ var DrawingSettingsPopup = class {
|
|
|
31121
31319
|
contains(node) {
|
|
31122
31320
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31123
31321
|
}
|
|
31124
|
-
/** Open the quick toolbar for `
|
|
31125
|
-
* `
|
|
31126
|
-
|
|
31322
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31323
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31324
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31325
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31326
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31327
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31328
|
+
* modifier press (multi-select) from a plain one. */
|
|
31329
|
+
open(drawings, anchor, actions, onClose) {
|
|
31127
31330
|
this.close();
|
|
31331
|
+
const drawing = drawings[0];
|
|
31332
|
+
if (!drawing) return;
|
|
31128
31333
|
ensureStyles5();
|
|
31129
31334
|
this.onClose = onClose ?? null;
|
|
31130
31335
|
const t = this.theme;
|
|
31336
|
+
const multi = drawings.length > 1;
|
|
31131
31337
|
const schema = drawing.schema();
|
|
31132
|
-
const paths =
|
|
31133
|
-
const
|
|
31338
|
+
const paths = sharedPaths(drawings);
|
|
31339
|
+
const common = (read) => commonValue(drawings, read);
|
|
31340
|
+
const every = (pred) => drawings.every(pred);
|
|
31341
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31342
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31134
31343
|
const el = document.createElement("div");
|
|
31135
31344
|
el.className = "vela-dpop";
|
|
31136
31345
|
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;`;
|
|
@@ -31144,75 +31353,84 @@ var DrawingSettingsPopup = class {
|
|
|
31144
31353
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31145
31354
|
bar.appendChild(this.dragHandle());
|
|
31146
31355
|
if (paths.has("glyph")) {
|
|
31147
|
-
const cur =
|
|
31356
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31148
31357
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31149
31358
|
}
|
|
31150
31359
|
if (paths.has("size")) {
|
|
31151
|
-
const sz =
|
|
31360
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31152
31361
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31153
31362
|
}
|
|
31154
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31363
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31364
|
+
const mag = (d) => d;
|
|
31155
31365
|
const options = this.lowerTimeframeOptions();
|
|
31156
31366
|
if (options.length > 0) {
|
|
31157
31367
|
bar.appendChild(
|
|
31158
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31368
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31159
31369
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31160
31370
|
labelInTrigger: true
|
|
31161
31371
|
})
|
|
31162
31372
|
);
|
|
31163
31373
|
}
|
|
31164
|
-
bar.appendChild(
|
|
31165
|
-
bar.appendChild(
|
|
31374
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31375
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31166
31376
|
}
|
|
31167
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31377
|
+
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"));
|
|
31168
31378
|
if (paths.has("style.lineWidth")) {
|
|
31169
|
-
const
|
|
31170
|
-
|
|
31171
|
-
|
|
31379
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31380
|
+
const wf = widthField2(drawing);
|
|
31381
|
+
const width = common((d) => d.style.lineWidth);
|
|
31382
|
+
const markers = every((d) => {
|
|
31383
|
+
const f = widthField2(d);
|
|
31384
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31385
|
+
});
|
|
31386
|
+
if (markers && wf?.kind === "number") {
|
|
31387
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31172
31388
|
} else {
|
|
31173
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31389
|
+
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 }));
|
|
31174
31390
|
}
|
|
31175
31391
|
}
|
|
31176
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
31177
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
31392
|
+
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 }));
|
|
31393
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
31178
31394
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31179
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31395
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31180
31396
|
if (isPosition) {
|
|
31181
|
-
const pos =
|
|
31182
|
-
bar.appendChild(
|
|
31183
|
-
bar.appendChild(
|
|
31397
|
+
const pos = (d) => d;
|
|
31398
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31399
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31184
31400
|
}
|
|
31185
|
-
const
|
|
31186
|
-
if (paths.has("reg.midColor") &&
|
|
31401
|
+
const regOf = (d) => d.reg;
|
|
31402
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31403
|
+
const reg = (d) => regOf(d);
|
|
31187
31404
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31188
|
-
bar.appendChild(
|
|
31189
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31190
|
-
bar.appendChild(
|
|
31191
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31192
|
-
bar.appendChild(
|
|
31193
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31194
|
-
bar.appendChild(
|
|
31195
|
-
bar.appendChild(
|
|
31196
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
31197
|
-
}
|
|
31198
|
-
const
|
|
31199
|
-
if (paths.has("vwap.midColor") &&
|
|
31405
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31406
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31407
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31408
|
+
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 }));
|
|
31409
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31410
|
+
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 }));
|
|
31411
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31412
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31413
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31414
|
+
}
|
|
31415
|
+
const vwapOf = (d) => d.vwap;
|
|
31416
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31417
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31200
31418
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31201
31419
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
31202
|
-
bar.appendChild(
|
|
31203
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31420
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31421
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31204
31422
|
bar.appendChild(
|
|
31205
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31423
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31206
31424
|
label: (v) => `${v}\u03C3`,
|
|
31207
31425
|
labelInTrigger: true
|
|
31208
31426
|
})
|
|
31209
31427
|
);
|
|
31210
|
-
bar.appendChild(
|
|
31211
|
-
bar.appendChild(
|
|
31212
|
-
bar.appendChild(
|
|
31428
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31429
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31430
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31213
31431
|
}
|
|
31214
31432
|
if (paths.has("maxCurvature")) {
|
|
31215
|
-
const cur =
|
|
31433
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31216
31434
|
bar.appendChild(
|
|
31217
31435
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31218
31436
|
label: (v) => `n=${v}`,
|
|
@@ -31221,7 +31439,7 @@ var DrawingSettingsPopup = class {
|
|
|
31221
31439
|
);
|
|
31222
31440
|
}
|
|
31223
31441
|
if (paths.has("mach")) {
|
|
31224
|
-
const cur =
|
|
31442
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31225
31443
|
bar.appendChild(
|
|
31226
31444
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31227
31445
|
label: (v) => `M=${v}`,
|
|
@@ -31230,7 +31448,7 @@ var DrawingSettingsPopup = class {
|
|
|
31230
31448
|
);
|
|
31231
31449
|
}
|
|
31232
31450
|
if (paths.has("waveCount")) {
|
|
31233
|
-
const cur =
|
|
31451
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31234
31452
|
bar.appendChild(
|
|
31235
31453
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31236
31454
|
label: (v) => `${v}`,
|
|
@@ -31238,26 +31456,27 @@ var DrawingSettingsPopup = class {
|
|
|
31238
31456
|
})
|
|
31239
31457
|
);
|
|
31240
31458
|
}
|
|
31241
|
-
const
|
|
31242
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
31243
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
31244
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
31245
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
31246
|
-
|
|
31247
|
-
if (paths.has("text.
|
|
31248
|
-
if (paths.has("text.
|
|
31459
|
+
const flags = (d) => d;
|
|
31460
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31461
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31462
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31463
|
+
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 }));
|
|
31464
|
+
const textField = !multi && paths.has("text.value");
|
|
31465
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31466
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31467
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31249
31468
|
const editableLevels = drawing.editableLevels();
|
|
31250
|
-
if (editableLevels && !(
|
|
31251
|
-
const fib =
|
|
31469
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31470
|
+
const fib = (d) => d;
|
|
31252
31471
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
31253
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31254
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31472
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31473
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31255
31474
|
}
|
|
31256
31475
|
bar.appendChild(this.divider());
|
|
31257
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31258
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31259
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31260
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
31476
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31477
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31478
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31479
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31261
31480
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31262
31481
|
del.style.color = "var(--vela-danger)";
|
|
31263
31482
|
bar.appendChild(del);
|
|
@@ -31443,7 +31662,7 @@ var DrawingSettingsPopup = class {
|
|
|
31443
31662
|
});
|
|
31444
31663
|
return b;
|
|
31445
31664
|
}
|
|
31446
|
-
/** The trailing kebab overflow — z-order actions
|
|
31665
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31447
31666
|
kebabButton(actions) {
|
|
31448
31667
|
const b = this.base("More");
|
|
31449
31668
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31455,6 +31674,7 @@ var DrawingSettingsPopup = class {
|
|
|
31455
31674
|
return;
|
|
31456
31675
|
}
|
|
31457
31676
|
this.openActionMenu(b, [
|
|
31677
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31458
31678
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31459
31679
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31460
31680
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31611,26 +31831,30 @@ var DrawingSettingsPopup = class {
|
|
|
31611
31831
|
this.tipEl?.remove();
|
|
31612
31832
|
this.tipEl = null;
|
|
31613
31833
|
}
|
|
31614
|
-
|
|
31834
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
31835
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
31836
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
31837
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31615
31838
|
const b = this.base(tip);
|
|
31616
|
-
b.innerHTML = sized(icon2);
|
|
31617
31839
|
const set = (on2) => {
|
|
31618
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
31840
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
31841
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31619
31842
|
};
|
|
31620
31843
|
set(active);
|
|
31621
31844
|
let on = active;
|
|
31622
31845
|
b.addEventListener("click", () => {
|
|
31623
|
-
on = !on;
|
|
31846
|
+
on = on === MIXED ? true : !on;
|
|
31624
31847
|
set(on);
|
|
31625
31848
|
onChange(on);
|
|
31626
31849
|
});
|
|
31627
31850
|
return b;
|
|
31628
31851
|
}
|
|
31629
31852
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
31630
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
31853
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
31854
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31631
31855
|
widthInput(tip, value, min, max, step, onChange) {
|
|
31632
31856
|
const ni = new NumberInput({
|
|
31633
|
-
value,
|
|
31857
|
+
value: value === MIXED ? min : value,
|
|
31634
31858
|
min,
|
|
31635
31859
|
max,
|
|
31636
31860
|
step,
|
|
@@ -31642,13 +31866,18 @@ var DrawingSettingsPopup = class {
|
|
|
31642
31866
|
title: tip,
|
|
31643
31867
|
onChange
|
|
31644
31868
|
});
|
|
31869
|
+
if (value === MIXED) {
|
|
31870
|
+
ni.input.value = "";
|
|
31871
|
+
ni.input.placeholder = "\u2014";
|
|
31872
|
+
}
|
|
31645
31873
|
ni.el.dataset.tip = tip;
|
|
31646
31874
|
trapChartKeys(ni.el);
|
|
31647
31875
|
return ni.el;
|
|
31648
31876
|
}
|
|
31649
31877
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31650
31878
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
31651
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31879
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31880
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31652
31881
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31653
31882
|
const b = this.base(tip);
|
|
31654
31883
|
b.style.width = "auto";
|
|
@@ -31658,14 +31887,20 @@ var DrawingSettingsPopup = class {
|
|
|
31658
31887
|
let cur = current;
|
|
31659
31888
|
const paint = (v) => {
|
|
31660
31889
|
b.replaceChildren();
|
|
31661
|
-
|
|
31890
|
+
if (v === MIXED) {
|
|
31891
|
+
const dash = document.createElement("span");
|
|
31892
|
+
dash.textContent = "\u2014";
|
|
31893
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
31894
|
+
b.appendChild(dash);
|
|
31895
|
+
}
|
|
31896
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31662
31897
|
if (glyph) {
|
|
31663
31898
|
const ic = document.createElement("span");
|
|
31664
31899
|
ic.style.cssText = "display:flex;";
|
|
31665
31900
|
ic.innerHTML = sized(glyph);
|
|
31666
31901
|
b.appendChild(ic);
|
|
31667
31902
|
}
|
|
31668
|
-
if (opts.label && opts.labelInTrigger) {
|
|
31903
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31669
31904
|
const tx = document.createElement("span");
|
|
31670
31905
|
tx.textContent = opts.label(v);
|
|
31671
31906
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -31735,8 +31970,10 @@ var DrawingSettingsPopup = class {
|
|
|
31735
31970
|
}
|
|
31736
31971
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
31737
31972
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
31738
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
31739
|
-
|
|
31973
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
31974
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
31975
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
31976
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
31740
31977
|
const b = document.createElement("button");
|
|
31741
31978
|
b.type = "button";
|
|
31742
31979
|
b.dataset.tip = tip;
|
|
@@ -31746,13 +31983,14 @@ var DrawingSettingsPopup = class {
|
|
|
31746
31983
|
ic.style.cssText = "display:flex;";
|
|
31747
31984
|
ic.innerHTML = sized(icon2, iconSize);
|
|
31748
31985
|
const bar = document.createElement("span");
|
|
31749
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
31986
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
31750
31987
|
b.append(ic, bar);
|
|
31751
31988
|
let cur = color;
|
|
31752
31989
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
31753
31990
|
b.addEventListener("click", (e) => {
|
|
31754
31991
|
e.stopPropagation();
|
|
31755
|
-
|
|
31992
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
31993
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
31756
31994
|
cur = v;
|
|
31757
31995
|
bar.style.background = v;
|
|
31758
31996
|
onChange(v);
|
|
@@ -31761,20 +31999,42 @@ var DrawingSettingsPopup = class {
|
|
|
31761
31999
|
return b;
|
|
31762
32000
|
}
|
|
31763
32001
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
31764
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
32002
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
31765
32003
|
if (this.colorOwner === anchor) {
|
|
31766
32004
|
this.closeColorPopover();
|
|
31767
32005
|
return;
|
|
31768
32006
|
}
|
|
31769
|
-
this.openColorPopover(anchor, color, onChange);
|
|
32007
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
31770
32008
|
}
|
|
31771
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31772
|
-
|
|
32009
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
32010
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
32011
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
31773
32012
|
const t = this.theme;
|
|
31774
32013
|
this.colorPop = this.hostFloat(anchor, {
|
|
31775
32014
|
zIndex: 25,
|
|
31776
32015
|
padding: "10px",
|
|
31777
|
-
fill: (el) => {
|
|
32016
|
+
fill: (el, pop) => {
|
|
32017
|
+
if (used.length > 1) {
|
|
32018
|
+
const row = document.createElement("div");
|
|
32019
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
32020
|
+
const lbl = document.createElement("span");
|
|
32021
|
+
lbl.textContent = "In use";
|
|
32022
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
32023
|
+
row.appendChild(lbl);
|
|
32024
|
+
for (const c of used) {
|
|
32025
|
+
const sw = document.createElement("button");
|
|
32026
|
+
sw.type = "button";
|
|
32027
|
+
sw.dataset.tip = c;
|
|
32028
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
32029
|
+
sw.addEventListener("click", (e) => {
|
|
32030
|
+
e.stopPropagation();
|
|
32031
|
+
pop.hide();
|
|
32032
|
+
onChange(c);
|
|
32033
|
+
});
|
|
32034
|
+
row.appendChild(sw);
|
|
32035
|
+
}
|
|
32036
|
+
el.appendChild(row);
|
|
32037
|
+
}
|
|
31778
32038
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
31779
32039
|
}
|
|
31780
32040
|
});
|
|
@@ -31795,6 +32055,7 @@ var TYPE_ICON = icon("type");
|
|
|
31795
32055
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
31796
32056
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
31797
32057
|
var LOCK_ICON = icon("lock");
|
|
32058
|
+
var UNLOCK_ICON = icon("unlock");
|
|
31798
32059
|
var FRONT_ICON = icon("bring-front");
|
|
31799
32060
|
var BACK_ICON = icon("send-back");
|
|
31800
32061
|
var TRASH_ICON = icon("trash");
|
|
@@ -31802,6 +32063,7 @@ var BOLD_ICON = icon("bold");
|
|
|
31802
32063
|
var ITALIC_ICON = icon("italic");
|
|
31803
32064
|
var GRIP_ICON = icon("grip");
|
|
31804
32065
|
var KEBAB_ICON = icon("kebab");
|
|
32066
|
+
var CLONE_ICON = icon("clone");
|
|
31805
32067
|
var RESET_ICON = icon("reset");
|
|
31806
32068
|
var GEAR_ICON = icon("gear");
|
|
31807
32069
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -31810,6 +32072,12 @@ var BANDS_ICON = icon("bands");
|
|
|
31810
32072
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
31811
32073
|
var SONIC_ICON2 = icon("sonic");
|
|
31812
32074
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32075
|
+
function stripes(colors) {
|
|
32076
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32077
|
+
const step = 100 / bands.length;
|
|
32078
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32079
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32080
|
+
}
|
|
31813
32081
|
function lineIcon(width, style) {
|
|
31814
32082
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
31815
32083
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -32093,6 +32361,20 @@ var UserDrawingController = class {
|
|
|
32093
32361
|
// reported exactly when it actually changes).
|
|
32094
32362
|
this.textEditor = null;
|
|
32095
32363
|
this.painter = new DrawingPainter();
|
|
32364
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
32365
|
+
* shrinking selection does with it. */
|
|
32366
|
+
this.popupMulti = false;
|
|
32367
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
32368
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
32369
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
32370
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
32371
|
+
* whose bar just closed must stay selected. */
|
|
32372
|
+
this.onPopupDismissed = (e) => {
|
|
32373
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
32374
|
+
const pressed = this.interaction.pressedId();
|
|
32375
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
32376
|
+
this.clearSelection();
|
|
32377
|
+
};
|
|
32096
32378
|
this.ctx = canvas.getContext("2d");
|
|
32097
32379
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32098
32380
|
this.toolbar = new DrawingToolbar(
|
|
@@ -32269,6 +32551,12 @@ var UserDrawingController = class {
|
|
|
32269
32551
|
}
|
|
32270
32552
|
setSelection(ids) {
|
|
32271
32553
|
this.selectedIds = new Set(ids);
|
|
32554
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
32555
|
+
else if (this.popupMulti) {
|
|
32556
|
+
this.popupMulti = false;
|
|
32557
|
+
this.popup.close();
|
|
32558
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
32559
|
+
}
|
|
32272
32560
|
this.render();
|
|
32273
32561
|
}
|
|
32274
32562
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -32314,12 +32602,16 @@ var UserDrawingController = class {
|
|
|
32314
32602
|
});
|
|
32315
32603
|
}
|
|
32316
32604
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32317
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
32318
|
-
|
|
32605
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
32606
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
32607
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
32608
|
+
deleteAt(x, y, withSelection = false) {
|
|
32319
32609
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
32320
|
-
if (!hit
|
|
32610
|
+
if (!hit) return false;
|
|
32611
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
32612
|
+
if (ids.length === 0) return false;
|
|
32321
32613
|
this.popup.close();
|
|
32322
|
-
this.emit({ kind: "delete", ids
|
|
32614
|
+
this.emit({ kind: "delete", ids });
|
|
32323
32615
|
return true;
|
|
32324
32616
|
}
|
|
32325
32617
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -32345,7 +32637,14 @@ var UserDrawingController = class {
|
|
|
32345
32637
|
this.render();
|
|
32346
32638
|
}
|
|
32347
32639
|
}
|
|
32348
|
-
|
|
32640
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
32641
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
32642
|
+
beginMarqueeAt(x, y) {
|
|
32643
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
32644
|
+
this.render();
|
|
32645
|
+
return true;
|
|
32646
|
+
}
|
|
32647
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32349
32648
|
if (this.eraserMode) {
|
|
32350
32649
|
this.erasing = true;
|
|
32351
32650
|
this.deleteAt(x, y);
|
|
@@ -32365,9 +32664,9 @@ var UserDrawingController = class {
|
|
|
32365
32664
|
return;
|
|
32366
32665
|
}
|
|
32367
32666
|
}
|
|
32368
|
-
this.interaction.down(x, y, snap, shift2);
|
|
32667
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
32369
32668
|
}
|
|
32370
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
32669
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32371
32670
|
if (this.eraserMode) {
|
|
32372
32671
|
if (this.erasing) this.deleteAt(x, y);
|
|
32373
32672
|
return;
|
|
@@ -32379,12 +32678,14 @@ var UserDrawingController = class {
|
|
|
32379
32678
|
return;
|
|
32380
32679
|
}
|
|
32381
32680
|
this.interaction.move(x, y, snap, shift2);
|
|
32382
|
-
this.updateHover(x, y);
|
|
32681
|
+
this.updateHover(x, y, mod);
|
|
32383
32682
|
}
|
|
32384
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
32385
|
-
|
|
32683
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
32684
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
32685
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
32686
|
+
updateHover(x, y, mod = false) {
|
|
32386
32687
|
let id = null;
|
|
32387
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32688
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32388
32689
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
32389
32690
|
}
|
|
32390
32691
|
if (id !== this.hoveredId) {
|
|
@@ -32404,7 +32705,12 @@ var UserDrawingController = class {
|
|
|
32404
32705
|
this.render();
|
|
32405
32706
|
return;
|
|
32406
32707
|
}
|
|
32708
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
32407
32709
|
this.interaction.up(x, y);
|
|
32710
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
32711
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
32712
|
+
else this.openSettingsById(dragged, x, y);
|
|
32713
|
+
}
|
|
32408
32714
|
}
|
|
32409
32715
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
32410
32716
|
toggleMeasure() {
|
|
@@ -32669,7 +32975,7 @@ var UserDrawingController = class {
|
|
|
32669
32975
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
32670
32976
|
break;
|
|
32671
32977
|
case "delete": {
|
|
32672
|
-
const ids = this.selectedIds.size ? this.
|
|
32978
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
32673
32979
|
if (ids.length) {
|
|
32674
32980
|
this.popup.close();
|
|
32675
32981
|
this.emit({ kind: "delete", ids });
|
|
@@ -32685,6 +32991,9 @@ var UserDrawingController = class {
|
|
|
32685
32991
|
selectionIds() {
|
|
32686
32992
|
return [...this.selectedIds];
|
|
32687
32993
|
}
|
|
32994
|
+
deletableSelection() {
|
|
32995
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
32996
|
+
}
|
|
32688
32997
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
32689
32998
|
nudgeSelection(dx, dy) {
|
|
32690
32999
|
const proj = this.deps.projector();
|
|
@@ -32793,6 +33102,8 @@ var UserDrawingController = class {
|
|
|
32793
33102
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
32794
33103
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32795
33104
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33105
|
+
const clones = this.interaction.dragClones();
|
|
33106
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
32796
33107
|
this.layoutTextEditor();
|
|
32797
33108
|
const ghost = this.interaction.ghost();
|
|
32798
33109
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -32808,6 +33119,8 @@ var UserDrawingController = class {
|
|
|
32808
33119
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
32809
33120
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
32810
33121
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
33122
|
+
const marquee = this.interaction.marqueeRect();
|
|
33123
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
32811
33124
|
}
|
|
32812
33125
|
destroy() {
|
|
32813
33126
|
this.closeTextEditor();
|
|
@@ -32825,15 +33138,80 @@ var UserDrawingController = class {
|
|
|
32825
33138
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
32826
33139
|
this.render();
|
|
32827
33140
|
}
|
|
33141
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
33142
|
+
emitEdits(drawings) {
|
|
33143
|
+
const docs = drawings.map((d) => d.serialize());
|
|
33144
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
33145
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
33146
|
+
}
|
|
33147
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
33148
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
33149
|
+
openSettingsForSelection(ids) {
|
|
33150
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
33151
|
+
const drawings = live();
|
|
33152
|
+
if (drawings.length < 2) return;
|
|
33153
|
+
const proj = this.deps.projector();
|
|
33154
|
+
let anchor = null;
|
|
33155
|
+
for (const d of drawings) {
|
|
33156
|
+
const b = d.bounds(proj);
|
|
33157
|
+
if (!b) continue;
|
|
33158
|
+
if (!anchor) anchor = { ...b };
|
|
33159
|
+
else {
|
|
33160
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
33161
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
33162
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
33163
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
33164
|
+
anchor.w = right - anchor.x;
|
|
33165
|
+
anchor.h = bottom - anchor.y;
|
|
33166
|
+
}
|
|
33167
|
+
}
|
|
33168
|
+
this.popupMulti = true;
|
|
33169
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
33170
|
+
this.popup.open(drawings, anchor, {
|
|
33171
|
+
resolve: () => live()[0] ?? null,
|
|
33172
|
+
patch: (p) => {
|
|
33173
|
+
const ds = live();
|
|
33174
|
+
for (const d of ds) d.applySettings(p);
|
|
33175
|
+
this.render();
|
|
33176
|
+
this.emitEdits(ds);
|
|
33177
|
+
},
|
|
33178
|
+
setLocked: (v) => {
|
|
33179
|
+
const ds = live();
|
|
33180
|
+
for (const d of ds) d.locked = v;
|
|
33181
|
+
this.emitEdits(ds);
|
|
33182
|
+
},
|
|
33183
|
+
reorder: (to) => {
|
|
33184
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
33185
|
+
},
|
|
33186
|
+
duplicate: () => {
|
|
33187
|
+
this.popup.close();
|
|
33188
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
33189
|
+
},
|
|
33190
|
+
resetSettings: () => {
|
|
33191
|
+
const ds = live();
|
|
33192
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
33193
|
+
this.render();
|
|
33194
|
+
this.emitEdits(ds);
|
|
33195
|
+
this.openSettingsForSelection(ids);
|
|
33196
|
+
},
|
|
33197
|
+
remove: () => {
|
|
33198
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
33199
|
+
if (ids2.length === 0) return;
|
|
33200
|
+
this.popup.close();
|
|
33201
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
33202
|
+
}
|
|
33203
|
+
}, this.onPopupDismissed);
|
|
33204
|
+
}
|
|
32828
33205
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
32829
33206
|
openSettingsById(id, _x, _y) {
|
|
32830
33207
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
32831
33208
|
if (!drawing) return;
|
|
32832
33209
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
33210
|
+
this.popupMulti = false;
|
|
32833
33211
|
this.emit({ kind: "select", ids: [id] });
|
|
32834
33212
|
this.emit({ kind: "settings", id });
|
|
32835
33213
|
const anchor = drawing.bounds(this.deps.projector());
|
|
32836
|
-
this.popup.open(drawing, anchor, {
|
|
33214
|
+
this.popup.open([drawing], anchor, {
|
|
32837
33215
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
32838
33216
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
32839
33217
|
resolve: () => live() ?? null,
|
|
@@ -32851,6 +33229,11 @@ var UserDrawingController = class {
|
|
|
32851
33229
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32852
33230
|
},
|
|
32853
33231
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
33232
|
+
duplicate: () => {
|
|
33233
|
+
this.finishTextEditor(true);
|
|
33234
|
+
this.popup.close();
|
|
33235
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
33236
|
+
},
|
|
32854
33237
|
resetSettings: () => {
|
|
32855
33238
|
const d = live();
|
|
32856
33239
|
if (!d) return;
|
|
@@ -32873,7 +33256,13 @@ var UserDrawingController = class {
|
|
|
32873
33256
|
this.popup.close();
|
|
32874
33257
|
this.emit({ kind: "delete", ids: [id] });
|
|
32875
33258
|
}
|
|
32876
|
-
},
|
|
33259
|
+
}, this.onPopupDismissed);
|
|
33260
|
+
}
|
|
33261
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
33262
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
33263
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
33264
|
+
deselect() {
|
|
33265
|
+
this.clearSelection();
|
|
32877
33266
|
}
|
|
32878
33267
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
32879
33268
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -32899,6 +33288,13 @@ var UserDrawingController = class {
|
|
|
32899
33288
|
}, 0);
|
|
32900
33289
|
return;
|
|
32901
33290
|
}
|
|
33291
|
+
if (i.kind === "clone") {
|
|
33292
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
33293
|
+
this.intentCb?.(i);
|
|
33294
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
33295
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
33296
|
+
return;
|
|
33297
|
+
}
|
|
32902
33298
|
this.intentCb?.(i);
|
|
32903
33299
|
}
|
|
32904
33300
|
};
|
|
@@ -35074,7 +35470,10 @@ var NativeRenderer = class {
|
|
|
35074
35470
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35075
35471
|
fling: (v) => this.fling(v),
|
|
35076
35472
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35077
|
-
onClick: (x) =>
|
|
35473
|
+
onClick: (x) => {
|
|
35474
|
+
this.userDrawings?.deselect();
|
|
35475
|
+
this.handleClick(x);
|
|
35476
|
+
},
|
|
35078
35477
|
onAxisLongPress: (axis, x, y) => {
|
|
35079
35478
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
35080
35479
|
},
|
|
@@ -35092,11 +35491,13 @@ var NativeRenderer = class {
|
|
|
35092
35491
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
35093
35492
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
35094
35493
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
35095
|
-
|
|
35494
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
35495
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
35496
|
+
// middle-click: a selected hit takes the whole selection
|
|
35096
35497
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
35097
35498
|
drawingsSnapMode: () => this.snapMode,
|
|
35098
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
35099
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
35499
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35500
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35100
35501
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35101
35502
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
35102
35503
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -37430,6 +37831,10 @@ function str3(inputs, key, fallback) {
|
|
|
37430
37831
|
const v = inputs[key];
|
|
37431
37832
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
37432
37833
|
}
|
|
37834
|
+
function bool(inputs, key, fallback) {
|
|
37835
|
+
const v = inputs[key];
|
|
37836
|
+
return typeof v === "boolean" ? v : fallback;
|
|
37837
|
+
}
|
|
37433
37838
|
function toPoints(bars, values, colors) {
|
|
37434
37839
|
const pts = new Array(bars.length);
|
|
37435
37840
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -38961,10 +39366,6 @@ var aroon = {
|
|
|
38961
39366
|
plots: [
|
|
38962
39367
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
38963
39368
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
38964
|
-
],
|
|
38965
|
-
levels: [
|
|
38966
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
38967
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
38968
39369
|
]
|
|
38969
39370
|
};
|
|
38970
39371
|
}
|
|
@@ -39462,8 +39863,10 @@ var volumeSpecs = [
|
|
|
39462
39863
|
var DAY_MS2 = 864e5;
|
|
39463
39864
|
function periodKey(anchor, time) {
|
|
39464
39865
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
39465
|
-
if (anchor === "Month") {
|
|
39866
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
39466
39867
|
const d = new Date(time);
|
|
39868
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
39869
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
39467
39870
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
39468
39871
|
}
|
|
39469
39872
|
return Math.floor(time / DAY_MS2);
|
|
@@ -39515,40 +39918,86 @@ var parabolicSar = {
|
|
|
39515
39918
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
39516
39919
|
}
|
|
39517
39920
|
};
|
|
39921
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
39922
|
+
var VWAP_STYLE = "Style";
|
|
39923
|
+
var VWAP_BANDS = [
|
|
39924
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
39925
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
39926
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
39927
|
+
];
|
|
39518
39928
|
var vwap = {
|
|
39519
39929
|
type: "vwap",
|
|
39520
39930
|
title: "Volume Weighted Average Price",
|
|
39521
39931
|
shortTitle: "VWAP",
|
|
39522
39932
|
overlay: true,
|
|
39523
39933
|
inputs: [
|
|
39524
|
-
{ key: "anchor", title: "
|
|
39525
|
-
|
|
39526
|
-
|
|
39934
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
39935
|
+
sourceInput("HLC3"),
|
|
39936
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
39937
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39938
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
39939
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
39940
|
+
]),
|
|
39941
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
39942
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
39943
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39944
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
39945
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
39946
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
39947
|
+
])
|
|
39527
39948
|
],
|
|
39528
39949
|
compute: (bars, inputs) => {
|
|
39529
39950
|
const anchor = str3(inputs, "anchor", "Day");
|
|
39530
|
-
const
|
|
39531
|
-
const
|
|
39951
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
39952
|
+
const n = bars.length;
|
|
39953
|
+
const values = new Array(n).fill(Number.NaN);
|
|
39954
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
39955
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
39956
|
+
color: str3(inputs, b.color, b.defColor),
|
|
39957
|
+
fill: bool(inputs, b.fill, true),
|
|
39958
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
39959
|
+
up: new Array(n).fill(Number.NaN),
|
|
39960
|
+
down: new Array(n).fill(Number.NaN)
|
|
39961
|
+
}));
|
|
39532
39962
|
let period = Number.NaN;
|
|
39533
39963
|
let cumPV = 0;
|
|
39964
|
+
let cumPV2 = 0;
|
|
39534
39965
|
let cumV = 0;
|
|
39535
|
-
for (let i = 0; i <
|
|
39966
|
+
for (let i = 0; i < n; i++) {
|
|
39536
39967
|
const b = bars[i];
|
|
39537
39968
|
const key = periodKey(anchor, b.time);
|
|
39538
39969
|
if (key !== period) {
|
|
39539
39970
|
period = key;
|
|
39540
39971
|
cumPV = 0;
|
|
39972
|
+
cumPV2 = 0;
|
|
39541
39973
|
cumV = 0;
|
|
39542
39974
|
}
|
|
39543
39975
|
const v = b.volume;
|
|
39544
39976
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
39545
|
-
const tp =
|
|
39977
|
+
const tp = src[i];
|
|
39546
39978
|
cumPV += tp * v;
|
|
39979
|
+
cumPV2 += tp * tp * v;
|
|
39547
39980
|
cumV += v;
|
|
39548
39981
|
}
|
|
39549
|
-
if (cumV
|
|
39550
|
-
|
|
39551
|
-
|
|
39982
|
+
if (cumV <= 0) continue;
|
|
39983
|
+
const mean = cumPV / cumV;
|
|
39984
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
39985
|
+
values[i] = mean;
|
|
39986
|
+
for (const band of bands) {
|
|
39987
|
+
band.up[i] = mean + band.mult * sd;
|
|
39988
|
+
band.down[i] = mean - band.mult * sd;
|
|
39989
|
+
}
|
|
39990
|
+
}
|
|
39991
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
39992
|
+
const fills = [];
|
|
39993
|
+
bands.forEach((band, i) => {
|
|
39994
|
+
plots.push(
|
|
39995
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
39996
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
39997
|
+
);
|
|
39998
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
39999
|
+
});
|
|
40000
|
+
return { plots, bands: fills };
|
|
39552
40001
|
}
|
|
39553
40002
|
};
|
|
39554
40003
|
var pivotPoints = {
|
|
@@ -40062,6 +40511,24 @@ function resolveElement(container) {
|
|
|
40062
40511
|
function segmentVisibility(parts, chartHidden = false) {
|
|
40063
40512
|
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
40064
40513
|
}
|
|
40514
|
+
var STATUSLINE_LADDER = [
|
|
40515
|
+
{ stacked: false, level: "full" },
|
|
40516
|
+
{ stacked: true, level: "full" },
|
|
40517
|
+
{ stacked: true, level: "compact" },
|
|
40518
|
+
{ stacked: true, level: "minimal" }
|
|
40519
|
+
];
|
|
40520
|
+
function readoutCells(level, readout) {
|
|
40521
|
+
if (readout === "value") return [{ key: "close", label: "" }];
|
|
40522
|
+
if (level === "full") {
|
|
40523
|
+
return [
|
|
40524
|
+
{ key: "open", label: "O" },
|
|
40525
|
+
{ key: "high", label: "H" },
|
|
40526
|
+
{ key: "low", label: "L" },
|
|
40527
|
+
{ key: "close", label: "C" }
|
|
40528
|
+
];
|
|
40529
|
+
}
|
|
40530
|
+
return [{ key: "close", label: level === "compact" ? "C" : "" }];
|
|
40531
|
+
}
|
|
40065
40532
|
function statuslineMenuItems(parts, chartVisible) {
|
|
40066
40533
|
return [
|
|
40067
40534
|
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
@@ -40090,6 +40557,11 @@ function fmtChange(open2, close) {
|
|
|
40090
40557
|
const sign = diff >= 0 ? "+" : "";
|
|
40091
40558
|
return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
|
|
40092
40559
|
}
|
|
40560
|
+
function fmtChangePct(open2, close) {
|
|
40561
|
+
if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
|
|
40562
|
+
const pct = (close - open2) / open2 * 100;
|
|
40563
|
+
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
|
40564
|
+
}
|
|
40093
40565
|
|
|
40094
40566
|
// src/widget/statusline.ts
|
|
40095
40567
|
var STYLE_ID24 = "vela-widget-statusline";
|
|
@@ -40106,6 +40578,11 @@ var CSS21 = `
|
|
|
40106
40578
|
display: flex;
|
|
40107
40579
|
align-items: baseline;
|
|
40108
40580
|
gap: var(--vela-space-2);
|
|
40581
|
+
/* The chip never grows past the plot: fit() measures overflow against this width to
|
|
40582
|
+
* walk the layout ladder (stack, then shed values), so overflow:hidden only guards
|
|
40583
|
+
* the transient between a resize and the next measure. */
|
|
40584
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40585
|
+
overflow: hidden;
|
|
40109
40586
|
color: var(--vela-fg);
|
|
40110
40587
|
font-size: var(--vela-font-size-md);
|
|
40111
40588
|
/* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
|
|
@@ -40127,6 +40604,31 @@ var CSS21 = `
|
|
|
40127
40604
|
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
40128
40605
|
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
40129
40606
|
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
40607
|
+
/* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
|
|
40608
|
+
* value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
|
|
40609
|
+
* side by side on one line; the STACKED layouts (fit() decides, see the ladder in
|
|
40610
|
+
* statusline-model) turn the chip into a column so the values drop under the symbol. */
|
|
40611
|
+
.vela-statusline .vela-sl-row {
|
|
40612
|
+
display: flex;
|
|
40613
|
+
align-items: baseline;
|
|
40614
|
+
gap: var(--vela-space-2);
|
|
40615
|
+
white-space: nowrap;
|
|
40616
|
+
}
|
|
40617
|
+
.vela-statusline.vela-sl-stacked {
|
|
40618
|
+
flex-direction: column;
|
|
40619
|
+
align-items: flex-start;
|
|
40620
|
+
/* The same air between the two rows as between the values row and the legend row
|
|
40621
|
+
* that follows the chip (see the stacked legend shift below). */
|
|
40622
|
+
row-gap: var(--vela-space-1);
|
|
40623
|
+
}
|
|
40624
|
+
/* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
|
|
40625
|
+
* never does, so its overflow is what fit() measures to keep descending the ladder. */
|
|
40626
|
+
.vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
|
|
40627
|
+
.vela-statusline.vela-sl-stacked .vela-sl-meta {
|
|
40628
|
+
min-width: 0;
|
|
40629
|
+
overflow: hidden;
|
|
40630
|
+
text-overflow: ellipsis;
|
|
40631
|
+
}
|
|
40130
40632
|
.vela-statusline .vela-sl-avatar {
|
|
40131
40633
|
width: 18px;
|
|
40132
40634
|
height: 18px;
|
|
@@ -40178,59 +40680,13 @@ var CSS21 = `
|
|
|
40178
40680
|
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
40179
40681
|
* chart on the page, including statusline-less ones. */
|
|
40180
40682
|
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40181
|
-
/*
|
|
40182
|
-
*
|
|
40183
|
-
|
|
40184
|
-
|
|
40185
|
-
*
|
|
40186
|
-
|
|
40187
|
-
[data-layout='mobile'] .vela-statusline {
|
|
40188
|
-
display: grid;
|
|
40189
|
-
grid-template-columns: auto auto minmax(0, auto) auto;
|
|
40190
|
-
justify-content: start;
|
|
40191
|
-
align-items: center;
|
|
40192
|
-
row-gap: 1px;
|
|
40193
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40194
|
-
font-size: var(--vela-font-size-sm);
|
|
40195
|
-
}
|
|
40196
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-symbol {
|
|
40197
|
-
font-size: var(--vela-font-size-md);
|
|
40198
|
-
line-height: 1;
|
|
40199
|
-
}
|
|
40200
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-meta {
|
|
40201
|
-
min-width: 0;
|
|
40202
|
-
overflow: hidden;
|
|
40203
|
-
text-overflow: ellipsis;
|
|
40204
|
-
white-space: nowrap;
|
|
40205
|
-
line-height: 1;
|
|
40206
|
-
}
|
|
40207
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-avatar,
|
|
40208
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-market { align-self: center; }
|
|
40209
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-ohlc { display: none !important; }
|
|
40210
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-change {
|
|
40211
|
-
/* Second row, under the text column \u2014 the avatar keeps the first column. */
|
|
40212
|
-
grid-column: 2 / -1;
|
|
40213
|
-
font-size: var(--vela-font-size-sm);
|
|
40214
|
-
line-height: 1.2;
|
|
40215
|
-
}
|
|
40216
|
-
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40217
|
-
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
40218
|
-
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
40219
|
-
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
40220
|
-
* Placed after the mobile block on purpose: same specificity, later wins. */
|
|
40221
|
-
.vela-statusline.vela-sl-fit {
|
|
40222
|
-
display: flex; /* undo the mobile grid \u2014 fit mode is one flex row again */
|
|
40223
|
-
flex-wrap: nowrap;
|
|
40224
|
-
align-items: center;
|
|
40225
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40226
|
-
overflow: hidden;
|
|
40227
|
-
}
|
|
40228
|
-
.vela-statusline.vela-sl-fit .vela-sl-change {
|
|
40229
|
-
flex-basis: auto;
|
|
40230
|
-
padding-left: 0;
|
|
40231
|
-
}
|
|
40232
|
-
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
40233
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40683
|
+
/* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
|
|
40684
|
+
* host whenever the ladder lands on a stacked layout. */
|
|
40685
|
+
.vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40686
|
+
/* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
|
|
40687
|
+
* of the readout fits, exactly as on a narrow desktop chart. */
|
|
40688
|
+
[data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
|
|
40689
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
|
|
40234
40690
|
`;
|
|
40235
40691
|
function baseOfTicker(ticker) {
|
|
40236
40692
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -40301,6 +40757,8 @@ var Statusline = class {
|
|
|
40301
40757
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
40302
40758
|
this.fitMode = false;
|
|
40303
40759
|
this.fitRO = null;
|
|
40760
|
+
/** The ladder rung currently applied — see {@link fit}. */
|
|
40761
|
+
this.layout = { stacked: false, level: "full" };
|
|
40304
40762
|
this.onContextMenu = (e) => {
|
|
40305
40763
|
if (!this.menu || !this.menuHooks) return;
|
|
40306
40764
|
e.preventDefault();
|
|
@@ -40347,8 +40805,18 @@ var Statusline = class {
|
|
|
40347
40805
|
this.menuHooks?.setChartVisible(true);
|
|
40348
40806
|
this.setChartHidden(false);
|
|
40349
40807
|
});
|
|
40350
|
-
this.
|
|
40808
|
+
this.identityRow = doc.createElement("span");
|
|
40809
|
+
this.identityRow.className = "vela-sl-row vela-sl-identity";
|
|
40810
|
+
this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
|
|
40811
|
+
this.valuesRow = doc.createElement("span");
|
|
40812
|
+
this.valuesRow.className = "vela-sl-row vela-sl-values";
|
|
40813
|
+
this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
|
|
40814
|
+
this.el.append(this.identityRow, this.valuesRow);
|
|
40351
40815
|
host.appendChild(this.el);
|
|
40816
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40817
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40818
|
+
this.fitRO.observe(host);
|
|
40819
|
+
}
|
|
40352
40820
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
40353
40821
|
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
40354
40822
|
this.setMarketStatus("open");
|
|
@@ -40364,27 +40832,15 @@ var Statusline = class {
|
|
|
40364
40832
|
this.fit();
|
|
40365
40833
|
}
|
|
40366
40834
|
/**
|
|
40367
|
-
* Multi-chart cells:
|
|
40368
|
-
*
|
|
40369
|
-
*
|
|
40370
|
-
* market badge; the logo + ticker always stay.
|
|
40835
|
+
* Multi-chart cells: the same width ladder as a single chart, plus a last resort for
|
|
40836
|
+
* cells too narrow for even its bottom rung — whatever still overflows is hidden
|
|
40837
|
+
* outright rather than clipped mid-glyph, least important first: the values, the
|
|
40838
|
+
* venue/timeframe meta, then the market badge; the logo + ticker always stay.
|
|
40371
40839
|
*/
|
|
40372
40840
|
setFitMode(on) {
|
|
40373
40841
|
if (on === this.fitMode) return;
|
|
40374
40842
|
this.fitMode = on;
|
|
40375
|
-
this.
|
|
40376
|
-
this.host.classList.toggle("vela-sl-fit-host", on);
|
|
40377
|
-
if (on) {
|
|
40378
|
-
if (typeof ResizeObserver !== "undefined" && !this.fitRO) {
|
|
40379
|
-
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40380
|
-
this.fitRO.observe(this.host);
|
|
40381
|
-
}
|
|
40382
|
-
this.fit();
|
|
40383
|
-
} else {
|
|
40384
|
-
this.fitRO?.disconnect();
|
|
40385
|
-
this.fitRO = null;
|
|
40386
|
-
this.syncParts();
|
|
40387
|
-
}
|
|
40843
|
+
this.fit();
|
|
40388
40844
|
}
|
|
40389
40845
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
40390
40846
|
syncParts() {
|
|
@@ -40396,22 +40852,42 @@ var Statusline = class {
|
|
|
40396
40852
|
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
40397
40853
|
this.changeEl.style.display = seg.change ? "" : "none";
|
|
40398
40854
|
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
40855
|
+
this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
|
|
40856
|
+
}
|
|
40857
|
+
/** Apply one ladder rung: the stacked/inline shape and the readout's level. */
|
|
40858
|
+
applyLayout(layout) {
|
|
40859
|
+
this.layout = layout;
|
|
40860
|
+
this.el.classList.toggle("vela-sl-stacked", layout.stacked);
|
|
40861
|
+
this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
|
|
40862
|
+
this.renderValues();
|
|
40399
40863
|
}
|
|
40400
|
-
/**
|
|
40864
|
+
/**
|
|
40865
|
+
* Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
|
|
40866
|
+
* max-width: the full one-line readout, then the values stacked under the symbol
|
|
40867
|
+
* line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
|
|
40868
|
+
* still overflows, whole segments hide, least important first. Without layout
|
|
40869
|
+
* (detached host, node tests) nothing overflows, so the widest rung stays.
|
|
40870
|
+
*/
|
|
40401
40871
|
fit() {
|
|
40402
|
-
if (!this.fitMode) return;
|
|
40403
40872
|
this.syncParts();
|
|
40404
40873
|
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
40874
|
+
const overflows = () => this.el.scrollWidth > this.el.clientWidth;
|
|
40875
|
+
const hasValues = seg.ohlc || seg.change;
|
|
40876
|
+
for (const rung of STATUSLINE_LADDER) {
|
|
40877
|
+
this.applyLayout(rung);
|
|
40878
|
+
if (!hasValues || !overflows()) break;
|
|
40879
|
+
}
|
|
40880
|
+
if (!this.fitMode) return;
|
|
40405
40881
|
const order = [
|
|
40406
|
-
[this.
|
|
40407
|
-
[this.changeEl, seg.change],
|
|
40882
|
+
[this.valuesRow, hasValues],
|
|
40408
40883
|
[this.metaEl, seg.meta],
|
|
40409
40884
|
[this.marketEl, seg.market]
|
|
40410
40885
|
];
|
|
40411
40886
|
for (const [el, shown] of order) {
|
|
40412
|
-
if (
|
|
40887
|
+
if (!overflows()) break;
|
|
40413
40888
|
if (shown) el.style.display = "none";
|
|
40414
40889
|
}
|
|
40890
|
+
if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
|
|
40415
40891
|
}
|
|
40416
40892
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
40417
40893
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -40524,7 +41000,7 @@ var Statusline = class {
|
|
|
40524
41000
|
this.marketTip.destroy();
|
|
40525
41001
|
this.eyeTip.destroy();
|
|
40526
41002
|
this.marketBubble.destroy();
|
|
40527
|
-
this.host.classList.remove("vela-has-statusline", "vela-sl-
|
|
41003
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
|
|
40528
41004
|
this.el.remove();
|
|
40529
41005
|
}
|
|
40530
41006
|
detach() {
|
|
@@ -40533,6 +41009,10 @@ var Statusline = class {
|
|
|
40533
41009
|
}
|
|
40534
41010
|
render() {
|
|
40535
41011
|
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
41012
|
+
this.fit();
|
|
41013
|
+
}
|
|
41014
|
+
/** Write the value readout for the current bar at the current ladder level. */
|
|
41015
|
+
renderValues() {
|
|
40536
41016
|
const bar = this.hoverBar ?? this.lastBar;
|
|
40537
41017
|
if (!bar) {
|
|
40538
41018
|
this.ohlcEl.replaceChildren();
|
|
@@ -40552,12 +41032,11 @@ var Statusline = class {
|
|
|
40552
41032
|
s.appendChild(b);
|
|
40553
41033
|
return s;
|
|
40554
41034
|
};
|
|
40555
|
-
|
|
40556
|
-
|
|
40557
|
-
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
41035
|
+
const { level } = this.layout;
|
|
41036
|
+
this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
|
|
41037
|
+
this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
|
|
40558
41038
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
40559
41039
|
this.changeEl.style.color = ink;
|
|
40560
|
-
this.fit();
|
|
40561
41040
|
}
|
|
40562
41041
|
};
|
|
40563
41042
|
|
|
@@ -42691,6 +43170,17 @@ function nextAutoCellId(taken) {
|
|
|
42691
43170
|
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
42692
43171
|
}
|
|
42693
43172
|
}
|
|
43173
|
+
function resolveTyping(key, open2) {
|
|
43174
|
+
if (open2.dialogs > 0) {
|
|
43175
|
+
if (key.length !== 1) return null;
|
|
43176
|
+
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
43177
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
43178
|
+
return null;
|
|
43179
|
+
}
|
|
43180
|
+
if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
43181
|
+
if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
43182
|
+
return null;
|
|
43183
|
+
}
|
|
42694
43184
|
var VelaWorkspace = class {
|
|
42695
43185
|
constructor(container, opts = {}) {
|
|
42696
43186
|
this.events = new TypedEventBus();
|
|
@@ -44324,17 +44814,14 @@ var VelaWorkspace = class {
|
|
|
44324
44814
|
}
|
|
44325
44815
|
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
44326
44816
|
routeTyping(ev) {
|
|
44327
|
-
if (this.destroyed
|
|
44817
|
+
if (this.destroyed) return;
|
|
44328
44818
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
44329
44819
|
if (isEditableTarget(ev)) return;
|
|
44330
|
-
const
|
|
44331
|
-
if (
|
|
44332
|
-
|
|
44333
|
-
|
|
44334
|
-
|
|
44335
|
-
ev.preventDefault();
|
|
44336
|
-
this.tfQuick.open(key);
|
|
44337
|
-
}
|
|
44820
|
+
const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
|
|
44821
|
+
if (!hit) return;
|
|
44822
|
+
ev.preventDefault();
|
|
44823
|
+
if (hit.target === "symbol") this.symbolPicker.type(hit.text);
|
|
44824
|
+
else this.tfQuick.type(hit.text);
|
|
44338
44825
|
}
|
|
44339
44826
|
trackDialog(open2) {
|
|
44340
44827
|
this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
|