@luxalgo/vela 0.6.17 → 0.6.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
- package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
- package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +563 -149
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
- package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
- package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
- package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +563 -149
- package/dist/vela.global.min.js +53 -51
- package/dist/widget.cjs +814 -268
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +814 -268
- package/dist/workspace.d.cts +14 -5
- package/dist/workspace.d.ts +14 -5
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
package/dist/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;
|
|
@@ -17145,9 +17186,10 @@ async function resolveIndicators(config, fetchImpl = fetch) {
|
|
|
17145
17186
|
return out;
|
|
17146
17187
|
}
|
|
17147
17188
|
var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
|
|
17189
|
+
var ledgerNativeType = (e) => typeof e === "string" ? e : e.type;
|
|
17148
17190
|
function indicatorLedger(i) {
|
|
17149
17191
|
const natives = [...i.present];
|
|
17150
|
-
if (i.volumePending && !natives.
|
|
17192
|
+
if (i.volumePending && !natives.some((e) => ledgerNativeType(e) === "volume")) natives.push("volume");
|
|
17151
17193
|
return {
|
|
17152
17194
|
manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
|
|
17153
17195
|
natives
|
|
@@ -18102,18 +18144,28 @@ function sanitizeCell(raw) {
|
|
|
18102
18144
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
18103
18145
|
const ind = c.indicators;
|
|
18104
18146
|
if (ind != null && typeof ind === "object") {
|
|
18147
|
+
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
18105
18148
|
const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
|
|
18106
18149
|
if (typeof n === "string") return [n];
|
|
18107
18150
|
if (n != null && typeof n === "object" && typeof n.name === "string") {
|
|
18108
18151
|
const e = n;
|
|
18109
|
-
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
18110
18152
|
const inputs = bag(e.inputs);
|
|
18111
18153
|
const props = bag(e.props);
|
|
18112
|
-
|
|
18154
|
+
const hidden = e.hidden === true;
|
|
18155
|
+
return [inputs || props || hidden ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {}, ...hidden ? { hidden } : {} } : e.name];
|
|
18156
|
+
}
|
|
18157
|
+
return [];
|
|
18158
|
+
}) : [];
|
|
18159
|
+
const natives = Array.isArray(ind.natives) ? ind.natives.flatMap((n) => {
|
|
18160
|
+
if (typeof n === "string") return [n];
|
|
18161
|
+
if (n != null && typeof n === "object" && typeof n.type === "string") {
|
|
18162
|
+
const e = n;
|
|
18163
|
+
const inputs = bag(e.inputs);
|
|
18164
|
+
const hidden = e.hidden === true;
|
|
18165
|
+
return [inputs || hidden ? { type: e.type, ...inputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : e.type];
|
|
18113
18166
|
}
|
|
18114
18167
|
return [];
|
|
18115
18168
|
}) : [];
|
|
18116
|
-
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
18117
18169
|
out.indicators = { manifest, natives };
|
|
18118
18170
|
}
|
|
18119
18171
|
const ext = sanitizeExt(c.ext);
|
|
@@ -19912,7 +19964,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19912
19964
|
if (!model) continue;
|
|
19913
19965
|
const paneId = model.paneId ?? "price";
|
|
19914
19966
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19915
|
-
byPane.get(paneId).push({
|
|
19967
|
+
byPane.get(paneId).push({
|
|
19968
|
+
id: r.id,
|
|
19969
|
+
title: model.title || r.title,
|
|
19970
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
19971
|
+
ownScale: model.ownScale === true
|
|
19972
|
+
});
|
|
19916
19973
|
}
|
|
19917
19974
|
const panes = [];
|
|
19918
19975
|
for (const paneId of this.paneOrder) {
|
|
@@ -24545,7 +24602,7 @@ var InputController = class {
|
|
|
24545
24602
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24546
24603
|
if (Number.isNaN(this.cursorX)) return;
|
|
24547
24604
|
if (this.dragging && this.region !== "drawing") return;
|
|
24548
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24605
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24549
24606
|
};
|
|
24550
24607
|
this.onDown = (e) => {
|
|
24551
24608
|
if (e.button === 1) {
|
|
@@ -24577,7 +24634,7 @@ var InputController = class {
|
|
|
24577
24634
|
this.startY = y;
|
|
24578
24635
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24579
24636
|
this.region = "drawing";
|
|
24580
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24637
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24581
24638
|
this.capture(e.pointerId);
|
|
24582
24639
|
return;
|
|
24583
24640
|
}
|
|
@@ -24586,6 +24643,11 @@ var InputController = class {
|
|
|
24586
24643
|
this.capture(e.pointerId);
|
|
24587
24644
|
return;
|
|
24588
24645
|
}
|
|
24646
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24647
|
+
this.region = "drawing";
|
|
24648
|
+
this.capture(e.pointerId);
|
|
24649
|
+
return;
|
|
24650
|
+
}
|
|
24589
24651
|
const vp = this.deps.getCoords().getViewport();
|
|
24590
24652
|
this.deps.apply(vp);
|
|
24591
24653
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24634,7 +24696,7 @@ var InputController = class {
|
|
|
24634
24696
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24635
24697
|
} else if (this.region === "drawing") {
|
|
24636
24698
|
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);
|
|
24699
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24638
24700
|
} else {
|
|
24639
24701
|
const coords = this.deps.getCoords();
|
|
24640
24702
|
const vp = coords.getViewport();
|
|
@@ -24657,7 +24719,7 @@ var InputController = class {
|
|
|
24657
24719
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24658
24720
|
const r = this.regionAt(x, y);
|
|
24659
24721
|
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);
|
|
24722
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24661
24723
|
}
|
|
24662
24724
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24663
24725
|
};
|
|
@@ -24998,12 +25060,12 @@ function defaultTradeMarkersState() {
|
|
|
24998
25060
|
function mergeTradeMarkersState(base, patch) {
|
|
24999
25061
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
25000
25062
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
25001
|
-
const
|
|
25063
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
25002
25064
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
25003
25065
|
return {
|
|
25004
|
-
visible:
|
|
25005
|
-
labels:
|
|
25006
|
-
qty:
|
|
25066
|
+
visible: bool2(p.visible, base.visible),
|
|
25067
|
+
labels: bool2(p.labels, base.labels),
|
|
25068
|
+
qty: bool2(p.qty, base.qty),
|
|
25007
25069
|
colors: {
|
|
25008
25070
|
long: color(c.long, base.colors.long),
|
|
25009
25071
|
short: color(c.short, base.colors.short),
|
|
@@ -28863,6 +28925,18 @@ var DrawingPainter = class {
|
|
|
28863
28925
|
}
|
|
28864
28926
|
this.paintHandles(ctx, pts);
|
|
28865
28927
|
}
|
|
28928
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
28929
|
+
* accent under a thin dashed outline. */
|
|
28930
|
+
paintMarquee(ctx, rect) {
|
|
28931
|
+
ctx.save();
|
|
28932
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
28933
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
28934
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
28935
|
+
ctx.lineWidth = 1;
|
|
28936
|
+
ctx.setLineDash([4, 3]);
|
|
28937
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
28938
|
+
ctx.restore();
|
|
28939
|
+
}
|
|
28866
28940
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
28867
28941
|
paintSnapRing(ctx, x, y, theme) {
|
|
28868
28942
|
ctx.setLineDash([]);
|
|
@@ -30331,6 +30405,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
30331
30405
|
}
|
|
30332
30406
|
return null;
|
|
30333
30407
|
}
|
|
30408
|
+
function deletableSelection(selected, drawings) {
|
|
30409
|
+
const ids = [...selected];
|
|
30410
|
+
if (ids.length < 2) return ids;
|
|
30411
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30412
|
+
}
|
|
30413
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30414
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30415
|
+
return hit.locked ? [] : [hit.id];
|
|
30416
|
+
}
|
|
30334
30417
|
|
|
30335
30418
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30336
30419
|
var DRAG_SLOP3 = 3;
|
|
@@ -30434,10 +30517,32 @@ var DrawingInteraction = class {
|
|
|
30434
30517
|
activeDragId() {
|
|
30435
30518
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30436
30519
|
}
|
|
30520
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30521
|
+
pressedId() {
|
|
30522
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30523
|
+
}
|
|
30524
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30525
|
+
dragClones() {
|
|
30526
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30527
|
+
}
|
|
30528
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30529
|
+
marqueeRect() {
|
|
30530
|
+
if (this.state.kind !== "marquee") return null;
|
|
30531
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30532
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30533
|
+
}
|
|
30534
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30535
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30536
|
+
beginMarquee(x, y) {
|
|
30537
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30538
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30539
|
+
return true;
|
|
30540
|
+
}
|
|
30437
30541
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30438
30542
|
* `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
|
-
|
|
30543
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30544
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30545
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
30441
30546
|
const proj = this.deps.projector();
|
|
30442
30547
|
if (this.state.kind === "placing") {
|
|
30443
30548
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30465,7 +30570,50 @@ var DrawingInteraction = class {
|
|
|
30465
30570
|
return;
|
|
30466
30571
|
}
|
|
30467
30572
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30468
|
-
|
|
30573
|
+
const riders = [];
|
|
30574
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30575
|
+
for (const id of this.deps.selectedIds()) {
|
|
30576
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30577
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30578
|
+
}
|
|
30579
|
+
}
|
|
30580
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30581
|
+
}
|
|
30582
|
+
}
|
|
30583
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30584
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30585
|
+
bodyDelta(paneId, x, y) {
|
|
30586
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30587
|
+
const proj = this.deps.projector();
|
|
30588
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30589
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30590
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30591
|
+
}
|
|
30592
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30593
|
+
dragSources(d) {
|
|
30594
|
+
if (this.state.kind !== "pressed") return [];
|
|
30595
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30596
|
+
for (const r of this.state.riders) {
|
|
30597
|
+
const rd = this.byId(r.id);
|
|
30598
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30599
|
+
}
|
|
30600
|
+
return out;
|
|
30601
|
+
}
|
|
30602
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30603
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30604
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30605
|
+
dragBody(d, x, y) {
|
|
30606
|
+
if (this.state.kind !== "pressed") return;
|
|
30607
|
+
this.snapAt = null;
|
|
30608
|
+
if (this.state.mod && !this.state.clones) {
|
|
30609
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30610
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30611
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30612
|
+
});
|
|
30613
|
+
}
|
|
30614
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30615
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30616
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30469
30617
|
}
|
|
30470
30618
|
}
|
|
30471
30619
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30489,12 +30637,20 @@ var DrawingInteraction = class {
|
|
|
30489
30637
|
this.deps.changed();
|
|
30490
30638
|
return;
|
|
30491
30639
|
}
|
|
30640
|
+
if (this.state.kind === "marquee") {
|
|
30641
|
+
this.state.x1 = x;
|
|
30642
|
+
this.state.y1 = y;
|
|
30643
|
+
this.deps.changed();
|
|
30644
|
+
return;
|
|
30645
|
+
}
|
|
30492
30646
|
if (this.state.kind !== "pressed") return;
|
|
30493
30647
|
const d = this.byId(this.state.id);
|
|
30494
30648
|
if (!d || d.locked) return;
|
|
30495
30649
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
|
|
30496
30650
|
this.state.moved = true;
|
|
30497
|
-
if (this.state.handle
|
|
30651
|
+
if (this.state.handle < 0) {
|
|
30652
|
+
this.dragBody(d, x, y);
|
|
30653
|
+
} else {
|
|
30498
30654
|
const a = d.anchors[this.state.handle];
|
|
30499
30655
|
if (a) {
|
|
30500
30656
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30502,12 +30658,6 @@ var DrawingInteraction = class {
|
|
|
30502
30658
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30503
30659
|
}
|
|
30504
30660
|
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
30661
|
}
|
|
30512
30662
|
this.deps.changed();
|
|
30513
30663
|
}
|
|
@@ -30527,16 +30677,46 @@ var DrawingInteraction = class {
|
|
|
30527
30677
|
}
|
|
30528
30678
|
return;
|
|
30529
30679
|
}
|
|
30680
|
+
if (this.state.kind === "marquee") {
|
|
30681
|
+
const rect = this.marqueeRect();
|
|
30682
|
+
this.state = { kind: "idle" };
|
|
30683
|
+
if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
|
|
30684
|
+
this.deps.changed();
|
|
30685
|
+
return;
|
|
30686
|
+
}
|
|
30530
30687
|
if (this.state.kind !== "pressed") return;
|
|
30531
|
-
const { id, moved } = this.state;
|
|
30688
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30532
30689
|
this.state = { kind: "idle" };
|
|
30533
30690
|
const d = this.byId(id);
|
|
30534
|
-
if (moved) {
|
|
30535
|
-
if (
|
|
30536
|
-
this.deps.
|
|
30537
|
-
|
|
30538
|
-
|
|
30691
|
+
if (!moved) {
|
|
30692
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30693
|
+
else this.deps.openSettings(id, x, y);
|
|
30694
|
+
return;
|
|
30695
|
+
}
|
|
30696
|
+
if (clones) {
|
|
30697
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30698
|
+
} else if (d) {
|
|
30699
|
+
const docs = [d.serialize()];
|
|
30700
|
+
for (const r of riders) {
|
|
30701
|
+
const rd = this.byId(r.id);
|
|
30702
|
+
if (rd) docs.push(rd.serialize());
|
|
30703
|
+
}
|
|
30704
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30705
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30539
30706
|
}
|
|
30707
|
+
this.deps.changed();
|
|
30708
|
+
}
|
|
30709
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30710
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30711
|
+
selectWithin(rect) {
|
|
30712
|
+
const proj = this.deps.projector();
|
|
30713
|
+
const ids = [...this.deps.selectedIds()];
|
|
30714
|
+
for (const d of this.deps.drawings()) {
|
|
30715
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30716
|
+
const b = d.bounds(proj);
|
|
30717
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30718
|
+
}
|
|
30719
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30540
30720
|
}
|
|
30541
30721
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30542
30722
|
cancel() {
|
|
@@ -30549,8 +30729,15 @@ var DrawingInteraction = class {
|
|
|
30549
30729
|
return true;
|
|
30550
30730
|
}
|
|
30551
30731
|
if (this.state.kind === "pressed") {
|
|
30552
|
-
|
|
30553
|
-
|
|
30732
|
+
if (this.state.moved && !this.state.clones) {
|
|
30733
|
+
const d = this.byId(this.state.id);
|
|
30734
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30735
|
+
}
|
|
30736
|
+
this.state = { kind: "idle" };
|
|
30737
|
+
this.deps.changed();
|
|
30738
|
+
return true;
|
|
30739
|
+
}
|
|
30740
|
+
if (this.state.kind === "marquee") {
|
|
30554
30741
|
this.state = { kind: "idle" };
|
|
30555
30742
|
this.deps.changed();
|
|
30556
30743
|
return true;
|
|
@@ -30665,6 +30852,9 @@ var DrawingInteraction = class {
|
|
|
30665
30852
|
function cloneAnchors(d) {
|
|
30666
30853
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30667
30854
|
}
|
|
30855
|
+
function rectsIntersect(a, b) {
|
|
30856
|
+
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;
|
|
30857
|
+
}
|
|
30668
30858
|
function applyFree(anchor, pt, free) {
|
|
30669
30859
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
30670
30860
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -31050,10 +31240,27 @@ function btn(label, primary, onClick) {
|
|
|
31050
31240
|
}
|
|
31051
31241
|
|
|
31052
31242
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31243
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31244
|
+
function sharedPaths(drawings) {
|
|
31245
|
+
const [first, ...rest] = drawings;
|
|
31246
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
31247
|
+
for (const d of rest) {
|
|
31248
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31249
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31250
|
+
}
|
|
31251
|
+
return paths;
|
|
31252
|
+
}
|
|
31253
|
+
function commonValue(drawings, read) {
|
|
31254
|
+
const first = read(drawings[0]);
|
|
31255
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
31256
|
+
}
|
|
31257
|
+
function distinctValues(drawings, read) {
|
|
31258
|
+
return [...new Set(drawings.map(read))];
|
|
31259
|
+
}
|
|
31053
31260
|
var BTN = 30;
|
|
31054
31261
|
var ICON = 21;
|
|
31055
31262
|
var STYLE_ID23 = "vela-drawing-popup-styles";
|
|
31056
|
-
var STYLE_REV = "
|
|
31263
|
+
var STYLE_REV = "4";
|
|
31057
31264
|
function ensureStyles5() {
|
|
31058
31265
|
if (typeof document === "undefined") return;
|
|
31059
31266
|
const existing = document.getElementById(STYLE_ID23);
|
|
@@ -31065,6 +31272,8 @@ function ensureStyles5() {
|
|
|
31065
31272
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
31066
31273
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
31067
31274
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31275
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31276
|
+
.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
31277
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
31069
31278
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
31070
31279
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -31096,7 +31305,7 @@ var DrawingSettingsPopup = class {
|
|
|
31096
31305
|
if (this.isOwnChrome(node)) return;
|
|
31097
31306
|
const cb = this.onClose;
|
|
31098
31307
|
this.close();
|
|
31099
|
-
cb?.();
|
|
31308
|
+
cb?.(e);
|
|
31100
31309
|
};
|
|
31101
31310
|
this.theme = theme;
|
|
31102
31311
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31121,16 +31330,27 @@ var DrawingSettingsPopup = class {
|
|
|
31121
31330
|
contains(node) {
|
|
31122
31331
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31123
31332
|
}
|
|
31124
|
-
/** Open the quick toolbar for `
|
|
31125
|
-
* `
|
|
31126
|
-
|
|
31333
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31334
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31335
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31336
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31337
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31338
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31339
|
+
* modifier press (multi-select) from a plain one. */
|
|
31340
|
+
open(drawings, anchor, actions, onClose) {
|
|
31127
31341
|
this.close();
|
|
31342
|
+
const drawing = drawings[0];
|
|
31343
|
+
if (!drawing) return;
|
|
31128
31344
|
ensureStyles5();
|
|
31129
31345
|
this.onClose = onClose ?? null;
|
|
31130
31346
|
const t = this.theme;
|
|
31347
|
+
const multi = drawings.length > 1;
|
|
31131
31348
|
const schema = drawing.schema();
|
|
31132
|
-
const paths =
|
|
31133
|
-
const
|
|
31349
|
+
const paths = sharedPaths(drawings);
|
|
31350
|
+
const common = (read) => commonValue(drawings, read);
|
|
31351
|
+
const every = (pred) => drawings.every(pred);
|
|
31352
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31353
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31134
31354
|
const el = document.createElement("div");
|
|
31135
31355
|
el.className = "vela-dpop";
|
|
31136
31356
|
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 +31364,84 @@ var DrawingSettingsPopup = class {
|
|
|
31144
31364
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31145
31365
|
bar.appendChild(this.dragHandle());
|
|
31146
31366
|
if (paths.has("glyph")) {
|
|
31147
|
-
const cur =
|
|
31367
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31148
31368
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31149
31369
|
}
|
|
31150
31370
|
if (paths.has("size")) {
|
|
31151
|
-
const sz =
|
|
31371
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31152
31372
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31153
31373
|
}
|
|
31154
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31374
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31375
|
+
const mag = (d) => d;
|
|
31155
31376
|
const options = this.lowerTimeframeOptions();
|
|
31156
31377
|
if (options.length > 0) {
|
|
31157
31378
|
bar.appendChild(
|
|
31158
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31379
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31159
31380
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31160
31381
|
labelInTrigger: true
|
|
31161
31382
|
})
|
|
31162
31383
|
);
|
|
31163
31384
|
}
|
|
31164
|
-
bar.appendChild(
|
|
31165
|
-
bar.appendChild(
|
|
31385
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31386
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31166
31387
|
}
|
|
31167
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31388
|
+
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
31389
|
if (paths.has("style.lineWidth")) {
|
|
31169
|
-
const
|
|
31170
|
-
|
|
31171
|
-
|
|
31390
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31391
|
+
const wf = widthField2(drawing);
|
|
31392
|
+
const width = common((d) => d.style.lineWidth);
|
|
31393
|
+
const markers = every((d) => {
|
|
31394
|
+
const f = widthField2(d);
|
|
31395
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31396
|
+
});
|
|
31397
|
+
if (markers && wf?.kind === "number") {
|
|
31398
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31172
31399
|
} else {
|
|
31173
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31400
|
+
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
31401
|
}
|
|
31175
31402
|
}
|
|
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(
|
|
31403
|
+
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 }));
|
|
31404
|
+
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
31405
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31179
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31406
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31180
31407
|
if (isPosition) {
|
|
31181
|
-
const pos =
|
|
31182
|
-
bar.appendChild(
|
|
31183
|
-
bar.appendChild(
|
|
31408
|
+
const pos = (d) => d;
|
|
31409
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31410
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31184
31411
|
}
|
|
31185
|
-
const
|
|
31186
|
-
if (paths.has("reg.midColor") &&
|
|
31412
|
+
const regOf = (d) => d.reg;
|
|
31413
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31414
|
+
const reg = (d) => regOf(d);
|
|
31187
31415
|
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") &&
|
|
31416
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31417
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31418
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31419
|
+
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 }));
|
|
31420
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31421
|
+
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 }));
|
|
31422
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31423
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31424
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31425
|
+
}
|
|
31426
|
+
const vwapOf = (d) => d.vwap;
|
|
31427
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31428
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31200
31429
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31201
31430
|
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 }));
|
|
31431
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31432
|
+
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
31433
|
bar.appendChild(
|
|
31205
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31434
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31206
31435
|
label: (v) => `${v}\u03C3`,
|
|
31207
31436
|
labelInTrigger: true
|
|
31208
31437
|
})
|
|
31209
31438
|
);
|
|
31210
|
-
bar.appendChild(
|
|
31211
|
-
bar.appendChild(
|
|
31212
|
-
bar.appendChild(
|
|
31439
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31440
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31441
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31213
31442
|
}
|
|
31214
31443
|
if (paths.has("maxCurvature")) {
|
|
31215
|
-
const cur =
|
|
31444
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31216
31445
|
bar.appendChild(
|
|
31217
31446
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31218
31447
|
label: (v) => `n=${v}`,
|
|
@@ -31221,7 +31450,7 @@ var DrawingSettingsPopup = class {
|
|
|
31221
31450
|
);
|
|
31222
31451
|
}
|
|
31223
31452
|
if (paths.has("mach")) {
|
|
31224
|
-
const cur =
|
|
31453
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31225
31454
|
bar.appendChild(
|
|
31226
31455
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31227
31456
|
label: (v) => `M=${v}`,
|
|
@@ -31230,7 +31459,7 @@ var DrawingSettingsPopup = class {
|
|
|
31230
31459
|
);
|
|
31231
31460
|
}
|
|
31232
31461
|
if (paths.has("waveCount")) {
|
|
31233
|
-
const cur =
|
|
31462
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31234
31463
|
bar.appendChild(
|
|
31235
31464
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31236
31465
|
label: (v) => `${v}`,
|
|
@@ -31238,26 +31467,27 @@ var DrawingSettingsPopup = class {
|
|
|
31238
31467
|
})
|
|
31239
31468
|
);
|
|
31240
31469
|
}
|
|
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.
|
|
31470
|
+
const flags = (d) => d;
|
|
31471
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31472
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31473
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31474
|
+
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 }));
|
|
31475
|
+
const textField = !multi && paths.has("text.value");
|
|
31476
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31477
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31478
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31249
31479
|
const editableLevels = drawing.editableLevels();
|
|
31250
|
-
if (editableLevels && !(
|
|
31251
|
-
const fib =
|
|
31480
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31481
|
+
const fib = (d) => d;
|
|
31252
31482
|
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 }));
|
|
31483
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31484
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31255
31485
|
}
|
|
31256
31486
|
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,
|
|
31487
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31488
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31489
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31490
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31261
31491
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31262
31492
|
del.style.color = "var(--vela-danger)";
|
|
31263
31493
|
bar.appendChild(del);
|
|
@@ -31443,7 +31673,7 @@ var DrawingSettingsPopup = class {
|
|
|
31443
31673
|
});
|
|
31444
31674
|
return b;
|
|
31445
31675
|
}
|
|
31446
|
-
/** The trailing kebab overflow — z-order actions
|
|
31676
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31447
31677
|
kebabButton(actions) {
|
|
31448
31678
|
const b = this.base("More");
|
|
31449
31679
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31455,6 +31685,7 @@ var DrawingSettingsPopup = class {
|
|
|
31455
31685
|
return;
|
|
31456
31686
|
}
|
|
31457
31687
|
this.openActionMenu(b, [
|
|
31688
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31458
31689
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31459
31690
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31460
31691
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31611,26 +31842,30 @@ var DrawingSettingsPopup = class {
|
|
|
31611
31842
|
this.tipEl?.remove();
|
|
31612
31843
|
this.tipEl = null;
|
|
31613
31844
|
}
|
|
31614
|
-
|
|
31845
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
31846
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
31847
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
31848
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31615
31849
|
const b = this.base(tip);
|
|
31616
|
-
b.innerHTML = sized(icon2);
|
|
31617
31850
|
const set = (on2) => {
|
|
31618
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
31851
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
31852
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31619
31853
|
};
|
|
31620
31854
|
set(active);
|
|
31621
31855
|
let on = active;
|
|
31622
31856
|
b.addEventListener("click", () => {
|
|
31623
|
-
on = !on;
|
|
31857
|
+
on = on === MIXED ? true : !on;
|
|
31624
31858
|
set(on);
|
|
31625
31859
|
onChange(on);
|
|
31626
31860
|
});
|
|
31627
31861
|
return b;
|
|
31628
31862
|
}
|
|
31629
31863
|
/** 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.
|
|
31864
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
31865
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31631
31866
|
widthInput(tip, value, min, max, step, onChange) {
|
|
31632
31867
|
const ni = new NumberInput({
|
|
31633
|
-
value,
|
|
31868
|
+
value: value === MIXED ? min : value,
|
|
31634
31869
|
min,
|
|
31635
31870
|
max,
|
|
31636
31871
|
step,
|
|
@@ -31642,13 +31877,18 @@ var DrawingSettingsPopup = class {
|
|
|
31642
31877
|
title: tip,
|
|
31643
31878
|
onChange
|
|
31644
31879
|
});
|
|
31880
|
+
if (value === MIXED) {
|
|
31881
|
+
ni.input.value = "";
|
|
31882
|
+
ni.input.placeholder = "\u2014";
|
|
31883
|
+
}
|
|
31645
31884
|
ni.el.dataset.tip = tip;
|
|
31646
31885
|
trapChartKeys(ni.el);
|
|
31647
31886
|
return ni.el;
|
|
31648
31887
|
}
|
|
31649
31888
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31650
31889
|
* 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).
|
|
31890
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31891
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31652
31892
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31653
31893
|
const b = this.base(tip);
|
|
31654
31894
|
b.style.width = "auto";
|
|
@@ -31658,14 +31898,20 @@ var DrawingSettingsPopup = class {
|
|
|
31658
31898
|
let cur = current;
|
|
31659
31899
|
const paint = (v) => {
|
|
31660
31900
|
b.replaceChildren();
|
|
31661
|
-
|
|
31901
|
+
if (v === MIXED) {
|
|
31902
|
+
const dash = document.createElement("span");
|
|
31903
|
+
dash.textContent = "\u2014";
|
|
31904
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
31905
|
+
b.appendChild(dash);
|
|
31906
|
+
}
|
|
31907
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31662
31908
|
if (glyph) {
|
|
31663
31909
|
const ic = document.createElement("span");
|
|
31664
31910
|
ic.style.cssText = "display:flex;";
|
|
31665
31911
|
ic.innerHTML = sized(glyph);
|
|
31666
31912
|
b.appendChild(ic);
|
|
31667
31913
|
}
|
|
31668
|
-
if (opts.label && opts.labelInTrigger) {
|
|
31914
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31669
31915
|
const tx = document.createElement("span");
|
|
31670
31916
|
tx.textContent = opts.label(v);
|
|
31671
31917
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -31735,8 +31981,10 @@ var DrawingSettingsPopup = class {
|
|
|
31735
31981
|
}
|
|
31736
31982
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
31737
31983
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
31738
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
31739
|
-
|
|
31984
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
31985
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
31986
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
31987
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
31740
31988
|
const b = document.createElement("button");
|
|
31741
31989
|
b.type = "button";
|
|
31742
31990
|
b.dataset.tip = tip;
|
|
@@ -31746,13 +31994,14 @@ var DrawingSettingsPopup = class {
|
|
|
31746
31994
|
ic.style.cssText = "display:flex;";
|
|
31747
31995
|
ic.innerHTML = sized(icon2, iconSize);
|
|
31748
31996
|
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};`;
|
|
31997
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
31750
31998
|
b.append(ic, bar);
|
|
31751
31999
|
let cur = color;
|
|
31752
32000
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
31753
32001
|
b.addEventListener("click", (e) => {
|
|
31754
32002
|
e.stopPropagation();
|
|
31755
|
-
|
|
32003
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
32004
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
31756
32005
|
cur = v;
|
|
31757
32006
|
bar.style.background = v;
|
|
31758
32007
|
onChange(v);
|
|
@@ -31761,20 +32010,42 @@ var DrawingSettingsPopup = class {
|
|
|
31761
32010
|
return b;
|
|
31762
32011
|
}
|
|
31763
32012
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
31764
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
32013
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
31765
32014
|
if (this.colorOwner === anchor) {
|
|
31766
32015
|
this.closeColorPopover();
|
|
31767
32016
|
return;
|
|
31768
32017
|
}
|
|
31769
|
-
this.openColorPopover(anchor, color, onChange);
|
|
32018
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
31770
32019
|
}
|
|
31771
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31772
|
-
|
|
32020
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
32021
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
32022
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
31773
32023
|
const t = this.theme;
|
|
31774
32024
|
this.colorPop = this.hostFloat(anchor, {
|
|
31775
32025
|
zIndex: 25,
|
|
31776
32026
|
padding: "10px",
|
|
31777
|
-
fill: (el) => {
|
|
32027
|
+
fill: (el, pop) => {
|
|
32028
|
+
if (used.length > 1) {
|
|
32029
|
+
const row = document.createElement("div");
|
|
32030
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
32031
|
+
const lbl = document.createElement("span");
|
|
32032
|
+
lbl.textContent = "In use";
|
|
32033
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
32034
|
+
row.appendChild(lbl);
|
|
32035
|
+
for (const c of used) {
|
|
32036
|
+
const sw = document.createElement("button");
|
|
32037
|
+
sw.type = "button";
|
|
32038
|
+
sw.dataset.tip = c;
|
|
32039
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
32040
|
+
sw.addEventListener("click", (e) => {
|
|
32041
|
+
e.stopPropagation();
|
|
32042
|
+
pop.hide();
|
|
32043
|
+
onChange(c);
|
|
32044
|
+
});
|
|
32045
|
+
row.appendChild(sw);
|
|
32046
|
+
}
|
|
32047
|
+
el.appendChild(row);
|
|
32048
|
+
}
|
|
31778
32049
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
31779
32050
|
}
|
|
31780
32051
|
});
|
|
@@ -31795,6 +32066,7 @@ var TYPE_ICON = icon("type");
|
|
|
31795
32066
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
31796
32067
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
31797
32068
|
var LOCK_ICON = icon("lock");
|
|
32069
|
+
var UNLOCK_ICON = icon("unlock");
|
|
31798
32070
|
var FRONT_ICON = icon("bring-front");
|
|
31799
32071
|
var BACK_ICON = icon("send-back");
|
|
31800
32072
|
var TRASH_ICON = icon("trash");
|
|
@@ -31802,6 +32074,7 @@ var BOLD_ICON = icon("bold");
|
|
|
31802
32074
|
var ITALIC_ICON = icon("italic");
|
|
31803
32075
|
var GRIP_ICON = icon("grip");
|
|
31804
32076
|
var KEBAB_ICON = icon("kebab");
|
|
32077
|
+
var CLONE_ICON = icon("clone");
|
|
31805
32078
|
var RESET_ICON = icon("reset");
|
|
31806
32079
|
var GEAR_ICON = icon("gear");
|
|
31807
32080
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -31810,6 +32083,12 @@ var BANDS_ICON = icon("bands");
|
|
|
31810
32083
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
31811
32084
|
var SONIC_ICON2 = icon("sonic");
|
|
31812
32085
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32086
|
+
function stripes(colors) {
|
|
32087
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32088
|
+
const step = 100 / bands.length;
|
|
32089
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32090
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32091
|
+
}
|
|
31813
32092
|
function lineIcon(width, style) {
|
|
31814
32093
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
31815
32094
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -32093,6 +32372,20 @@ var UserDrawingController = class {
|
|
|
32093
32372
|
// reported exactly when it actually changes).
|
|
32094
32373
|
this.textEditor = null;
|
|
32095
32374
|
this.painter = new DrawingPainter();
|
|
32375
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
32376
|
+
* shrinking selection does with it. */
|
|
32377
|
+
this.popupMulti = false;
|
|
32378
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
32379
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
32380
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
32381
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
32382
|
+
* whose bar just closed must stay selected. */
|
|
32383
|
+
this.onPopupDismissed = (e) => {
|
|
32384
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
32385
|
+
const pressed = this.interaction.pressedId();
|
|
32386
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
32387
|
+
this.clearSelection();
|
|
32388
|
+
};
|
|
32096
32389
|
this.ctx = canvas.getContext("2d");
|
|
32097
32390
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32098
32391
|
this.toolbar = new DrawingToolbar(
|
|
@@ -32269,6 +32562,12 @@ var UserDrawingController = class {
|
|
|
32269
32562
|
}
|
|
32270
32563
|
setSelection(ids) {
|
|
32271
32564
|
this.selectedIds = new Set(ids);
|
|
32565
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
32566
|
+
else if (this.popupMulti) {
|
|
32567
|
+
this.popupMulti = false;
|
|
32568
|
+
this.popup.close();
|
|
32569
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
32570
|
+
}
|
|
32272
32571
|
this.render();
|
|
32273
32572
|
}
|
|
32274
32573
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -32314,12 +32613,16 @@ var UserDrawingController = class {
|
|
|
32314
32613
|
});
|
|
32315
32614
|
}
|
|
32316
32615
|
/** 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
|
-
|
|
32616
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
32617
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
32618
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
32619
|
+
deleteAt(x, y, withSelection = false) {
|
|
32319
32620
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
32320
|
-
if (!hit
|
|
32621
|
+
if (!hit) return false;
|
|
32622
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
32623
|
+
if (ids.length === 0) return false;
|
|
32321
32624
|
this.popup.close();
|
|
32322
|
-
this.emit({ kind: "delete", ids
|
|
32625
|
+
this.emit({ kind: "delete", ids });
|
|
32323
32626
|
return true;
|
|
32324
32627
|
}
|
|
32325
32628
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -32345,7 +32648,14 @@ var UserDrawingController = class {
|
|
|
32345
32648
|
this.render();
|
|
32346
32649
|
}
|
|
32347
32650
|
}
|
|
32348
|
-
|
|
32651
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
32652
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
32653
|
+
beginMarqueeAt(x, y) {
|
|
32654
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
32655
|
+
this.render();
|
|
32656
|
+
return true;
|
|
32657
|
+
}
|
|
32658
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32349
32659
|
if (this.eraserMode) {
|
|
32350
32660
|
this.erasing = true;
|
|
32351
32661
|
this.deleteAt(x, y);
|
|
@@ -32365,9 +32675,9 @@ var UserDrawingController = class {
|
|
|
32365
32675
|
return;
|
|
32366
32676
|
}
|
|
32367
32677
|
}
|
|
32368
|
-
this.interaction.down(x, y, snap, shift2);
|
|
32678
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
32369
32679
|
}
|
|
32370
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
32680
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32371
32681
|
if (this.eraserMode) {
|
|
32372
32682
|
if (this.erasing) this.deleteAt(x, y);
|
|
32373
32683
|
return;
|
|
@@ -32379,12 +32689,14 @@ var UserDrawingController = class {
|
|
|
32379
32689
|
return;
|
|
32380
32690
|
}
|
|
32381
32691
|
this.interaction.move(x, y, snap, shift2);
|
|
32382
|
-
this.updateHover(x, y);
|
|
32692
|
+
this.updateHover(x, y, mod);
|
|
32383
32693
|
}
|
|
32384
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
32385
|
-
|
|
32694
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
32695
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
32696
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
32697
|
+
updateHover(x, y, mod = false) {
|
|
32386
32698
|
let id = null;
|
|
32387
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32699
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32388
32700
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
32389
32701
|
}
|
|
32390
32702
|
if (id !== this.hoveredId) {
|
|
@@ -32404,7 +32716,12 @@ var UserDrawingController = class {
|
|
|
32404
32716
|
this.render();
|
|
32405
32717
|
return;
|
|
32406
32718
|
}
|
|
32719
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
32407
32720
|
this.interaction.up(x, y);
|
|
32721
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
32722
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
32723
|
+
else this.openSettingsById(dragged, x, y);
|
|
32724
|
+
}
|
|
32408
32725
|
}
|
|
32409
32726
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
32410
32727
|
toggleMeasure() {
|
|
@@ -32669,7 +32986,7 @@ var UserDrawingController = class {
|
|
|
32669
32986
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
32670
32987
|
break;
|
|
32671
32988
|
case "delete": {
|
|
32672
|
-
const ids = this.selectedIds.size ? this.
|
|
32989
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
32673
32990
|
if (ids.length) {
|
|
32674
32991
|
this.popup.close();
|
|
32675
32992
|
this.emit({ kind: "delete", ids });
|
|
@@ -32685,6 +33002,9 @@ var UserDrawingController = class {
|
|
|
32685
33002
|
selectionIds() {
|
|
32686
33003
|
return [...this.selectedIds];
|
|
32687
33004
|
}
|
|
33005
|
+
deletableSelection() {
|
|
33006
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
33007
|
+
}
|
|
32688
33008
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
32689
33009
|
nudgeSelection(dx, dy) {
|
|
32690
33010
|
const proj = this.deps.projector();
|
|
@@ -32793,6 +33113,8 @@ var UserDrawingController = class {
|
|
|
32793
33113
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
32794
33114
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32795
33115
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33116
|
+
const clones = this.interaction.dragClones();
|
|
33117
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
32796
33118
|
this.layoutTextEditor();
|
|
32797
33119
|
const ghost = this.interaction.ghost();
|
|
32798
33120
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -32808,6 +33130,8 @@ var UserDrawingController = class {
|
|
|
32808
33130
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
32809
33131
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
32810
33132
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
33133
|
+
const marquee = this.interaction.marqueeRect();
|
|
33134
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
32811
33135
|
}
|
|
32812
33136
|
destroy() {
|
|
32813
33137
|
this.closeTextEditor();
|
|
@@ -32825,15 +33149,80 @@ var UserDrawingController = class {
|
|
|
32825
33149
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
32826
33150
|
this.render();
|
|
32827
33151
|
}
|
|
33152
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
33153
|
+
emitEdits(drawings) {
|
|
33154
|
+
const docs = drawings.map((d) => d.serialize());
|
|
33155
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
33156
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
33157
|
+
}
|
|
33158
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
33159
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
33160
|
+
openSettingsForSelection(ids) {
|
|
33161
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
33162
|
+
const drawings = live();
|
|
33163
|
+
if (drawings.length < 2) return;
|
|
33164
|
+
const proj = this.deps.projector();
|
|
33165
|
+
let anchor = null;
|
|
33166
|
+
for (const d of drawings) {
|
|
33167
|
+
const b = d.bounds(proj);
|
|
33168
|
+
if (!b) continue;
|
|
33169
|
+
if (!anchor) anchor = { ...b };
|
|
33170
|
+
else {
|
|
33171
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
33172
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
33173
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
33174
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
33175
|
+
anchor.w = right - anchor.x;
|
|
33176
|
+
anchor.h = bottom - anchor.y;
|
|
33177
|
+
}
|
|
33178
|
+
}
|
|
33179
|
+
this.popupMulti = true;
|
|
33180
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
33181
|
+
this.popup.open(drawings, anchor, {
|
|
33182
|
+
resolve: () => live()[0] ?? null,
|
|
33183
|
+
patch: (p) => {
|
|
33184
|
+
const ds = live();
|
|
33185
|
+
for (const d of ds) d.applySettings(p);
|
|
33186
|
+
this.render();
|
|
33187
|
+
this.emitEdits(ds);
|
|
33188
|
+
},
|
|
33189
|
+
setLocked: (v) => {
|
|
33190
|
+
const ds = live();
|
|
33191
|
+
for (const d of ds) d.locked = v;
|
|
33192
|
+
this.emitEdits(ds);
|
|
33193
|
+
},
|
|
33194
|
+
reorder: (to) => {
|
|
33195
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
33196
|
+
},
|
|
33197
|
+
duplicate: () => {
|
|
33198
|
+
this.popup.close();
|
|
33199
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
33200
|
+
},
|
|
33201
|
+
resetSettings: () => {
|
|
33202
|
+
const ds = live();
|
|
33203
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
33204
|
+
this.render();
|
|
33205
|
+
this.emitEdits(ds);
|
|
33206
|
+
this.openSettingsForSelection(ids);
|
|
33207
|
+
},
|
|
33208
|
+
remove: () => {
|
|
33209
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
33210
|
+
if (ids2.length === 0) return;
|
|
33211
|
+
this.popup.close();
|
|
33212
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
33213
|
+
}
|
|
33214
|
+
}, this.onPopupDismissed);
|
|
33215
|
+
}
|
|
32828
33216
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
32829
33217
|
openSettingsById(id, _x, _y) {
|
|
32830
33218
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
32831
33219
|
if (!drawing) return;
|
|
32832
33220
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
33221
|
+
this.popupMulti = false;
|
|
32833
33222
|
this.emit({ kind: "select", ids: [id] });
|
|
32834
33223
|
this.emit({ kind: "settings", id });
|
|
32835
33224
|
const anchor = drawing.bounds(this.deps.projector());
|
|
32836
|
-
this.popup.open(drawing, anchor, {
|
|
33225
|
+
this.popup.open([drawing], anchor, {
|
|
32837
33226
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
32838
33227
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
32839
33228
|
resolve: () => live() ?? null,
|
|
@@ -32851,6 +33240,11 @@ var UserDrawingController = class {
|
|
|
32851
33240
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32852
33241
|
},
|
|
32853
33242
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
33243
|
+
duplicate: () => {
|
|
33244
|
+
this.finishTextEditor(true);
|
|
33245
|
+
this.popup.close();
|
|
33246
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
33247
|
+
},
|
|
32854
33248
|
resetSettings: () => {
|
|
32855
33249
|
const d = live();
|
|
32856
33250
|
if (!d) return;
|
|
@@ -32873,7 +33267,13 @@ var UserDrawingController = class {
|
|
|
32873
33267
|
this.popup.close();
|
|
32874
33268
|
this.emit({ kind: "delete", ids: [id] });
|
|
32875
33269
|
}
|
|
32876
|
-
},
|
|
33270
|
+
}, this.onPopupDismissed);
|
|
33271
|
+
}
|
|
33272
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
33273
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
33274
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
33275
|
+
deselect() {
|
|
33276
|
+
this.clearSelection();
|
|
32877
33277
|
}
|
|
32878
33278
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
32879
33279
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -32899,6 +33299,13 @@ var UserDrawingController = class {
|
|
|
32899
33299
|
}, 0);
|
|
32900
33300
|
return;
|
|
32901
33301
|
}
|
|
33302
|
+
if (i.kind === "clone") {
|
|
33303
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
33304
|
+
this.intentCb?.(i);
|
|
33305
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
33306
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
33307
|
+
return;
|
|
33308
|
+
}
|
|
32902
33309
|
this.intentCb?.(i);
|
|
32903
33310
|
}
|
|
32904
33311
|
};
|
|
@@ -35074,7 +35481,10 @@ var NativeRenderer = class {
|
|
|
35074
35481
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35075
35482
|
fling: (v) => this.fling(v),
|
|
35076
35483
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35077
|
-
onClick: (x) =>
|
|
35484
|
+
onClick: (x) => {
|
|
35485
|
+
this.userDrawings?.deselect();
|
|
35486
|
+
this.handleClick(x);
|
|
35487
|
+
},
|
|
35078
35488
|
onAxisLongPress: (axis, x, y) => {
|
|
35079
35489
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
35080
35490
|
},
|
|
@@ -35092,11 +35502,13 @@ var NativeRenderer = class {
|
|
|
35092
35502
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
35093
35503
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
35094
35504
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
35095
|
-
|
|
35505
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
35506
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
35507
|
+
// middle-click: a selected hit takes the whole selection
|
|
35096
35508
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
35097
35509
|
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),
|
|
35510
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35511
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35100
35512
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35101
35513
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
35102
35514
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -37430,6 +37842,10 @@ function str3(inputs, key, fallback) {
|
|
|
37430
37842
|
const v = inputs[key];
|
|
37431
37843
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
37432
37844
|
}
|
|
37845
|
+
function bool(inputs, key, fallback) {
|
|
37846
|
+
const v = inputs[key];
|
|
37847
|
+
return typeof v === "boolean" ? v : fallback;
|
|
37848
|
+
}
|
|
37433
37849
|
function toPoints(bars, values, colors) {
|
|
37434
37850
|
const pts = new Array(bars.length);
|
|
37435
37851
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -38961,10 +39377,6 @@ var aroon = {
|
|
|
38961
39377
|
plots: [
|
|
38962
39378
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
38963
39379
|
{ 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
39380
|
]
|
|
38969
39381
|
};
|
|
38970
39382
|
}
|
|
@@ -39462,8 +39874,10 @@ var volumeSpecs = [
|
|
|
39462
39874
|
var DAY_MS2 = 864e5;
|
|
39463
39875
|
function periodKey(anchor, time) {
|
|
39464
39876
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
39465
|
-
if (anchor === "Month") {
|
|
39877
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
39466
39878
|
const d = new Date(time);
|
|
39879
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
39880
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
39467
39881
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
39468
39882
|
}
|
|
39469
39883
|
return Math.floor(time / DAY_MS2);
|
|
@@ -39515,40 +39929,86 @@ var parabolicSar = {
|
|
|
39515
39929
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
39516
39930
|
}
|
|
39517
39931
|
};
|
|
39932
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
39933
|
+
var VWAP_STYLE = "Style";
|
|
39934
|
+
var VWAP_BANDS = [
|
|
39935
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
39936
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
39937
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
39938
|
+
];
|
|
39518
39939
|
var vwap = {
|
|
39519
39940
|
type: "vwap",
|
|
39520
39941
|
title: "Volume Weighted Average Price",
|
|
39521
39942
|
shortTitle: "VWAP",
|
|
39522
39943
|
overlay: true,
|
|
39523
39944
|
inputs: [
|
|
39524
|
-
{ key: "anchor", title: "
|
|
39525
|
-
|
|
39526
|
-
|
|
39945
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
39946
|
+
sourceInput("HLC3"),
|
|
39947
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
39948
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39949
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
39950
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
39951
|
+
]),
|
|
39952
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
39953
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
39954
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39955
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
39956
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
39957
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
39958
|
+
])
|
|
39527
39959
|
],
|
|
39528
39960
|
compute: (bars, inputs) => {
|
|
39529
39961
|
const anchor = str3(inputs, "anchor", "Day");
|
|
39530
|
-
const
|
|
39531
|
-
const
|
|
39962
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
39963
|
+
const n = bars.length;
|
|
39964
|
+
const values = new Array(n).fill(Number.NaN);
|
|
39965
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
39966
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
39967
|
+
color: str3(inputs, b.color, b.defColor),
|
|
39968
|
+
fill: bool(inputs, b.fill, true),
|
|
39969
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
39970
|
+
up: new Array(n).fill(Number.NaN),
|
|
39971
|
+
down: new Array(n).fill(Number.NaN)
|
|
39972
|
+
}));
|
|
39532
39973
|
let period = Number.NaN;
|
|
39533
39974
|
let cumPV = 0;
|
|
39975
|
+
let cumPV2 = 0;
|
|
39534
39976
|
let cumV = 0;
|
|
39535
|
-
for (let i = 0; i <
|
|
39977
|
+
for (let i = 0; i < n; i++) {
|
|
39536
39978
|
const b = bars[i];
|
|
39537
39979
|
const key = periodKey(anchor, b.time);
|
|
39538
39980
|
if (key !== period) {
|
|
39539
39981
|
period = key;
|
|
39540
39982
|
cumPV = 0;
|
|
39983
|
+
cumPV2 = 0;
|
|
39541
39984
|
cumV = 0;
|
|
39542
39985
|
}
|
|
39543
39986
|
const v = b.volume;
|
|
39544
39987
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
39545
|
-
const tp =
|
|
39988
|
+
const tp = src[i];
|
|
39546
39989
|
cumPV += tp * v;
|
|
39990
|
+
cumPV2 += tp * tp * v;
|
|
39547
39991
|
cumV += v;
|
|
39548
39992
|
}
|
|
39549
|
-
if (cumV
|
|
39550
|
-
|
|
39551
|
-
|
|
39993
|
+
if (cumV <= 0) continue;
|
|
39994
|
+
const mean = cumPV / cumV;
|
|
39995
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
39996
|
+
values[i] = mean;
|
|
39997
|
+
for (const band of bands) {
|
|
39998
|
+
band.up[i] = mean + band.mult * sd;
|
|
39999
|
+
band.down[i] = mean - band.mult * sd;
|
|
40000
|
+
}
|
|
40001
|
+
}
|
|
40002
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
40003
|
+
const fills = [];
|
|
40004
|
+
bands.forEach((band, i) => {
|
|
40005
|
+
plots.push(
|
|
40006
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
40007
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
40008
|
+
);
|
|
40009
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
40010
|
+
});
|
|
40011
|
+
return { plots, bands: fills };
|
|
39552
40012
|
}
|
|
39553
40013
|
};
|
|
39554
40014
|
var pivotPoints = {
|
|
@@ -40062,6 +40522,24 @@ function resolveElement(container) {
|
|
|
40062
40522
|
function segmentVisibility(parts, chartHidden = false) {
|
|
40063
40523
|
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
40064
40524
|
}
|
|
40525
|
+
var STATUSLINE_LADDER = [
|
|
40526
|
+
{ stacked: false, level: "full" },
|
|
40527
|
+
{ stacked: true, level: "full" },
|
|
40528
|
+
{ stacked: true, level: "compact" },
|
|
40529
|
+
{ stacked: true, level: "minimal" }
|
|
40530
|
+
];
|
|
40531
|
+
function readoutCells(level, readout) {
|
|
40532
|
+
if (readout === "value") return [{ key: "close", label: "" }];
|
|
40533
|
+
if (level === "full") {
|
|
40534
|
+
return [
|
|
40535
|
+
{ key: "open", label: "O" },
|
|
40536
|
+
{ key: "high", label: "H" },
|
|
40537
|
+
{ key: "low", label: "L" },
|
|
40538
|
+
{ key: "close", label: "C" }
|
|
40539
|
+
];
|
|
40540
|
+
}
|
|
40541
|
+
return [{ key: "close", label: level === "compact" ? "C" : "" }];
|
|
40542
|
+
}
|
|
40065
40543
|
function statuslineMenuItems(parts, chartVisible) {
|
|
40066
40544
|
return [
|
|
40067
40545
|
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
@@ -40090,6 +40568,11 @@ function fmtChange(open2, close) {
|
|
|
40090
40568
|
const sign = diff >= 0 ? "+" : "";
|
|
40091
40569
|
return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
|
|
40092
40570
|
}
|
|
40571
|
+
function fmtChangePct(open2, close) {
|
|
40572
|
+
if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
|
|
40573
|
+
const pct = (close - open2) / open2 * 100;
|
|
40574
|
+
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
|
40575
|
+
}
|
|
40093
40576
|
|
|
40094
40577
|
// src/widget/statusline.ts
|
|
40095
40578
|
var STYLE_ID24 = "vela-widget-statusline";
|
|
@@ -40106,6 +40589,11 @@ var CSS21 = `
|
|
|
40106
40589
|
display: flex;
|
|
40107
40590
|
align-items: baseline;
|
|
40108
40591
|
gap: var(--vela-space-2);
|
|
40592
|
+
/* The chip never grows past the plot: fit() measures overflow against this width to
|
|
40593
|
+
* walk the layout ladder (stack, then shed values), so overflow:hidden only guards
|
|
40594
|
+
* the transient between a resize and the next measure. */
|
|
40595
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40596
|
+
overflow: hidden;
|
|
40109
40597
|
color: var(--vela-fg);
|
|
40110
40598
|
font-size: var(--vela-font-size-md);
|
|
40111
40599
|
/* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
|
|
@@ -40127,6 +40615,31 @@ var CSS21 = `
|
|
|
40127
40615
|
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
40128
40616
|
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
40129
40617
|
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
40618
|
+
/* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
|
|
40619
|
+
* value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
|
|
40620
|
+
* side by side on one line; the STACKED layouts (fit() decides, see the ladder in
|
|
40621
|
+
* statusline-model) turn the chip into a column so the values drop under the symbol. */
|
|
40622
|
+
.vela-statusline .vela-sl-row {
|
|
40623
|
+
display: flex;
|
|
40624
|
+
align-items: baseline;
|
|
40625
|
+
gap: var(--vela-space-2);
|
|
40626
|
+
white-space: nowrap;
|
|
40627
|
+
}
|
|
40628
|
+
.vela-statusline.vela-sl-stacked {
|
|
40629
|
+
flex-direction: column;
|
|
40630
|
+
align-items: flex-start;
|
|
40631
|
+
/* The same air between the two rows as between the values row and the legend row
|
|
40632
|
+
* that follows the chip (see the stacked legend shift below). */
|
|
40633
|
+
row-gap: var(--vela-space-1);
|
|
40634
|
+
}
|
|
40635
|
+
/* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
|
|
40636
|
+
* never does, so its overflow is what fit() measures to keep descending the ladder. */
|
|
40637
|
+
.vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
|
|
40638
|
+
.vela-statusline.vela-sl-stacked .vela-sl-meta {
|
|
40639
|
+
min-width: 0;
|
|
40640
|
+
overflow: hidden;
|
|
40641
|
+
text-overflow: ellipsis;
|
|
40642
|
+
}
|
|
40130
40643
|
.vela-statusline .vela-sl-avatar {
|
|
40131
40644
|
width: 18px;
|
|
40132
40645
|
height: 18px;
|
|
@@ -40178,59 +40691,13 @@ var CSS21 = `
|
|
|
40178
40691
|
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
40179
40692
|
* chart on the page, including statusline-less ones. */
|
|
40180
40693
|
.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); }
|
|
40694
|
+
/* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
|
|
40695
|
+
* host whenever the ladder lands on a stacked layout. */
|
|
40696
|
+
.vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40697
|
+
/* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
|
|
40698
|
+
* of the readout fits, exactly as on a narrow desktop chart. */
|
|
40699
|
+
[data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
|
|
40700
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
|
|
40234
40701
|
`;
|
|
40235
40702
|
function baseOfTicker(ticker) {
|
|
40236
40703
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -40301,6 +40768,8 @@ var Statusline = class {
|
|
|
40301
40768
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
40302
40769
|
this.fitMode = false;
|
|
40303
40770
|
this.fitRO = null;
|
|
40771
|
+
/** The ladder rung currently applied — see {@link fit}. */
|
|
40772
|
+
this.layout = { stacked: false, level: "full" };
|
|
40304
40773
|
this.onContextMenu = (e) => {
|
|
40305
40774
|
if (!this.menu || !this.menuHooks) return;
|
|
40306
40775
|
e.preventDefault();
|
|
@@ -40347,8 +40816,18 @@ var Statusline = class {
|
|
|
40347
40816
|
this.menuHooks?.setChartVisible(true);
|
|
40348
40817
|
this.setChartHidden(false);
|
|
40349
40818
|
});
|
|
40350
|
-
this.
|
|
40819
|
+
this.identityRow = doc.createElement("span");
|
|
40820
|
+
this.identityRow.className = "vela-sl-row vela-sl-identity";
|
|
40821
|
+
this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
|
|
40822
|
+
this.valuesRow = doc.createElement("span");
|
|
40823
|
+
this.valuesRow.className = "vela-sl-row vela-sl-values";
|
|
40824
|
+
this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
|
|
40825
|
+
this.el.append(this.identityRow, this.valuesRow);
|
|
40351
40826
|
host.appendChild(this.el);
|
|
40827
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40828
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40829
|
+
this.fitRO.observe(host);
|
|
40830
|
+
}
|
|
40352
40831
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
40353
40832
|
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
40354
40833
|
this.setMarketStatus("open");
|
|
@@ -40364,27 +40843,15 @@ var Statusline = class {
|
|
|
40364
40843
|
this.fit();
|
|
40365
40844
|
}
|
|
40366
40845
|
/**
|
|
40367
|
-
* Multi-chart cells:
|
|
40368
|
-
*
|
|
40369
|
-
*
|
|
40370
|
-
* market badge; the logo + ticker always stay.
|
|
40846
|
+
* Multi-chart cells: the same width ladder as a single chart, plus a last resort for
|
|
40847
|
+
* cells too narrow for even its bottom rung — whatever still overflows is hidden
|
|
40848
|
+
* outright rather than clipped mid-glyph, least important first: the values, the
|
|
40849
|
+
* venue/timeframe meta, then the market badge; the logo + ticker always stay.
|
|
40371
40850
|
*/
|
|
40372
40851
|
setFitMode(on) {
|
|
40373
40852
|
if (on === this.fitMode) return;
|
|
40374
40853
|
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
|
-
}
|
|
40854
|
+
this.fit();
|
|
40388
40855
|
}
|
|
40389
40856
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
40390
40857
|
syncParts() {
|
|
@@ -40396,22 +40863,42 @@ var Statusline = class {
|
|
|
40396
40863
|
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
40397
40864
|
this.changeEl.style.display = seg.change ? "" : "none";
|
|
40398
40865
|
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
40866
|
+
this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
|
|
40867
|
+
}
|
|
40868
|
+
/** Apply one ladder rung: the stacked/inline shape and the readout's level. */
|
|
40869
|
+
applyLayout(layout) {
|
|
40870
|
+
this.layout = layout;
|
|
40871
|
+
this.el.classList.toggle("vela-sl-stacked", layout.stacked);
|
|
40872
|
+
this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
|
|
40873
|
+
this.renderValues();
|
|
40399
40874
|
}
|
|
40400
|
-
/**
|
|
40875
|
+
/**
|
|
40876
|
+
* Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
|
|
40877
|
+
* max-width: the full one-line readout, then the values stacked under the symbol
|
|
40878
|
+
* line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
|
|
40879
|
+
* still overflows, whole segments hide, least important first. Without layout
|
|
40880
|
+
* (detached host, node tests) nothing overflows, so the widest rung stays.
|
|
40881
|
+
*/
|
|
40401
40882
|
fit() {
|
|
40402
|
-
if (!this.fitMode) return;
|
|
40403
40883
|
this.syncParts();
|
|
40404
40884
|
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
40885
|
+
const overflows = () => this.el.scrollWidth > this.el.clientWidth;
|
|
40886
|
+
const hasValues = seg.ohlc || seg.change;
|
|
40887
|
+
for (const rung of STATUSLINE_LADDER) {
|
|
40888
|
+
this.applyLayout(rung);
|
|
40889
|
+
if (!hasValues || !overflows()) break;
|
|
40890
|
+
}
|
|
40891
|
+
if (!this.fitMode) return;
|
|
40405
40892
|
const order = [
|
|
40406
|
-
[this.
|
|
40407
|
-
[this.changeEl, seg.change],
|
|
40893
|
+
[this.valuesRow, hasValues],
|
|
40408
40894
|
[this.metaEl, seg.meta],
|
|
40409
40895
|
[this.marketEl, seg.market]
|
|
40410
40896
|
];
|
|
40411
40897
|
for (const [el, shown] of order) {
|
|
40412
|
-
if (
|
|
40898
|
+
if (!overflows()) break;
|
|
40413
40899
|
if (shown) el.style.display = "none";
|
|
40414
40900
|
}
|
|
40901
|
+
if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
|
|
40415
40902
|
}
|
|
40416
40903
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
40417
40904
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -40524,7 +41011,7 @@ var Statusline = class {
|
|
|
40524
41011
|
this.marketTip.destroy();
|
|
40525
41012
|
this.eyeTip.destroy();
|
|
40526
41013
|
this.marketBubble.destroy();
|
|
40527
|
-
this.host.classList.remove("vela-has-statusline", "vela-sl-
|
|
41014
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
|
|
40528
41015
|
this.el.remove();
|
|
40529
41016
|
}
|
|
40530
41017
|
detach() {
|
|
@@ -40533,6 +41020,10 @@ var Statusline = class {
|
|
|
40533
41020
|
}
|
|
40534
41021
|
render() {
|
|
40535
41022
|
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
41023
|
+
this.fit();
|
|
41024
|
+
}
|
|
41025
|
+
/** Write the value readout for the current bar at the current ladder level. */
|
|
41026
|
+
renderValues() {
|
|
40536
41027
|
const bar = this.hoverBar ?? this.lastBar;
|
|
40537
41028
|
if (!bar) {
|
|
40538
41029
|
this.ohlcEl.replaceChildren();
|
|
@@ -40552,12 +41043,11 @@ var Statusline = class {
|
|
|
40552
41043
|
s.appendChild(b);
|
|
40553
41044
|
return s;
|
|
40554
41045
|
};
|
|
40555
|
-
|
|
40556
|
-
|
|
40557
|
-
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
41046
|
+
const { level } = this.layout;
|
|
41047
|
+
this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
|
|
41048
|
+
this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
|
|
40558
41049
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
40559
41050
|
this.changeEl.style.color = ink;
|
|
40560
|
-
this.fit();
|
|
40561
41051
|
}
|
|
40562
41052
|
};
|
|
40563
41053
|
|
|
@@ -41325,6 +41815,11 @@ function cellDrawings(opt) {
|
|
|
41325
41815
|
if (opt === true || opt == null) return { toolbar: false };
|
|
41326
41816
|
return { ...opt, toolbar: false };
|
|
41327
41817
|
}
|
|
41818
|
+
function nativeLedgerEntry(handle) {
|
|
41819
|
+
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
41820
|
+
const hidden = !handle.visible;
|
|
41821
|
+
return inputs || hidden ? { type: handle.nativeType, ...inputs ? { inputs } : {}, ...hidden ? { hidden: true } : {} } : handle.nativeType;
|
|
41822
|
+
}
|
|
41328
41823
|
function instanceDeltas(handle) {
|
|
41329
41824
|
if (!handle) return void 0;
|
|
41330
41825
|
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
@@ -41447,10 +41942,13 @@ var ChartCell = class {
|
|
|
41447
41942
|
if (seed.rendererConfig != null) this.inner.renderer.applyConfig(seed.rendererConfig);
|
|
41448
41943
|
if (seed.drawings != null) this.inner.drawings.fromJSON(seed.drawings);
|
|
41449
41944
|
if (seed.indicators) {
|
|
41450
|
-
for (const
|
|
41945
|
+
for (const entry of seed.indicators.natives) {
|
|
41946
|
+
const handle = this.inner.addNativeIndicator(ledgerNativeType(entry));
|
|
41947
|
+
this.applyNativeLedgerEntry(handle, entry);
|
|
41948
|
+
}
|
|
41451
41949
|
this.pendingManifestNames = [...seed.indicators.manifest];
|
|
41452
41950
|
}
|
|
41453
|
-
this.volumeIntent = seed.indicators ? seed.indicators.natives.
|
|
41951
|
+
this.volumeIntent = seed.indicators ? seed.indicators.natives.some((e) => ledgerNativeType(e) === "volume") : deps.volume;
|
|
41454
41952
|
this.extState = { ...seed.ext ?? {} };
|
|
41455
41953
|
this.inner.on("load:end", () => {
|
|
41456
41954
|
this.volumeMayBePending = false;
|
|
@@ -41522,6 +42020,7 @@ var ChartCell = class {
|
|
|
41522
42020
|
this.refreshNativeCatalog();
|
|
41523
42021
|
});
|
|
41524
42022
|
this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
|
|
42023
|
+
this.inner.on("indicator:visibility", () => this.deps.onStateDirty());
|
|
41525
42024
|
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
41526
42025
|
if (this.destroyed) return;
|
|
41527
42026
|
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
@@ -41966,7 +42465,7 @@ var ChartCell = class {
|
|
|
41966
42465
|
if (list.length === 0) return;
|
|
41967
42466
|
for (const led of this.pendingManifestNames) {
|
|
41968
42467
|
const entry = list.find((e) => e.name === ledgerEntryName(led));
|
|
41969
|
-
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
|
|
42468
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props, hidden: led.hidden } : {} });
|
|
41970
42469
|
}
|
|
41971
42470
|
this.pendingManifestNames = null;
|
|
41972
42471
|
return;
|
|
@@ -41985,23 +42484,35 @@ var ChartCell = class {
|
|
|
41985
42484
|
applyIndicatorLedger(led) {
|
|
41986
42485
|
const chart = this.inner;
|
|
41987
42486
|
if (!chart) return;
|
|
41988
|
-
this.volumeIntent = led.natives.
|
|
42487
|
+
this.volumeIntent = led.natives.some((e) => ledgerNativeType(e) === "volume");
|
|
41989
42488
|
this.history.silently(() => {
|
|
41990
42489
|
const owed = /* @__PURE__ */ new Map();
|
|
41991
|
-
for (const
|
|
42490
|
+
for (const e of led.natives) {
|
|
42491
|
+
const type = ledgerNativeType(e);
|
|
42492
|
+
const queue = owed.get(type);
|
|
42493
|
+
if (queue) queue.push(e);
|
|
42494
|
+
else owed.set(type, [e]);
|
|
42495
|
+
}
|
|
41992
42496
|
for (const h of this.nativeHandles()) {
|
|
41993
|
-
const
|
|
41994
|
-
|
|
41995
|
-
if (n > 0) owed.set(type, n - 1);
|
|
42497
|
+
const entry = owed.get(h.nativeType)?.shift();
|
|
42498
|
+
if (entry !== void 0) this.applyNativeLedgerEntry(h, entry);
|
|
41996
42499
|
else h.remove();
|
|
41997
42500
|
}
|
|
41998
|
-
for (const
|
|
42501
|
+
for (const queue of owed.values()) {
|
|
42502
|
+
for (const e of queue) {
|
|
42503
|
+
const h = chart.addNativeIndicator(ledgerNativeType(e));
|
|
42504
|
+
this.applyNativeLedgerEntry(h, e);
|
|
42505
|
+
}
|
|
42506
|
+
}
|
|
41999
42507
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
42000
42508
|
if (this.manifest.length > 0) {
|
|
42001
42509
|
for (const item of led.manifest) {
|
|
42002
42510
|
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
42003
42511
|
if (entry)
|
|
42004
|
-
this.addManifestInstance(entry, {
|
|
42512
|
+
this.addManifestInstance(entry, {
|
|
42513
|
+
record: false,
|
|
42514
|
+
...typeof item === "object" ? { inputs: item.inputs, props: item.props, hidden: item.hidden } : {}
|
|
42515
|
+
});
|
|
42005
42516
|
}
|
|
42006
42517
|
this.pendingManifestNames = null;
|
|
42007
42518
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -42068,6 +42579,7 @@ var ChartCell = class {
|
|
|
42068
42579
|
if (this.destroyed) return;
|
|
42069
42580
|
const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
|
|
42070
42581
|
const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
|
|
42582
|
+
if (opts.hidden) it.handle?.setVisible(false);
|
|
42071
42583
|
this.instances.push(it);
|
|
42072
42584
|
this.deps.onIndicatorsChanged(this.id);
|
|
42073
42585
|
if (opts.record === false) return;
|
|
@@ -42111,6 +42623,29 @@ var ChartCell = class {
|
|
|
42111
42623
|
/** Add a native indicator. A multi-instance type gets a fresh instance every time; a
|
|
42112
42624
|
* single-instance type already on the chart hands back its existing one — nothing
|
|
42113
42625
|
* changed, so nothing enters the undo timeline. */
|
|
42626
|
+
/**
|
|
42627
|
+
* Converge one native handle to a restored ledger entry: every schema key gets the
|
|
42628
|
+
* entry's value or its declaration default (the DOCUMENT is the truth — a live
|
|
42629
|
+
* tweak must not survive a restore that says otherwise), and visibility follows
|
|
42630
|
+
* the `hidden` flag. Skips the input write when nothing differs — `setInputs`
|
|
42631
|
+
* re-runs the indicator, and a no-op restore must not flicker every native.
|
|
42632
|
+
*/
|
|
42633
|
+
applyNativeLedgerEntry(handle, entry) {
|
|
42634
|
+
const wanted = typeof entry === "string" ? void 0 : entry.inputs;
|
|
42635
|
+
if (handle.inputs.length > 0) {
|
|
42636
|
+
const current = handle.inputValues();
|
|
42637
|
+
const next = {};
|
|
42638
|
+
for (const schema of handle.inputs) {
|
|
42639
|
+
const target = wanted?.[schema.key] ?? schema.defval;
|
|
42640
|
+
if (JSON.stringify(current[schema.key]) !== JSON.stringify(target)) next[schema.key] = target;
|
|
42641
|
+
}
|
|
42642
|
+
if (Object.keys(next).length > 0) handle.setInputs(next);
|
|
42643
|
+
} else if (wanted) {
|
|
42644
|
+
handle.setInputs(wanted);
|
|
42645
|
+
}
|
|
42646
|
+
const visible = !(typeof entry === "object" && entry.hidden);
|
|
42647
|
+
if (handle.visible !== visible) handle.setVisible(visible);
|
|
42648
|
+
}
|
|
42114
42649
|
addNative(type) {
|
|
42115
42650
|
const chart = this.inner;
|
|
42116
42651
|
if (!chart) return;
|
|
@@ -42274,10 +42809,13 @@ var ChartCell = class {
|
|
|
42274
42809
|
// (`ctx.addIndicator`) stay out: their names would never resolve against the
|
|
42275
42810
|
// manifest — their plugin persists them via the `ext` seam instead.
|
|
42276
42811
|
indicators: indicatorLedger({
|
|
42277
|
-
|
|
42812
|
+
// Live handles carry what the registry read cannot: the input DELTAS and
|
|
42813
|
+
// the hidden flag, so native settings/visibility survive the round-trip.
|
|
42814
|
+
present: this.inner ? this.nativeHandles().map((h) => nativeLedgerEntry(h)) : [],
|
|
42278
42815
|
instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
|
|
42279
42816
|
const d = it.handle ? instanceDeltas(it.handle) : it.values;
|
|
42280
|
-
|
|
42817
|
+
const hidden = it.handle ? !it.handle.visible : false;
|
|
42818
|
+
return d || hidden ? { name: it.entry.name, ...d ?? {}, ...hidden ? { hidden: true } : {} } : it.entry.name;
|
|
42281
42819
|
}),
|
|
42282
42820
|
pendingManifest: this.pendingManifestNames,
|
|
42283
42821
|
manifestSettled: this.deps.manifestSettled(),
|
|
@@ -42691,6 +43229,17 @@ function nextAutoCellId(taken) {
|
|
|
42691
43229
|
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
42692
43230
|
}
|
|
42693
43231
|
}
|
|
43232
|
+
function resolveTyping(key, open2) {
|
|
43233
|
+
if (open2.dialogs > 0) {
|
|
43234
|
+
if (key.length !== 1) return null;
|
|
43235
|
+
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
43236
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
43237
|
+
return null;
|
|
43238
|
+
}
|
|
43239
|
+
if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
43240
|
+
if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
43241
|
+
return null;
|
|
43242
|
+
}
|
|
42694
43243
|
var VelaWorkspace = class {
|
|
42695
43244
|
constructor(container, opts = {}) {
|
|
42696
43245
|
this.events = new TypedEventBus();
|
|
@@ -44324,17 +44873,14 @@ var VelaWorkspace = class {
|
|
|
44324
44873
|
}
|
|
44325
44874
|
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
44326
44875
|
routeTyping(ev) {
|
|
44327
|
-
if (this.destroyed
|
|
44876
|
+
if (this.destroyed) return;
|
|
44328
44877
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
44329
44878
|
if (isEditableTarget(ev)) return;
|
|
44330
|
-
const
|
|
44331
|
-
if (
|
|
44332
|
-
|
|
44333
|
-
|
|
44334
|
-
|
|
44335
|
-
ev.preventDefault();
|
|
44336
|
-
this.tfQuick.open(key);
|
|
44337
|
-
}
|
|
44879
|
+
const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
|
|
44880
|
+
if (!hit) return;
|
|
44881
|
+
ev.preventDefault();
|
|
44882
|
+
if (hit.target === "symbol") this.symbolPicker.type(hit.text);
|
|
44883
|
+
else this.tfQuick.type(hit.text);
|
|
44338
44884
|
}
|
|
44339
44885
|
trackDialog(open2) {
|
|
44340
44886
|
this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
|