@luxalgo/vela 0.6.16 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/{DataProvider-XLdJB86x.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-xH2VBcFT.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-GIJYO4LO.js → chunk-22QJKGXX.js} +666 -189
- package/dist/{chunk-SEOO6YG7.js → chunk-BXF7VNNE.js} +181 -111
- package/dist/{chunk-6DPAYEGO.js → chunk-CAFCLMPF.js} +1 -0
- package/dist/{chunk-I77F2SVE.js → chunk-IFJJPXSV.js} +1 -1
- package/dist/{chunk-MGU3USTP.js → chunk-T5Z5YUCF.js} +1 -1
- package/dist/{chunk-Q3TFJNJW.js → chunk-XCBJU674.js} +2 -2
- package/dist/{contributions-BEkxwA5F.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-CoAX7_SF.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +662 -185
- package/dist/index.d.cts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.js +4 -4
- package/dist/{options-bdSAtp7y.d.ts → options-BCRmYALw.d.cts} +24 -5
- package/dist/{options-bdSAtp7y.d.cts → options-BCRmYALw.d.ts} +24 -5
- package/dist/{plugin-CNUgbRNq.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-Dxyq35Zf.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-CbPza-Wc.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
- package/dist/{statusline-model-D7cWkw8A.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
- package/dist/ui.cjs +1 -0
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +662 -185
- package/dist/vela.global.min.js +52 -53
- package/dist/widget.cjs +839 -291
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +839 -291
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -1234,6 +1234,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
|
|
|
1234
1234
|
registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
|
|
1235
1235
|
registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
|
|
1236
1236
|
registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
|
|
1237
|
+
registerIcon("info", S('<path d="M8 7.4v4.4"/><circle cx="8" cy="4.6" r="0.9" fill="currentColor" stroke="none"/>'));
|
|
1237
1238
|
registerIcon("reset", S('<path d="M2 8a6 6 0 1 0 6-6 6.5 6.5 0 0 0-4.5 1.83L2 5.33"/><path d="M2 2v3.33h3.33"/>'));
|
|
1238
1239
|
registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
|
|
1239
1240
|
registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
|
|
@@ -5038,15 +5039,15 @@ function candleOverrideFor(style, bags) {
|
|
|
5038
5039
|
if (!hasOwnCandlePaint(style)) return null;
|
|
5039
5040
|
const bag = bags[style] ?? {};
|
|
5040
5041
|
const color = (v) => isColor(v) ? v : null;
|
|
5041
|
-
const
|
|
5042
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
5042
5043
|
return {
|
|
5043
5044
|
upColor: color(bag.candleUpColor),
|
|
5044
5045
|
downColor: color(bag.candleDownColor),
|
|
5045
|
-
bodyVisible:
|
|
5046
|
-
borderVisible:
|
|
5046
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
5047
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
5047
5048
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
5048
5049
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
5049
|
-
wickVisible:
|
|
5050
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
5050
5051
|
wickUpColor: color(bag.candleWickUpColor),
|
|
5051
5052
|
wickDownColor: color(bag.candleWickDownColor)
|
|
5052
5053
|
};
|
|
@@ -5191,7 +5192,8 @@ function mergeConfig(base, patch) {
|
|
|
5191
5192
|
labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
|
|
5192
5193
|
currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
|
|
5193
5194
|
priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
|
|
5194
|
-
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
|
|
5195
|
+
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
5196
|
+
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
5195
5197
|
},
|
|
5196
5198
|
panes: {
|
|
5197
5199
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
@@ -12032,6 +12034,9 @@ function paneLabel(pane, fallbackTitle) {
|
|
|
12032
12034
|
function indicatorLabel(i, fallbackTitle) {
|
|
12033
12035
|
return i.title || fallbackTitle(i.id) || i.id;
|
|
12034
12036
|
}
|
|
12037
|
+
function indicatorRowLabel(i, fallbackTitle) {
|
|
12038
|
+
return i.shorttitle || indicatorLabel(i, fallbackTitle);
|
|
12039
|
+
}
|
|
12035
12040
|
function paneRows(pane) {
|
|
12036
12041
|
return pane.items.flatMap((it) => it.kind === "row" ? [it.row] : []);
|
|
12037
12042
|
}
|
|
@@ -12163,7 +12168,7 @@ function paneItems(snap, pane, units) {
|
|
|
12163
12168
|
const indicatorRow = (i) => ({
|
|
12164
12169
|
kind: "indicator",
|
|
12165
12170
|
id: i.id,
|
|
12166
|
-
label:
|
|
12171
|
+
label: indicatorRowLabel(i, snap.handleTitle),
|
|
12167
12172
|
visible: snap.indicatorVisible(i.id),
|
|
12168
12173
|
ownScale: i.ownScale
|
|
12169
12174
|
});
|
|
@@ -12576,9 +12581,13 @@ var CSS6 = `
|
|
|
12576
12581
|
.vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
|
|
12577
12582
|
|
|
12578
12583
|
/* \u2500\u2500 drawing groups \u2500\u2500 */
|
|
12579
|
-
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block.
|
|
12580
|
-
|
|
12581
|
-
|
|
12584
|
+
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
|
|
12585
|
+
border of its own \u2014 units stack flush, so consecutive highlighted rows read as one block; the
|
|
12586
|
+
drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
|
|
12587
|
+
.vela-ot-unit { border-radius: 6px; }
|
|
12588
|
+
/* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
|
|
12589
|
+
.vela-ot-unit:has(> .vela-ot-row[data-picked]) + .vela-ot-unit > .vela-ot-row[data-picked] { border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
12590
|
+
.vela-ot-unit:has(+ .vela-ot-unit > .vela-ot-row[data-picked]) > .vela-ot-row[data-picked] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
12582
12591
|
.vela-ot-row[data-row-kind='group'] > .vela-icon { width: 12px; font-size: 11px; }
|
|
12583
12592
|
/* A member sits indented under its group's header. */
|
|
12584
12593
|
.vela-ot-subrow { padding-left: 26px; }
|
|
@@ -12635,8 +12644,10 @@ var CSS6 = `
|
|
|
12635
12644
|
.vela-ot-gap[data-drop] { height: 4px; background: var(--vela-fg-bright); }
|
|
12636
12645
|
.vela-ot-gap[data-drop]::before { display: none; }
|
|
12637
12646
|
/* A whole container accepts the drop: merging into a pane, or a pane with no drawings yet.
|
|
12638
|
-
The pane block's transparent border reserves the room for this outline
|
|
12647
|
+
The pane block's transparent border reserves the room for this outline; a group unit has no
|
|
12648
|
+
border to color, so it draws the same line as an inset outline (no layout footprint). */
|
|
12639
12649
|
.vela-ot [data-drop='target'] { border-color: var(--vela-fg-bright); background: var(--vela-hover); }
|
|
12650
|
+
.vela-ot-unit[data-drop='target'] { outline: 1px solid var(--vela-fg-bright); outline-offset: -1px; }
|
|
12640
12651
|
/* Where a reorder would insert. */
|
|
12641
12652
|
.vela-ot [data-drop='before'] { box-shadow: inset 0 2px 0 var(--vela-fg-bright); }
|
|
12642
12653
|
.vela-ot [data-drop='after'] { box-shadow: inset 0 -2px 0 var(--vela-fg-bright); }
|
|
@@ -12699,7 +12710,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
12699
12710
|
super(host, "Object tree", "vela-ot");
|
|
12700
12711
|
this.iconFor = iconFor;
|
|
12701
12712
|
this.chart = null;
|
|
12702
|
-
|
|
12713
|
+
/** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
|
|
12714
|
+
this.selectedDrawings = /* @__PURE__ */ new Set();
|
|
12703
12715
|
this.symbolName = "";
|
|
12704
12716
|
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
12705
12717
|
this.symbolRaw = "";
|
|
@@ -12750,12 +12762,9 @@ var ObjectTree = class extends SidePanel {
|
|
|
12750
12762
|
this.unsubs.push(chart.on(ev, () => this.refresh()));
|
|
12751
12763
|
}
|
|
12752
12764
|
this.unsubs.push(
|
|
12753
|
-
chart.on("drawing:selected", ({
|
|
12754
|
-
this.
|
|
12755
|
-
if (!this.syncingSelection)
|
|
12756
|
-
this.picked.clear();
|
|
12757
|
-
if (id) this.picked.add(id);
|
|
12758
|
-
}
|
|
12765
|
+
chart.on("drawing:selected", ({ ids }) => {
|
|
12766
|
+
this.selectedDrawings = new Set(ids);
|
|
12767
|
+
if (!this.syncingSelection) this.picked = new Set(ids);
|
|
12759
12768
|
this.refresh();
|
|
12760
12769
|
})
|
|
12761
12770
|
);
|
|
@@ -13103,7 +13112,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
13103
13112
|
el.dataset.drag = "1";
|
|
13104
13113
|
el.title = "Drag to restack, or onto another pane to move it there";
|
|
13105
13114
|
if (inGroup) el.classList.add("vela-ot-subrow");
|
|
13106
|
-
if (d.id
|
|
13115
|
+
if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
|
|
13107
13116
|
if (this.picked.has(d.id)) el.dataset.picked = "1";
|
|
13108
13117
|
el.addEventListener("click", (e) => this.onDrawClick(e, d.id));
|
|
13109
13118
|
return el;
|
|
@@ -14267,6 +14276,20 @@ var SymbolPicker = class {
|
|
|
14267
14276
|
this.seed = initialQuery;
|
|
14268
14277
|
this.dialog.show();
|
|
14269
14278
|
}
|
|
14279
|
+
get isOpen() {
|
|
14280
|
+
return this.dialog.open;
|
|
14281
|
+
}
|
|
14282
|
+
/** Text typed at the picker that never reached its field: closed, it opens seeded
|
|
14283
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14284
|
+
* fast typist's next key lands in that tick), it extends the query. */
|
|
14285
|
+
type(text) {
|
|
14286
|
+
if (!this.isOpen) {
|
|
14287
|
+
this.open(text);
|
|
14288
|
+
return;
|
|
14289
|
+
}
|
|
14290
|
+
this.input.value = (this.input.value + text).toUpperCase();
|
|
14291
|
+
this.refresh();
|
|
14292
|
+
}
|
|
14270
14293
|
close() {
|
|
14271
14294
|
this.dialog.hide();
|
|
14272
14295
|
}
|
|
@@ -14665,6 +14688,20 @@ var TimeframeQuick = class {
|
|
|
14665
14688
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
14666
14689
|
}, 0);
|
|
14667
14690
|
}
|
|
14691
|
+
get isOpen() {
|
|
14692
|
+
return this.dialog.open;
|
|
14693
|
+
}
|
|
14694
|
+
/** Text typed at the entry that never reached its field: closed, it opens seeded
|
|
14695
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14696
|
+
* fast typist's next key lands in that tick), it extends the entry. */
|
|
14697
|
+
type(text) {
|
|
14698
|
+
if (!this.isOpen) {
|
|
14699
|
+
this.open(text);
|
|
14700
|
+
return;
|
|
14701
|
+
}
|
|
14702
|
+
this.input.value += text;
|
|
14703
|
+
this.renderHint();
|
|
14704
|
+
}
|
|
14668
14705
|
close() {
|
|
14669
14706
|
this.dialog.hide();
|
|
14670
14707
|
}
|
|
@@ -16735,15 +16772,18 @@ var DrawingController = class {
|
|
|
16735
16772
|
next = [...ids];
|
|
16736
16773
|
}
|
|
16737
16774
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
16775
|
+
this.announceSelection();
|
|
16776
|
+
}
|
|
16777
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
16778
|
+
announceSelection() {
|
|
16738
16779
|
this.port?.setSelection(this.selectedIds);
|
|
16739
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16780
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
16740
16781
|
}
|
|
16741
16782
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
16742
16783
|
restoreSnapshot(doc) {
|
|
16743
16784
|
this.store.load(doc);
|
|
16744
16785
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
16745
|
-
this.
|
|
16746
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16786
|
+
this.announceSelection();
|
|
16747
16787
|
}
|
|
16748
16788
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
16749
16789
|
deleteMany(ids) {
|
|
@@ -16876,6 +16916,9 @@ var DrawingController = class {
|
|
|
16876
16916
|
case "duplicate":
|
|
16877
16917
|
this.duplicate(i.ids);
|
|
16878
16918
|
break;
|
|
16919
|
+
case "clone":
|
|
16920
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
16921
|
+
break;
|
|
16879
16922
|
case "copy":
|
|
16880
16923
|
this.copy(i.ids);
|
|
16881
16924
|
break;
|
|
@@ -17126,6 +17169,23 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
17126
17169
|
clear();
|
|
17127
17170
|
};
|
|
17128
17171
|
}
|
|
17172
|
+
function chromeHint(text, opts) {
|
|
17173
|
+
const px = opts.size ?? 16;
|
|
17174
|
+
const el = document.createElement("span");
|
|
17175
|
+
el.className = "vela-hint";
|
|
17176
|
+
el.setAttribute("role", "img");
|
|
17177
|
+
el.setAttribute("aria-label", text);
|
|
17178
|
+
el.innerHTML = iconAt("info", px);
|
|
17179
|
+
el.style.cssText = `flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:${px}px;height:${px}px;border-radius:50%;cursor:help;user-select:none;background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;`;
|
|
17180
|
+
const hover = (on) => {
|
|
17181
|
+
el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
|
|
17182
|
+
el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
|
|
17183
|
+
};
|
|
17184
|
+
el.addEventListener("pointerenter", () => hover(true));
|
|
17185
|
+
el.addEventListener("pointerleave", () => hover(false));
|
|
17186
|
+
const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
|
|
17187
|
+
return { el, dispose };
|
|
17188
|
+
}
|
|
17129
17189
|
|
|
17130
17190
|
// src/renderers/native/drawings/DrawingToolbar.ts
|
|
17131
17191
|
var TOOLBAR_WIDTH = 44;
|
|
@@ -18131,6 +18191,24 @@ function localStorageAdapter(storageKey) {
|
|
|
18131
18191
|
};
|
|
18132
18192
|
}
|
|
18133
18193
|
|
|
18194
|
+
// src/core/options.ts
|
|
18195
|
+
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
18196
|
+
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
18197
|
+
var LIVE_BAR_EASE_MAX_MS = 1e3;
|
|
18198
|
+
function resolveLiveBarEaseMs(value) {
|
|
18199
|
+
if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
|
|
18200
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
18201
|
+
return Math.min(value, LIVE_BAR_EASE_MAX_MS);
|
|
18202
|
+
}
|
|
18203
|
+
function resolveAnimations(animations) {
|
|
18204
|
+
if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
|
|
18205
|
+
return {
|
|
18206
|
+
animZoom: animations?.zoom ?? true,
|
|
18207
|
+
animPan: animations?.pan ?? true,
|
|
18208
|
+
animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
|
|
18209
|
+
};
|
|
18210
|
+
}
|
|
18211
|
+
|
|
18134
18212
|
// src/core/model/series.ts
|
|
18135
18213
|
function isLineLikeSeries(spec) {
|
|
18136
18214
|
return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
|
|
@@ -19807,7 +19885,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19807
19885
|
if (!model) continue;
|
|
19808
19886
|
const paneId = model.paneId ?? "price";
|
|
19809
19887
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19810
|
-
byPane.get(paneId).push({
|
|
19888
|
+
byPane.get(paneId).push({
|
|
19889
|
+
id: r.id,
|
|
19890
|
+
title: model.title || r.title,
|
|
19891
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
19892
|
+
ownScale: model.ownScale === true
|
|
19893
|
+
});
|
|
19811
19894
|
}
|
|
19812
19895
|
const panes = [];
|
|
19813
19896
|
for (const paneId of this.paneOrder) {
|
|
@@ -21096,14 +21179,11 @@ var IndicatorInputsDialog = class {
|
|
|
21096
21179
|
buildTextField(id, current, emit) {
|
|
21097
21180
|
return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
|
|
21098
21181
|
}
|
|
21099
|
-
/**
|
|
21182
|
+
/** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
|
|
21100
21183
|
infoButton(tooltip2) {
|
|
21101
|
-
const
|
|
21102
|
-
|
|
21103
|
-
|
|
21104
|
-
b.className = "vela-ind-hint";
|
|
21105
|
-
b.style.cssText = "flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-weight:600;font-size:11px;font-family:inherit;line-height:1;cursor:help;user-select:none;";
|
|
21106
|
-
return b;
|
|
21184
|
+
const { el, dispose } = chromeHint(tooltip2, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
|
|
21185
|
+
this.dialogTips.push(dispose);
|
|
21186
|
+
return el;
|
|
21107
21187
|
}
|
|
21108
21188
|
/** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
|
|
21109
21189
|
select(id, options, current, emit) {
|
|
@@ -21540,8 +21620,6 @@ function ensureDialogStyles() {
|
|
|
21540
21620
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
21541
21621
|
.vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
21542
21622
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
21543
|
-
.vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
21544
|
-
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
21545
21623
|
.vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
|
|
21546
21624
|
.vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
|
|
21547
21625
|
.vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
|
|
@@ -24445,7 +24523,7 @@ var InputController = class {
|
|
|
24445
24523
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24446
24524
|
if (Number.isNaN(this.cursorX)) return;
|
|
24447
24525
|
if (this.dragging && this.region !== "drawing") return;
|
|
24448
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24526
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24449
24527
|
};
|
|
24450
24528
|
this.onDown = (e) => {
|
|
24451
24529
|
if (e.button === 1) {
|
|
@@ -24477,7 +24555,7 @@ var InputController = class {
|
|
|
24477
24555
|
this.startY = y;
|
|
24478
24556
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24479
24557
|
this.region = "drawing";
|
|
24480
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24558
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24481
24559
|
this.capture(e.pointerId);
|
|
24482
24560
|
return;
|
|
24483
24561
|
}
|
|
@@ -24486,6 +24564,11 @@ var InputController = class {
|
|
|
24486
24564
|
this.capture(e.pointerId);
|
|
24487
24565
|
return;
|
|
24488
24566
|
}
|
|
24567
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24568
|
+
this.region = "drawing";
|
|
24569
|
+
this.capture(e.pointerId);
|
|
24570
|
+
return;
|
|
24571
|
+
}
|
|
24489
24572
|
const vp = this.deps.getCoords().getViewport();
|
|
24490
24573
|
this.deps.apply(vp);
|
|
24491
24574
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24534,7 +24617,7 @@ var InputController = class {
|
|
|
24534
24617
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24535
24618
|
} else if (this.region === "drawing") {
|
|
24536
24619
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
24537
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24620
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24538
24621
|
} else {
|
|
24539
24622
|
const coords = this.deps.getCoords();
|
|
24540
24623
|
const vp = coords.getViewport();
|
|
@@ -24557,7 +24640,7 @@ var InputController = class {
|
|
|
24557
24640
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24558
24641
|
const r = this.regionAt(x, y);
|
|
24559
24642
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
24560
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24643
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24561
24644
|
}
|
|
24562
24645
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24563
24646
|
};
|
|
@@ -24898,12 +24981,12 @@ function defaultTradeMarkersState() {
|
|
|
24898
24981
|
function mergeTradeMarkersState(base, patch) {
|
|
24899
24982
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
24900
24983
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
24901
|
-
const
|
|
24984
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
24902
24985
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
24903
24986
|
return {
|
|
24904
|
-
visible:
|
|
24905
|
-
labels:
|
|
24906
|
-
qty:
|
|
24987
|
+
visible: bool2(p.visible, base.visible),
|
|
24988
|
+
labels: bool2(p.labels, base.labels),
|
|
24989
|
+
qty: bool2(p.qty, base.qty),
|
|
24907
24990
|
colors: {
|
|
24908
24991
|
long: color(c.long, base.colors.long),
|
|
24909
24992
|
short: color(c.short, base.colors.short),
|
|
@@ -27587,6 +27670,8 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27587
27670
|
"symbol.style.baseline.fill-bottom",
|
|
27588
27671
|
"symbol.style.baseline.base-level",
|
|
27589
27672
|
"symbol.style.baseline.width",
|
|
27673
|
+
"symbol.animation",
|
|
27674
|
+
"symbol.animation.price-changes",
|
|
27590
27675
|
"symbol.timezone",
|
|
27591
27676
|
"scales",
|
|
27592
27677
|
"scales.price-scale",
|
|
@@ -27729,8 +27814,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
27729
27814
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
27730
27815
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
27731
27816
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
27732
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
27733
|
-
`;
|
|
27817
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
|
|
27734
27818
|
if (!existing) document.head.appendChild(st);
|
|
27735
27819
|
}
|
|
27736
27820
|
var SettingsDialog = class {
|
|
@@ -27751,6 +27835,8 @@ var SettingsDialog = class {
|
|
|
27751
27835
|
this.themeControl = null;
|
|
27752
27836
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
27753
27837
|
this.tabs = [];
|
|
27838
|
+
this.hintTips = [];
|
|
27839
|
+
// ⓘ tooltip disposers of the open build (see hint)
|
|
27754
27840
|
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
27755
27841
|
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
27756
27842
|
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
@@ -27939,6 +28025,13 @@ var SettingsDialog = class {
|
|
|
27939
28025
|
body.append(g);
|
|
27940
28026
|
}
|
|
27941
28027
|
showActive(config.series.style);
|
|
28028
|
+
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
28029
|
+
body.append(sid(this.boolRow(
|
|
28030
|
+
"Animate price changes",
|
|
28031
|
+
config.priceScale.animateLastPrice,
|
|
28032
|
+
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28033
|
+
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28034
|
+
), "symbol.animation.price-changes"));
|
|
27942
28035
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
27943
28036
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
27944
28037
|
const renderHostSections = (placement) => {
|
|
@@ -28146,6 +28239,8 @@ var SettingsDialog = class {
|
|
|
28146
28239
|
this.ui = null;
|
|
28147
28240
|
this.root = null;
|
|
28148
28241
|
this.tabs = [];
|
|
28242
|
+
for (const dispose of this.hintTips) dispose();
|
|
28243
|
+
this.hintTips = [];
|
|
28149
28244
|
ui?.destroy();
|
|
28150
28245
|
}
|
|
28151
28246
|
destroy() {
|
|
@@ -28559,23 +28654,31 @@ var SettingsDialog = class {
|
|
|
28559
28654
|
}).el
|
|
28560
28655
|
});
|
|
28561
28656
|
}
|
|
28562
|
-
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
28563
|
-
|
|
28564
|
-
|
|
28657
|
+
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
28658
|
+
* `info` (see {@link hint}) rides after the label. */
|
|
28659
|
+
boolRow(label, value, onChange, info) {
|
|
28660
|
+
return this.toggleRow(label, value, onChange, [], void 0, info);
|
|
28565
28661
|
}
|
|
28566
28662
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
28567
28663
|
* shared control column; the control group dims and ignores input while the toggle
|
|
28568
28664
|
* is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
|
|
28569
|
-
toggleRow(label, value, onToggle, controls, get) {
|
|
28665
|
+
toggleRow(label, value, onToggle, controls, get, info) {
|
|
28570
28666
|
return fieldRow({
|
|
28571
28667
|
label,
|
|
28572
28668
|
labelSize: "sm",
|
|
28573
28669
|
bool: controls.length === 0,
|
|
28574
28670
|
toggle: { checked: value, onChange: onToggle, get },
|
|
28575
28671
|
control: controls,
|
|
28672
|
+
info,
|
|
28576
28673
|
className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
|
|
28577
28674
|
});
|
|
28578
28675
|
}
|
|
28676
|
+
/** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
|
|
28677
|
+
hint(text) {
|
|
28678
|
+
const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
|
|
28679
|
+
this.hintTips.push(dispose);
|
|
28680
|
+
return el;
|
|
28681
|
+
}
|
|
28579
28682
|
numberRow(label, value, min, max, step, onChange) {
|
|
28580
28683
|
return fieldRow({
|
|
28581
28684
|
label,
|
|
@@ -28743,6 +28846,18 @@ var DrawingPainter = class {
|
|
|
28743
28846
|
}
|
|
28744
28847
|
this.paintHandles(ctx, pts);
|
|
28745
28848
|
}
|
|
28849
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
28850
|
+
* accent under a thin dashed outline. */
|
|
28851
|
+
paintMarquee(ctx, rect) {
|
|
28852
|
+
ctx.save();
|
|
28853
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
28854
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
28855
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
28856
|
+
ctx.lineWidth = 1;
|
|
28857
|
+
ctx.setLineDash([4, 3]);
|
|
28858
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
28859
|
+
ctx.restore();
|
|
28860
|
+
}
|
|
28746
28861
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
28747
28862
|
paintSnapRing(ctx, x, y, theme) {
|
|
28748
28863
|
ctx.setLineDash([]);
|
|
@@ -30211,6 +30326,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
30211
30326
|
}
|
|
30212
30327
|
return null;
|
|
30213
30328
|
}
|
|
30329
|
+
function deletableSelection(selected, drawings) {
|
|
30330
|
+
const ids = [...selected];
|
|
30331
|
+
if (ids.length < 2) return ids;
|
|
30332
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30333
|
+
}
|
|
30334
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30335
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30336
|
+
return hit.locked ? [] : [hit.id];
|
|
30337
|
+
}
|
|
30214
30338
|
|
|
30215
30339
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30216
30340
|
var DRAG_SLOP3 = 3;
|
|
@@ -30314,10 +30438,32 @@ var DrawingInteraction = class {
|
|
|
30314
30438
|
activeDragId() {
|
|
30315
30439
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30316
30440
|
}
|
|
30441
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30442
|
+
pressedId() {
|
|
30443
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30444
|
+
}
|
|
30445
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30446
|
+
dragClones() {
|
|
30447
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30448
|
+
}
|
|
30449
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30450
|
+
marqueeRect() {
|
|
30451
|
+
if (this.state.kind !== "marquee") return null;
|
|
30452
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30453
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30454
|
+
}
|
|
30455
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30456
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30457
|
+
beginMarquee(x, y) {
|
|
30458
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30459
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30460
|
+
return true;
|
|
30461
|
+
}
|
|
30317
30462
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30318
30463
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
30319
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
30320
|
-
|
|
30464
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30465
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30466
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
30321
30467
|
const proj = this.deps.projector();
|
|
30322
30468
|
if (this.state.kind === "placing") {
|
|
30323
30469
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30345,7 +30491,50 @@ var DrawingInteraction = class {
|
|
|
30345
30491
|
return;
|
|
30346
30492
|
}
|
|
30347
30493
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30348
|
-
|
|
30494
|
+
const riders = [];
|
|
30495
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30496
|
+
for (const id of this.deps.selectedIds()) {
|
|
30497
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30498
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30499
|
+
}
|
|
30500
|
+
}
|
|
30501
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30502
|
+
}
|
|
30503
|
+
}
|
|
30504
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30505
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30506
|
+
bodyDelta(paneId, x, y) {
|
|
30507
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30508
|
+
const proj = this.deps.projector();
|
|
30509
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30510
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30511
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30512
|
+
}
|
|
30513
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30514
|
+
dragSources(d) {
|
|
30515
|
+
if (this.state.kind !== "pressed") return [];
|
|
30516
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30517
|
+
for (const r of this.state.riders) {
|
|
30518
|
+
const rd = this.byId(r.id);
|
|
30519
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30520
|
+
}
|
|
30521
|
+
return out;
|
|
30522
|
+
}
|
|
30523
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30524
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30525
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30526
|
+
dragBody(d, x, y) {
|
|
30527
|
+
if (this.state.kind !== "pressed") return;
|
|
30528
|
+
this.snapAt = null;
|
|
30529
|
+
if (this.state.mod && !this.state.clones) {
|
|
30530
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30531
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30532
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30533
|
+
});
|
|
30534
|
+
}
|
|
30535
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30536
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30537
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30349
30538
|
}
|
|
30350
30539
|
}
|
|
30351
30540
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30369,12 +30558,20 @@ var DrawingInteraction = class {
|
|
|
30369
30558
|
this.deps.changed();
|
|
30370
30559
|
return;
|
|
30371
30560
|
}
|
|
30561
|
+
if (this.state.kind === "marquee") {
|
|
30562
|
+
this.state.x1 = x;
|
|
30563
|
+
this.state.y1 = y;
|
|
30564
|
+
this.deps.changed();
|
|
30565
|
+
return;
|
|
30566
|
+
}
|
|
30372
30567
|
if (this.state.kind !== "pressed") return;
|
|
30373
30568
|
const d = this.byId(this.state.id);
|
|
30374
30569
|
if (!d || d.locked) return;
|
|
30375
30570
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
|
|
30376
30571
|
this.state.moved = true;
|
|
30377
|
-
if (this.state.handle
|
|
30572
|
+
if (this.state.handle < 0) {
|
|
30573
|
+
this.dragBody(d, x, y);
|
|
30574
|
+
} else {
|
|
30378
30575
|
const a = d.anchors[this.state.handle];
|
|
30379
30576
|
if (a) {
|
|
30380
30577
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30382,12 +30579,6 @@ var DrawingInteraction = class {
|
|
|
30382
30579
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30383
30580
|
}
|
|
30384
30581
|
d.constrainHandleDrag(this.state.handle);
|
|
30385
|
-
} else {
|
|
30386
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
30387
|
-
this.snapAt = null;
|
|
30388
|
-
const dt = pt.time - this.state.grab.time;
|
|
30389
|
-
const dp = pt.price - this.state.grab.price;
|
|
30390
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
30391
30582
|
}
|
|
30392
30583
|
this.deps.changed();
|
|
30393
30584
|
}
|
|
@@ -30407,16 +30598,46 @@ var DrawingInteraction = class {
|
|
|
30407
30598
|
}
|
|
30408
30599
|
return;
|
|
30409
30600
|
}
|
|
30601
|
+
if (this.state.kind === "marquee") {
|
|
30602
|
+
const rect = this.marqueeRect();
|
|
30603
|
+
this.state = { kind: "idle" };
|
|
30604
|
+
if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
|
|
30605
|
+
this.deps.changed();
|
|
30606
|
+
return;
|
|
30607
|
+
}
|
|
30410
30608
|
if (this.state.kind !== "pressed") return;
|
|
30411
|
-
const { id, moved } = this.state;
|
|
30609
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30412
30610
|
this.state = { kind: "idle" };
|
|
30413
30611
|
const d = this.byId(id);
|
|
30414
|
-
if (moved) {
|
|
30415
|
-
if (
|
|
30416
|
-
this.deps.
|
|
30417
|
-
|
|
30418
|
-
this.deps.openSettings(id, x, y);
|
|
30612
|
+
if (!moved) {
|
|
30613
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30614
|
+
else this.deps.openSettings(id, x, y);
|
|
30615
|
+
return;
|
|
30419
30616
|
}
|
|
30617
|
+
if (clones) {
|
|
30618
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30619
|
+
} else if (d) {
|
|
30620
|
+
const docs = [d.serialize()];
|
|
30621
|
+
for (const r of riders) {
|
|
30622
|
+
const rd = this.byId(r.id);
|
|
30623
|
+
if (rd) docs.push(rd.serialize());
|
|
30624
|
+
}
|
|
30625
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30626
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30627
|
+
}
|
|
30628
|
+
this.deps.changed();
|
|
30629
|
+
}
|
|
30630
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30631
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30632
|
+
selectWithin(rect) {
|
|
30633
|
+
const proj = this.deps.projector();
|
|
30634
|
+
const ids = [...this.deps.selectedIds()];
|
|
30635
|
+
for (const d of this.deps.drawings()) {
|
|
30636
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30637
|
+
const b = d.bounds(proj);
|
|
30638
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30639
|
+
}
|
|
30640
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30420
30641
|
}
|
|
30421
30642
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30422
30643
|
cancel() {
|
|
@@ -30429,8 +30650,15 @@ var DrawingInteraction = class {
|
|
|
30429
30650
|
return true;
|
|
30430
30651
|
}
|
|
30431
30652
|
if (this.state.kind === "pressed") {
|
|
30432
|
-
|
|
30433
|
-
|
|
30653
|
+
if (this.state.moved && !this.state.clones) {
|
|
30654
|
+
const d = this.byId(this.state.id);
|
|
30655
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30656
|
+
}
|
|
30657
|
+
this.state = { kind: "idle" };
|
|
30658
|
+
this.deps.changed();
|
|
30659
|
+
return true;
|
|
30660
|
+
}
|
|
30661
|
+
if (this.state.kind === "marquee") {
|
|
30434
30662
|
this.state = { kind: "idle" };
|
|
30435
30663
|
this.deps.changed();
|
|
30436
30664
|
return true;
|
|
@@ -30545,6 +30773,9 @@ var DrawingInteraction = class {
|
|
|
30545
30773
|
function cloneAnchors(d) {
|
|
30546
30774
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30547
30775
|
}
|
|
30776
|
+
function rectsIntersect(a, b) {
|
|
30777
|
+
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
|
|
30778
|
+
}
|
|
30548
30779
|
function applyFree(anchor, pt, free) {
|
|
30549
30780
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
30550
30781
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -30930,10 +31161,27 @@ function btn(label, primary, onClick) {
|
|
|
30930
31161
|
}
|
|
30931
31162
|
|
|
30932
31163
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31164
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31165
|
+
function sharedPaths(drawings) {
|
|
31166
|
+
const [first, ...rest] = drawings;
|
|
31167
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
31168
|
+
for (const d of rest) {
|
|
31169
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31170
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31171
|
+
}
|
|
31172
|
+
return paths;
|
|
31173
|
+
}
|
|
31174
|
+
function commonValue(drawings, read) {
|
|
31175
|
+
const first = read(drawings[0]);
|
|
31176
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
31177
|
+
}
|
|
31178
|
+
function distinctValues(drawings, read) {
|
|
31179
|
+
return [...new Set(drawings.map(read))];
|
|
31180
|
+
}
|
|
30933
31181
|
var BTN = 30;
|
|
30934
31182
|
var ICON = 21;
|
|
30935
31183
|
var STYLE_ID23 = "vela-drawing-popup-styles";
|
|
30936
|
-
var STYLE_REV = "
|
|
31184
|
+
var STYLE_REV = "4";
|
|
30937
31185
|
function ensureStyles5() {
|
|
30938
31186
|
if (typeof document === "undefined") return;
|
|
30939
31187
|
const existing = document.getElementById(STYLE_ID23);
|
|
@@ -30945,6 +31193,8 @@ function ensureStyles5() {
|
|
|
30945
31193
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
30946
31194
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
30947
31195
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31196
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31197
|
+
.vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
|
|
30948
31198
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
30949
31199
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
30950
31200
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -30976,7 +31226,7 @@ var DrawingSettingsPopup = class {
|
|
|
30976
31226
|
if (this.isOwnChrome(node)) return;
|
|
30977
31227
|
const cb = this.onClose;
|
|
30978
31228
|
this.close();
|
|
30979
|
-
cb?.();
|
|
31229
|
+
cb?.(e);
|
|
30980
31230
|
};
|
|
30981
31231
|
this.theme = theme;
|
|
30982
31232
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31001,16 +31251,27 @@ var DrawingSettingsPopup = class {
|
|
|
31001
31251
|
contains(node) {
|
|
31002
31252
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31003
31253
|
}
|
|
31004
|
-
/** Open the quick toolbar for `
|
|
31005
|
-
* `
|
|
31006
|
-
|
|
31254
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31255
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31256
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31257
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31258
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31259
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31260
|
+
* modifier press (multi-select) from a plain one. */
|
|
31261
|
+
open(drawings, anchor, actions, onClose) {
|
|
31007
31262
|
this.close();
|
|
31263
|
+
const drawing = drawings[0];
|
|
31264
|
+
if (!drawing) return;
|
|
31008
31265
|
ensureStyles5();
|
|
31009
31266
|
this.onClose = onClose ?? null;
|
|
31010
31267
|
const t = this.theme;
|
|
31268
|
+
const multi = drawings.length > 1;
|
|
31011
31269
|
const schema = drawing.schema();
|
|
31012
|
-
const paths =
|
|
31013
|
-
const
|
|
31270
|
+
const paths = sharedPaths(drawings);
|
|
31271
|
+
const common = (read) => commonValue(drawings, read);
|
|
31272
|
+
const every = (pred) => drawings.every(pred);
|
|
31273
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31274
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31014
31275
|
const el = document.createElement("div");
|
|
31015
31276
|
el.className = "vela-dpop";
|
|
31016
31277
|
el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
|
|
@@ -31024,75 +31285,84 @@ var DrawingSettingsPopup = class {
|
|
|
31024
31285
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31025
31286
|
bar.appendChild(this.dragHandle());
|
|
31026
31287
|
if (paths.has("glyph")) {
|
|
31027
|
-
const cur =
|
|
31288
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31028
31289
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31029
31290
|
}
|
|
31030
31291
|
if (paths.has("size")) {
|
|
31031
|
-
const sz =
|
|
31292
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31032
31293
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31033
31294
|
}
|
|
31034
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31295
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31296
|
+
const mag = (d) => d;
|
|
31035
31297
|
const options = this.lowerTimeframeOptions();
|
|
31036
31298
|
if (options.length > 0) {
|
|
31037
31299
|
bar.appendChild(
|
|
31038
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31300
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31039
31301
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31040
31302
|
labelInTrigger: true
|
|
31041
31303
|
})
|
|
31042
31304
|
);
|
|
31043
31305
|
}
|
|
31044
|
-
bar.appendChild(
|
|
31045
|
-
bar.appendChild(
|
|
31306
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31307
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31046
31308
|
}
|
|
31047
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31309
|
+
if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
|
|
31048
31310
|
if (paths.has("style.lineWidth")) {
|
|
31049
|
-
const
|
|
31050
|
-
|
|
31051
|
-
|
|
31311
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31312
|
+
const wf = widthField2(drawing);
|
|
31313
|
+
const width = common((d) => d.style.lineWidth);
|
|
31314
|
+
const markers = every((d) => {
|
|
31315
|
+
const f = widthField2(d);
|
|
31316
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31317
|
+
});
|
|
31318
|
+
if (markers && wf?.kind === "number") {
|
|
31319
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31052
31320
|
} else {
|
|
31053
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31321
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
31054
31322
|
}
|
|
31055
31323
|
}
|
|
31056
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
31057
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
31324
|
+
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
31325
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
31058
31326
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31059
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31327
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31060
31328
|
if (isPosition) {
|
|
31061
|
-
const pos =
|
|
31062
|
-
bar.appendChild(
|
|
31063
|
-
bar.appendChild(
|
|
31329
|
+
const pos = (d) => d;
|
|
31330
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31331
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31064
31332
|
}
|
|
31065
|
-
const
|
|
31066
|
-
if (paths.has("reg.midColor") &&
|
|
31333
|
+
const regOf = (d) => d.reg;
|
|
31334
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31335
|
+
const reg = (d) => regOf(d);
|
|
31067
31336
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31068
|
-
bar.appendChild(
|
|
31069
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31070
|
-
bar.appendChild(
|
|
31071
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31072
|
-
bar.appendChild(
|
|
31073
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31074
|
-
bar.appendChild(
|
|
31075
|
-
bar.appendChild(
|
|
31076
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
31077
|
-
}
|
|
31078
|
-
const
|
|
31079
|
-
if (paths.has("vwap.midColor") &&
|
|
31337
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31338
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31339
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31340
|
+
bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31341
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31342
|
+
bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31343
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31344
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31345
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31346
|
+
}
|
|
31347
|
+
const vwapOf = (d) => d.vwap;
|
|
31348
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31349
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31080
31350
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31081
31351
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
31082
|
-
bar.appendChild(
|
|
31083
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31352
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31353
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31084
31354
|
bar.appendChild(
|
|
31085
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31355
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31086
31356
|
label: (v) => `${v}\u03C3`,
|
|
31087
31357
|
labelInTrigger: true
|
|
31088
31358
|
})
|
|
31089
31359
|
);
|
|
31090
|
-
bar.appendChild(
|
|
31091
|
-
bar.appendChild(
|
|
31092
|
-
bar.appendChild(
|
|
31360
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31361
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31362
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31093
31363
|
}
|
|
31094
31364
|
if (paths.has("maxCurvature")) {
|
|
31095
|
-
const cur =
|
|
31365
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31096
31366
|
bar.appendChild(
|
|
31097
31367
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31098
31368
|
label: (v) => `n=${v}`,
|
|
@@ -31101,7 +31371,7 @@ var DrawingSettingsPopup = class {
|
|
|
31101
31371
|
);
|
|
31102
31372
|
}
|
|
31103
31373
|
if (paths.has("mach")) {
|
|
31104
|
-
const cur =
|
|
31374
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31105
31375
|
bar.appendChild(
|
|
31106
31376
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31107
31377
|
label: (v) => `M=${v}`,
|
|
@@ -31110,7 +31380,7 @@ var DrawingSettingsPopup = class {
|
|
|
31110
31380
|
);
|
|
31111
31381
|
}
|
|
31112
31382
|
if (paths.has("waveCount")) {
|
|
31113
|
-
const cur =
|
|
31383
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31114
31384
|
bar.appendChild(
|
|
31115
31385
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31116
31386
|
label: (v) => `${v}`,
|
|
@@ -31118,26 +31388,27 @@ var DrawingSettingsPopup = class {
|
|
|
31118
31388
|
})
|
|
31119
31389
|
);
|
|
31120
31390
|
}
|
|
31121
|
-
const
|
|
31122
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
31123
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
31124
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
31125
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
31126
|
-
|
|
31127
|
-
if (paths.has("text.
|
|
31128
|
-
if (paths.has("text.
|
|
31391
|
+
const flags = (d) => d;
|
|
31392
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31393
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31394
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31395
|
+
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
|
|
31396
|
+
const textField = !multi && paths.has("text.value");
|
|
31397
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31398
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31399
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31129
31400
|
const editableLevels = drawing.editableLevels();
|
|
31130
|
-
if (editableLevels && !(
|
|
31131
|
-
const fib =
|
|
31401
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31402
|
+
const fib = (d) => d;
|
|
31132
31403
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
31133
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31134
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31404
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31405
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31135
31406
|
}
|
|
31136
31407
|
bar.appendChild(this.divider());
|
|
31137
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31138
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31139
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31140
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
31408
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31409
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31410
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31411
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31141
31412
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31142
31413
|
del.style.color = "var(--vela-danger)";
|
|
31143
31414
|
bar.appendChild(del);
|
|
@@ -31323,7 +31594,7 @@ var DrawingSettingsPopup = class {
|
|
|
31323
31594
|
});
|
|
31324
31595
|
return b;
|
|
31325
31596
|
}
|
|
31326
|
-
/** The trailing kebab overflow — z-order actions
|
|
31597
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31327
31598
|
kebabButton(actions) {
|
|
31328
31599
|
const b = this.base("More");
|
|
31329
31600
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31335,6 +31606,7 @@ var DrawingSettingsPopup = class {
|
|
|
31335
31606
|
return;
|
|
31336
31607
|
}
|
|
31337
31608
|
this.openActionMenu(b, [
|
|
31609
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31338
31610
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31339
31611
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31340
31612
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31491,26 +31763,30 @@ var DrawingSettingsPopup = class {
|
|
|
31491
31763
|
this.tipEl?.remove();
|
|
31492
31764
|
this.tipEl = null;
|
|
31493
31765
|
}
|
|
31494
|
-
|
|
31766
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
31767
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
31768
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
31769
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31495
31770
|
const b = this.base(tip);
|
|
31496
|
-
b.innerHTML = sized(icon2);
|
|
31497
31771
|
const set = (on2) => {
|
|
31498
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
31772
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
31773
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31499
31774
|
};
|
|
31500
31775
|
set(active);
|
|
31501
31776
|
let on = active;
|
|
31502
31777
|
b.addEventListener("click", () => {
|
|
31503
|
-
on = !on;
|
|
31778
|
+
on = on === MIXED ? true : !on;
|
|
31504
31779
|
set(on);
|
|
31505
31780
|
onChange(on);
|
|
31506
31781
|
});
|
|
31507
31782
|
return b;
|
|
31508
31783
|
}
|
|
31509
31784
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
31510
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
31785
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
31786
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31511
31787
|
widthInput(tip, value, min, max, step, onChange) {
|
|
31512
31788
|
const ni = new NumberInput({
|
|
31513
|
-
value,
|
|
31789
|
+
value: value === MIXED ? min : value,
|
|
31514
31790
|
min,
|
|
31515
31791
|
max,
|
|
31516
31792
|
step,
|
|
@@ -31522,13 +31798,18 @@ var DrawingSettingsPopup = class {
|
|
|
31522
31798
|
title: tip,
|
|
31523
31799
|
onChange
|
|
31524
31800
|
});
|
|
31801
|
+
if (value === MIXED) {
|
|
31802
|
+
ni.input.value = "";
|
|
31803
|
+
ni.input.placeholder = "\u2014";
|
|
31804
|
+
}
|
|
31525
31805
|
ni.el.dataset.tip = tip;
|
|
31526
31806
|
trapChartKeys(ni.el);
|
|
31527
31807
|
return ni.el;
|
|
31528
31808
|
}
|
|
31529
31809
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31530
31810
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
31531
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31811
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31812
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31532
31813
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31533
31814
|
const b = this.base(tip);
|
|
31534
31815
|
b.style.width = "auto";
|
|
@@ -31538,14 +31819,20 @@ var DrawingSettingsPopup = class {
|
|
|
31538
31819
|
let cur = current;
|
|
31539
31820
|
const paint = (v) => {
|
|
31540
31821
|
b.replaceChildren();
|
|
31541
|
-
|
|
31822
|
+
if (v === MIXED) {
|
|
31823
|
+
const dash = document.createElement("span");
|
|
31824
|
+
dash.textContent = "\u2014";
|
|
31825
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
31826
|
+
b.appendChild(dash);
|
|
31827
|
+
}
|
|
31828
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31542
31829
|
if (glyph) {
|
|
31543
31830
|
const ic = document.createElement("span");
|
|
31544
31831
|
ic.style.cssText = "display:flex;";
|
|
31545
31832
|
ic.innerHTML = sized(glyph);
|
|
31546
31833
|
b.appendChild(ic);
|
|
31547
31834
|
}
|
|
31548
|
-
if (opts.label && opts.labelInTrigger) {
|
|
31835
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31549
31836
|
const tx = document.createElement("span");
|
|
31550
31837
|
tx.textContent = opts.label(v);
|
|
31551
31838
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -31615,8 +31902,10 @@ var DrawingSettingsPopup = class {
|
|
|
31615
31902
|
}
|
|
31616
31903
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
31617
31904
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
31618
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
31619
|
-
|
|
31905
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
31906
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
31907
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
31908
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
31620
31909
|
const b = document.createElement("button");
|
|
31621
31910
|
b.type = "button";
|
|
31622
31911
|
b.dataset.tip = tip;
|
|
@@ -31626,13 +31915,14 @@ var DrawingSettingsPopup = class {
|
|
|
31626
31915
|
ic.style.cssText = "display:flex;";
|
|
31627
31916
|
ic.innerHTML = sized(icon2, iconSize);
|
|
31628
31917
|
const bar = document.createElement("span");
|
|
31629
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
31918
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
31630
31919
|
b.append(ic, bar);
|
|
31631
31920
|
let cur = color;
|
|
31632
31921
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
31633
31922
|
b.addEventListener("click", (e) => {
|
|
31634
31923
|
e.stopPropagation();
|
|
31635
|
-
|
|
31924
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
31925
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
31636
31926
|
cur = v;
|
|
31637
31927
|
bar.style.background = v;
|
|
31638
31928
|
onChange(v);
|
|
@@ -31641,20 +31931,42 @@ var DrawingSettingsPopup = class {
|
|
|
31641
31931
|
return b;
|
|
31642
31932
|
}
|
|
31643
31933
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
31644
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
31934
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
31645
31935
|
if (this.colorOwner === anchor) {
|
|
31646
31936
|
this.closeColorPopover();
|
|
31647
31937
|
return;
|
|
31648
31938
|
}
|
|
31649
|
-
this.openColorPopover(anchor, color, onChange);
|
|
31939
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
31650
31940
|
}
|
|
31651
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31652
|
-
|
|
31941
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31942
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
31943
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
31653
31944
|
const t = this.theme;
|
|
31654
31945
|
this.colorPop = this.hostFloat(anchor, {
|
|
31655
31946
|
zIndex: 25,
|
|
31656
31947
|
padding: "10px",
|
|
31657
|
-
fill: (el) => {
|
|
31948
|
+
fill: (el, pop) => {
|
|
31949
|
+
if (used.length > 1) {
|
|
31950
|
+
const row = document.createElement("div");
|
|
31951
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
31952
|
+
const lbl = document.createElement("span");
|
|
31953
|
+
lbl.textContent = "In use";
|
|
31954
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
31955
|
+
row.appendChild(lbl);
|
|
31956
|
+
for (const c of used) {
|
|
31957
|
+
const sw = document.createElement("button");
|
|
31958
|
+
sw.type = "button";
|
|
31959
|
+
sw.dataset.tip = c;
|
|
31960
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
31961
|
+
sw.addEventListener("click", (e) => {
|
|
31962
|
+
e.stopPropagation();
|
|
31963
|
+
pop.hide();
|
|
31964
|
+
onChange(c);
|
|
31965
|
+
});
|
|
31966
|
+
row.appendChild(sw);
|
|
31967
|
+
}
|
|
31968
|
+
el.appendChild(row);
|
|
31969
|
+
}
|
|
31658
31970
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
31659
31971
|
}
|
|
31660
31972
|
});
|
|
@@ -31675,6 +31987,7 @@ var TYPE_ICON = icon("type");
|
|
|
31675
31987
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
31676
31988
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
31677
31989
|
var LOCK_ICON = icon("lock");
|
|
31990
|
+
var UNLOCK_ICON = icon("unlock");
|
|
31678
31991
|
var FRONT_ICON = icon("bring-front");
|
|
31679
31992
|
var BACK_ICON = icon("send-back");
|
|
31680
31993
|
var TRASH_ICON = icon("trash");
|
|
@@ -31682,6 +31995,7 @@ var BOLD_ICON = icon("bold");
|
|
|
31682
31995
|
var ITALIC_ICON = icon("italic");
|
|
31683
31996
|
var GRIP_ICON = icon("grip");
|
|
31684
31997
|
var KEBAB_ICON = icon("kebab");
|
|
31998
|
+
var CLONE_ICON = icon("clone");
|
|
31685
31999
|
var RESET_ICON = icon("reset");
|
|
31686
32000
|
var GEAR_ICON = icon("gear");
|
|
31687
32001
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -31690,6 +32004,12 @@ var BANDS_ICON = icon("bands");
|
|
|
31690
32004
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
31691
32005
|
var SONIC_ICON2 = icon("sonic");
|
|
31692
32006
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32007
|
+
function stripes(colors) {
|
|
32008
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32009
|
+
const step = 100 / bands.length;
|
|
32010
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32011
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32012
|
+
}
|
|
31693
32013
|
function lineIcon(width, style) {
|
|
31694
32014
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
31695
32015
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -31973,6 +32293,20 @@ var UserDrawingController = class {
|
|
|
31973
32293
|
// reported exactly when it actually changes).
|
|
31974
32294
|
this.textEditor = null;
|
|
31975
32295
|
this.painter = new DrawingPainter();
|
|
32296
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
32297
|
+
* shrinking selection does with it. */
|
|
32298
|
+
this.popupMulti = false;
|
|
32299
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
32300
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
32301
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
32302
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
32303
|
+
* whose bar just closed must stay selected. */
|
|
32304
|
+
this.onPopupDismissed = (e) => {
|
|
32305
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
32306
|
+
const pressed = this.interaction.pressedId();
|
|
32307
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
32308
|
+
this.clearSelection();
|
|
32309
|
+
};
|
|
31976
32310
|
this.ctx = canvas.getContext("2d");
|
|
31977
32311
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
31978
32312
|
this.toolbar = new DrawingToolbar(
|
|
@@ -32149,6 +32483,12 @@ var UserDrawingController = class {
|
|
|
32149
32483
|
}
|
|
32150
32484
|
setSelection(ids) {
|
|
32151
32485
|
this.selectedIds = new Set(ids);
|
|
32486
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
32487
|
+
else if (this.popupMulti) {
|
|
32488
|
+
this.popupMulti = false;
|
|
32489
|
+
this.popup.close();
|
|
32490
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
32491
|
+
}
|
|
32152
32492
|
this.render();
|
|
32153
32493
|
}
|
|
32154
32494
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -32194,12 +32534,16 @@ var UserDrawingController = class {
|
|
|
32194
32534
|
});
|
|
32195
32535
|
}
|
|
32196
32536
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32197
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
32198
|
-
|
|
32537
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
32538
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
32539
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
32540
|
+
deleteAt(x, y, withSelection = false) {
|
|
32199
32541
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
32200
|
-
if (!hit
|
|
32542
|
+
if (!hit) return false;
|
|
32543
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
32544
|
+
if (ids.length === 0) return false;
|
|
32201
32545
|
this.popup.close();
|
|
32202
|
-
this.emit({ kind: "delete", ids
|
|
32546
|
+
this.emit({ kind: "delete", ids });
|
|
32203
32547
|
return true;
|
|
32204
32548
|
}
|
|
32205
32549
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -32225,7 +32569,14 @@ var UserDrawingController = class {
|
|
|
32225
32569
|
this.render();
|
|
32226
32570
|
}
|
|
32227
32571
|
}
|
|
32228
|
-
|
|
32572
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
32573
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
32574
|
+
beginMarqueeAt(x, y) {
|
|
32575
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
32576
|
+
this.render();
|
|
32577
|
+
return true;
|
|
32578
|
+
}
|
|
32579
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32229
32580
|
if (this.eraserMode) {
|
|
32230
32581
|
this.erasing = true;
|
|
32231
32582
|
this.deleteAt(x, y);
|
|
@@ -32245,9 +32596,9 @@ var UserDrawingController = class {
|
|
|
32245
32596
|
return;
|
|
32246
32597
|
}
|
|
32247
32598
|
}
|
|
32248
|
-
this.interaction.down(x, y, snap, shift2);
|
|
32599
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
32249
32600
|
}
|
|
32250
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
32601
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32251
32602
|
if (this.eraserMode) {
|
|
32252
32603
|
if (this.erasing) this.deleteAt(x, y);
|
|
32253
32604
|
return;
|
|
@@ -32259,12 +32610,14 @@ var UserDrawingController = class {
|
|
|
32259
32610
|
return;
|
|
32260
32611
|
}
|
|
32261
32612
|
this.interaction.move(x, y, snap, shift2);
|
|
32262
|
-
this.updateHover(x, y);
|
|
32613
|
+
this.updateHover(x, y, mod);
|
|
32263
32614
|
}
|
|
32264
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
32265
|
-
|
|
32615
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
32616
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
32617
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
32618
|
+
updateHover(x, y, mod = false) {
|
|
32266
32619
|
let id = null;
|
|
32267
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32620
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32268
32621
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
32269
32622
|
}
|
|
32270
32623
|
if (id !== this.hoveredId) {
|
|
@@ -32284,7 +32637,12 @@ var UserDrawingController = class {
|
|
|
32284
32637
|
this.render();
|
|
32285
32638
|
return;
|
|
32286
32639
|
}
|
|
32640
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
32287
32641
|
this.interaction.up(x, y);
|
|
32642
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
32643
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
32644
|
+
else this.openSettingsById(dragged, x, y);
|
|
32645
|
+
}
|
|
32288
32646
|
}
|
|
32289
32647
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
32290
32648
|
toggleMeasure() {
|
|
@@ -32549,7 +32907,7 @@ var UserDrawingController = class {
|
|
|
32549
32907
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
32550
32908
|
break;
|
|
32551
32909
|
case "delete": {
|
|
32552
|
-
const ids = this.selectedIds.size ? this.
|
|
32910
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
32553
32911
|
if (ids.length) {
|
|
32554
32912
|
this.popup.close();
|
|
32555
32913
|
this.emit({ kind: "delete", ids });
|
|
@@ -32565,6 +32923,9 @@ var UserDrawingController = class {
|
|
|
32565
32923
|
selectionIds() {
|
|
32566
32924
|
return [...this.selectedIds];
|
|
32567
32925
|
}
|
|
32926
|
+
deletableSelection() {
|
|
32927
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
32928
|
+
}
|
|
32568
32929
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
32569
32930
|
nudgeSelection(dx, dy) {
|
|
32570
32931
|
const proj = this.deps.projector();
|
|
@@ -32673,6 +33034,8 @@ var UserDrawingController = class {
|
|
|
32673
33034
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
32674
33035
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32675
33036
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33037
|
+
const clones = this.interaction.dragClones();
|
|
33038
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
32676
33039
|
this.layoutTextEditor();
|
|
32677
33040
|
const ghost = this.interaction.ghost();
|
|
32678
33041
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -32688,6 +33051,8 @@ var UserDrawingController = class {
|
|
|
32688
33051
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
32689
33052
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
32690
33053
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
33054
|
+
const marquee = this.interaction.marqueeRect();
|
|
33055
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
32691
33056
|
}
|
|
32692
33057
|
destroy() {
|
|
32693
33058
|
this.closeTextEditor();
|
|
@@ -32705,15 +33070,80 @@ var UserDrawingController = class {
|
|
|
32705
33070
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
32706
33071
|
this.render();
|
|
32707
33072
|
}
|
|
33073
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
33074
|
+
emitEdits(drawings) {
|
|
33075
|
+
const docs = drawings.map((d) => d.serialize());
|
|
33076
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
33077
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
33078
|
+
}
|
|
33079
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
33080
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
33081
|
+
openSettingsForSelection(ids) {
|
|
33082
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
33083
|
+
const drawings = live();
|
|
33084
|
+
if (drawings.length < 2) return;
|
|
33085
|
+
const proj = this.deps.projector();
|
|
33086
|
+
let anchor = null;
|
|
33087
|
+
for (const d of drawings) {
|
|
33088
|
+
const b = d.bounds(proj);
|
|
33089
|
+
if (!b) continue;
|
|
33090
|
+
if (!anchor) anchor = { ...b };
|
|
33091
|
+
else {
|
|
33092
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
33093
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
33094
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
33095
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
33096
|
+
anchor.w = right - anchor.x;
|
|
33097
|
+
anchor.h = bottom - anchor.y;
|
|
33098
|
+
}
|
|
33099
|
+
}
|
|
33100
|
+
this.popupMulti = true;
|
|
33101
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
33102
|
+
this.popup.open(drawings, anchor, {
|
|
33103
|
+
resolve: () => live()[0] ?? null,
|
|
33104
|
+
patch: (p) => {
|
|
33105
|
+
const ds = live();
|
|
33106
|
+
for (const d of ds) d.applySettings(p);
|
|
33107
|
+
this.render();
|
|
33108
|
+
this.emitEdits(ds);
|
|
33109
|
+
},
|
|
33110
|
+
setLocked: (v) => {
|
|
33111
|
+
const ds = live();
|
|
33112
|
+
for (const d of ds) d.locked = v;
|
|
33113
|
+
this.emitEdits(ds);
|
|
33114
|
+
},
|
|
33115
|
+
reorder: (to) => {
|
|
33116
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
33117
|
+
},
|
|
33118
|
+
duplicate: () => {
|
|
33119
|
+
this.popup.close();
|
|
33120
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
33121
|
+
},
|
|
33122
|
+
resetSettings: () => {
|
|
33123
|
+
const ds = live();
|
|
33124
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
33125
|
+
this.render();
|
|
33126
|
+
this.emitEdits(ds);
|
|
33127
|
+
this.openSettingsForSelection(ids);
|
|
33128
|
+
},
|
|
33129
|
+
remove: () => {
|
|
33130
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
33131
|
+
if (ids2.length === 0) return;
|
|
33132
|
+
this.popup.close();
|
|
33133
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
33134
|
+
}
|
|
33135
|
+
}, this.onPopupDismissed);
|
|
33136
|
+
}
|
|
32708
33137
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
32709
33138
|
openSettingsById(id, _x, _y) {
|
|
32710
33139
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
32711
33140
|
if (!drawing) return;
|
|
32712
33141
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
33142
|
+
this.popupMulti = false;
|
|
32713
33143
|
this.emit({ kind: "select", ids: [id] });
|
|
32714
33144
|
this.emit({ kind: "settings", id });
|
|
32715
33145
|
const anchor = drawing.bounds(this.deps.projector());
|
|
32716
|
-
this.popup.open(drawing, anchor, {
|
|
33146
|
+
this.popup.open([drawing], anchor, {
|
|
32717
33147
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
32718
33148
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
32719
33149
|
resolve: () => live() ?? null,
|
|
@@ -32731,6 +33161,11 @@ var UserDrawingController = class {
|
|
|
32731
33161
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32732
33162
|
},
|
|
32733
33163
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
33164
|
+
duplicate: () => {
|
|
33165
|
+
this.finishTextEditor(true);
|
|
33166
|
+
this.popup.close();
|
|
33167
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
33168
|
+
},
|
|
32734
33169
|
resetSettings: () => {
|
|
32735
33170
|
const d = live();
|
|
32736
33171
|
if (!d) return;
|
|
@@ -32753,7 +33188,13 @@ var UserDrawingController = class {
|
|
|
32753
33188
|
this.popup.close();
|
|
32754
33189
|
this.emit({ kind: "delete", ids: [id] });
|
|
32755
33190
|
}
|
|
32756
|
-
},
|
|
33191
|
+
}, this.onPopupDismissed);
|
|
33192
|
+
}
|
|
33193
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
33194
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
33195
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
33196
|
+
deselect() {
|
|
33197
|
+
this.clearSelection();
|
|
32757
33198
|
}
|
|
32758
33199
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
32759
33200
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -32779,6 +33220,13 @@ var UserDrawingController = class {
|
|
|
32779
33220
|
}, 0);
|
|
32780
33221
|
return;
|
|
32781
33222
|
}
|
|
33223
|
+
if (i.kind === "clone") {
|
|
33224
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
33225
|
+
this.intentCb?.(i);
|
|
33226
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
33227
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
33228
|
+
return;
|
|
33229
|
+
}
|
|
32782
33230
|
this.intentCb?.(i);
|
|
32783
33231
|
}
|
|
32784
33232
|
};
|
|
@@ -33807,7 +34255,6 @@ var ZOOM_OUT_MARGIN_BARS = 6;
|
|
|
33807
34255
|
var ZOOM_TAU_MS = 70;
|
|
33808
34256
|
var SCALE_TAU_MS = 80;
|
|
33809
34257
|
var FLING_TAU_MS = 110;
|
|
33810
|
-
var LIVE_BAR_TAU_MS = 90;
|
|
33811
34258
|
var SCROLL_TO_TAU_MS = 130;
|
|
33812
34259
|
var FLING_STOP_PX = 0.02;
|
|
33813
34260
|
var PRICE_SCALE_K = 4e-3;
|
|
@@ -33878,9 +34325,13 @@ var NativeRenderer = class {
|
|
|
33878
34325
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
33879
34326
|
this.historyChordsEnabled = true;
|
|
33880
34327
|
this.liveRegion = null;
|
|
33881
|
-
// ── animation state (eased zoom + inertial pan) ──
|
|
34328
|
+
// ── animation state (eased zoom + inertial pan + live-bar glide) ──
|
|
33882
34329
|
this.animZoom = true;
|
|
33883
34330
|
this.animPan = true;
|
|
34331
|
+
this.animLiveBarMs = 0;
|
|
34332
|
+
// forming-bar OHLC glide time-constant; 0 = each tick snaps
|
|
34333
|
+
this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
|
|
34334
|
+
// the duration the settings-dialog on/off toggle restores (last non-zero value configured)
|
|
33884
34335
|
// Brand default candles.
|
|
33885
34336
|
this.candleUp = BULLISH;
|
|
33886
34337
|
this.candleDown = BEARISH;
|
|
@@ -34005,7 +34456,7 @@ var NativeRenderer = class {
|
|
|
34005
34456
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
34006
34457
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
34007
34458
|
this.name = "native";
|
|
34008
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
34459
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
34009
34460
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
34010
34461
|
* so moving onto the button doesn't count as leaving). */
|
|
34011
34462
|
this.onScrollProximityMove = (e) => {
|
|
@@ -34039,6 +34490,7 @@ var NativeRenderer = class {
|
|
|
34039
34490
|
this.backendMode = opts.nativeBackend;
|
|
34040
34491
|
this.animZoom = opts.animZoom;
|
|
34041
34492
|
this.animPan = opts.animPan;
|
|
34493
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
|
|
34042
34494
|
this.glowAmount = opts.glow;
|
|
34043
34495
|
this.candleUp = opts.upColor;
|
|
34044
34496
|
this.candleDown = opts.downColor;
|
|
@@ -34088,6 +34540,10 @@ var NativeRenderer = class {
|
|
|
34088
34540
|
case "animPan":
|
|
34089
34541
|
this.animPan = Boolean(value);
|
|
34090
34542
|
return;
|
|
34543
|
+
case "animLiveBar":
|
|
34544
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(value));
|
|
34545
|
+
return;
|
|
34546
|
+
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
34091
34547
|
case "intro": {
|
|
34092
34548
|
const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
|
|
34093
34549
|
this.introStyle = s;
|
|
@@ -34224,6 +34680,8 @@ var NativeRenderer = class {
|
|
|
34224
34680
|
return this.animZoom;
|
|
34225
34681
|
case "animPan":
|
|
34226
34682
|
return this.animPan;
|
|
34683
|
+
case "animLiveBar":
|
|
34684
|
+
return this.animLiveBarMs;
|
|
34227
34685
|
case "intro":
|
|
34228
34686
|
return this.introStyle;
|
|
34229
34687
|
case "zoomAnchor":
|
|
@@ -34392,7 +34850,8 @@ var NativeRenderer = class {
|
|
|
34392
34850
|
labelsVisible: this.scene.showAxisLabels,
|
|
34393
34851
|
currentPriceLine: this.scene.showPriceLine,
|
|
34394
34852
|
priceLabel: this.scene.showPriceLabel,
|
|
34395
|
-
countdown: this.scene.showCountdown
|
|
34853
|
+
countdown: this.scene.showCountdown,
|
|
34854
|
+
animateLastPrice: this.animLiveBarMs > 0
|
|
34396
34855
|
},
|
|
34397
34856
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
34398
34857
|
trades: {
|
|
@@ -34499,6 +34958,7 @@ var NativeRenderer = class {
|
|
|
34499
34958
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
34500
34959
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
34501
34960
|
this.syncCountdownTimer();
|
|
34961
|
+
this.animLiveBarMs = next.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
|
|
34502
34962
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
34503
34963
|
this.scene.tradeMarkers = {
|
|
34504
34964
|
visible: next.trades.visible,
|
|
@@ -34942,7 +35402,10 @@ var NativeRenderer = class {
|
|
|
34942
35402
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
34943
35403
|
fling: (v) => this.fling(v),
|
|
34944
35404
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
34945
|
-
onClick: (x) =>
|
|
35405
|
+
onClick: (x) => {
|
|
35406
|
+
this.userDrawings?.deselect();
|
|
35407
|
+
this.handleClick(x);
|
|
35408
|
+
},
|
|
34946
35409
|
onAxisLongPress: (axis, x, y) => {
|
|
34947
35410
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
34948
35411
|
},
|
|
@@ -34960,11 +35423,13 @@ var NativeRenderer = class {
|
|
|
34960
35423
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
34961
35424
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
34962
35425
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
34963
|
-
|
|
35426
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
35427
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
35428
|
+
// middle-click: a selected hit takes the whole selection
|
|
34964
35429
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
34965
35430
|
drawingsSnapMode: () => this.snapMode,
|
|
34966
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
34967
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
35431
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35432
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
34968
35433
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
34969
35434
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
34970
35435
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -35315,8 +35780,11 @@ var NativeRenderer = class {
|
|
|
35315
35780
|
const last = this.bars[n - 1];
|
|
35316
35781
|
if (last && bar.time === last.time) {
|
|
35317
35782
|
this.bars[n - 1] = bar;
|
|
35318
|
-
if (this.liveEaseTime !== bar.time)
|
|
35319
|
-
|
|
35783
|
+
if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
|
|
35784
|
+
this.syncLiveEase(bar);
|
|
35785
|
+
} else {
|
|
35786
|
+
this.animator.start();
|
|
35787
|
+
}
|
|
35320
35788
|
} else if (!last || bar.time > last.time) {
|
|
35321
35789
|
this.bars.push(bar);
|
|
35322
35790
|
this.syncLiveEase(bar);
|
|
@@ -35328,6 +35796,12 @@ var NativeRenderer = class {
|
|
|
35328
35796
|
this.scene.bars = this.bars;
|
|
35329
35797
|
this.scheduler.invalidate(4 /* Full */);
|
|
35330
35798
|
}
|
|
35799
|
+
/** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
|
|
35800
|
+
* what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
|
|
35801
|
+
setLiveBarEase(ms) {
|
|
35802
|
+
this.animLiveBarMs = ms;
|
|
35803
|
+
if (ms > 0) this.animLiveBarOnMs = ms;
|
|
35804
|
+
}
|
|
35331
35805
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
35332
35806
|
syncLiveEase(bar) {
|
|
35333
35807
|
this.liveEaseTime = bar.time;
|
|
@@ -35340,9 +35814,10 @@ var NativeRenderer = class {
|
|
|
35340
35814
|
const target = this.bars[this.bars.length - 1];
|
|
35341
35815
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
35342
35816
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
35343
|
-
const
|
|
35344
|
-
const
|
|
35345
|
-
const
|
|
35817
|
+
const tau = this.animLiveBarMs;
|
|
35818
|
+
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
35819
|
+
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
35820
|
+
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
35346
35821
|
const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
|
|
35347
35822
|
this.liveEaseHigh = active ? nh : target.high;
|
|
35348
35823
|
this.liveEaseLow = active ? nl : target.low;
|
|
@@ -37288,6 +37763,10 @@ function str3(inputs, key, fallback) {
|
|
|
37288
37763
|
const v = inputs[key];
|
|
37289
37764
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
37290
37765
|
}
|
|
37766
|
+
function bool(inputs, key, fallback) {
|
|
37767
|
+
const v = inputs[key];
|
|
37768
|
+
return typeof v === "boolean" ? v : fallback;
|
|
37769
|
+
}
|
|
37291
37770
|
function toPoints(bars, values, colors) {
|
|
37292
37771
|
const pts = new Array(bars.length);
|
|
37293
37772
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -38819,10 +39298,6 @@ var aroon = {
|
|
|
38819
39298
|
plots: [
|
|
38820
39299
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
38821
39300
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
38822
|
-
],
|
|
38823
|
-
levels: [
|
|
38824
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
38825
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
38826
39301
|
]
|
|
38827
39302
|
};
|
|
38828
39303
|
}
|
|
@@ -39320,8 +39795,10 @@ var volumeSpecs = [
|
|
|
39320
39795
|
var DAY_MS2 = 864e5;
|
|
39321
39796
|
function periodKey(anchor, time) {
|
|
39322
39797
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
39323
|
-
if (anchor === "Month") {
|
|
39798
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
39324
39799
|
const d = new Date(time);
|
|
39800
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
39801
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
39325
39802
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
39326
39803
|
}
|
|
39327
39804
|
return Math.floor(time / DAY_MS2);
|
|
@@ -39373,40 +39850,86 @@ var parabolicSar = {
|
|
|
39373
39850
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
39374
39851
|
}
|
|
39375
39852
|
};
|
|
39853
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
39854
|
+
var VWAP_STYLE = "Style";
|
|
39855
|
+
var VWAP_BANDS = [
|
|
39856
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
39857
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
39858
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
39859
|
+
];
|
|
39376
39860
|
var vwap = {
|
|
39377
39861
|
type: "vwap",
|
|
39378
39862
|
title: "Volume Weighted Average Price",
|
|
39379
39863
|
shortTitle: "VWAP",
|
|
39380
39864
|
overlay: true,
|
|
39381
39865
|
inputs: [
|
|
39382
|
-
{ key: "anchor", title: "
|
|
39383
|
-
|
|
39384
|
-
|
|
39866
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
39867
|
+
sourceInput("HLC3"),
|
|
39868
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
39869
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39870
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
39871
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
39872
|
+
]),
|
|
39873
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
39874
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
39875
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39876
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
39877
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
39878
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
39879
|
+
])
|
|
39385
39880
|
],
|
|
39386
39881
|
compute: (bars, inputs) => {
|
|
39387
39882
|
const anchor = str3(inputs, "anchor", "Day");
|
|
39388
|
-
const
|
|
39389
|
-
const
|
|
39883
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
39884
|
+
const n = bars.length;
|
|
39885
|
+
const values = new Array(n).fill(Number.NaN);
|
|
39886
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
39887
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
39888
|
+
color: str3(inputs, b.color, b.defColor),
|
|
39889
|
+
fill: bool(inputs, b.fill, true),
|
|
39890
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
39891
|
+
up: new Array(n).fill(Number.NaN),
|
|
39892
|
+
down: new Array(n).fill(Number.NaN)
|
|
39893
|
+
}));
|
|
39390
39894
|
let period = Number.NaN;
|
|
39391
39895
|
let cumPV = 0;
|
|
39896
|
+
let cumPV2 = 0;
|
|
39392
39897
|
let cumV = 0;
|
|
39393
|
-
for (let i = 0; i <
|
|
39898
|
+
for (let i = 0; i < n; i++) {
|
|
39394
39899
|
const b = bars[i];
|
|
39395
39900
|
const key = periodKey(anchor, b.time);
|
|
39396
39901
|
if (key !== period) {
|
|
39397
39902
|
period = key;
|
|
39398
39903
|
cumPV = 0;
|
|
39904
|
+
cumPV2 = 0;
|
|
39399
39905
|
cumV = 0;
|
|
39400
39906
|
}
|
|
39401
39907
|
const v = b.volume;
|
|
39402
39908
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
39403
|
-
const tp =
|
|
39909
|
+
const tp = src[i];
|
|
39404
39910
|
cumPV += tp * v;
|
|
39911
|
+
cumPV2 += tp * tp * v;
|
|
39405
39912
|
cumV += v;
|
|
39406
39913
|
}
|
|
39407
|
-
if (cumV
|
|
39408
|
-
|
|
39409
|
-
|
|
39914
|
+
if (cumV <= 0) continue;
|
|
39915
|
+
const mean = cumPV / cumV;
|
|
39916
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
39917
|
+
values[i] = mean;
|
|
39918
|
+
for (const band of bands) {
|
|
39919
|
+
band.up[i] = mean + band.mult * sd;
|
|
39920
|
+
band.down[i] = mean - band.mult * sd;
|
|
39921
|
+
}
|
|
39922
|
+
}
|
|
39923
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
39924
|
+
const fills = [];
|
|
39925
|
+
bands.forEach((band, i) => {
|
|
39926
|
+
plots.push(
|
|
39927
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
39928
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
39929
|
+
);
|
|
39930
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
39931
|
+
});
|
|
39932
|
+
return { plots, bands: fills };
|
|
39410
39933
|
}
|
|
39411
39934
|
};
|
|
39412
39935
|
var pivotPoints = {
|
|
@@ -39597,21 +40120,11 @@ var Vela = class {
|
|
|
39597
40120
|
registerClassicIndicators();
|
|
39598
40121
|
const element = resolveElement(container);
|
|
39599
40122
|
const theme = resolveTheme(options.theme);
|
|
39600
|
-
let animZoom = true;
|
|
39601
|
-
let animPan = true;
|
|
39602
|
-
if (typeof options.animations === "boolean") {
|
|
39603
|
-
animZoom = options.animations;
|
|
39604
|
-
animPan = options.animations;
|
|
39605
|
-
} else if (options.animations) {
|
|
39606
|
-
animZoom = options.animations.zoom ?? true;
|
|
39607
|
-
animPan = options.animations.pan ?? true;
|
|
39608
|
-
}
|
|
39609
40123
|
const display = {
|
|
39610
40124
|
currentPriceLine: options.currentPriceLine ?? true,
|
|
39611
40125
|
logScale: options.logScale ?? false,
|
|
39612
40126
|
nativeBackend: options.nativeBackend ?? "auto",
|
|
39613
|
-
|
|
39614
|
-
animPan,
|
|
40127
|
+
...resolveAnimations(options.animations),
|
|
39615
40128
|
glow: options.glow ?? 0,
|
|
39616
40129
|
upColor: options.upColor ?? BULLISH,
|
|
39617
40130
|
downColor: options.downColor ?? BEARISH,
|
|
@@ -39926,13 +40439,28 @@ function resolveElement(container) {
|
|
|
39926
40439
|
return element;
|
|
39927
40440
|
}
|
|
39928
40441
|
|
|
39929
|
-
// src/core/options.ts
|
|
39930
|
-
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
39931
|
-
|
|
39932
40442
|
// src/widget/statusline-model.ts
|
|
39933
40443
|
function segmentVisibility(parts, chartHidden = false) {
|
|
39934
40444
|
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
39935
40445
|
}
|
|
40446
|
+
var STATUSLINE_LADDER = [
|
|
40447
|
+
{ stacked: false, level: "full" },
|
|
40448
|
+
{ stacked: true, level: "full" },
|
|
40449
|
+
{ stacked: true, level: "compact" },
|
|
40450
|
+
{ stacked: true, level: "minimal" }
|
|
40451
|
+
];
|
|
40452
|
+
function readoutCells(level, readout) {
|
|
40453
|
+
if (readout === "value") return [{ key: "close", label: "" }];
|
|
40454
|
+
if (level === "full") {
|
|
40455
|
+
return [
|
|
40456
|
+
{ key: "open", label: "O" },
|
|
40457
|
+
{ key: "high", label: "H" },
|
|
40458
|
+
{ key: "low", label: "L" },
|
|
40459
|
+
{ key: "close", label: "C" }
|
|
40460
|
+
];
|
|
40461
|
+
}
|
|
40462
|
+
return [{ key: "close", label: level === "compact" ? "C" : "" }];
|
|
40463
|
+
}
|
|
39936
40464
|
function statuslineMenuItems(parts, chartVisible) {
|
|
39937
40465
|
return [
|
|
39938
40466
|
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
@@ -39961,6 +40489,11 @@ function fmtChange(open2, close) {
|
|
|
39961
40489
|
const sign = diff >= 0 ? "+" : "";
|
|
39962
40490
|
return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
|
|
39963
40491
|
}
|
|
40492
|
+
function fmtChangePct(open2, close) {
|
|
40493
|
+
if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
|
|
40494
|
+
const pct = (close - open2) / open2 * 100;
|
|
40495
|
+
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
|
40496
|
+
}
|
|
39964
40497
|
|
|
39965
40498
|
// src/widget/statusline.ts
|
|
39966
40499
|
var STYLE_ID24 = "vela-widget-statusline";
|
|
@@ -39977,6 +40510,11 @@ var CSS21 = `
|
|
|
39977
40510
|
display: flex;
|
|
39978
40511
|
align-items: baseline;
|
|
39979
40512
|
gap: var(--vela-space-2);
|
|
40513
|
+
/* The chip never grows past the plot: fit() measures overflow against this width to
|
|
40514
|
+
* walk the layout ladder (stack, then shed values), so overflow:hidden only guards
|
|
40515
|
+
* the transient between a resize and the next measure. */
|
|
40516
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40517
|
+
overflow: hidden;
|
|
39980
40518
|
color: var(--vela-fg);
|
|
39981
40519
|
font-size: var(--vela-font-size-md);
|
|
39982
40520
|
/* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
|
|
@@ -39998,6 +40536,31 @@ var CSS21 = `
|
|
|
39998
40536
|
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
39999
40537
|
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
40000
40538
|
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
40539
|
+
/* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
|
|
40540
|
+
* value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
|
|
40541
|
+
* side by side on one line; the STACKED layouts (fit() decides, see the ladder in
|
|
40542
|
+
* statusline-model) turn the chip into a column so the values drop under the symbol. */
|
|
40543
|
+
.vela-statusline .vela-sl-row {
|
|
40544
|
+
display: flex;
|
|
40545
|
+
align-items: baseline;
|
|
40546
|
+
gap: var(--vela-space-2);
|
|
40547
|
+
white-space: nowrap;
|
|
40548
|
+
}
|
|
40549
|
+
.vela-statusline.vela-sl-stacked {
|
|
40550
|
+
flex-direction: column;
|
|
40551
|
+
align-items: flex-start;
|
|
40552
|
+
/* The same air between the two rows as between the values row and the legend row
|
|
40553
|
+
* that follows the chip (see the stacked legend shift below). */
|
|
40554
|
+
row-gap: var(--vela-space-1);
|
|
40555
|
+
}
|
|
40556
|
+
/* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
|
|
40557
|
+
* never does, so its overflow is what fit() measures to keep descending the ladder. */
|
|
40558
|
+
.vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
|
|
40559
|
+
.vela-statusline.vela-sl-stacked .vela-sl-meta {
|
|
40560
|
+
min-width: 0;
|
|
40561
|
+
overflow: hidden;
|
|
40562
|
+
text-overflow: ellipsis;
|
|
40563
|
+
}
|
|
40001
40564
|
.vela-statusline .vela-sl-avatar {
|
|
40002
40565
|
width: 18px;
|
|
40003
40566
|
height: 18px;
|
|
@@ -40049,59 +40612,13 @@ var CSS21 = `
|
|
|
40049
40612
|
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
40050
40613
|
* chart on the page, including statusline-less ones. */
|
|
40051
40614
|
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40052
|
-
/*
|
|
40053
|
-
*
|
|
40054
|
-
|
|
40055
|
-
|
|
40056
|
-
*
|
|
40057
|
-
|
|
40058
|
-
[data-layout='mobile'] .vela-statusline {
|
|
40059
|
-
display: grid;
|
|
40060
|
-
grid-template-columns: auto auto minmax(0, auto) auto;
|
|
40061
|
-
justify-content: start;
|
|
40062
|
-
align-items: center;
|
|
40063
|
-
row-gap: 1px;
|
|
40064
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40065
|
-
font-size: var(--vela-font-size-sm);
|
|
40066
|
-
}
|
|
40067
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-symbol {
|
|
40068
|
-
font-size: var(--vela-font-size-md);
|
|
40069
|
-
line-height: 1;
|
|
40070
|
-
}
|
|
40071
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-meta {
|
|
40072
|
-
min-width: 0;
|
|
40073
|
-
overflow: hidden;
|
|
40074
|
-
text-overflow: ellipsis;
|
|
40075
|
-
white-space: nowrap;
|
|
40076
|
-
line-height: 1;
|
|
40077
|
-
}
|
|
40078
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-avatar,
|
|
40079
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-market { align-self: center; }
|
|
40080
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-ohlc { display: none !important; }
|
|
40081
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-change {
|
|
40082
|
-
/* Second row, under the text column \u2014 the avatar keeps the first column. */
|
|
40083
|
-
grid-column: 2 / -1;
|
|
40084
|
-
font-size: var(--vela-font-size-sm);
|
|
40085
|
-
line-height: 1.2;
|
|
40086
|
-
}
|
|
40087
|
-
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40088
|
-
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
40089
|
-
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
40090
|
-
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
40091
|
-
* Placed after the mobile block on purpose: same specificity, later wins. */
|
|
40092
|
-
.vela-statusline.vela-sl-fit {
|
|
40093
|
-
display: flex; /* undo the mobile grid \u2014 fit mode is one flex row again */
|
|
40094
|
-
flex-wrap: nowrap;
|
|
40095
|
-
align-items: center;
|
|
40096
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40097
|
-
overflow: hidden;
|
|
40098
|
-
}
|
|
40099
|
-
.vela-statusline.vela-sl-fit .vela-sl-change {
|
|
40100
|
-
flex-basis: auto;
|
|
40101
|
-
padding-left: 0;
|
|
40102
|
-
}
|
|
40103
|
-
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
40104
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40615
|
+
/* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
|
|
40616
|
+
* host whenever the ladder lands on a stacked layout. */
|
|
40617
|
+
.vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40618
|
+
/* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
|
|
40619
|
+
* of the readout fits, exactly as on a narrow desktop chart. */
|
|
40620
|
+
[data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
|
|
40621
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
|
|
40105
40622
|
`;
|
|
40106
40623
|
function baseOfTicker(ticker) {
|
|
40107
40624
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -40172,6 +40689,8 @@ var Statusline = class {
|
|
|
40172
40689
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
40173
40690
|
this.fitMode = false;
|
|
40174
40691
|
this.fitRO = null;
|
|
40692
|
+
/** The ladder rung currently applied — see {@link fit}. */
|
|
40693
|
+
this.layout = { stacked: false, level: "full" };
|
|
40175
40694
|
this.onContextMenu = (e) => {
|
|
40176
40695
|
if (!this.menu || !this.menuHooks) return;
|
|
40177
40696
|
e.preventDefault();
|
|
@@ -40218,8 +40737,18 @@ var Statusline = class {
|
|
|
40218
40737
|
this.menuHooks?.setChartVisible(true);
|
|
40219
40738
|
this.setChartHidden(false);
|
|
40220
40739
|
});
|
|
40221
|
-
this.
|
|
40740
|
+
this.identityRow = doc.createElement("span");
|
|
40741
|
+
this.identityRow.className = "vela-sl-row vela-sl-identity";
|
|
40742
|
+
this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
|
|
40743
|
+
this.valuesRow = doc.createElement("span");
|
|
40744
|
+
this.valuesRow.className = "vela-sl-row vela-sl-values";
|
|
40745
|
+
this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
|
|
40746
|
+
this.el.append(this.identityRow, this.valuesRow);
|
|
40222
40747
|
host.appendChild(this.el);
|
|
40748
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40749
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40750
|
+
this.fitRO.observe(host);
|
|
40751
|
+
}
|
|
40223
40752
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
40224
40753
|
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
40225
40754
|
this.setMarketStatus("open");
|
|
@@ -40235,27 +40764,15 @@ var Statusline = class {
|
|
|
40235
40764
|
this.fit();
|
|
40236
40765
|
}
|
|
40237
40766
|
/**
|
|
40238
|
-
* Multi-chart cells:
|
|
40239
|
-
*
|
|
40240
|
-
*
|
|
40241
|
-
* market badge; the logo + ticker always stay.
|
|
40767
|
+
* Multi-chart cells: the same width ladder as a single chart, plus a last resort for
|
|
40768
|
+
* cells too narrow for even its bottom rung — whatever still overflows is hidden
|
|
40769
|
+
* outright rather than clipped mid-glyph, least important first: the values, the
|
|
40770
|
+
* venue/timeframe meta, then the market badge; the logo + ticker always stay.
|
|
40242
40771
|
*/
|
|
40243
40772
|
setFitMode(on) {
|
|
40244
40773
|
if (on === this.fitMode) return;
|
|
40245
40774
|
this.fitMode = on;
|
|
40246
|
-
this.
|
|
40247
|
-
this.host.classList.toggle("vela-sl-fit-host", on);
|
|
40248
|
-
if (on) {
|
|
40249
|
-
if (typeof ResizeObserver !== "undefined" && !this.fitRO) {
|
|
40250
|
-
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40251
|
-
this.fitRO.observe(this.host);
|
|
40252
|
-
}
|
|
40253
|
-
this.fit();
|
|
40254
|
-
} else {
|
|
40255
|
-
this.fitRO?.disconnect();
|
|
40256
|
-
this.fitRO = null;
|
|
40257
|
-
this.syncParts();
|
|
40258
|
-
}
|
|
40775
|
+
this.fit();
|
|
40259
40776
|
}
|
|
40260
40777
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
40261
40778
|
syncParts() {
|
|
@@ -40267,22 +40784,42 @@ var Statusline = class {
|
|
|
40267
40784
|
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
40268
40785
|
this.changeEl.style.display = seg.change ? "" : "none";
|
|
40269
40786
|
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
40787
|
+
this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
|
|
40788
|
+
}
|
|
40789
|
+
/** Apply one ladder rung: the stacked/inline shape and the readout's level. */
|
|
40790
|
+
applyLayout(layout) {
|
|
40791
|
+
this.layout = layout;
|
|
40792
|
+
this.el.classList.toggle("vela-sl-stacked", layout.stacked);
|
|
40793
|
+
this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
|
|
40794
|
+
this.renderValues();
|
|
40270
40795
|
}
|
|
40271
|
-
/**
|
|
40796
|
+
/**
|
|
40797
|
+
* Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
|
|
40798
|
+
* max-width: the full one-line readout, then the values stacked under the symbol
|
|
40799
|
+
* line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
|
|
40800
|
+
* still overflows, whole segments hide, least important first. Without layout
|
|
40801
|
+
* (detached host, node tests) nothing overflows, so the widest rung stays.
|
|
40802
|
+
*/
|
|
40272
40803
|
fit() {
|
|
40273
|
-
if (!this.fitMode) return;
|
|
40274
40804
|
this.syncParts();
|
|
40275
40805
|
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
40806
|
+
const overflows = () => this.el.scrollWidth > this.el.clientWidth;
|
|
40807
|
+
const hasValues = seg.ohlc || seg.change;
|
|
40808
|
+
for (const rung of STATUSLINE_LADDER) {
|
|
40809
|
+
this.applyLayout(rung);
|
|
40810
|
+
if (!hasValues || !overflows()) break;
|
|
40811
|
+
}
|
|
40812
|
+
if (!this.fitMode) return;
|
|
40276
40813
|
const order = [
|
|
40277
|
-
[this.
|
|
40278
|
-
[this.changeEl, seg.change],
|
|
40814
|
+
[this.valuesRow, hasValues],
|
|
40279
40815
|
[this.metaEl, seg.meta],
|
|
40280
40816
|
[this.marketEl, seg.market]
|
|
40281
40817
|
];
|
|
40282
40818
|
for (const [el, shown] of order) {
|
|
40283
|
-
if (
|
|
40819
|
+
if (!overflows()) break;
|
|
40284
40820
|
if (shown) el.style.display = "none";
|
|
40285
40821
|
}
|
|
40822
|
+
if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
|
|
40286
40823
|
}
|
|
40287
40824
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
40288
40825
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -40395,7 +40932,7 @@ var Statusline = class {
|
|
|
40395
40932
|
this.marketTip.destroy();
|
|
40396
40933
|
this.eyeTip.destroy();
|
|
40397
40934
|
this.marketBubble.destroy();
|
|
40398
|
-
this.host.classList.remove("vela-has-statusline", "vela-sl-
|
|
40935
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
|
|
40399
40936
|
this.el.remove();
|
|
40400
40937
|
}
|
|
40401
40938
|
detach() {
|
|
@@ -40404,6 +40941,10 @@ var Statusline = class {
|
|
|
40404
40941
|
}
|
|
40405
40942
|
render() {
|
|
40406
40943
|
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
40944
|
+
this.fit();
|
|
40945
|
+
}
|
|
40946
|
+
/** Write the value readout for the current bar at the current ladder level. */
|
|
40947
|
+
renderValues() {
|
|
40407
40948
|
const bar = this.hoverBar ?? this.lastBar;
|
|
40408
40949
|
if (!bar) {
|
|
40409
40950
|
this.ohlcEl.replaceChildren();
|
|
@@ -40423,12 +40964,11 @@ var Statusline = class {
|
|
|
40423
40964
|
s.appendChild(b);
|
|
40424
40965
|
return s;
|
|
40425
40966
|
};
|
|
40426
|
-
|
|
40427
|
-
|
|
40428
|
-
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
40967
|
+
const { level } = this.layout;
|
|
40968
|
+
this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
|
|
40969
|
+
this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
|
|
40429
40970
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
40430
40971
|
this.changeEl.style.color = ink;
|
|
40431
|
-
this.fit();
|
|
40432
40972
|
}
|
|
40433
40973
|
};
|
|
40434
40974
|
|
|
@@ -42568,6 +43108,17 @@ function nextAutoCellId(taken) {
|
|
|
42568
43108
|
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
42569
43109
|
}
|
|
42570
43110
|
}
|
|
43111
|
+
function resolveTyping(key, open2) {
|
|
43112
|
+
if (open2.dialogs > 0) {
|
|
43113
|
+
if (key.length !== 1) return null;
|
|
43114
|
+
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
43115
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
43116
|
+
return null;
|
|
43117
|
+
}
|
|
43118
|
+
if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
43119
|
+
if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
43120
|
+
return null;
|
|
43121
|
+
}
|
|
42571
43122
|
var VelaWorkspace = class {
|
|
42572
43123
|
constructor(container, opts = {}) {
|
|
42573
43124
|
this.events = new TypedEventBus();
|
|
@@ -44201,17 +44752,14 @@ var VelaWorkspace = class {
|
|
|
44201
44752
|
}
|
|
44202
44753
|
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
44203
44754
|
routeTyping(ev) {
|
|
44204
|
-
if (this.destroyed
|
|
44755
|
+
if (this.destroyed) return;
|
|
44205
44756
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
44206
44757
|
if (isEditableTarget(ev)) return;
|
|
44207
|
-
const
|
|
44208
|
-
if (
|
|
44209
|
-
|
|
44210
|
-
|
|
44211
|
-
|
|
44212
|
-
ev.preventDefault();
|
|
44213
|
-
this.tfQuick.open(key);
|
|
44214
|
-
}
|
|
44758
|
+
const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
|
|
44759
|
+
if (!hit) return;
|
|
44760
|
+
ev.preventDefault();
|
|
44761
|
+
if (hit.target === "symbol") this.symbolPicker.type(hit.text);
|
|
44762
|
+
else this.tfQuick.type(hit.text);
|
|
44215
44763
|
}
|
|
44216
44764
|
trackDialog(open2) {
|
|
44217
44765
|
this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
|