@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/widget.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"/>'));
|
|
@@ -5092,15 +5093,15 @@ function candleOverrideFor(style, bags) {
|
|
|
5092
5093
|
if (!hasOwnCandlePaint(style)) return null;
|
|
5093
5094
|
const bag = bags[style] ?? {};
|
|
5094
5095
|
const color = (v) => isColor(v) ? v : null;
|
|
5095
|
-
const
|
|
5096
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
5096
5097
|
return {
|
|
5097
5098
|
upColor: color(bag.candleUpColor),
|
|
5098
5099
|
downColor: color(bag.candleDownColor),
|
|
5099
|
-
bodyVisible:
|
|
5100
|
-
borderVisible:
|
|
5100
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
5101
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
5101
5102
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
5102
5103
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
5103
|
-
wickVisible:
|
|
5104
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
5104
5105
|
wickUpColor: color(bag.candleWickUpColor),
|
|
5105
5106
|
wickDownColor: color(bag.candleWickDownColor)
|
|
5106
5107
|
};
|
|
@@ -5245,7 +5246,8 @@ function mergeConfig(base, patch) {
|
|
|
5245
5246
|
labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
|
|
5246
5247
|
currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
|
|
5247
5248
|
priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
|
|
5248
|
-
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
|
|
5249
|
+
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
5250
|
+
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
5249
5251
|
},
|
|
5250
5252
|
panes: {
|
|
5251
5253
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
@@ -12086,6 +12088,9 @@ function paneLabel(pane, fallbackTitle) {
|
|
|
12086
12088
|
function indicatorLabel(i, fallbackTitle) {
|
|
12087
12089
|
return i.title || fallbackTitle(i.id) || i.id;
|
|
12088
12090
|
}
|
|
12091
|
+
function indicatorRowLabel(i, fallbackTitle) {
|
|
12092
|
+
return i.shorttitle || indicatorLabel(i, fallbackTitle);
|
|
12093
|
+
}
|
|
12089
12094
|
function paneRows(pane) {
|
|
12090
12095
|
return pane.items.flatMap((it) => it.kind === "row" ? [it.row] : []);
|
|
12091
12096
|
}
|
|
@@ -12217,7 +12222,7 @@ function paneItems(snap, pane, units) {
|
|
|
12217
12222
|
const indicatorRow = (i) => ({
|
|
12218
12223
|
kind: "indicator",
|
|
12219
12224
|
id: i.id,
|
|
12220
|
-
label:
|
|
12225
|
+
label: indicatorRowLabel(i, snap.handleTitle),
|
|
12221
12226
|
visible: snap.indicatorVisible(i.id),
|
|
12222
12227
|
ownScale: i.ownScale
|
|
12223
12228
|
});
|
|
@@ -12630,9 +12635,13 @@ var CSS6 = `
|
|
|
12630
12635
|
.vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
|
|
12631
12636
|
|
|
12632
12637
|
/* \u2500\u2500 drawing groups \u2500\u2500 */
|
|
12633
|
-
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block.
|
|
12634
|
-
|
|
12635
|
-
|
|
12638
|
+
/* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
|
|
12639
|
+
border of its own \u2014 units stack flush, so consecutive highlighted rows read as one block; the
|
|
12640
|
+
drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
|
|
12641
|
+
.vela-ot-unit { border-radius: 6px; }
|
|
12642
|
+
/* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
|
|
12643
|
+
.vela-ot-unit:has(> .vela-ot-row[data-picked]) + .vela-ot-unit > .vela-ot-row[data-picked] { border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
12644
|
+
.vela-ot-unit:has(+ .vela-ot-unit > .vela-ot-row[data-picked]) > .vela-ot-row[data-picked] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
12636
12645
|
.vela-ot-row[data-row-kind='group'] > .vela-icon { width: 12px; font-size: 11px; }
|
|
12637
12646
|
/* A member sits indented under its group's header. */
|
|
12638
12647
|
.vela-ot-subrow { padding-left: 26px; }
|
|
@@ -12689,8 +12698,10 @@ var CSS6 = `
|
|
|
12689
12698
|
.vela-ot-gap[data-drop] { height: 4px; background: var(--vela-fg-bright); }
|
|
12690
12699
|
.vela-ot-gap[data-drop]::before { display: none; }
|
|
12691
12700
|
/* A whole container accepts the drop: merging into a pane, or a pane with no drawings yet.
|
|
12692
|
-
The pane block's transparent border reserves the room for this outline
|
|
12701
|
+
The pane block's transparent border reserves the room for this outline; a group unit has no
|
|
12702
|
+
border to color, so it draws the same line as an inset outline (no layout footprint). */
|
|
12693
12703
|
.vela-ot [data-drop='target'] { border-color: var(--vela-fg-bright); background: var(--vela-hover); }
|
|
12704
|
+
.vela-ot-unit[data-drop='target'] { outline: 1px solid var(--vela-fg-bright); outline-offset: -1px; }
|
|
12694
12705
|
/* Where a reorder would insert. */
|
|
12695
12706
|
.vela-ot [data-drop='before'] { box-shadow: inset 0 2px 0 var(--vela-fg-bright); }
|
|
12696
12707
|
.vela-ot [data-drop='after'] { box-shadow: inset 0 -2px 0 var(--vela-fg-bright); }
|
|
@@ -12753,7 +12764,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
12753
12764
|
super(host, "Object tree", "vela-ot");
|
|
12754
12765
|
this.iconFor = iconFor;
|
|
12755
12766
|
this.chart = null;
|
|
12756
|
-
|
|
12767
|
+
/** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
|
|
12768
|
+
this.selectedDrawings = /* @__PURE__ */ new Set();
|
|
12757
12769
|
this.symbolName = "";
|
|
12758
12770
|
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
12759
12771
|
this.symbolRaw = "";
|
|
@@ -12804,12 +12816,9 @@ var ObjectTree = class extends SidePanel {
|
|
|
12804
12816
|
this.unsubs.push(chart.on(ev, () => this.refresh()));
|
|
12805
12817
|
}
|
|
12806
12818
|
this.unsubs.push(
|
|
12807
|
-
chart.on("drawing:selected", ({
|
|
12808
|
-
this.
|
|
12809
|
-
if (!this.syncingSelection)
|
|
12810
|
-
this.picked.clear();
|
|
12811
|
-
if (id) this.picked.add(id);
|
|
12812
|
-
}
|
|
12819
|
+
chart.on("drawing:selected", ({ ids }) => {
|
|
12820
|
+
this.selectedDrawings = new Set(ids);
|
|
12821
|
+
if (!this.syncingSelection) this.picked = new Set(ids);
|
|
12813
12822
|
this.refresh();
|
|
12814
12823
|
})
|
|
12815
12824
|
);
|
|
@@ -13157,7 +13166,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
13157
13166
|
el.dataset.drag = "1";
|
|
13158
13167
|
el.title = "Drag to restack, or onto another pane to move it there";
|
|
13159
13168
|
if (inGroup) el.classList.add("vela-ot-subrow");
|
|
13160
|
-
if (d.id
|
|
13169
|
+
if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
|
|
13161
13170
|
if (this.picked.has(d.id)) el.dataset.picked = "1";
|
|
13162
13171
|
el.addEventListener("click", (e) => this.onDrawClick(e, d.id));
|
|
13163
13172
|
return el;
|
|
@@ -14334,6 +14343,20 @@ var SymbolPicker = class {
|
|
|
14334
14343
|
this.seed = initialQuery;
|
|
14335
14344
|
this.dialog.show();
|
|
14336
14345
|
}
|
|
14346
|
+
get isOpen() {
|
|
14347
|
+
return this.dialog.open;
|
|
14348
|
+
}
|
|
14349
|
+
/** Text typed at the picker that never reached its field: closed, it opens seeded
|
|
14350
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14351
|
+
* fast typist's next key lands in that tick), it extends the query. */
|
|
14352
|
+
type(text) {
|
|
14353
|
+
if (!this.isOpen) {
|
|
14354
|
+
this.open(text);
|
|
14355
|
+
return;
|
|
14356
|
+
}
|
|
14357
|
+
this.input.value = (this.input.value + text).toUpperCase();
|
|
14358
|
+
this.refresh();
|
|
14359
|
+
}
|
|
14337
14360
|
close() {
|
|
14338
14361
|
this.dialog.hide();
|
|
14339
14362
|
}
|
|
@@ -14747,6 +14770,20 @@ var TimeframeQuick = class {
|
|
|
14747
14770
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
14748
14771
|
}, 0);
|
|
14749
14772
|
}
|
|
14773
|
+
get isOpen() {
|
|
14774
|
+
return this.dialog.open;
|
|
14775
|
+
}
|
|
14776
|
+
/** Text typed at the entry that never reached its field: closed, it opens seeded
|
|
14777
|
+
* with the text; open (the field takes focus a tick after the dialog does, and a
|
|
14778
|
+
* fast typist's next key lands in that tick), it extends the entry. */
|
|
14779
|
+
type(text) {
|
|
14780
|
+
if (!this.isOpen) {
|
|
14781
|
+
this.open(text);
|
|
14782
|
+
return;
|
|
14783
|
+
}
|
|
14784
|
+
this.input.value += text;
|
|
14785
|
+
this.renderHint();
|
|
14786
|
+
}
|
|
14750
14787
|
close() {
|
|
14751
14788
|
this.dialog.hide();
|
|
14752
14789
|
}
|
|
@@ -16817,15 +16854,18 @@ var DrawingController = class {
|
|
|
16817
16854
|
next = [...ids];
|
|
16818
16855
|
}
|
|
16819
16856
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
16857
|
+
this.announceSelection();
|
|
16858
|
+
}
|
|
16859
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
16860
|
+
announceSelection() {
|
|
16820
16861
|
this.port?.setSelection(this.selectedIds);
|
|
16821
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16862
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
16822
16863
|
}
|
|
16823
16864
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
16824
16865
|
restoreSnapshot(doc) {
|
|
16825
16866
|
this.store.load(doc);
|
|
16826
16867
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
16827
|
-
this.
|
|
16828
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
16868
|
+
this.announceSelection();
|
|
16829
16869
|
}
|
|
16830
16870
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
16831
16871
|
deleteMany(ids) {
|
|
@@ -16958,6 +16998,9 @@ var DrawingController = class {
|
|
|
16958
16998
|
case "duplicate":
|
|
16959
16999
|
this.duplicate(i.ids);
|
|
16960
17000
|
break;
|
|
17001
|
+
case "clone":
|
|
17002
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
17003
|
+
break;
|
|
16961
17004
|
case "copy":
|
|
16962
17005
|
this.copy(i.ids);
|
|
16963
17006
|
break;
|
|
@@ -17208,6 +17251,23 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
17208
17251
|
clear();
|
|
17209
17252
|
};
|
|
17210
17253
|
}
|
|
17254
|
+
function chromeHint(text, opts) {
|
|
17255
|
+
const px = opts.size ?? 16;
|
|
17256
|
+
const el = document.createElement("span");
|
|
17257
|
+
el.className = "vela-hint";
|
|
17258
|
+
el.setAttribute("role", "img");
|
|
17259
|
+
el.setAttribute("aria-label", text);
|
|
17260
|
+
el.innerHTML = iconAt("info", px);
|
|
17261
|
+
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;`;
|
|
17262
|
+
const hover = (on) => {
|
|
17263
|
+
el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
|
|
17264
|
+
el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
|
|
17265
|
+
};
|
|
17266
|
+
el.addEventListener("pointerenter", () => hover(true));
|
|
17267
|
+
el.addEventListener("pointerleave", () => hover(false));
|
|
17268
|
+
const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
|
|
17269
|
+
return { el, dispose };
|
|
17270
|
+
}
|
|
17211
17271
|
|
|
17212
17272
|
// src/renderers/native/drawings/DrawingToolbar.ts
|
|
17213
17273
|
var TOOLBAR_WIDTH = 44;
|
|
@@ -18199,6 +18259,24 @@ function localStorageAdapter(storageKey) {
|
|
|
18199
18259
|
};
|
|
18200
18260
|
}
|
|
18201
18261
|
|
|
18262
|
+
// src/core/options.ts
|
|
18263
|
+
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
18264
|
+
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
18265
|
+
var LIVE_BAR_EASE_MAX_MS = 1e3;
|
|
18266
|
+
function resolveLiveBarEaseMs(value) {
|
|
18267
|
+
if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
|
|
18268
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
18269
|
+
return Math.min(value, LIVE_BAR_EASE_MAX_MS);
|
|
18270
|
+
}
|
|
18271
|
+
function resolveAnimations(animations) {
|
|
18272
|
+
if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
|
|
18273
|
+
return {
|
|
18274
|
+
animZoom: animations?.zoom ?? true,
|
|
18275
|
+
animPan: animations?.pan ?? true,
|
|
18276
|
+
animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
|
|
18277
|
+
};
|
|
18278
|
+
}
|
|
18279
|
+
|
|
18202
18280
|
// src/core/model/series.ts
|
|
18203
18281
|
function isLineLikeSeries(spec) {
|
|
18204
18282
|
return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
|
|
@@ -19875,7 +19953,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19875
19953
|
if (!model) continue;
|
|
19876
19954
|
const paneId = model.paneId ?? "price";
|
|
19877
19955
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19878
|
-
byPane.get(paneId).push({
|
|
19956
|
+
byPane.get(paneId).push({
|
|
19957
|
+
id: r.id,
|
|
19958
|
+
title: model.title || r.title,
|
|
19959
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
19960
|
+
ownScale: model.ownScale === true
|
|
19961
|
+
});
|
|
19879
19962
|
}
|
|
19880
19963
|
const panes = [];
|
|
19881
19964
|
for (const paneId of this.paneOrder) {
|
|
@@ -21164,14 +21247,11 @@ var IndicatorInputsDialog = class {
|
|
|
21164
21247
|
buildTextField(id, current, emit) {
|
|
21165
21248
|
return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
|
|
21166
21249
|
}
|
|
21167
|
-
/**
|
|
21250
|
+
/** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
|
|
21168
21251
|
infoButton(tooltip2) {
|
|
21169
|
-
const
|
|
21170
|
-
|
|
21171
|
-
|
|
21172
|
-
b.className = "vela-ind-hint";
|
|
21173
|
-
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;";
|
|
21174
|
-
return b;
|
|
21252
|
+
const { el, dispose } = chromeHint(tooltip2, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
|
|
21253
|
+
this.dialogTips.push(dispose);
|
|
21254
|
+
return el;
|
|
21175
21255
|
}
|
|
21176
21256
|
/** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
|
|
21177
21257
|
select(id, options, current, emit) {
|
|
@@ -21608,8 +21688,6 @@ function ensureDialogStyles() {
|
|
|
21608
21688
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
21609
21689
|
.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;}
|
|
21610
21690
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
21611
|
-
.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;}
|
|
21612
|
-
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
21613
21691
|
.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;}
|
|
21614
21692
|
.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;}
|
|
21615
21693
|
.vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
|
|
@@ -24513,7 +24591,7 @@ var InputController = class {
|
|
|
24513
24591
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24514
24592
|
if (Number.isNaN(this.cursorX)) return;
|
|
24515
24593
|
if (this.dragging && this.region !== "drawing") return;
|
|
24516
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24594
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24517
24595
|
};
|
|
24518
24596
|
this.onDown = (e) => {
|
|
24519
24597
|
if (e.button === 1) {
|
|
@@ -24545,7 +24623,7 @@ var InputController = class {
|
|
|
24545
24623
|
this.startY = y;
|
|
24546
24624
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24547
24625
|
this.region = "drawing";
|
|
24548
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24626
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24549
24627
|
this.capture(e.pointerId);
|
|
24550
24628
|
return;
|
|
24551
24629
|
}
|
|
@@ -24554,6 +24632,11 @@ var InputController = class {
|
|
|
24554
24632
|
this.capture(e.pointerId);
|
|
24555
24633
|
return;
|
|
24556
24634
|
}
|
|
24635
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24636
|
+
this.region = "drawing";
|
|
24637
|
+
this.capture(e.pointerId);
|
|
24638
|
+
return;
|
|
24639
|
+
}
|
|
24557
24640
|
const vp = this.deps.getCoords().getViewport();
|
|
24558
24641
|
this.deps.apply(vp);
|
|
24559
24642
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24602,7 +24685,7 @@ var InputController = class {
|
|
|
24602
24685
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24603
24686
|
} else if (this.region === "drawing") {
|
|
24604
24687
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
24605
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24688
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24606
24689
|
} else {
|
|
24607
24690
|
const coords = this.deps.getCoords();
|
|
24608
24691
|
const vp = coords.getViewport();
|
|
@@ -24625,7 +24708,7 @@ var InputController = class {
|
|
|
24625
24708
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24626
24709
|
const r = this.regionAt(x, y);
|
|
24627
24710
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
24628
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24711
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24629
24712
|
}
|
|
24630
24713
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24631
24714
|
};
|
|
@@ -24966,12 +25049,12 @@ function defaultTradeMarkersState() {
|
|
|
24966
25049
|
function mergeTradeMarkersState(base, patch) {
|
|
24967
25050
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
24968
25051
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
24969
|
-
const
|
|
25052
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
24970
25053
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
24971
25054
|
return {
|
|
24972
|
-
visible:
|
|
24973
|
-
labels:
|
|
24974
|
-
qty:
|
|
25055
|
+
visible: bool2(p.visible, base.visible),
|
|
25056
|
+
labels: bool2(p.labels, base.labels),
|
|
25057
|
+
qty: bool2(p.qty, base.qty),
|
|
24975
25058
|
colors: {
|
|
24976
25059
|
long: color(c.long, base.colors.long),
|
|
24977
25060
|
short: color(c.short, base.colors.short),
|
|
@@ -27655,6 +27738,8 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27655
27738
|
"symbol.style.baseline.fill-bottom",
|
|
27656
27739
|
"symbol.style.baseline.base-level",
|
|
27657
27740
|
"symbol.style.baseline.width",
|
|
27741
|
+
"symbol.animation",
|
|
27742
|
+
"symbol.animation.price-changes",
|
|
27658
27743
|
"symbol.timezone",
|
|
27659
27744
|
"scales",
|
|
27660
27745
|
"scales.price-scale",
|
|
@@ -27797,8 +27882,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
27797
27882
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
27798
27883
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
27799
27884
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
27800
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
27801
|
-
`;
|
|
27885
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
|
|
27802
27886
|
if (!existing) document.head.appendChild(st);
|
|
27803
27887
|
}
|
|
27804
27888
|
var SettingsDialog = class {
|
|
@@ -27819,6 +27903,8 @@ var SettingsDialog = class {
|
|
|
27819
27903
|
this.themeControl = null;
|
|
27820
27904
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
27821
27905
|
this.tabs = [];
|
|
27906
|
+
this.hintTips = [];
|
|
27907
|
+
// ⓘ tooltip disposers of the open build (see hint)
|
|
27822
27908
|
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
27823
27909
|
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
27824
27910
|
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
@@ -28007,6 +28093,13 @@ var SettingsDialog = class {
|
|
|
28007
28093
|
body.append(g);
|
|
28008
28094
|
}
|
|
28009
28095
|
showActive(config.series.style);
|
|
28096
|
+
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
28097
|
+
body.append(sid(this.boolRow(
|
|
28098
|
+
"Animate price changes",
|
|
28099
|
+
config.priceScale.animateLastPrice,
|
|
28100
|
+
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28101
|
+
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28102
|
+
), "symbol.animation.price-changes"));
|
|
28010
28103
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
28011
28104
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
28012
28105
|
const renderHostSections = (placement) => {
|
|
@@ -28214,6 +28307,8 @@ var SettingsDialog = class {
|
|
|
28214
28307
|
this.ui = null;
|
|
28215
28308
|
this.root = null;
|
|
28216
28309
|
this.tabs = [];
|
|
28310
|
+
for (const dispose of this.hintTips) dispose();
|
|
28311
|
+
this.hintTips = [];
|
|
28217
28312
|
ui?.destroy();
|
|
28218
28313
|
}
|
|
28219
28314
|
destroy() {
|
|
@@ -28627,23 +28722,31 @@ var SettingsDialog = class {
|
|
|
28627
28722
|
}).el
|
|
28628
28723
|
});
|
|
28629
28724
|
}
|
|
28630
|
-
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
28631
|
-
|
|
28632
|
-
|
|
28725
|
+
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
28726
|
+
* `info` (see {@link hint}) rides after the label. */
|
|
28727
|
+
boolRow(label, value, onChange, info) {
|
|
28728
|
+
return this.toggleRow(label, value, onChange, [], void 0, info);
|
|
28633
28729
|
}
|
|
28634
28730
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
28635
28731
|
* shared control column; the control group dims and ignores input while the toggle
|
|
28636
28732
|
* is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
|
|
28637
|
-
toggleRow(label, value, onToggle, controls, get) {
|
|
28733
|
+
toggleRow(label, value, onToggle, controls, get, info) {
|
|
28638
28734
|
return fieldRow({
|
|
28639
28735
|
label,
|
|
28640
28736
|
labelSize: "sm",
|
|
28641
28737
|
bool: controls.length === 0,
|
|
28642
28738
|
toggle: { checked: value, onChange: onToggle, get },
|
|
28643
28739
|
control: controls,
|
|
28740
|
+
info,
|
|
28644
28741
|
className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
|
|
28645
28742
|
});
|
|
28646
28743
|
}
|
|
28744
|
+
/** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
|
|
28745
|
+
hint(text) {
|
|
28746
|
+
const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
|
|
28747
|
+
this.hintTips.push(dispose);
|
|
28748
|
+
return el;
|
|
28749
|
+
}
|
|
28647
28750
|
numberRow(label, value, min, max, step, onChange) {
|
|
28648
28751
|
return fieldRow({
|
|
28649
28752
|
label,
|
|
@@ -28811,6 +28914,18 @@ var DrawingPainter = class {
|
|
|
28811
28914
|
}
|
|
28812
28915
|
this.paintHandles(ctx, pts);
|
|
28813
28916
|
}
|
|
28917
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
28918
|
+
* accent under a thin dashed outline. */
|
|
28919
|
+
paintMarquee(ctx, rect) {
|
|
28920
|
+
ctx.save();
|
|
28921
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
28922
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
28923
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
28924
|
+
ctx.lineWidth = 1;
|
|
28925
|
+
ctx.setLineDash([4, 3]);
|
|
28926
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
28927
|
+
ctx.restore();
|
|
28928
|
+
}
|
|
28814
28929
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
28815
28930
|
paintSnapRing(ctx, x, y, theme) {
|
|
28816
28931
|
ctx.setLineDash([]);
|
|
@@ -30279,6 +30394,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
30279
30394
|
}
|
|
30280
30395
|
return null;
|
|
30281
30396
|
}
|
|
30397
|
+
function deletableSelection(selected, drawings) {
|
|
30398
|
+
const ids = [...selected];
|
|
30399
|
+
if (ids.length < 2) return ids;
|
|
30400
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30401
|
+
}
|
|
30402
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30403
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30404
|
+
return hit.locked ? [] : [hit.id];
|
|
30405
|
+
}
|
|
30282
30406
|
|
|
30283
30407
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30284
30408
|
var DRAG_SLOP3 = 3;
|
|
@@ -30382,10 +30506,32 @@ var DrawingInteraction = class {
|
|
|
30382
30506
|
activeDragId() {
|
|
30383
30507
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30384
30508
|
}
|
|
30509
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30510
|
+
pressedId() {
|
|
30511
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30512
|
+
}
|
|
30513
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30514
|
+
dragClones() {
|
|
30515
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30516
|
+
}
|
|
30517
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30518
|
+
marqueeRect() {
|
|
30519
|
+
if (this.state.kind !== "marquee") return null;
|
|
30520
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30521
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30522
|
+
}
|
|
30523
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30524
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30525
|
+
beginMarquee(x, y) {
|
|
30526
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30527
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30528
|
+
return true;
|
|
30529
|
+
}
|
|
30385
30530
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30386
30531
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
30387
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
30388
|
-
|
|
30532
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30533
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30534
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
30389
30535
|
const proj = this.deps.projector();
|
|
30390
30536
|
if (this.state.kind === "placing") {
|
|
30391
30537
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30413,7 +30559,50 @@ var DrawingInteraction = class {
|
|
|
30413
30559
|
return;
|
|
30414
30560
|
}
|
|
30415
30561
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30416
|
-
|
|
30562
|
+
const riders = [];
|
|
30563
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30564
|
+
for (const id of this.deps.selectedIds()) {
|
|
30565
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30566
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30567
|
+
}
|
|
30568
|
+
}
|
|
30569
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30570
|
+
}
|
|
30571
|
+
}
|
|
30572
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30573
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30574
|
+
bodyDelta(paneId, x, y) {
|
|
30575
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30576
|
+
const proj = this.deps.projector();
|
|
30577
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30578
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30579
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30580
|
+
}
|
|
30581
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30582
|
+
dragSources(d) {
|
|
30583
|
+
if (this.state.kind !== "pressed") return [];
|
|
30584
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30585
|
+
for (const r of this.state.riders) {
|
|
30586
|
+
const rd = this.byId(r.id);
|
|
30587
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30588
|
+
}
|
|
30589
|
+
return out;
|
|
30590
|
+
}
|
|
30591
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30592
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30593
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30594
|
+
dragBody(d, x, y) {
|
|
30595
|
+
if (this.state.kind !== "pressed") return;
|
|
30596
|
+
this.snapAt = null;
|
|
30597
|
+
if (this.state.mod && !this.state.clones) {
|
|
30598
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30599
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30600
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30601
|
+
});
|
|
30602
|
+
}
|
|
30603
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30604
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30605
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30417
30606
|
}
|
|
30418
30607
|
}
|
|
30419
30608
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30437,12 +30626,20 @@ var DrawingInteraction = class {
|
|
|
30437
30626
|
this.deps.changed();
|
|
30438
30627
|
return;
|
|
30439
30628
|
}
|
|
30629
|
+
if (this.state.kind === "marquee") {
|
|
30630
|
+
this.state.x1 = x;
|
|
30631
|
+
this.state.y1 = y;
|
|
30632
|
+
this.deps.changed();
|
|
30633
|
+
return;
|
|
30634
|
+
}
|
|
30440
30635
|
if (this.state.kind !== "pressed") return;
|
|
30441
30636
|
const d = this.byId(this.state.id);
|
|
30442
30637
|
if (!d || d.locked) return;
|
|
30443
30638
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP3 && Math.abs(y - this.state.py) <= DRAG_SLOP3) return;
|
|
30444
30639
|
this.state.moved = true;
|
|
30445
|
-
if (this.state.handle
|
|
30640
|
+
if (this.state.handle < 0) {
|
|
30641
|
+
this.dragBody(d, x, y);
|
|
30642
|
+
} else {
|
|
30446
30643
|
const a = d.anchors[this.state.handle];
|
|
30447
30644
|
if (a) {
|
|
30448
30645
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30450,12 +30647,6 @@ var DrawingInteraction = class {
|
|
|
30450
30647
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30451
30648
|
}
|
|
30452
30649
|
d.constrainHandleDrag(this.state.handle);
|
|
30453
|
-
} else {
|
|
30454
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
30455
|
-
this.snapAt = null;
|
|
30456
|
-
const dt = pt.time - this.state.grab.time;
|
|
30457
|
-
const dp = pt.price - this.state.grab.price;
|
|
30458
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
30459
30650
|
}
|
|
30460
30651
|
this.deps.changed();
|
|
30461
30652
|
}
|
|
@@ -30475,16 +30666,46 @@ var DrawingInteraction = class {
|
|
|
30475
30666
|
}
|
|
30476
30667
|
return;
|
|
30477
30668
|
}
|
|
30669
|
+
if (this.state.kind === "marquee") {
|
|
30670
|
+
const rect = this.marqueeRect();
|
|
30671
|
+
this.state = { kind: "idle" };
|
|
30672
|
+
if (rect.w > DRAG_SLOP3 || rect.h > DRAG_SLOP3) this.selectWithin(rect);
|
|
30673
|
+
this.deps.changed();
|
|
30674
|
+
return;
|
|
30675
|
+
}
|
|
30478
30676
|
if (this.state.kind !== "pressed") return;
|
|
30479
|
-
const { id, moved } = this.state;
|
|
30677
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30480
30678
|
this.state = { kind: "idle" };
|
|
30481
30679
|
const d = this.byId(id);
|
|
30482
|
-
if (moved) {
|
|
30483
|
-
if (
|
|
30484
|
-
this.deps.
|
|
30485
|
-
|
|
30486
|
-
this.deps.openSettings(id, x, y);
|
|
30680
|
+
if (!moved) {
|
|
30681
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30682
|
+
else this.deps.openSettings(id, x, y);
|
|
30683
|
+
return;
|
|
30487
30684
|
}
|
|
30685
|
+
if (clones) {
|
|
30686
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30687
|
+
} else if (d) {
|
|
30688
|
+
const docs = [d.serialize()];
|
|
30689
|
+
for (const r of riders) {
|
|
30690
|
+
const rd = this.byId(r.id);
|
|
30691
|
+
if (rd) docs.push(rd.serialize());
|
|
30692
|
+
}
|
|
30693
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30694
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30695
|
+
}
|
|
30696
|
+
this.deps.changed();
|
|
30697
|
+
}
|
|
30698
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30699
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30700
|
+
selectWithin(rect) {
|
|
30701
|
+
const proj = this.deps.projector();
|
|
30702
|
+
const ids = [...this.deps.selectedIds()];
|
|
30703
|
+
for (const d of this.deps.drawings()) {
|
|
30704
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30705
|
+
const b = d.bounds(proj);
|
|
30706
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30707
|
+
}
|
|
30708
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30488
30709
|
}
|
|
30489
30710
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30490
30711
|
cancel() {
|
|
@@ -30497,8 +30718,15 @@ var DrawingInteraction = class {
|
|
|
30497
30718
|
return true;
|
|
30498
30719
|
}
|
|
30499
30720
|
if (this.state.kind === "pressed") {
|
|
30500
|
-
|
|
30501
|
-
|
|
30721
|
+
if (this.state.moved && !this.state.clones) {
|
|
30722
|
+
const d = this.byId(this.state.id);
|
|
30723
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30724
|
+
}
|
|
30725
|
+
this.state = { kind: "idle" };
|
|
30726
|
+
this.deps.changed();
|
|
30727
|
+
return true;
|
|
30728
|
+
}
|
|
30729
|
+
if (this.state.kind === "marquee") {
|
|
30502
30730
|
this.state = { kind: "idle" };
|
|
30503
30731
|
this.deps.changed();
|
|
30504
30732
|
return true;
|
|
@@ -30613,6 +30841,9 @@ var DrawingInteraction = class {
|
|
|
30613
30841
|
function cloneAnchors(d) {
|
|
30614
30842
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30615
30843
|
}
|
|
30844
|
+
function rectsIntersect(a, b) {
|
|
30845
|
+
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
|
|
30846
|
+
}
|
|
30616
30847
|
function applyFree(anchor, pt, free) {
|
|
30617
30848
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
30618
30849
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -30998,10 +31229,27 @@ function btn(label, primary, onClick) {
|
|
|
30998
31229
|
}
|
|
30999
31230
|
|
|
31000
31231
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31232
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31233
|
+
function sharedPaths(drawings) {
|
|
31234
|
+
const [first, ...rest] = drawings;
|
|
31235
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
31236
|
+
for (const d of rest) {
|
|
31237
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31238
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31239
|
+
}
|
|
31240
|
+
return paths;
|
|
31241
|
+
}
|
|
31242
|
+
function commonValue(drawings, read) {
|
|
31243
|
+
const first = read(drawings[0]);
|
|
31244
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
31245
|
+
}
|
|
31246
|
+
function distinctValues(drawings, read) {
|
|
31247
|
+
return [...new Set(drawings.map(read))];
|
|
31248
|
+
}
|
|
31001
31249
|
var BTN = 30;
|
|
31002
31250
|
var ICON = 21;
|
|
31003
31251
|
var STYLE_ID23 = "vela-drawing-popup-styles";
|
|
31004
|
-
var STYLE_REV = "
|
|
31252
|
+
var STYLE_REV = "4";
|
|
31005
31253
|
function ensureStyles5() {
|
|
31006
31254
|
if (typeof document === "undefined") return;
|
|
31007
31255
|
const existing = document.getElementById(STYLE_ID23);
|
|
@@ -31013,6 +31261,8 @@ function ensureStyles5() {
|
|
|
31013
31261
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
31014
31262
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
31015
31263
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31264
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31265
|
+
.vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
|
|
31016
31266
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
31017
31267
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
31018
31268
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -31044,7 +31294,7 @@ var DrawingSettingsPopup = class {
|
|
|
31044
31294
|
if (this.isOwnChrome(node)) return;
|
|
31045
31295
|
const cb = this.onClose;
|
|
31046
31296
|
this.close();
|
|
31047
|
-
cb?.();
|
|
31297
|
+
cb?.(e);
|
|
31048
31298
|
};
|
|
31049
31299
|
this.theme = theme;
|
|
31050
31300
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31069,16 +31319,27 @@ var DrawingSettingsPopup = class {
|
|
|
31069
31319
|
contains(node) {
|
|
31070
31320
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31071
31321
|
}
|
|
31072
|
-
/** Open the quick toolbar for `
|
|
31073
|
-
* `
|
|
31074
|
-
|
|
31322
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31323
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31324
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31325
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31326
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31327
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31328
|
+
* modifier press (multi-select) from a plain one. */
|
|
31329
|
+
open(drawings, anchor, actions, onClose) {
|
|
31075
31330
|
this.close();
|
|
31331
|
+
const drawing = drawings[0];
|
|
31332
|
+
if (!drawing) return;
|
|
31076
31333
|
ensureStyles5();
|
|
31077
31334
|
this.onClose = onClose ?? null;
|
|
31078
31335
|
const t = this.theme;
|
|
31336
|
+
const multi = drawings.length > 1;
|
|
31079
31337
|
const schema = drawing.schema();
|
|
31080
|
-
const paths =
|
|
31081
|
-
const
|
|
31338
|
+
const paths = sharedPaths(drawings);
|
|
31339
|
+
const common = (read) => commonValue(drawings, read);
|
|
31340
|
+
const every = (pred) => drawings.every(pred);
|
|
31341
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31342
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31082
31343
|
const el = document.createElement("div");
|
|
31083
31344
|
el.className = "vela-dpop";
|
|
31084
31345
|
el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
|
|
@@ -31092,75 +31353,84 @@ var DrawingSettingsPopup = class {
|
|
|
31092
31353
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31093
31354
|
bar.appendChild(this.dragHandle());
|
|
31094
31355
|
if (paths.has("glyph")) {
|
|
31095
|
-
const cur =
|
|
31356
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31096
31357
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31097
31358
|
}
|
|
31098
31359
|
if (paths.has("size")) {
|
|
31099
|
-
const sz =
|
|
31360
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31100
31361
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31101
31362
|
}
|
|
31102
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31363
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31364
|
+
const mag = (d) => d;
|
|
31103
31365
|
const options = this.lowerTimeframeOptions();
|
|
31104
31366
|
if (options.length > 0) {
|
|
31105
31367
|
bar.appendChild(
|
|
31106
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31368
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31107
31369
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31108
31370
|
labelInTrigger: true
|
|
31109
31371
|
})
|
|
31110
31372
|
);
|
|
31111
31373
|
}
|
|
31112
|
-
bar.appendChild(
|
|
31113
|
-
bar.appendChild(
|
|
31374
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31375
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31114
31376
|
}
|
|
31115
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31377
|
+
if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
|
|
31116
31378
|
if (paths.has("style.lineWidth")) {
|
|
31117
|
-
const
|
|
31118
|
-
|
|
31119
|
-
|
|
31379
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31380
|
+
const wf = widthField2(drawing);
|
|
31381
|
+
const width = common((d) => d.style.lineWidth);
|
|
31382
|
+
const markers = every((d) => {
|
|
31383
|
+
const f = widthField2(d);
|
|
31384
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31385
|
+
});
|
|
31386
|
+
if (markers && wf?.kind === "number") {
|
|
31387
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31120
31388
|
} else {
|
|
31121
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31389
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
31122
31390
|
}
|
|
31123
31391
|
}
|
|
31124
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
31125
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
31392
|
+
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
31393
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
31126
31394
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31127
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31395
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31128
31396
|
if (isPosition) {
|
|
31129
|
-
const pos =
|
|
31130
|
-
bar.appendChild(
|
|
31131
|
-
bar.appendChild(
|
|
31397
|
+
const pos = (d) => d;
|
|
31398
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31399
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31132
31400
|
}
|
|
31133
|
-
const
|
|
31134
|
-
if (paths.has("reg.midColor") &&
|
|
31401
|
+
const regOf = (d) => d.reg;
|
|
31402
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31403
|
+
const reg = (d) => regOf(d);
|
|
31135
31404
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31136
|
-
bar.appendChild(
|
|
31137
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31138
|
-
bar.appendChild(
|
|
31139
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31140
|
-
bar.appendChild(
|
|
31141
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31142
|
-
bar.appendChild(
|
|
31143
|
-
bar.appendChild(
|
|
31144
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
31145
|
-
}
|
|
31146
|
-
const
|
|
31147
|
-
if (paths.has("vwap.midColor") &&
|
|
31405
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31406
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31407
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31408
|
+
bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31409
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31410
|
+
bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31411
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31412
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31413
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31414
|
+
}
|
|
31415
|
+
const vwapOf = (d) => d.vwap;
|
|
31416
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31417
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31148
31418
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31149
31419
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
31150
|
-
bar.appendChild(
|
|
31151
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31420
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31421
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31152
31422
|
bar.appendChild(
|
|
31153
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31423
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31154
31424
|
label: (v) => `${v}\u03C3`,
|
|
31155
31425
|
labelInTrigger: true
|
|
31156
31426
|
})
|
|
31157
31427
|
);
|
|
31158
|
-
bar.appendChild(
|
|
31159
|
-
bar.appendChild(
|
|
31160
|
-
bar.appendChild(
|
|
31428
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31429
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31430
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31161
31431
|
}
|
|
31162
31432
|
if (paths.has("maxCurvature")) {
|
|
31163
|
-
const cur =
|
|
31433
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31164
31434
|
bar.appendChild(
|
|
31165
31435
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31166
31436
|
label: (v) => `n=${v}`,
|
|
@@ -31169,7 +31439,7 @@ var DrawingSettingsPopup = class {
|
|
|
31169
31439
|
);
|
|
31170
31440
|
}
|
|
31171
31441
|
if (paths.has("mach")) {
|
|
31172
|
-
const cur =
|
|
31442
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31173
31443
|
bar.appendChild(
|
|
31174
31444
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31175
31445
|
label: (v) => `M=${v}`,
|
|
@@ -31178,7 +31448,7 @@ var DrawingSettingsPopup = class {
|
|
|
31178
31448
|
);
|
|
31179
31449
|
}
|
|
31180
31450
|
if (paths.has("waveCount")) {
|
|
31181
|
-
const cur =
|
|
31451
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31182
31452
|
bar.appendChild(
|
|
31183
31453
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31184
31454
|
label: (v) => `${v}`,
|
|
@@ -31186,26 +31456,27 @@ var DrawingSettingsPopup = class {
|
|
|
31186
31456
|
})
|
|
31187
31457
|
);
|
|
31188
31458
|
}
|
|
31189
|
-
const
|
|
31190
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
31191
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
31192
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
31193
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
31194
|
-
|
|
31195
|
-
if (paths.has("text.
|
|
31196
|
-
if (paths.has("text.
|
|
31459
|
+
const flags = (d) => d;
|
|
31460
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31461
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31462
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31463
|
+
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
|
|
31464
|
+
const textField = !multi && paths.has("text.value");
|
|
31465
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31466
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31467
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31197
31468
|
const editableLevels = drawing.editableLevels();
|
|
31198
|
-
if (editableLevels && !(
|
|
31199
|
-
const fib =
|
|
31469
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31470
|
+
const fib = (d) => d;
|
|
31200
31471
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
31201
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31202
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31472
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31473
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31203
31474
|
}
|
|
31204
31475
|
bar.appendChild(this.divider());
|
|
31205
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31206
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31207
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31208
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
31476
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31477
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31478
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31479
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31209
31480
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31210
31481
|
del.style.color = "var(--vela-danger)";
|
|
31211
31482
|
bar.appendChild(del);
|
|
@@ -31391,7 +31662,7 @@ var DrawingSettingsPopup = class {
|
|
|
31391
31662
|
});
|
|
31392
31663
|
return b;
|
|
31393
31664
|
}
|
|
31394
|
-
/** The trailing kebab overflow — z-order actions
|
|
31665
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31395
31666
|
kebabButton(actions) {
|
|
31396
31667
|
const b = this.base("More");
|
|
31397
31668
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31403,6 +31674,7 @@ var DrawingSettingsPopup = class {
|
|
|
31403
31674
|
return;
|
|
31404
31675
|
}
|
|
31405
31676
|
this.openActionMenu(b, [
|
|
31677
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31406
31678
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31407
31679
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31408
31680
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31559,26 +31831,30 @@ var DrawingSettingsPopup = class {
|
|
|
31559
31831
|
this.tipEl?.remove();
|
|
31560
31832
|
this.tipEl = null;
|
|
31561
31833
|
}
|
|
31562
|
-
|
|
31834
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
31835
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
31836
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
31837
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31563
31838
|
const b = this.base(tip);
|
|
31564
|
-
b.innerHTML = sized(icon2);
|
|
31565
31839
|
const set = (on2) => {
|
|
31566
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
31840
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
31841
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31567
31842
|
};
|
|
31568
31843
|
set(active);
|
|
31569
31844
|
let on = active;
|
|
31570
31845
|
b.addEventListener("click", () => {
|
|
31571
|
-
on = !on;
|
|
31846
|
+
on = on === MIXED ? true : !on;
|
|
31572
31847
|
set(on);
|
|
31573
31848
|
onChange(on);
|
|
31574
31849
|
});
|
|
31575
31850
|
return b;
|
|
31576
31851
|
}
|
|
31577
31852
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
31578
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
31853
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
31854
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31579
31855
|
widthInput(tip, value, min, max, step, onChange) {
|
|
31580
31856
|
const ni = new NumberInput({
|
|
31581
|
-
value,
|
|
31857
|
+
value: value === MIXED ? min : value,
|
|
31582
31858
|
min,
|
|
31583
31859
|
max,
|
|
31584
31860
|
step,
|
|
@@ -31590,13 +31866,18 @@ var DrawingSettingsPopup = class {
|
|
|
31590
31866
|
title: tip,
|
|
31591
31867
|
onChange
|
|
31592
31868
|
});
|
|
31869
|
+
if (value === MIXED) {
|
|
31870
|
+
ni.input.value = "";
|
|
31871
|
+
ni.input.placeholder = "\u2014";
|
|
31872
|
+
}
|
|
31593
31873
|
ni.el.dataset.tip = tip;
|
|
31594
31874
|
trapChartKeys(ni.el);
|
|
31595
31875
|
return ni.el;
|
|
31596
31876
|
}
|
|
31597
31877
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31598
31878
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
31599
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31879
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
31880
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31600
31881
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31601
31882
|
const b = this.base(tip);
|
|
31602
31883
|
b.style.width = "auto";
|
|
@@ -31606,14 +31887,20 @@ var DrawingSettingsPopup = class {
|
|
|
31606
31887
|
let cur = current;
|
|
31607
31888
|
const paint = (v) => {
|
|
31608
31889
|
b.replaceChildren();
|
|
31609
|
-
|
|
31890
|
+
if (v === MIXED) {
|
|
31891
|
+
const dash = document.createElement("span");
|
|
31892
|
+
dash.textContent = "\u2014";
|
|
31893
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
31894
|
+
b.appendChild(dash);
|
|
31895
|
+
}
|
|
31896
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31610
31897
|
if (glyph) {
|
|
31611
31898
|
const ic = document.createElement("span");
|
|
31612
31899
|
ic.style.cssText = "display:flex;";
|
|
31613
31900
|
ic.innerHTML = sized(glyph);
|
|
31614
31901
|
b.appendChild(ic);
|
|
31615
31902
|
}
|
|
31616
|
-
if (opts.label && opts.labelInTrigger) {
|
|
31903
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31617
31904
|
const tx = document.createElement("span");
|
|
31618
31905
|
tx.textContent = opts.label(v);
|
|
31619
31906
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -31683,8 +31970,10 @@ var DrawingSettingsPopup = class {
|
|
|
31683
31970
|
}
|
|
31684
31971
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
31685
31972
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
31686
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
31687
|
-
|
|
31973
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
31974
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
31975
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
31976
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
31688
31977
|
const b = document.createElement("button");
|
|
31689
31978
|
b.type = "button";
|
|
31690
31979
|
b.dataset.tip = tip;
|
|
@@ -31694,13 +31983,14 @@ var DrawingSettingsPopup = class {
|
|
|
31694
31983
|
ic.style.cssText = "display:flex;";
|
|
31695
31984
|
ic.innerHTML = sized(icon2, iconSize);
|
|
31696
31985
|
const bar = document.createElement("span");
|
|
31697
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
31986
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
31698
31987
|
b.append(ic, bar);
|
|
31699
31988
|
let cur = color;
|
|
31700
31989
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
31701
31990
|
b.addEventListener("click", (e) => {
|
|
31702
31991
|
e.stopPropagation();
|
|
31703
|
-
|
|
31992
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
31993
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
31704
31994
|
cur = v;
|
|
31705
31995
|
bar.style.background = v;
|
|
31706
31996
|
onChange(v);
|
|
@@ -31709,20 +31999,42 @@ var DrawingSettingsPopup = class {
|
|
|
31709
31999
|
return b;
|
|
31710
32000
|
}
|
|
31711
32001
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
31712
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
32002
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
31713
32003
|
if (this.colorOwner === anchor) {
|
|
31714
32004
|
this.closeColorPopover();
|
|
31715
32005
|
return;
|
|
31716
32006
|
}
|
|
31717
|
-
this.openColorPopover(anchor, color, onChange);
|
|
32007
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
31718
32008
|
}
|
|
31719
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
31720
|
-
|
|
32009
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
32010
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
32011
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
31721
32012
|
const t = this.theme;
|
|
31722
32013
|
this.colorPop = this.hostFloat(anchor, {
|
|
31723
32014
|
zIndex: 25,
|
|
31724
32015
|
padding: "10px",
|
|
31725
|
-
fill: (el) => {
|
|
32016
|
+
fill: (el, pop) => {
|
|
32017
|
+
if (used.length > 1) {
|
|
32018
|
+
const row = document.createElement("div");
|
|
32019
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
32020
|
+
const lbl = document.createElement("span");
|
|
32021
|
+
lbl.textContent = "In use";
|
|
32022
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
32023
|
+
row.appendChild(lbl);
|
|
32024
|
+
for (const c of used) {
|
|
32025
|
+
const sw = document.createElement("button");
|
|
32026
|
+
sw.type = "button";
|
|
32027
|
+
sw.dataset.tip = c;
|
|
32028
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
32029
|
+
sw.addEventListener("click", (e) => {
|
|
32030
|
+
e.stopPropagation();
|
|
32031
|
+
pop.hide();
|
|
32032
|
+
onChange(c);
|
|
32033
|
+
});
|
|
32034
|
+
row.appendChild(sw);
|
|
32035
|
+
}
|
|
32036
|
+
el.appendChild(row);
|
|
32037
|
+
}
|
|
31726
32038
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
31727
32039
|
}
|
|
31728
32040
|
});
|
|
@@ -31743,6 +32055,7 @@ var TYPE_ICON = icon("type");
|
|
|
31743
32055
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
31744
32056
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
31745
32057
|
var LOCK_ICON = icon("lock");
|
|
32058
|
+
var UNLOCK_ICON = icon("unlock");
|
|
31746
32059
|
var FRONT_ICON = icon("bring-front");
|
|
31747
32060
|
var BACK_ICON = icon("send-back");
|
|
31748
32061
|
var TRASH_ICON = icon("trash");
|
|
@@ -31750,6 +32063,7 @@ var BOLD_ICON = icon("bold");
|
|
|
31750
32063
|
var ITALIC_ICON = icon("italic");
|
|
31751
32064
|
var GRIP_ICON = icon("grip");
|
|
31752
32065
|
var KEBAB_ICON = icon("kebab");
|
|
32066
|
+
var CLONE_ICON = icon("clone");
|
|
31753
32067
|
var RESET_ICON = icon("reset");
|
|
31754
32068
|
var GEAR_ICON = icon("gear");
|
|
31755
32069
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -31758,6 +32072,12 @@ var BANDS_ICON = icon("bands");
|
|
|
31758
32072
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
31759
32073
|
var SONIC_ICON2 = icon("sonic");
|
|
31760
32074
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32075
|
+
function stripes(colors) {
|
|
32076
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32077
|
+
const step = 100 / bands.length;
|
|
32078
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32079
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32080
|
+
}
|
|
31761
32081
|
function lineIcon(width, style) {
|
|
31762
32082
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
31763
32083
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -32041,6 +32361,20 @@ var UserDrawingController = class {
|
|
|
32041
32361
|
// reported exactly when it actually changes).
|
|
32042
32362
|
this.textEditor = null;
|
|
32043
32363
|
this.painter = new DrawingPainter();
|
|
32364
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
32365
|
+
* shrinking selection does with it. */
|
|
32366
|
+
this.popupMulti = false;
|
|
32367
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
32368
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
32369
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
32370
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
32371
|
+
* whose bar just closed must stay selected. */
|
|
32372
|
+
this.onPopupDismissed = (e) => {
|
|
32373
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
32374
|
+
const pressed = this.interaction.pressedId();
|
|
32375
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
32376
|
+
this.clearSelection();
|
|
32377
|
+
};
|
|
32044
32378
|
this.ctx = canvas.getContext("2d");
|
|
32045
32379
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32046
32380
|
this.toolbar = new DrawingToolbar(
|
|
@@ -32217,6 +32551,12 @@ var UserDrawingController = class {
|
|
|
32217
32551
|
}
|
|
32218
32552
|
setSelection(ids) {
|
|
32219
32553
|
this.selectedIds = new Set(ids);
|
|
32554
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
32555
|
+
else if (this.popupMulti) {
|
|
32556
|
+
this.popupMulti = false;
|
|
32557
|
+
this.popup.close();
|
|
32558
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
32559
|
+
}
|
|
32220
32560
|
this.render();
|
|
32221
32561
|
}
|
|
32222
32562
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -32262,12 +32602,16 @@ var UserDrawingController = class {
|
|
|
32262
32602
|
});
|
|
32263
32603
|
}
|
|
32264
32604
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32265
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
32266
|
-
|
|
32605
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
32606
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
32607
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
32608
|
+
deleteAt(x, y, withSelection = false) {
|
|
32267
32609
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
32268
|
-
if (!hit
|
|
32610
|
+
if (!hit) return false;
|
|
32611
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
32612
|
+
if (ids.length === 0) return false;
|
|
32269
32613
|
this.popup.close();
|
|
32270
|
-
this.emit({ kind: "delete", ids
|
|
32614
|
+
this.emit({ kind: "delete", ids });
|
|
32271
32615
|
return true;
|
|
32272
32616
|
}
|
|
32273
32617
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -32293,7 +32637,14 @@ var UserDrawingController = class {
|
|
|
32293
32637
|
this.render();
|
|
32294
32638
|
}
|
|
32295
32639
|
}
|
|
32296
|
-
|
|
32640
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
32641
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
32642
|
+
beginMarqueeAt(x, y) {
|
|
32643
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
32644
|
+
this.render();
|
|
32645
|
+
return true;
|
|
32646
|
+
}
|
|
32647
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32297
32648
|
if (this.eraserMode) {
|
|
32298
32649
|
this.erasing = true;
|
|
32299
32650
|
this.deleteAt(x, y);
|
|
@@ -32313,9 +32664,9 @@ var UserDrawingController = class {
|
|
|
32313
32664
|
return;
|
|
32314
32665
|
}
|
|
32315
32666
|
}
|
|
32316
|
-
this.interaction.down(x, y, snap, shift2);
|
|
32667
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
32317
32668
|
}
|
|
32318
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
32669
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
32319
32670
|
if (this.eraserMode) {
|
|
32320
32671
|
if (this.erasing) this.deleteAt(x, y);
|
|
32321
32672
|
return;
|
|
@@ -32327,12 +32678,14 @@ var UserDrawingController = class {
|
|
|
32327
32678
|
return;
|
|
32328
32679
|
}
|
|
32329
32680
|
this.interaction.move(x, y, snap, shift2);
|
|
32330
|
-
this.updateHover(x, y);
|
|
32681
|
+
this.updateHover(x, y, mod);
|
|
32331
32682
|
}
|
|
32332
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
32333
|
-
|
|
32683
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
32684
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
32685
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
32686
|
+
updateHover(x, y, mod = false) {
|
|
32334
32687
|
let id = null;
|
|
32335
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32688
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
32336
32689
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
32337
32690
|
}
|
|
32338
32691
|
if (id !== this.hoveredId) {
|
|
@@ -32352,7 +32705,12 @@ var UserDrawingController = class {
|
|
|
32352
32705
|
this.render();
|
|
32353
32706
|
return;
|
|
32354
32707
|
}
|
|
32708
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
32355
32709
|
this.interaction.up(x, y);
|
|
32710
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
32711
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
32712
|
+
else this.openSettingsById(dragged, x, y);
|
|
32713
|
+
}
|
|
32356
32714
|
}
|
|
32357
32715
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
32358
32716
|
toggleMeasure() {
|
|
@@ -32617,7 +32975,7 @@ var UserDrawingController = class {
|
|
|
32617
32975
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
32618
32976
|
break;
|
|
32619
32977
|
case "delete": {
|
|
32620
|
-
const ids = this.selectedIds.size ? this.
|
|
32978
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
32621
32979
|
if (ids.length) {
|
|
32622
32980
|
this.popup.close();
|
|
32623
32981
|
this.emit({ kind: "delete", ids });
|
|
@@ -32633,6 +32991,9 @@ var UserDrawingController = class {
|
|
|
32633
32991
|
selectionIds() {
|
|
32634
32992
|
return [...this.selectedIds];
|
|
32635
32993
|
}
|
|
32994
|
+
deletableSelection() {
|
|
32995
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
32996
|
+
}
|
|
32636
32997
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
32637
32998
|
nudgeSelection(dx, dy) {
|
|
32638
32999
|
const proj = this.deps.projector();
|
|
@@ -32741,6 +33102,8 @@ var UserDrawingController = class {
|
|
|
32741
33102
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
32742
33103
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32743
33104
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33105
|
+
const clones = this.interaction.dragClones();
|
|
33106
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
32744
33107
|
this.layoutTextEditor();
|
|
32745
33108
|
const ghost = this.interaction.ghost();
|
|
32746
33109
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -32756,6 +33119,8 @@ var UserDrawingController = class {
|
|
|
32756
33119
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
32757
33120
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
32758
33121
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
33122
|
+
const marquee = this.interaction.marqueeRect();
|
|
33123
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
32759
33124
|
}
|
|
32760
33125
|
destroy() {
|
|
32761
33126
|
this.closeTextEditor();
|
|
@@ -32773,15 +33138,80 @@ var UserDrawingController = class {
|
|
|
32773
33138
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
32774
33139
|
this.render();
|
|
32775
33140
|
}
|
|
33141
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
33142
|
+
emitEdits(drawings) {
|
|
33143
|
+
const docs = drawings.map((d) => d.serialize());
|
|
33144
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
33145
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
33146
|
+
}
|
|
33147
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
33148
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
33149
|
+
openSettingsForSelection(ids) {
|
|
33150
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
33151
|
+
const drawings = live();
|
|
33152
|
+
if (drawings.length < 2) return;
|
|
33153
|
+
const proj = this.deps.projector();
|
|
33154
|
+
let anchor = null;
|
|
33155
|
+
for (const d of drawings) {
|
|
33156
|
+
const b = d.bounds(proj);
|
|
33157
|
+
if (!b) continue;
|
|
33158
|
+
if (!anchor) anchor = { ...b };
|
|
33159
|
+
else {
|
|
33160
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
33161
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
33162
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
33163
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
33164
|
+
anchor.w = right - anchor.x;
|
|
33165
|
+
anchor.h = bottom - anchor.y;
|
|
33166
|
+
}
|
|
33167
|
+
}
|
|
33168
|
+
this.popupMulti = true;
|
|
33169
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
33170
|
+
this.popup.open(drawings, anchor, {
|
|
33171
|
+
resolve: () => live()[0] ?? null,
|
|
33172
|
+
patch: (p) => {
|
|
33173
|
+
const ds = live();
|
|
33174
|
+
for (const d of ds) d.applySettings(p);
|
|
33175
|
+
this.render();
|
|
33176
|
+
this.emitEdits(ds);
|
|
33177
|
+
},
|
|
33178
|
+
setLocked: (v) => {
|
|
33179
|
+
const ds = live();
|
|
33180
|
+
for (const d of ds) d.locked = v;
|
|
33181
|
+
this.emitEdits(ds);
|
|
33182
|
+
},
|
|
33183
|
+
reorder: (to) => {
|
|
33184
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
33185
|
+
},
|
|
33186
|
+
duplicate: () => {
|
|
33187
|
+
this.popup.close();
|
|
33188
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
33189
|
+
},
|
|
33190
|
+
resetSettings: () => {
|
|
33191
|
+
const ds = live();
|
|
33192
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
33193
|
+
this.render();
|
|
33194
|
+
this.emitEdits(ds);
|
|
33195
|
+
this.openSettingsForSelection(ids);
|
|
33196
|
+
},
|
|
33197
|
+
remove: () => {
|
|
33198
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
33199
|
+
if (ids2.length === 0) return;
|
|
33200
|
+
this.popup.close();
|
|
33201
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
33202
|
+
}
|
|
33203
|
+
}, this.onPopupDismissed);
|
|
33204
|
+
}
|
|
32776
33205
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
32777
33206
|
openSettingsById(id, _x, _y) {
|
|
32778
33207
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
32779
33208
|
if (!drawing) return;
|
|
32780
33209
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
33210
|
+
this.popupMulti = false;
|
|
32781
33211
|
this.emit({ kind: "select", ids: [id] });
|
|
32782
33212
|
this.emit({ kind: "settings", id });
|
|
32783
33213
|
const anchor = drawing.bounds(this.deps.projector());
|
|
32784
|
-
this.popup.open(drawing, anchor, {
|
|
33214
|
+
this.popup.open([drawing], anchor, {
|
|
32785
33215
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
32786
33216
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
32787
33217
|
resolve: () => live() ?? null,
|
|
@@ -32799,6 +33229,11 @@ var UserDrawingController = class {
|
|
|
32799
33229
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32800
33230
|
},
|
|
32801
33231
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
33232
|
+
duplicate: () => {
|
|
33233
|
+
this.finishTextEditor(true);
|
|
33234
|
+
this.popup.close();
|
|
33235
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
33236
|
+
},
|
|
32802
33237
|
resetSettings: () => {
|
|
32803
33238
|
const d = live();
|
|
32804
33239
|
if (!d) return;
|
|
@@ -32821,7 +33256,13 @@ var UserDrawingController = class {
|
|
|
32821
33256
|
this.popup.close();
|
|
32822
33257
|
this.emit({ kind: "delete", ids: [id] });
|
|
32823
33258
|
}
|
|
32824
|
-
},
|
|
33259
|
+
}, this.onPopupDismissed);
|
|
33260
|
+
}
|
|
33261
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
33262
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
33263
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
33264
|
+
deselect() {
|
|
33265
|
+
this.clearSelection();
|
|
32825
33266
|
}
|
|
32826
33267
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
32827
33268
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -32847,6 +33288,13 @@ var UserDrawingController = class {
|
|
|
32847
33288
|
}, 0);
|
|
32848
33289
|
return;
|
|
32849
33290
|
}
|
|
33291
|
+
if (i.kind === "clone") {
|
|
33292
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
33293
|
+
this.intentCb?.(i);
|
|
33294
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
33295
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
33296
|
+
return;
|
|
33297
|
+
}
|
|
32850
33298
|
this.intentCb?.(i);
|
|
32851
33299
|
}
|
|
32852
33300
|
};
|
|
@@ -33875,7 +34323,6 @@ var ZOOM_OUT_MARGIN_BARS = 6;
|
|
|
33875
34323
|
var ZOOM_TAU_MS = 70;
|
|
33876
34324
|
var SCALE_TAU_MS = 80;
|
|
33877
34325
|
var FLING_TAU_MS = 110;
|
|
33878
|
-
var LIVE_BAR_TAU_MS = 90;
|
|
33879
34326
|
var SCROLL_TO_TAU_MS = 130;
|
|
33880
34327
|
var FLING_STOP_PX = 0.02;
|
|
33881
34328
|
var PRICE_SCALE_K = 4e-3;
|
|
@@ -33946,9 +34393,13 @@ var NativeRenderer = class {
|
|
|
33946
34393
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
33947
34394
|
this.historyChordsEnabled = true;
|
|
33948
34395
|
this.liveRegion = null;
|
|
33949
|
-
// ── animation state (eased zoom + inertial pan) ──
|
|
34396
|
+
// ── animation state (eased zoom + inertial pan + live-bar glide) ──
|
|
33950
34397
|
this.animZoom = true;
|
|
33951
34398
|
this.animPan = true;
|
|
34399
|
+
this.animLiveBarMs = 0;
|
|
34400
|
+
// forming-bar OHLC glide time-constant; 0 = each tick snaps
|
|
34401
|
+
this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
|
|
34402
|
+
// the duration the settings-dialog on/off toggle restores (last non-zero value configured)
|
|
33952
34403
|
// Brand default candles.
|
|
33953
34404
|
this.candleUp = BULLISH;
|
|
33954
34405
|
this.candleDown = BEARISH;
|
|
@@ -34073,7 +34524,7 @@ var NativeRenderer = class {
|
|
|
34073
34524
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
34074
34525
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
34075
34526
|
this.name = "native";
|
|
34076
|
-
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"];
|
|
34527
|
+
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"];
|
|
34077
34528
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
34078
34529
|
* so moving onto the button doesn't count as leaving). */
|
|
34079
34530
|
this.onScrollProximityMove = (e) => {
|
|
@@ -34107,6 +34558,7 @@ var NativeRenderer = class {
|
|
|
34107
34558
|
this.backendMode = opts.nativeBackend;
|
|
34108
34559
|
this.animZoom = opts.animZoom;
|
|
34109
34560
|
this.animPan = opts.animPan;
|
|
34561
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
|
|
34110
34562
|
this.glowAmount = opts.glow;
|
|
34111
34563
|
this.candleUp = opts.upColor;
|
|
34112
34564
|
this.candleDown = opts.downColor;
|
|
@@ -34156,6 +34608,10 @@ var NativeRenderer = class {
|
|
|
34156
34608
|
case "animPan":
|
|
34157
34609
|
this.animPan = Boolean(value);
|
|
34158
34610
|
return;
|
|
34611
|
+
case "animLiveBar":
|
|
34612
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(value));
|
|
34613
|
+
return;
|
|
34614
|
+
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
34159
34615
|
case "intro": {
|
|
34160
34616
|
const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
|
|
34161
34617
|
this.introStyle = s;
|
|
@@ -34292,6 +34748,8 @@ var NativeRenderer = class {
|
|
|
34292
34748
|
return this.animZoom;
|
|
34293
34749
|
case "animPan":
|
|
34294
34750
|
return this.animPan;
|
|
34751
|
+
case "animLiveBar":
|
|
34752
|
+
return this.animLiveBarMs;
|
|
34295
34753
|
case "intro":
|
|
34296
34754
|
return this.introStyle;
|
|
34297
34755
|
case "zoomAnchor":
|
|
@@ -34460,7 +34918,8 @@ var NativeRenderer = class {
|
|
|
34460
34918
|
labelsVisible: this.scene.showAxisLabels,
|
|
34461
34919
|
currentPriceLine: this.scene.showPriceLine,
|
|
34462
34920
|
priceLabel: this.scene.showPriceLabel,
|
|
34463
|
-
countdown: this.scene.showCountdown
|
|
34921
|
+
countdown: this.scene.showCountdown,
|
|
34922
|
+
animateLastPrice: this.animLiveBarMs > 0
|
|
34464
34923
|
},
|
|
34465
34924
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
34466
34925
|
trades: {
|
|
@@ -34567,6 +35026,7 @@ var NativeRenderer = class {
|
|
|
34567
35026
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
34568
35027
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
34569
35028
|
this.syncCountdownTimer();
|
|
35029
|
+
this.animLiveBarMs = next.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
|
|
34570
35030
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
34571
35031
|
this.scene.tradeMarkers = {
|
|
34572
35032
|
visible: next.trades.visible,
|
|
@@ -35010,7 +35470,10 @@ var NativeRenderer = class {
|
|
|
35010
35470
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35011
35471
|
fling: (v) => this.fling(v),
|
|
35012
35472
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35013
|
-
onClick: (x) =>
|
|
35473
|
+
onClick: (x) => {
|
|
35474
|
+
this.userDrawings?.deselect();
|
|
35475
|
+
this.handleClick(x);
|
|
35476
|
+
},
|
|
35014
35477
|
onAxisLongPress: (axis, x, y) => {
|
|
35015
35478
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
35016
35479
|
},
|
|
@@ -35028,11 +35491,13 @@ var NativeRenderer = class {
|
|
|
35028
35491
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
35029
35492
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
35030
35493
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
35031
|
-
|
|
35494
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
35495
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
35496
|
+
// middle-click: a selected hit takes the whole selection
|
|
35032
35497
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
35033
35498
|
drawingsSnapMode: () => this.snapMode,
|
|
35034
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
35035
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
35499
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35500
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35036
35501
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35037
35502
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
35038
35503
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -35383,8 +35848,11 @@ var NativeRenderer = class {
|
|
|
35383
35848
|
const last = this.bars[n - 1];
|
|
35384
35849
|
if (last && bar.time === last.time) {
|
|
35385
35850
|
this.bars[n - 1] = bar;
|
|
35386
|
-
if (this.liveEaseTime !== bar.time)
|
|
35387
|
-
|
|
35851
|
+
if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
|
|
35852
|
+
this.syncLiveEase(bar);
|
|
35853
|
+
} else {
|
|
35854
|
+
this.animator.start();
|
|
35855
|
+
}
|
|
35388
35856
|
} else if (!last || bar.time > last.time) {
|
|
35389
35857
|
this.bars.push(bar);
|
|
35390
35858
|
this.syncLiveEase(bar);
|
|
@@ -35396,6 +35864,12 @@ var NativeRenderer = class {
|
|
|
35396
35864
|
this.scene.bars = this.bars;
|
|
35397
35865
|
this.scheduler.invalidate(4 /* Full */);
|
|
35398
35866
|
}
|
|
35867
|
+
/** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
|
|
35868
|
+
* what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
|
|
35869
|
+
setLiveBarEase(ms) {
|
|
35870
|
+
this.animLiveBarMs = ms;
|
|
35871
|
+
if (ms > 0) this.animLiveBarOnMs = ms;
|
|
35872
|
+
}
|
|
35399
35873
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
35400
35874
|
syncLiveEase(bar) {
|
|
35401
35875
|
this.liveEaseTime = bar.time;
|
|
@@ -35408,9 +35882,10 @@ var NativeRenderer = class {
|
|
|
35408
35882
|
const target = this.bars[this.bars.length - 1];
|
|
35409
35883
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
35410
35884
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
35411
|
-
const
|
|
35412
|
-
const
|
|
35413
|
-
const
|
|
35885
|
+
const tau = this.animLiveBarMs;
|
|
35886
|
+
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
35887
|
+
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
35888
|
+
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
35414
35889
|
const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
|
|
35415
35890
|
this.liveEaseHigh = active ? nh : target.high;
|
|
35416
35891
|
this.liveEaseLow = active ? nl : target.low;
|
|
@@ -37356,6 +37831,10 @@ function str3(inputs, key, fallback) {
|
|
|
37356
37831
|
const v = inputs[key];
|
|
37357
37832
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
37358
37833
|
}
|
|
37834
|
+
function bool(inputs, key, fallback) {
|
|
37835
|
+
const v = inputs[key];
|
|
37836
|
+
return typeof v === "boolean" ? v : fallback;
|
|
37837
|
+
}
|
|
37359
37838
|
function toPoints(bars, values, colors) {
|
|
37360
37839
|
const pts = new Array(bars.length);
|
|
37361
37840
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -38887,10 +39366,6 @@ var aroon = {
|
|
|
38887
39366
|
plots: [
|
|
38888
39367
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
38889
39368
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
38890
|
-
],
|
|
38891
|
-
levels: [
|
|
38892
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
38893
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
38894
39369
|
]
|
|
38895
39370
|
};
|
|
38896
39371
|
}
|
|
@@ -39388,8 +39863,10 @@ var volumeSpecs = [
|
|
|
39388
39863
|
var DAY_MS2 = 864e5;
|
|
39389
39864
|
function periodKey(anchor, time) {
|
|
39390
39865
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
39391
|
-
if (anchor === "Month") {
|
|
39866
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
39392
39867
|
const d = new Date(time);
|
|
39868
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
39869
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
39393
39870
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
39394
39871
|
}
|
|
39395
39872
|
return Math.floor(time / DAY_MS2);
|
|
@@ -39441,40 +39918,86 @@ var parabolicSar = {
|
|
|
39441
39918
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
39442
39919
|
}
|
|
39443
39920
|
};
|
|
39921
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
39922
|
+
var VWAP_STYLE = "Style";
|
|
39923
|
+
var VWAP_BANDS = [
|
|
39924
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
39925
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
39926
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
39927
|
+
];
|
|
39444
39928
|
var vwap = {
|
|
39445
39929
|
type: "vwap",
|
|
39446
39930
|
title: "Volume Weighted Average Price",
|
|
39447
39931
|
shortTitle: "VWAP",
|
|
39448
39932
|
overlay: true,
|
|
39449
39933
|
inputs: [
|
|
39450
|
-
{ key: "anchor", title: "
|
|
39451
|
-
|
|
39452
|
-
|
|
39934
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
39935
|
+
sourceInput("HLC3"),
|
|
39936
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
39937
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39938
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
39939
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
39940
|
+
]),
|
|
39941
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
39942
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
39943
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
39944
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
39945
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
39946
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
39947
|
+
])
|
|
39453
39948
|
],
|
|
39454
39949
|
compute: (bars, inputs) => {
|
|
39455
39950
|
const anchor = str3(inputs, "anchor", "Day");
|
|
39456
|
-
const
|
|
39457
|
-
const
|
|
39951
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
39952
|
+
const n = bars.length;
|
|
39953
|
+
const values = new Array(n).fill(Number.NaN);
|
|
39954
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
39955
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
39956
|
+
color: str3(inputs, b.color, b.defColor),
|
|
39957
|
+
fill: bool(inputs, b.fill, true),
|
|
39958
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
39959
|
+
up: new Array(n).fill(Number.NaN),
|
|
39960
|
+
down: new Array(n).fill(Number.NaN)
|
|
39961
|
+
}));
|
|
39458
39962
|
let period = Number.NaN;
|
|
39459
39963
|
let cumPV = 0;
|
|
39964
|
+
let cumPV2 = 0;
|
|
39460
39965
|
let cumV = 0;
|
|
39461
|
-
for (let i = 0; i <
|
|
39966
|
+
for (let i = 0; i < n; i++) {
|
|
39462
39967
|
const b = bars[i];
|
|
39463
39968
|
const key = periodKey(anchor, b.time);
|
|
39464
39969
|
if (key !== period) {
|
|
39465
39970
|
period = key;
|
|
39466
39971
|
cumPV = 0;
|
|
39972
|
+
cumPV2 = 0;
|
|
39467
39973
|
cumV = 0;
|
|
39468
39974
|
}
|
|
39469
39975
|
const v = b.volume;
|
|
39470
39976
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
39471
|
-
const tp =
|
|
39977
|
+
const tp = src[i];
|
|
39472
39978
|
cumPV += tp * v;
|
|
39979
|
+
cumPV2 += tp * tp * v;
|
|
39473
39980
|
cumV += v;
|
|
39474
39981
|
}
|
|
39475
|
-
if (cumV
|
|
39476
|
-
|
|
39477
|
-
|
|
39982
|
+
if (cumV <= 0) continue;
|
|
39983
|
+
const mean = cumPV / cumV;
|
|
39984
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
39985
|
+
values[i] = mean;
|
|
39986
|
+
for (const band of bands) {
|
|
39987
|
+
band.up[i] = mean + band.mult * sd;
|
|
39988
|
+
band.down[i] = mean - band.mult * sd;
|
|
39989
|
+
}
|
|
39990
|
+
}
|
|
39991
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
39992
|
+
const fills = [];
|
|
39993
|
+
bands.forEach((band, i) => {
|
|
39994
|
+
plots.push(
|
|
39995
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
39996
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
39997
|
+
);
|
|
39998
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
39999
|
+
});
|
|
40000
|
+
return { plots, bands: fills };
|
|
39478
40001
|
}
|
|
39479
40002
|
};
|
|
39480
40003
|
var pivotPoints = {
|
|
@@ -39665,21 +40188,11 @@ var Vela = class {
|
|
|
39665
40188
|
registerClassicIndicators();
|
|
39666
40189
|
const element = resolveElement(container);
|
|
39667
40190
|
const theme = resolveTheme(options.theme);
|
|
39668
|
-
let animZoom = true;
|
|
39669
|
-
let animPan = true;
|
|
39670
|
-
if (typeof options.animations === "boolean") {
|
|
39671
|
-
animZoom = options.animations;
|
|
39672
|
-
animPan = options.animations;
|
|
39673
|
-
} else if (options.animations) {
|
|
39674
|
-
animZoom = options.animations.zoom ?? true;
|
|
39675
|
-
animPan = options.animations.pan ?? true;
|
|
39676
|
-
}
|
|
39677
40191
|
const display = {
|
|
39678
40192
|
currentPriceLine: options.currentPriceLine ?? true,
|
|
39679
40193
|
logScale: options.logScale ?? false,
|
|
39680
40194
|
nativeBackend: options.nativeBackend ?? "auto",
|
|
39681
|
-
|
|
39682
|
-
animPan,
|
|
40195
|
+
...resolveAnimations(options.animations),
|
|
39683
40196
|
glow: options.glow ?? 0,
|
|
39684
40197
|
upColor: options.upColor ?? BULLISH,
|
|
39685
40198
|
downColor: options.downColor ?? BEARISH,
|
|
@@ -39994,13 +40507,28 @@ function resolveElement(container) {
|
|
|
39994
40507
|
return element;
|
|
39995
40508
|
}
|
|
39996
40509
|
|
|
39997
|
-
// src/core/options.ts
|
|
39998
|
-
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
39999
|
-
|
|
40000
40510
|
// src/widget/statusline-model.ts
|
|
40001
40511
|
function segmentVisibility(parts, chartHidden = false) {
|
|
40002
40512
|
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
40003
40513
|
}
|
|
40514
|
+
var STATUSLINE_LADDER = [
|
|
40515
|
+
{ stacked: false, level: "full" },
|
|
40516
|
+
{ stacked: true, level: "full" },
|
|
40517
|
+
{ stacked: true, level: "compact" },
|
|
40518
|
+
{ stacked: true, level: "minimal" }
|
|
40519
|
+
];
|
|
40520
|
+
function readoutCells(level, readout) {
|
|
40521
|
+
if (readout === "value") return [{ key: "close", label: "" }];
|
|
40522
|
+
if (level === "full") {
|
|
40523
|
+
return [
|
|
40524
|
+
{ key: "open", label: "O" },
|
|
40525
|
+
{ key: "high", label: "H" },
|
|
40526
|
+
{ key: "low", label: "L" },
|
|
40527
|
+
{ key: "close", label: "C" }
|
|
40528
|
+
];
|
|
40529
|
+
}
|
|
40530
|
+
return [{ key: "close", label: level === "compact" ? "C" : "" }];
|
|
40531
|
+
}
|
|
40004
40532
|
function statuslineMenuItems(parts, chartVisible) {
|
|
40005
40533
|
return [
|
|
40006
40534
|
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
@@ -40029,6 +40557,11 @@ function fmtChange(open2, close) {
|
|
|
40029
40557
|
const sign = diff >= 0 ? "+" : "";
|
|
40030
40558
|
return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
|
|
40031
40559
|
}
|
|
40560
|
+
function fmtChangePct(open2, close) {
|
|
40561
|
+
if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
|
|
40562
|
+
const pct = (close - open2) / open2 * 100;
|
|
40563
|
+
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
|
40564
|
+
}
|
|
40032
40565
|
|
|
40033
40566
|
// src/widget/statusline.ts
|
|
40034
40567
|
var STYLE_ID24 = "vela-widget-statusline";
|
|
@@ -40045,6 +40578,11 @@ var CSS21 = `
|
|
|
40045
40578
|
display: flex;
|
|
40046
40579
|
align-items: baseline;
|
|
40047
40580
|
gap: var(--vela-space-2);
|
|
40581
|
+
/* The chip never grows past the plot: fit() measures overflow against this width to
|
|
40582
|
+
* walk the layout ladder (stack, then shed values), so overflow:hidden only guards
|
|
40583
|
+
* the transient between a resize and the next measure. */
|
|
40584
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40585
|
+
overflow: hidden;
|
|
40048
40586
|
color: var(--vela-fg);
|
|
40049
40587
|
font-size: var(--vela-font-size-md);
|
|
40050
40588
|
/* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
|
|
@@ -40066,6 +40604,31 @@ var CSS21 = `
|
|
|
40066
40604
|
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
40067
40605
|
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
40068
40606
|
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
40607
|
+
/* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
|
|
40608
|
+
* value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
|
|
40609
|
+
* side by side on one line; the STACKED layouts (fit() decides, see the ladder in
|
|
40610
|
+
* statusline-model) turn the chip into a column so the values drop under the symbol. */
|
|
40611
|
+
.vela-statusline .vela-sl-row {
|
|
40612
|
+
display: flex;
|
|
40613
|
+
align-items: baseline;
|
|
40614
|
+
gap: var(--vela-space-2);
|
|
40615
|
+
white-space: nowrap;
|
|
40616
|
+
}
|
|
40617
|
+
.vela-statusline.vela-sl-stacked {
|
|
40618
|
+
flex-direction: column;
|
|
40619
|
+
align-items: flex-start;
|
|
40620
|
+
/* The same air between the two rows as between the values row and the legend row
|
|
40621
|
+
* that follows the chip (see the stacked legend shift below). */
|
|
40622
|
+
row-gap: var(--vela-space-1);
|
|
40623
|
+
}
|
|
40624
|
+
/* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
|
|
40625
|
+
* never does, so its overflow is what fit() measures to keep descending the ladder. */
|
|
40626
|
+
.vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
|
|
40627
|
+
.vela-statusline.vela-sl-stacked .vela-sl-meta {
|
|
40628
|
+
min-width: 0;
|
|
40629
|
+
overflow: hidden;
|
|
40630
|
+
text-overflow: ellipsis;
|
|
40631
|
+
}
|
|
40069
40632
|
.vela-statusline .vela-sl-avatar {
|
|
40070
40633
|
width: 18px;
|
|
40071
40634
|
height: 18px;
|
|
@@ -40117,59 +40680,13 @@ var CSS21 = `
|
|
|
40117
40680
|
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
40118
40681
|
* chart on the page, including statusline-less ones. */
|
|
40119
40682
|
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40120
|
-
/*
|
|
40121
|
-
*
|
|
40122
|
-
|
|
40123
|
-
|
|
40124
|
-
*
|
|
40125
|
-
|
|
40126
|
-
[data-layout='mobile'] .vela-statusline {
|
|
40127
|
-
display: grid;
|
|
40128
|
-
grid-template-columns: auto auto minmax(0, auto) auto;
|
|
40129
|
-
justify-content: start;
|
|
40130
|
-
align-items: center;
|
|
40131
|
-
row-gap: 1px;
|
|
40132
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40133
|
-
font-size: var(--vela-font-size-sm);
|
|
40134
|
-
}
|
|
40135
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-symbol {
|
|
40136
|
-
font-size: var(--vela-font-size-md);
|
|
40137
|
-
line-height: 1;
|
|
40138
|
-
}
|
|
40139
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-meta {
|
|
40140
|
-
min-width: 0;
|
|
40141
|
-
overflow: hidden;
|
|
40142
|
-
text-overflow: ellipsis;
|
|
40143
|
-
white-space: nowrap;
|
|
40144
|
-
line-height: 1;
|
|
40145
|
-
}
|
|
40146
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-avatar,
|
|
40147
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-market { align-self: center; }
|
|
40148
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-ohlc { display: none !important; }
|
|
40149
|
-
[data-layout='mobile'] .vela-statusline .vela-sl-change {
|
|
40150
|
-
/* Second row, under the text column \u2014 the avatar keeps the first column. */
|
|
40151
|
-
grid-column: 2 / -1;
|
|
40152
|
-
font-size: var(--vela-font-size-sm);
|
|
40153
|
-
line-height: 1.2;
|
|
40154
|
-
}
|
|
40155
|
-
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40156
|
-
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
40157
|
-
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
40158
|
-
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
40159
|
-
* Placed after the mobile block on purpose: same specificity, later wins. */
|
|
40160
|
-
.vela-statusline.vela-sl-fit {
|
|
40161
|
-
display: flex; /* undo the mobile grid \u2014 fit mode is one flex row again */
|
|
40162
|
-
flex-wrap: nowrap;
|
|
40163
|
-
align-items: center;
|
|
40164
|
-
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
40165
|
-
overflow: hidden;
|
|
40166
|
-
}
|
|
40167
|
-
.vela-statusline.vela-sl-fit .vela-sl-change {
|
|
40168
|
-
flex-basis: auto;
|
|
40169
|
-
padding-left: 0;
|
|
40170
|
-
}
|
|
40171
|
-
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
40172
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
40683
|
+
/* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
|
|
40684
|
+
* host whenever the ladder lands on a stacked layout. */
|
|
40685
|
+
.vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
40686
|
+
/* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
|
|
40687
|
+
* of the readout fits, exactly as on a narrow desktop chart. */
|
|
40688
|
+
[data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
|
|
40689
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
|
|
40173
40690
|
`;
|
|
40174
40691
|
function baseOfTicker(ticker) {
|
|
40175
40692
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -40240,6 +40757,8 @@ var Statusline = class {
|
|
|
40240
40757
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
40241
40758
|
this.fitMode = false;
|
|
40242
40759
|
this.fitRO = null;
|
|
40760
|
+
/** The ladder rung currently applied — see {@link fit}. */
|
|
40761
|
+
this.layout = { stacked: false, level: "full" };
|
|
40243
40762
|
this.onContextMenu = (e) => {
|
|
40244
40763
|
if (!this.menu || !this.menuHooks) return;
|
|
40245
40764
|
e.preventDefault();
|
|
@@ -40286,8 +40805,18 @@ var Statusline = class {
|
|
|
40286
40805
|
this.menuHooks?.setChartVisible(true);
|
|
40287
40806
|
this.setChartHidden(false);
|
|
40288
40807
|
});
|
|
40289
|
-
this.
|
|
40808
|
+
this.identityRow = doc.createElement("span");
|
|
40809
|
+
this.identityRow.className = "vela-sl-row vela-sl-identity";
|
|
40810
|
+
this.identityRow.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl);
|
|
40811
|
+
this.valuesRow = doc.createElement("span");
|
|
40812
|
+
this.valuesRow.className = "vela-sl-row vela-sl-values";
|
|
40813
|
+
this.valuesRow.append(this.ohlcEl, this.changeEl, this.eyeEl);
|
|
40814
|
+
this.el.append(this.identityRow, this.valuesRow);
|
|
40290
40815
|
host.appendChild(this.el);
|
|
40816
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40817
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40818
|
+
this.fitRO.observe(host);
|
|
40819
|
+
}
|
|
40291
40820
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
40292
40821
|
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
40293
40822
|
this.setMarketStatus("open");
|
|
@@ -40303,27 +40832,15 @@ var Statusline = class {
|
|
|
40303
40832
|
this.fit();
|
|
40304
40833
|
}
|
|
40305
40834
|
/**
|
|
40306
|
-
* Multi-chart cells:
|
|
40307
|
-
*
|
|
40308
|
-
*
|
|
40309
|
-
* market badge; the logo + ticker always stay.
|
|
40835
|
+
* Multi-chart cells: the same width ladder as a single chart, plus a last resort for
|
|
40836
|
+
* cells too narrow for even its bottom rung — whatever still overflows is hidden
|
|
40837
|
+
* outright rather than clipped mid-glyph, least important first: the values, the
|
|
40838
|
+
* venue/timeframe meta, then the market badge; the logo + ticker always stay.
|
|
40310
40839
|
*/
|
|
40311
40840
|
setFitMode(on) {
|
|
40312
40841
|
if (on === this.fitMode) return;
|
|
40313
40842
|
this.fitMode = on;
|
|
40314
|
-
this.
|
|
40315
|
-
this.host.classList.toggle("vela-sl-fit-host", on);
|
|
40316
|
-
if (on) {
|
|
40317
|
-
if (typeof ResizeObserver !== "undefined" && !this.fitRO) {
|
|
40318
|
-
this.fitRO = new ResizeObserver(() => this.fit());
|
|
40319
|
-
this.fitRO.observe(this.host);
|
|
40320
|
-
}
|
|
40321
|
-
this.fit();
|
|
40322
|
-
} else {
|
|
40323
|
-
this.fitRO?.disconnect();
|
|
40324
|
-
this.fitRO = null;
|
|
40325
|
-
this.syncParts();
|
|
40326
|
-
}
|
|
40843
|
+
this.fit();
|
|
40327
40844
|
}
|
|
40328
40845
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
40329
40846
|
syncParts() {
|
|
@@ -40335,22 +40852,42 @@ var Statusline = class {
|
|
|
40335
40852
|
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
40336
40853
|
this.changeEl.style.display = seg.change ? "" : "none";
|
|
40337
40854
|
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
40855
|
+
this.valuesRow.style.display = seg.ohlc || seg.change || seg.eye ? "" : "none";
|
|
40856
|
+
}
|
|
40857
|
+
/** Apply one ladder rung: the stacked/inline shape and the readout's level. */
|
|
40858
|
+
applyLayout(layout) {
|
|
40859
|
+
this.layout = layout;
|
|
40860
|
+
this.el.classList.toggle("vela-sl-stacked", layout.stacked);
|
|
40861
|
+
this.host.classList.toggle("vela-sl-stacked-host", layout.stacked);
|
|
40862
|
+
this.renderValues();
|
|
40338
40863
|
}
|
|
40339
|
-
/**
|
|
40864
|
+
/**
|
|
40865
|
+
* Walk the width ladder (see {@link STATUSLINE_LADDER}) until the chip fits its
|
|
40866
|
+
* max-width: the full one-line readout, then the values stacked under the symbol
|
|
40867
|
+
* line, then O/H/L shed, then the absolute change. In fit mode, when the last rung
|
|
40868
|
+
* still overflows, whole segments hide, least important first. Without layout
|
|
40869
|
+
* (detached host, node tests) nothing overflows, so the widest rung stays.
|
|
40870
|
+
*/
|
|
40340
40871
|
fit() {
|
|
40341
|
-
if (!this.fitMode) return;
|
|
40342
40872
|
this.syncParts();
|
|
40343
40873
|
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
40874
|
+
const overflows = () => this.el.scrollWidth > this.el.clientWidth;
|
|
40875
|
+
const hasValues = seg.ohlc || seg.change;
|
|
40876
|
+
for (const rung of STATUSLINE_LADDER) {
|
|
40877
|
+
this.applyLayout(rung);
|
|
40878
|
+
if (!hasValues || !overflows()) break;
|
|
40879
|
+
}
|
|
40880
|
+
if (!this.fitMode) return;
|
|
40344
40881
|
const order = [
|
|
40345
|
-
[this.
|
|
40346
|
-
[this.changeEl, seg.change],
|
|
40882
|
+
[this.valuesRow, hasValues],
|
|
40347
40883
|
[this.metaEl, seg.meta],
|
|
40348
40884
|
[this.marketEl, seg.market]
|
|
40349
40885
|
];
|
|
40350
40886
|
for (const [el, shown] of order) {
|
|
40351
|
-
if (
|
|
40887
|
+
if (!overflows()) break;
|
|
40352
40888
|
if (shown) el.style.display = "none";
|
|
40353
40889
|
}
|
|
40890
|
+
if (this.valuesRow.style.display === "none") this.applyLayout({ ...this.layout, stacked: false });
|
|
40354
40891
|
}
|
|
40355
40892
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
40356
40893
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -40463,7 +41000,7 @@ var Statusline = class {
|
|
|
40463
41000
|
this.marketTip.destroy();
|
|
40464
41001
|
this.eyeTip.destroy();
|
|
40465
41002
|
this.marketBubble.destroy();
|
|
40466
|
-
this.host.classList.remove("vela-has-statusline", "vela-sl-
|
|
41003
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-stacked-host");
|
|
40467
41004
|
this.el.remove();
|
|
40468
41005
|
}
|
|
40469
41006
|
detach() {
|
|
@@ -40472,6 +41009,10 @@ var Statusline = class {
|
|
|
40472
41009
|
}
|
|
40473
41010
|
render() {
|
|
40474
41011
|
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
41012
|
+
this.fit();
|
|
41013
|
+
}
|
|
41014
|
+
/** Write the value readout for the current bar at the current ladder level. */
|
|
41015
|
+
renderValues() {
|
|
40475
41016
|
const bar = this.hoverBar ?? this.lastBar;
|
|
40476
41017
|
if (!bar) {
|
|
40477
41018
|
this.ohlcEl.replaceChildren();
|
|
@@ -40491,12 +41032,11 @@ var Statusline = class {
|
|
|
40491
41032
|
s.appendChild(b);
|
|
40492
41033
|
return s;
|
|
40493
41034
|
};
|
|
40494
|
-
|
|
40495
|
-
|
|
40496
|
-
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
41035
|
+
const { level } = this.layout;
|
|
41036
|
+
this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
|
|
41037
|
+
this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
|
|
40497
41038
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
40498
41039
|
this.changeEl.style.color = ink;
|
|
40499
|
-
this.fit();
|
|
40500
41040
|
}
|
|
40501
41041
|
};
|
|
40502
41042
|
|
|
@@ -42630,6 +43170,17 @@ function nextAutoCellId(taken) {
|
|
|
42630
43170
|
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
42631
43171
|
}
|
|
42632
43172
|
}
|
|
43173
|
+
function resolveTyping(key, open2) {
|
|
43174
|
+
if (open2.dialogs > 0) {
|
|
43175
|
+
if (key.length !== 1) return null;
|
|
43176
|
+
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
43177
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
43178
|
+
return null;
|
|
43179
|
+
}
|
|
43180
|
+
if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
43181
|
+
if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
43182
|
+
return null;
|
|
43183
|
+
}
|
|
42633
43184
|
var VelaWorkspace = class {
|
|
42634
43185
|
constructor(container, opts = {}) {
|
|
42635
43186
|
this.events = new TypedEventBus();
|
|
@@ -44263,17 +44814,14 @@ var VelaWorkspace = class {
|
|
|
44263
44814
|
}
|
|
44264
44815
|
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
44265
44816
|
routeTyping(ev) {
|
|
44266
|
-
if (this.destroyed
|
|
44817
|
+
if (this.destroyed) return;
|
|
44267
44818
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
44268
44819
|
if (isEditableTarget(ev)) return;
|
|
44269
|
-
const
|
|
44270
|
-
if (
|
|
44271
|
-
|
|
44272
|
-
|
|
44273
|
-
|
|
44274
|
-
ev.preventDefault();
|
|
44275
|
-
this.tfQuick.open(key);
|
|
44276
|
-
}
|
|
44820
|
+
const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
|
|
44821
|
+
if (!hit) return;
|
|
44822
|
+
ev.preventDefault();
|
|
44823
|
+
if (hit.target === "symbol") this.symbolPicker.type(hit.text);
|
|
44824
|
+
else this.tfQuick.type(hit.text);
|
|
44277
44825
|
}
|
|
44278
44826
|
trackDialog(open2) {
|
|
44279
44827
|
this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
|