@luxalgo/vela 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-BBQ3Hr21.d.cts} +1 -1
- package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DGesjOJT.d.ts} +1 -1
- package/dist/{chunk-OVQKKXLZ.js → chunk-5SE4PL3H.js} +402 -60
- package/dist/{chunk-RHIDOUFL.js → chunk-AIW3EJUE.js} +2 -2
- package/dist/{chunk-GYD2THPV.js → chunk-EP6NDJYK.js} +5 -0
- package/dist/{chunk-KCCZNKH7.js → chunk-FNOMKHEZ.js} +668 -78
- package/dist/{chunk-7UFX5ZIG.js → chunk-S6F6SQ2L.js} +7 -2
- package/dist/{chunk-KM6LHB3Y.js → chunk-ULIX6AE2.js} +6 -1
- package/dist/{contributions-hX3EUyjG.d.ts → contributions-6N0rbrPC.d.ts} +46 -31
- package/dist/{contributions-o1GRKPI_.d.cts → contributions-BlV3kb0O.d.cts} +46 -31
- package/dist/{history-Dzxz-MQj.d.ts → history-BTd52YMA.d.ts} +10 -5
- package/dist/{history-LJkz4-sS.d.cts → history-DX3Zne3a.d.cts} +10 -5
- package/dist/index.cjs +680 -75
- package/dist/index.d.cts +37 -12
- package/dist/index.d.ts +37 -12
- package/dist/index.js +4 -4
- package/dist/{options-Q-576hIi.d.cts → options-DbQholFV.d.cts} +14 -0
- package/dist/{options-Q-576hIi.d.ts → options-DbQholFV.d.ts} +14 -0
- package/dist/{plugin-D94muTV-.d.cts → plugin-O34MbXf-.d.cts} +157 -7
- package/dist/{plugin-aGUD1epn.d.ts → plugin-TN3n-9YN.d.ts} +157 -7
- package/dist/plugin.cjs +5 -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/ui.cjs +10 -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 +680 -75
- package/dist/vela.global.min.js +59 -28
- package/dist/widget.cjs +1081 -134
- package/dist/widget.d.cts +30 -16
- package/dist/widget.d.ts +30 -16
- package/dist/widget.js +9 -9
- package/dist/workspace.cjs +1372 -183
- package/dist/workspace.d.cts +66 -8
- package/dist/workspace.d.ts +66 -8
- package/dist/workspace.js +275 -37
- package/package.json +1 -1
package/dist/vela.global.js
CHANGED
|
@@ -532,6 +532,11 @@ var Vela = (function (exports) {
|
|
|
532
532
|
"pen-lock",
|
|
533
533
|
svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="13" y="14.5" width="8" height="6" rx="1.2"/><path d="M15 14.5v-1.6a2 2 0 0 1 4 0v1.6"/>')
|
|
534
534
|
);
|
|
535
|
+
registerIcon(
|
|
536
|
+
"pen-sync",
|
|
537
|
+
// Pen + two stacked panes — drawing onto every linked chart at once.
|
|
538
|
+
svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="12.5" y="13.5" width="6" height="4.8" rx="1"/><path d="M15.5 18.3v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.8a1 1 0 0 0-1-1h-1"/>')
|
|
539
|
+
);
|
|
535
540
|
registerIcon("brush", svg24('<path d="M9.5 12 17 4.5a2.12 2.12 0 0 1 3 3L12.5 15"/><path d="M7 14a3 3 0 0 0-3 3c0 1.3-1.2 1.5-1.5 2 .8.9 2 1.5 3.5 1.5a3.5 3.5 0 0 0 3.5-3.5 3 3 0 0 0-2.5-3Z"/>'));
|
|
536
541
|
registerIcon(
|
|
537
542
|
"bucket",
|
|
@@ -6535,6 +6540,11 @@ var Vela = (function (exports) {
|
|
|
6535
6540
|
setToolbar(option) {
|
|
6536
6541
|
this.port?.setToolbar(buildToolbar(option).definition);
|
|
6537
6542
|
}
|
|
6543
|
+
/** Display (or clear) another chart's in-progress placement as a ghost — silently
|
|
6544
|
+
* inert on a renderer without the optional `setExternalGhost` seam. */
|
|
6545
|
+
setExternalGhost(doc) {
|
|
6546
|
+
this.port?.setExternalGhost?.(doc);
|
|
6547
|
+
}
|
|
6538
6548
|
/** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
|
|
6539
6549
|
setToolShortcuts(map) {
|
|
6540
6550
|
this.port?.setToolShortcuts?.(map);
|
|
@@ -6765,6 +6775,9 @@ var Vela = (function (exports) {
|
|
|
6765
6775
|
case "arm":
|
|
6766
6776
|
this.setTool(i.type);
|
|
6767
6777
|
break;
|
|
6778
|
+
case "draft":
|
|
6779
|
+
this.events.emit("drawing:draft", { doc: i.doc });
|
|
6780
|
+
break;
|
|
6768
6781
|
case "create": {
|
|
6769
6782
|
const before = this.store.serialize();
|
|
6770
6783
|
const last = this.lastStyle.get(i.doc.type);
|
|
@@ -7040,6 +7053,11 @@ var Vela = (function (exports) {
|
|
|
7040
7053
|
};
|
|
7041
7054
|
|
|
7042
7055
|
// src/chart-types/registry.ts
|
|
7056
|
+
function settingsRowVisible(when, bag) {
|
|
7057
|
+
if (!when) return true;
|
|
7058
|
+
const conds = Array.isArray(when) ? when : [when];
|
|
7059
|
+
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === bag[c.key]) : bag[c.key] === c.equals);
|
|
7060
|
+
}
|
|
7043
7061
|
var registry2 = /* @__PURE__ */ new Map();
|
|
7044
7062
|
function registerChartType(def) {
|
|
7045
7063
|
registry2.set(def.id, def);
|
|
@@ -9384,6 +9402,14 @@ var Vela = (function (exports) {
|
|
|
9384
9402
|
if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
|
|
9385
9403
|
return this;
|
|
9386
9404
|
}
|
|
9405
|
+
/** Display another chart's in-progress placement as a GHOST at reduced opacity
|
|
9406
|
+
* (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
|
|
9407
|
+
* charts. Never a real drawing: no store entry, no selection, no persistence.
|
|
9408
|
+
* Silently inert on a renderer without the optional seam. */
|
|
9409
|
+
setExternalGhost(doc) {
|
|
9410
|
+
this.ctrl.setExternalGhost(doc);
|
|
9411
|
+
return this;
|
|
9412
|
+
}
|
|
9387
9413
|
/** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
|
|
9388
9414
|
add(type, init = {}) {
|
|
9389
9415
|
if (!this.ok("add")) return null;
|
|
@@ -9622,6 +9648,11 @@ var Vela = (function (exports) {
|
|
|
9622
9648
|
"--vela-hover-strong": wash(0.16),
|
|
9623
9649
|
"--vela-focus": withAlpha(t.textColor, 0.5),
|
|
9624
9650
|
"--vela-focus-soft": withAlpha(t.textColor, 0.12),
|
|
9651
|
+
// Separator hover — the SAME wash the chart's pane separators paint on hover
|
|
9652
|
+
// (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
|
|
9653
|
+
// (the workspace grid) and canvas-drawn ones read as one family.
|
|
9654
|
+
"--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
|
|
9655
|
+
"--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
|
|
9625
9656
|
"--vela-scroll": withAlpha(t.textColor, 0.3),
|
|
9626
9657
|
"--vela-accent": ACCENT,
|
|
9627
9658
|
"--vela-accent-bright": ACCENT_BRIGHT,
|
|
@@ -11465,6 +11496,44 @@ var Vela = (function (exports) {
|
|
|
11465
11496
|
for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
|
|
11466
11497
|
return "candles";
|
|
11467
11498
|
}
|
|
11499
|
+
function hasOwnCandlePaint(style) {
|
|
11500
|
+
if (BUILTIN_PRICE_STYLES.includes(style)) return false;
|
|
11501
|
+
for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
|
|
11502
|
+
return false;
|
|
11503
|
+
}
|
|
11504
|
+
function candleOverrideFor(style, bags) {
|
|
11505
|
+
if (!hasOwnCandlePaint(style)) return null;
|
|
11506
|
+
const bag = bags[style] ?? {};
|
|
11507
|
+
const color = (v) => isColor(v) ? v : null;
|
|
11508
|
+
const bool = (v) => isBool(v) ? v : null;
|
|
11509
|
+
return {
|
|
11510
|
+
upColor: color(bag.candleUpColor),
|
|
11511
|
+
downColor: color(bag.candleDownColor),
|
|
11512
|
+
bodyVisible: bool(bag.candleBodyVisible),
|
|
11513
|
+
borderVisible: bool(bag.candleBorderVisible),
|
|
11514
|
+
borderUpColor: color(bag.candleBorderUpColor),
|
|
11515
|
+
borderDownColor: color(bag.candleBorderDownColor),
|
|
11516
|
+
wickVisible: bool(bag.candleWickVisible),
|
|
11517
|
+
wickUpColor: color(bag.candleWickUpColor),
|
|
11518
|
+
wickDownColor: color(bag.candleWickDownColor)
|
|
11519
|
+
};
|
|
11520
|
+
}
|
|
11521
|
+
function effectiveCandlePaint(base, override, themeUp, themeDown) {
|
|
11522
|
+
if (!override) return { up: themeUp, down: themeDown, candle: base };
|
|
11523
|
+
return {
|
|
11524
|
+
up: override.upColor ?? themeUp,
|
|
11525
|
+
down: override.downColor ?? themeDown,
|
|
11526
|
+
candle: {
|
|
11527
|
+
bodyVisible: override.bodyVisible ?? base.bodyVisible,
|
|
11528
|
+
borderVisible: override.borderVisible ?? base.borderVisible,
|
|
11529
|
+
borderUpColor: override.borderUpColor ?? base.borderUpColor,
|
|
11530
|
+
borderDownColor: override.borderDownColor ?? base.borderDownColor,
|
|
11531
|
+
wickVisible: override.wickVisible ?? base.wickVisible,
|
|
11532
|
+
wickUpColor: override.wickUpColor ?? base.wickUpColor,
|
|
11533
|
+
wickDownColor: override.wickDownColor ?? base.wickDownColor
|
|
11534
|
+
}
|
|
11535
|
+
};
|
|
11536
|
+
}
|
|
11468
11537
|
function priceStyleIds() {
|
|
11469
11538
|
const out = [...BUILTIN_PRICE_STYLES];
|
|
11470
11539
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
@@ -11503,6 +11572,40 @@ var Vela = (function (exports) {
|
|
|
11503
11572
|
function clampSpacing(v) {
|
|
11504
11573
|
return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
|
|
11505
11574
|
}
|
|
11575
|
+
function factoryResetConfig(factory) {
|
|
11576
|
+
const bag = {};
|
|
11577
|
+
for (const t of chartTypes()) {
|
|
11578
|
+
const section = t.settings;
|
|
11579
|
+
if (!section) continue;
|
|
11580
|
+
const defaults2 = {};
|
|
11581
|
+
const addRows = (rows) => {
|
|
11582
|
+
for (const r of rows ?? []) {
|
|
11583
|
+
if (r.kind === "heading" || r.kind === "header") continue;
|
|
11584
|
+
if (r.kind === "range") {
|
|
11585
|
+
defaults2[r.minKey] = r.defval;
|
|
11586
|
+
defaults2[r.maxKey] = r.defval;
|
|
11587
|
+
continue;
|
|
11588
|
+
}
|
|
11589
|
+
defaults2[r.key] = r.defval;
|
|
11590
|
+
if (r.kind === "toggle") for (const c of r.colors ?? []) defaults2[c.key] = c.defval;
|
|
11591
|
+
}
|
|
11592
|
+
};
|
|
11593
|
+
for (const inst of section.instances ?? []) {
|
|
11594
|
+
if (inst.enableKey) defaults2[inst.enableKey] = false;
|
|
11595
|
+
addRows(inst.rows);
|
|
11596
|
+
}
|
|
11597
|
+
for (const sub of section.subsections ?? []) {
|
|
11598
|
+
if (sub.enableKey) defaults2[sub.enableKey] = false;
|
|
11599
|
+
addRows(sub.rows);
|
|
11600
|
+
}
|
|
11601
|
+
if (!section.instances) addRows(section.rows);
|
|
11602
|
+
bag[t.id] = defaults2;
|
|
11603
|
+
}
|
|
11604
|
+
for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
|
|
11605
|
+
bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
|
|
11606
|
+
}
|
|
11607
|
+
return { ...factory, chartTypes: bag };
|
|
11608
|
+
}
|
|
11506
11609
|
function mergeConfig(base, patch) {
|
|
11507
11610
|
const p = asObject(patch);
|
|
11508
11611
|
const ctPatch = asObject(p.chartTypes);
|
|
@@ -11817,6 +11920,9 @@ var Vela = (function (exports) {
|
|
|
11817
11920
|
this.priceStyle = "candles";
|
|
11818
11921
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
11819
11922
|
this.basePainting = "candles";
|
|
11923
|
+
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
11924
|
+
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
11925
|
+
this.candleOverride = null;
|
|
11820
11926
|
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
11821
11927
|
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
11822
11928
|
* range (resolved per frame via `baselinePriceFor`). */
|
|
@@ -13046,14 +13152,15 @@ void main() {
|
|
|
13046
13152
|
emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
13047
13153
|
const spacing = coords.bodySpacing();
|
|
13048
13154
|
const tier = candleTier(spacing);
|
|
13155
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
13049
13156
|
if (tier === "aggregate") {
|
|
13050
|
-
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane,
|
|
13157
|
+
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
13051
13158
|
return;
|
|
13052
13159
|
}
|
|
13053
13160
|
const drawBody = tier === "full";
|
|
13054
|
-
const up =
|
|
13055
|
-
const down =
|
|
13056
|
-
const cs =
|
|
13161
|
+
const up = paint.up;
|
|
13162
|
+
const down = paint.down;
|
|
13163
|
+
const cs = paint.candle;
|
|
13057
13164
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
13058
13165
|
for (let i = i0; i <= i1; i += 1) {
|
|
13059
13166
|
const bar = bars[i];
|
|
@@ -13645,7 +13752,6 @@ void main() {
|
|
|
13645
13752
|
return momentaryOverride ? "strong" : sticky;
|
|
13646
13753
|
}
|
|
13647
13754
|
var InputController = class {
|
|
13648
|
-
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
13649
13755
|
constructor(deps) {
|
|
13650
13756
|
this.deps = deps;
|
|
13651
13757
|
/** When true (the default), wheel-zoom pins the right edge instead of the bar
|
|
@@ -13673,9 +13779,23 @@ void main() {
|
|
|
13673
13779
|
this.vx = 0;
|
|
13674
13780
|
// smoothed pointer velocity, px/ms
|
|
13675
13781
|
this.middleDeleted = false;
|
|
13782
|
+
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
13783
|
+
// Last cursor position over the element (NaN once it leaves) — lets a modifier
|
|
13784
|
+
// press/release re-shape the drawings preview with the pointer stationary.
|
|
13785
|
+
this.cursorX = NaN;
|
|
13786
|
+
this.cursorY = NaN;
|
|
13676
13787
|
this.onMiddleGuard = (e) => {
|
|
13677
13788
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
13678
13789
|
};
|
|
13790
|
+
/** A modifier press/release with the pointer stationary: re-issue the last cursor
|
|
13791
|
+
* position so the placing ghost / drag preview reflects the new state immediately
|
|
13792
|
+
* (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
|
|
13793
|
+
this.onModifier = (e) => {
|
|
13794
|
+
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
13795
|
+
if (Number.isNaN(this.cursorX)) return;
|
|
13796
|
+
if (this.dragging && this.region !== "drawing") return;
|
|
13797
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
13798
|
+
};
|
|
13679
13799
|
this.onDown = (e) => {
|
|
13680
13800
|
if (e.button === 1) {
|
|
13681
13801
|
const { x: x2, y: y2 } = this.local(e);
|
|
@@ -13717,6 +13837,8 @@ void main() {
|
|
|
13717
13837
|
};
|
|
13718
13838
|
this.onMove = (e) => {
|
|
13719
13839
|
const { x, y } = this.local(e);
|
|
13840
|
+
this.cursorX = x;
|
|
13841
|
+
this.cursorY = y;
|
|
13720
13842
|
if (this.dragging) {
|
|
13721
13843
|
if (this.region === "price") {
|
|
13722
13844
|
if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
|
|
@@ -13730,7 +13852,7 @@ void main() {
|
|
|
13730
13852
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
13731
13853
|
} else if (this.region === "drawing") {
|
|
13732
13854
|
if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
|
|
13733
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
13855
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
13734
13856
|
} else {
|
|
13735
13857
|
const coords = this.deps.getCoords();
|
|
13736
13858
|
const vp = coords.getViewport();
|
|
@@ -13753,7 +13875,7 @@ void main() {
|
|
|
13753
13875
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
13754
13876
|
const r = this.regionAt(x, y);
|
|
13755
13877
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
13756
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
13878
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
13757
13879
|
}
|
|
13758
13880
|
this.deps.onPointerMove(x, y);
|
|
13759
13881
|
};
|
|
@@ -13778,6 +13900,8 @@ void main() {
|
|
|
13778
13900
|
}
|
|
13779
13901
|
};
|
|
13780
13902
|
this.onLeave = () => {
|
|
13903
|
+
this.cursorX = NaN;
|
|
13904
|
+
this.cursorY = NaN;
|
|
13781
13905
|
this.deps.onPointerMove(null, null);
|
|
13782
13906
|
};
|
|
13783
13907
|
this.onDblClick = (e) => {
|
|
@@ -13816,10 +13940,16 @@ void main() {
|
|
|
13816
13940
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
13817
13941
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
13818
13942
|
el.addEventListener("auxclick", this.onMiddleGuard);
|
|
13943
|
+
const win = el.ownerDocument?.defaultView;
|
|
13944
|
+
win?.addEventListener("keydown", this.onModifier);
|
|
13945
|
+
win?.addEventListener("keyup", this.onModifier);
|
|
13819
13946
|
}
|
|
13820
13947
|
detach() {
|
|
13821
13948
|
const el = this.el;
|
|
13822
13949
|
if (!el) return;
|
|
13950
|
+
const win = el.ownerDocument?.defaultView;
|
|
13951
|
+
win?.removeEventListener("keydown", this.onModifier);
|
|
13952
|
+
win?.removeEventListener("keyup", this.onModifier);
|
|
13823
13953
|
el.removeEventListener("pointerdown", this.onDown);
|
|
13824
13954
|
el.removeEventListener("pointermove", this.onMove);
|
|
13825
13955
|
el.removeEventListener("pointerup", this.onUp);
|
|
@@ -14237,12 +14367,13 @@ void main() {
|
|
|
14237
14367
|
drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
14238
14368
|
const spacing = coords.bodySpacing();
|
|
14239
14369
|
const tier = candleTier(spacing);
|
|
14370
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
14240
14371
|
if (tier === "aggregate") {
|
|
14241
|
-
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane,
|
|
14372
|
+
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
14242
14373
|
return;
|
|
14243
14374
|
}
|
|
14244
14375
|
const drawBody = tier === "full";
|
|
14245
|
-
const cs =
|
|
14376
|
+
const cs = paint.candle;
|
|
14246
14377
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
14247
14378
|
for (let i = i0; i <= i1; i += 1) {
|
|
14248
14379
|
const b = bars[i];
|
|
@@ -14250,7 +14381,7 @@ void main() {
|
|
|
14250
14381
|
const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
|
|
14251
14382
|
const x = g.center;
|
|
14252
14383
|
const up = b.close >= b.open;
|
|
14253
|
-
const dir = up ?
|
|
14384
|
+
const dir = up ? paint.up : paint.down;
|
|
14254
14385
|
const bc = barColors.get(b.time);
|
|
14255
14386
|
const color = bc ?? dir;
|
|
14256
14387
|
let top = 0;
|
|
@@ -15361,7 +15492,7 @@ void main() {
|
|
|
15361
15492
|
constructor() {
|
|
15362
15493
|
this.canvas = null;
|
|
15363
15494
|
this.ctx = null;
|
|
15364
|
-
// The color for axis tick labels — the
|
|
15495
|
+
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
15365
15496
|
this.axisTextColor = DARK_THEME.textColor;
|
|
15366
15497
|
// Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
|
|
15367
15498
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
@@ -15393,8 +15524,8 @@ void main() {
|
|
|
15393
15524
|
return dr;
|
|
15394
15525
|
}
|
|
15395
15526
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
15396
|
-
* `surface` (background + text) paints the axis-scale gutters
|
|
15397
|
-
*
|
|
15527
|
+
* `surface` (background + text) paints the axis-scale gutters — the host passes the live
|
|
15528
|
+
* chart background so the scales read as part of the plot, with contrast-corrected text.
|
|
15398
15529
|
* Falls back to the theme's own colors when no surface is supplied. */
|
|
15399
15530
|
render(scene, coords, theme, surface) {
|
|
15400
15531
|
const ctx = this.ctx;
|
|
@@ -16251,6 +16382,37 @@ void main() {
|
|
|
16251
16382
|
.vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
|
|
16252
16383
|
.vela-sd-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
16253
16384
|
.vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16385
|
+
/* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
|
|
16386
|
+
!important: the pane grid rewrites inline display ('contents') AFTER the initial
|
|
16387
|
+
visibility pass, so a class must win. */
|
|
16388
|
+
.vela-sd-hide{display:none !important;}
|
|
16389
|
+
/* Indented rail sub-entry (a chart-type section's subsection tab). */
|
|
16390
|
+
.vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
|
|
16391
|
+
/* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
|
|
16392
|
+
and a dashed + that turns on the next absent instance. The rule under it separates
|
|
16393
|
+
the strip from the instance's TOC + rows area. */
|
|
16394
|
+
.vela-sd-itabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:14px 0 0;padding-bottom:12px;border-bottom:1px solid var(--vela-border);}
|
|
16395
|
+
.vela-sd-itab{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;background:transparent;border:1px solid var(--vela-border);border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-family:inherit;font-size:var(--vela-font-size-md);font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
16396
|
+
.vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16397
|
+
.vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
|
|
16398
|
+
.vela-sd-ix{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:-4px;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-size:10px;line-height:1;}
|
|
16399
|
+
.vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16400
|
+
.vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
|
|
16401
|
+
/* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
|
|
16402
|
+
lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
|
|
16403
|
+
vertical rule sits on the rows column so it spans the full content height. When every
|
|
16404
|
+
group gates out the TOC hides and .no-toc drops the rule with it. */
|
|
16405
|
+
.vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
|
|
16406
|
+
.vela-sd-toc{position:sticky;top:0;flex:0 0 auto;min-width:104px;display:flex;flex-direction:column;gap:2px;padding:2px 14px 0 0;}
|
|
16407
|
+
.vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
|
|
16408
|
+
.vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
|
|
16409
|
+
.vela-sd-toc-btn{text-align:left;padding:6px 10px;background:transparent;border:none;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
16410
|
+
.vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16411
|
+
.vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
16412
|
+
/* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
|
|
16413
|
+
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
16414
|
+
!important beats the inline opacity on labels. */
|
|
16415
|
+
.vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
|
|
16254
16416
|
`;
|
|
16255
16417
|
document.head.appendChild(st);
|
|
16256
16418
|
}
|
|
@@ -16271,6 +16433,10 @@ void main() {
|
|
|
16271
16433
|
this.themeControl = null;
|
|
16272
16434
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
16273
16435
|
this.tabs = [];
|
|
16436
|
+
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
16437
|
+
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
16438
|
+
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
16439
|
+
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
16274
16440
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
16275
16441
|
this.activeSection = null;
|
|
16276
16442
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
@@ -16432,6 +16598,29 @@ void main() {
|
|
|
16432
16598
|
baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
|
|
16433
16599
|
groups.baseline = baseline;
|
|
16434
16600
|
body.append(baseline);
|
|
16601
|
+
for (const def of chartTypes()) {
|
|
16602
|
+
if (!hasOwnCandlePaint(def.id)) continue;
|
|
16603
|
+
const bag = config.chartTypes[def.id] ?? {};
|
|
16604
|
+
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
16605
|
+
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
16606
|
+
const g = this.group();
|
|
16607
|
+
g.append(this.sectionTitle("Candles"));
|
|
16608
|
+
g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
16609
|
+
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
16610
|
+
this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
|
|
16611
|
+
]));
|
|
16612
|
+
g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
16613
|
+
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
16614
|
+
this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
|
|
16615
|
+
]));
|
|
16616
|
+
g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
16617
|
+
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
16618
|
+
this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
|
|
16619
|
+
]));
|
|
16620
|
+
g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
16621
|
+
groups[def.id] = g;
|
|
16622
|
+
body.append(g);
|
|
16623
|
+
}
|
|
16435
16624
|
showActive(config.series.style);
|
|
16436
16625
|
body.append(this.sectionTitle("Time zone"));
|
|
16437
16626
|
body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
|
|
@@ -16446,7 +16635,16 @@ void main() {
|
|
|
16446
16635
|
}
|
|
16447
16636
|
}
|
|
16448
16637
|
};
|
|
16638
|
+
const renderChartTypeSections = (placement) => {
|
|
16639
|
+
for (const def of chartTypes()) {
|
|
16640
|
+
const typeSettings = def.settings;
|
|
16641
|
+
if (!typeSettings) continue;
|
|
16642
|
+
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
16643
|
+
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
16644
|
+
}
|
|
16645
|
+
};
|
|
16449
16646
|
renderHostSections("symbol");
|
|
16647
|
+
renderChartTypeSections("after-symbol");
|
|
16450
16648
|
renderHostSections("after-symbol");
|
|
16451
16649
|
body.append(this.section("Scales and lines"));
|
|
16452
16650
|
body.append(this.sectionTitle("Price scale"));
|
|
@@ -16487,27 +16685,7 @@ void main() {
|
|
|
16487
16685
|
body.append(this.sectionTitle("Theme"));
|
|
16488
16686
|
body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
|
|
16489
16687
|
}
|
|
16490
|
-
|
|
16491
|
-
const typeSettings = def.settings;
|
|
16492
|
-
if (!typeSettings) continue;
|
|
16493
|
-
const marker = this.section(typeSettings.title);
|
|
16494
|
-
marker.dataset.sdStyle = def.id;
|
|
16495
|
-
marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
|
|
16496
|
-
body.append(marker);
|
|
16497
|
-
const values = config.chartTypes[def.id] ?? {};
|
|
16498
|
-
for (const r of typeSettings.rows) {
|
|
16499
|
-
const current2 = values[r.key];
|
|
16500
|
-
if (r.kind === "toggle") {
|
|
16501
|
-
body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
|
|
16502
|
-
} else if (r.kind === "number") {
|
|
16503
|
-
body.append(this.numberRow(r.label, typeof current2 === "number" ? current2 : r.defval, r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => this.emitType(def.id, r.key, v)));
|
|
16504
|
-
} else if (r.kind === "color") {
|
|
16505
|
-
body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
|
|
16506
|
-
} else {
|
|
16507
|
-
body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
|
|
16508
|
-
}
|
|
16509
|
-
}
|
|
16510
|
-
}
|
|
16688
|
+
renderChartTypeSections("end");
|
|
16511
16689
|
renderHostSections("end");
|
|
16512
16690
|
const shell = document.createElement("div");
|
|
16513
16691
|
shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
@@ -16526,7 +16704,7 @@ void main() {
|
|
|
16526
16704
|
const tab = document.createElement("button");
|
|
16527
16705
|
tab.type = "button";
|
|
16528
16706
|
tab.textContent = title;
|
|
16529
|
-
tab.className = "vela-sd-tab";
|
|
16707
|
+
tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
|
|
16530
16708
|
panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
|
|
16531
16709
|
current = el;
|
|
16532
16710
|
child.remove();
|
|
@@ -16566,7 +16744,14 @@ void main() {
|
|
|
16566
16744
|
scrim.appendChild(dlg);
|
|
16567
16745
|
this.container.appendChild(scrim);
|
|
16568
16746
|
this.root = scrim;
|
|
16569
|
-
for (const p of panes)
|
|
16747
|
+
for (const p of panes) {
|
|
16748
|
+
const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
|
|
16749
|
+
if (hosts.length === 0) {
|
|
16750
|
+
this.layoutSettingsGrids(p.el, dlg);
|
|
16751
|
+
continue;
|
|
16752
|
+
}
|
|
16753
|
+
for (const h of hosts) this.layoutSettingsGrids(h, dlg);
|
|
16754
|
+
}
|
|
16570
16755
|
}
|
|
16571
16756
|
close() {
|
|
16572
16757
|
closeColorPopover();
|
|
@@ -16596,6 +16781,291 @@ void main() {
|
|
|
16596
16781
|
el.textContent = text;
|
|
16597
16782
|
return el;
|
|
16598
16783
|
}
|
|
16784
|
+
/**
|
|
16785
|
+
* One chart type's settings tab (and its subsection tabs), appended to the linear
|
|
16786
|
+
* `body` for the pane splitter to file. One live values BAG serves the whole section
|
|
16787
|
+
* — instances and subsections share the per-type store, so a `when` gate anywhere
|
|
16788
|
+
* can reference a key edited anywhere else; every edit runs every registered
|
|
16789
|
+
* refresher.
|
|
16790
|
+
*/
|
|
16791
|
+
chartTypeSection(typeId, section, config, body) {
|
|
16792
|
+
const marker = this.section(section.title);
|
|
16793
|
+
marker.dataset.sdStyle = typeId;
|
|
16794
|
+
marker.dataset.sdVisibility = section.visibility ?? "active";
|
|
16795
|
+
body.append(marker);
|
|
16796
|
+
const values = config.chartTypes[typeId] ?? {};
|
|
16797
|
+
const bag = {};
|
|
16798
|
+
const seedKey = (key, want, defval) => {
|
|
16799
|
+
const v = values[key];
|
|
16800
|
+
bag[key] = typeof v === want ? v : defval;
|
|
16801
|
+
};
|
|
16802
|
+
const seed = (rows) => {
|
|
16803
|
+
for (const r of rows) {
|
|
16804
|
+
if (r.kind === "heading" || r.kind === "header") continue;
|
|
16805
|
+
if (r.kind === "range") {
|
|
16806
|
+
seedKey(r.minKey, "number", r.defval);
|
|
16807
|
+
seedKey(r.maxKey, "number", r.defval);
|
|
16808
|
+
continue;
|
|
16809
|
+
}
|
|
16810
|
+
seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
|
|
16811
|
+
if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
|
|
16812
|
+
}
|
|
16813
|
+
};
|
|
16814
|
+
if (section.rows) seed(section.rows);
|
|
16815
|
+
for (const inst of section.instances ?? []) seed(inst.rows);
|
|
16816
|
+
for (const sub of section.subsections ?? []) seed(sub.rows);
|
|
16817
|
+
for (const inst of section.instances ?? []) {
|
|
16818
|
+
if (!inst.enableKey || inst.enableKey in bag) continue;
|
|
16819
|
+
bag[inst.enableKey] = values[inst.enableKey] === true;
|
|
16820
|
+
}
|
|
16821
|
+
for (const sub of section.subsections ?? []) {
|
|
16822
|
+
if (!sub.enableKey || sub.enableKey in bag) continue;
|
|
16823
|
+
bag[sub.enableKey] = values[sub.enableKey] === true;
|
|
16824
|
+
}
|
|
16825
|
+
const refreshers = [];
|
|
16826
|
+
const put = (key, v) => {
|
|
16827
|
+
bag[key] = v;
|
|
16828
|
+
this.emitType(typeId, key, v);
|
|
16829
|
+
for (const r of refreshers) r();
|
|
16830
|
+
};
|
|
16831
|
+
if (section.instances && section.instances.length > 0) {
|
|
16832
|
+
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
16833
|
+
} else if (section.rows) {
|
|
16834
|
+
this.flatTypeRows(section.rows, bag, put, refreshers, body);
|
|
16835
|
+
}
|
|
16836
|
+
for (const sub of section.subsections ?? []) {
|
|
16837
|
+
const subMarker = this.section(sub.title);
|
|
16838
|
+
subMarker.dataset.sdStyle = typeId;
|
|
16839
|
+
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
16840
|
+
subMarker.dataset.sdSub = "1";
|
|
16841
|
+
body.append(subMarker);
|
|
16842
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
|
|
16843
|
+
}
|
|
16844
|
+
for (const r of refreshers) r();
|
|
16845
|
+
}
|
|
16846
|
+
/** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
|
|
16847
|
+
* them), headings/headers as inline group titles, `when` gates refreshed live. */
|
|
16848
|
+
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
16849
|
+
const entries = [];
|
|
16850
|
+
for (const r of rows) {
|
|
16851
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
|
|
16852
|
+
entries.push({ el, when: r.when });
|
|
16853
|
+
body.append(el);
|
|
16854
|
+
}
|
|
16855
|
+
refreshers.push(() => {
|
|
16856
|
+
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
16857
|
+
});
|
|
16858
|
+
}
|
|
16859
|
+
/** One value row for a chart-type descriptor, writing through `put`. */
|
|
16860
|
+
typeRow(r, bag, put) {
|
|
16861
|
+
if (r.kind === "toggle") {
|
|
16862
|
+
const swatches = (r.colors ?? []).map((c) => {
|
|
16863
|
+
const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
|
|
16864
|
+
sw.title = c.label;
|
|
16865
|
+
return sw;
|
|
16866
|
+
});
|
|
16867
|
+
return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
|
|
16868
|
+
}
|
|
16869
|
+
if (r.kind === "number") return this.numberRow(r.label, bag[r.key], r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => put(r.key, v));
|
|
16870
|
+
if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
|
|
16871
|
+
if (r.kind === "range") return this.rangeRow(r, bag, put);
|
|
16872
|
+
return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
|
|
16873
|
+
}
|
|
16874
|
+
/**
|
|
16875
|
+
* A MIN–MAX row: two number inputs on one row (stored under the descriptor's
|
|
16876
|
+
* `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
|
|
16877
|
+
* empty showing it, and clearing an input stores the default back — the
|
|
16878
|
+
* placeholder names the unset state ('Off' for 0-disables bounds).
|
|
16879
|
+
*/
|
|
16880
|
+
rangeRow(r, bag, put) {
|
|
16881
|
+
const { wrap } = this.row(r.label);
|
|
16882
|
+
const input = (key, title) => {
|
|
16883
|
+
const ni = document.createElement("input");
|
|
16884
|
+
ni.type = "number";
|
|
16885
|
+
ni.className = "vela-sd-number";
|
|
16886
|
+
ni.min = String(r.min ?? 0);
|
|
16887
|
+
ni.max = String(r.max ?? 1e6);
|
|
16888
|
+
ni.step = String(r.step ?? 1);
|
|
16889
|
+
ni.title = title;
|
|
16890
|
+
const current = bag[key];
|
|
16891
|
+
if (r.placeholder !== void 0) {
|
|
16892
|
+
ni.placeholder = r.placeholder;
|
|
16893
|
+
if (current !== r.defval) ni.value = String(current);
|
|
16894
|
+
} else {
|
|
16895
|
+
ni.value = String(current);
|
|
16896
|
+
}
|
|
16897
|
+
ni.addEventListener("change", () => {
|
|
16898
|
+
const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
|
|
16899
|
+
const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
|
|
16900
|
+
ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
|
|
16901
|
+
put(key, v);
|
|
16902
|
+
});
|
|
16903
|
+
return ni;
|
|
16904
|
+
};
|
|
16905
|
+
const box = document.createElement("div");
|
|
16906
|
+
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
16907
|
+
const dash = document.createElement("span");
|
|
16908
|
+
dash.textContent = "\u2013";
|
|
16909
|
+
dash.style.cssText = "color:var(--vela-fg-muted);";
|
|
16910
|
+
box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
|
|
16911
|
+
wrap.appendChild(box);
|
|
16912
|
+
return wrap;
|
|
16913
|
+
}
|
|
16914
|
+
/**
|
|
16915
|
+
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
16916
|
+
* removable one), a dashed `+` while an instance is still absent, and one
|
|
16917
|
+
* grouped-rows content per instance below — only the active tab's content shows.
|
|
16918
|
+
* Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
|
|
16919
|
+
* every section edit, so gates elsewhere stay coherent.
|
|
16920
|
+
*/
|
|
16921
|
+
instancesBlock(typeId, instances, bag, put, refreshers) {
|
|
16922
|
+
const wrap = document.createElement("div");
|
|
16923
|
+
const strip = document.createElement("div");
|
|
16924
|
+
strip.className = "vela-sd-itabs";
|
|
16925
|
+
wrap.append(strip);
|
|
16926
|
+
const contents = instances.map((inst, i) => {
|
|
16927
|
+
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
16928
|
+
wrap.append(content);
|
|
16929
|
+
return content;
|
|
16930
|
+
});
|
|
16931
|
+
const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
|
|
16932
|
+
const refresh = () => {
|
|
16933
|
+
let active = this.typeActiveInstance.get(typeId) ?? 0;
|
|
16934
|
+
if (!present(instances[active] ?? instances[0])) active = 0;
|
|
16935
|
+
this.typeActiveInstance.set(typeId, active);
|
|
16936
|
+
strip.replaceChildren();
|
|
16937
|
+
instances.forEach((inst, i) => {
|
|
16938
|
+
if (!present(inst)) return;
|
|
16939
|
+
const tab = document.createElement("button");
|
|
16940
|
+
tab.type = "button";
|
|
16941
|
+
tab.className = "vela-sd-itab" + (i === active ? " on" : "");
|
|
16942
|
+
const lbl = document.createElement("span");
|
|
16943
|
+
lbl.textContent = inst.label;
|
|
16944
|
+
tab.append(lbl);
|
|
16945
|
+
if (i === active && inst.enableKey) {
|
|
16946
|
+
const x = document.createElement("span");
|
|
16947
|
+
x.className = "vela-sd-ix";
|
|
16948
|
+
x.textContent = "\u2715";
|
|
16949
|
+
x.title = `Remove ${inst.label}`;
|
|
16950
|
+
x.addEventListener("click", (e) => {
|
|
16951
|
+
e.stopPropagation();
|
|
16952
|
+
this.typeActiveInstance.set(typeId, 0);
|
|
16953
|
+
put(inst.enableKey, false);
|
|
16954
|
+
});
|
|
16955
|
+
tab.append(x);
|
|
16956
|
+
}
|
|
16957
|
+
tab.addEventListener("click", () => {
|
|
16958
|
+
this.typeActiveInstance.set(typeId, i);
|
|
16959
|
+
refresh();
|
|
16960
|
+
});
|
|
16961
|
+
strip.append(tab);
|
|
16962
|
+
});
|
|
16963
|
+
const absent = instances.findIndex((inst) => !present(inst));
|
|
16964
|
+
if (absent >= 0) {
|
|
16965
|
+
const add = document.createElement("button");
|
|
16966
|
+
add.type = "button";
|
|
16967
|
+
add.className = "vela-sd-itab vela-sd-itab-add";
|
|
16968
|
+
add.textContent = "+";
|
|
16969
|
+
add.title = `Add ${instances[absent].label}`;
|
|
16970
|
+
add.addEventListener("click", () => {
|
|
16971
|
+
this.typeActiveInstance.set(typeId, absent);
|
|
16972
|
+
put(instances[absent].enableKey, true);
|
|
16973
|
+
});
|
|
16974
|
+
strip.append(add);
|
|
16975
|
+
}
|
|
16976
|
+
contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
|
|
16977
|
+
};
|
|
16978
|
+
refreshers.push(refresh);
|
|
16979
|
+
return wrap;
|
|
16980
|
+
}
|
|
16981
|
+
/**
|
|
16982
|
+
* A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
|
|
16983
|
+
* ONE rows host holding every row — the active group's rows show, the rest hide.
|
|
16984
|
+
* Rows BEFORE the first heading are the always block, visible above every group.
|
|
16985
|
+
* `header` rows stay in the rows column as in-group subgroup titles (not TOC
|
|
16986
|
+
* entries). A group whose value rows are all gated out (or whose heading's own
|
|
16987
|
+
* `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
|
|
16988
|
+
* rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
|
|
16989
|
+
* across all groups.
|
|
16990
|
+
*
|
|
16991
|
+
* `enableKey` (optional): while that bag boolean is false, every row except the one
|
|
16992
|
+
* whose key matches is soft-disabled (visible but grayed / non-interactive) so the
|
|
16993
|
+
* pane stays browseable with the feature off.
|
|
16994
|
+
*/
|
|
16995
|
+
groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
|
|
16996
|
+
const wrap = document.createElement("div");
|
|
16997
|
+
wrap.className = "vela-sd-struct";
|
|
16998
|
+
const toc = document.createElement("div");
|
|
16999
|
+
toc.className = "vela-sd-toc";
|
|
17000
|
+
const rowsHost = document.createElement("div");
|
|
17001
|
+
rowsHost.dataset.sdRowsHost = "1";
|
|
17002
|
+
rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
17003
|
+
wrap.append(toc, rowsHost);
|
|
17004
|
+
const entries = [];
|
|
17005
|
+
const groups = [];
|
|
17006
|
+
const groupWhens = [];
|
|
17007
|
+
let g = -1;
|
|
17008
|
+
for (const r of rows) {
|
|
17009
|
+
if (r.kind === "heading") {
|
|
17010
|
+
g = groups.length;
|
|
17011
|
+
groups.push(r.label);
|
|
17012
|
+
groupWhens.push(r.when);
|
|
17013
|
+
continue;
|
|
17014
|
+
}
|
|
17015
|
+
if (r.kind === "header") {
|
|
17016
|
+
entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
|
|
17017
|
+
rowsHost.append(entries[entries.length - 1].el);
|
|
17018
|
+
continue;
|
|
17019
|
+
}
|
|
17020
|
+
const el = this.typeRow(r, bag, put);
|
|
17021
|
+
const key = r.kind === "range" ? void 0 : r.key;
|
|
17022
|
+
entries.push({ el, when: r.when, group: g, key });
|
|
17023
|
+
rowsHost.append(el);
|
|
17024
|
+
}
|
|
17025
|
+
const refresh = () => {
|
|
17026
|
+
const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
|
|
17027
|
+
let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
|
|
17028
|
+
if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
|
|
17029
|
+
if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
|
|
17030
|
+
toc.replaceChildren();
|
|
17031
|
+
groups.forEach((label, gi) => {
|
|
17032
|
+
if (!live[gi]) return;
|
|
17033
|
+
const b = document.createElement("button");
|
|
17034
|
+
b.type = "button";
|
|
17035
|
+
b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
|
|
17036
|
+
b.textContent = label;
|
|
17037
|
+
b.addEventListener("click", () => {
|
|
17038
|
+
this.typeActiveGroup.set(paneKey, label);
|
|
17039
|
+
refresh();
|
|
17040
|
+
});
|
|
17041
|
+
toc.append(b);
|
|
17042
|
+
});
|
|
17043
|
+
const anyGroup = live.some(Boolean);
|
|
17044
|
+
toc.classList.toggle("vela-sd-hide", !anyGroup);
|
|
17045
|
+
wrap.classList.toggle("no-toc", !anyGroup);
|
|
17046
|
+
const enabled = !enableKey || bag[enableKey] === true;
|
|
17047
|
+
for (let i = 0; i < entries.length; i++) {
|
|
17048
|
+
const e = entries[i];
|
|
17049
|
+
let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
|
|
17050
|
+
if (visible && e.header) {
|
|
17051
|
+
let hasContent = false;
|
|
17052
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
17053
|
+
const n = entries[j];
|
|
17054
|
+
if (n.group !== e.group || n.header) break;
|
|
17055
|
+
if (settingsRowVisible(n.when, bag)) {
|
|
17056
|
+
hasContent = true;
|
|
17057
|
+
break;
|
|
17058
|
+
}
|
|
17059
|
+
}
|
|
17060
|
+
visible = hasContent;
|
|
17061
|
+
}
|
|
17062
|
+
e.el.classList.toggle("vela-sd-hide", !visible);
|
|
17063
|
+
e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
|
|
17064
|
+
}
|
|
17065
|
+
};
|
|
17066
|
+
refreshers.push(refresh);
|
|
17067
|
+
return wrap;
|
|
17068
|
+
}
|
|
16599
17069
|
/** Vertical breathing space between row clusters — grouping reads from whitespace,
|
|
16600
17070
|
* so no drawn rule. */
|
|
16601
17071
|
separator() {
|
|
@@ -16858,7 +17328,7 @@ void main() {
|
|
|
16858
17328
|
const { wrap } = this.row(label);
|
|
16859
17329
|
const sel = document.createElement("select");
|
|
16860
17330
|
sel.className = "vela-sd-select";
|
|
16861
|
-
sel.style.cssText = "max-width:
|
|
17331
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
16862
17332
|
for (const [val, lbl] of options) {
|
|
16863
17333
|
const o = document.createElement("option");
|
|
16864
17334
|
o.value = val;
|
|
@@ -16874,7 +17344,7 @@ void main() {
|
|
|
16874
17344
|
const { wrap } = this.row(label);
|
|
16875
17345
|
const sel = document.createElement("select");
|
|
16876
17346
|
sel.className = "vela-sd-select";
|
|
16877
|
-
sel.style.cssText = "max-width:
|
|
17347
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
16878
17348
|
for (const opt of options) {
|
|
16879
17349
|
const o = document.createElement("option");
|
|
16880
17350
|
o.value = opt;
|
|
@@ -16911,6 +17381,9 @@ void main() {
|
|
|
16911
17381
|
}
|
|
16912
17382
|
};
|
|
16913
17383
|
var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
|
|
17384
|
+
function normalizeSelectOptions(options) {
|
|
17385
|
+
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
17386
|
+
}
|
|
16914
17387
|
function timezoneOptions(current) {
|
|
16915
17388
|
const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
|
|
16916
17389
|
const normalized = normalizeTimezone(current);
|
|
@@ -18269,6 +18742,8 @@ void main() {
|
|
|
18269
18742
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
18270
18743
|
var DRAG_SLOP2 = 3;
|
|
18271
18744
|
var FREEHAND_SAMPLE_PX = 4;
|
|
18745
|
+
var ANGLE_SNAP_STEP = Math.PI / 4;
|
|
18746
|
+
var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
|
|
18272
18747
|
var DrawingInteraction = class {
|
|
18273
18748
|
// magnet target for the ring
|
|
18274
18749
|
constructor(deps) {
|
|
@@ -18309,6 +18784,27 @@ void main() {
|
|
|
18309
18784
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
18310
18785
|
return snapped;
|
|
18311
18786
|
}
|
|
18787
|
+
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
18788
|
+
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
18789
|
+
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
18790
|
+
* the result to a candle afterwards would break the exact angle just enforced. */
|
|
18791
|
+
resolveAngleSnapped(pivot, paneId, x, y) {
|
|
18792
|
+
const proj = this.deps.projector();
|
|
18793
|
+
const px = proj.xOf(pivot.time);
|
|
18794
|
+
const py = proj.yOf(pivot.price, paneId);
|
|
18795
|
+
this.snapAt = null;
|
|
18796
|
+
if (py == null) return proj.pxToPoint(x, y, paneId);
|
|
18797
|
+
const r = Math.hypot(x - px, y - py);
|
|
18798
|
+
if (r < 1) return proj.pxToPoint(x, y, paneId);
|
|
18799
|
+
const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
|
|
18800
|
+
return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
|
|
18801
|
+
}
|
|
18802
|
+
/** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
|
|
18803
|
+
* the tool is not a two-point segment (or no anchor is committed yet). */
|
|
18804
|
+
placingPivot(draft) {
|
|
18805
|
+
if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
|
|
18806
|
+
return draft.anchors[draft.anchors.length - 1] ?? null;
|
|
18807
|
+
}
|
|
18312
18808
|
/** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
|
|
18313
18809
|
claim(x, y) {
|
|
18314
18810
|
if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
|
|
@@ -18330,12 +18826,15 @@ void main() {
|
|
|
18330
18826
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
18331
18827
|
}
|
|
18332
18828
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
18333
|
-
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging
|
|
18829
|
+
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
18830
|
+
* while placing a line tool it locks the segment angle to 45° steps. */
|
|
18334
18831
|
down(x, y, mode = "off", shift = false) {
|
|
18335
18832
|
const proj = this.deps.projector();
|
|
18336
18833
|
if (this.state.kind === "placing") {
|
|
18337
18834
|
if (this.state.draft.placementMode() !== "click") return;
|
|
18338
|
-
this.
|
|
18835
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
18836
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
18837
|
+
this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
|
|
18339
18838
|
if (this.state.draft.anchors.length >= this.state.need) this.finalize();
|
|
18340
18839
|
else this.deps.changed();
|
|
18341
18840
|
return;
|
|
@@ -18360,8 +18859,9 @@ void main() {
|
|
|
18360
18859
|
this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
|
|
18361
18860
|
}
|
|
18362
18861
|
}
|
|
18363
|
-
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
18364
|
-
|
|
18862
|
+
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
18863
|
+
* `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
|
|
18864
|
+
move(x, y, mode = "off", shift = false) {
|
|
18365
18865
|
if (this.state.kind === "idle") {
|
|
18366
18866
|
const tool = this.deps.activeTool();
|
|
18367
18867
|
const had = this.snapAt != null;
|
|
@@ -18375,7 +18875,8 @@ void main() {
|
|
|
18375
18875
|
this.captureFreehand(x, y);
|
|
18376
18876
|
return;
|
|
18377
18877
|
}
|
|
18378
|
-
|
|
18878
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
18879
|
+
this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
18379
18880
|
this.deps.changed();
|
|
18380
18881
|
return;
|
|
18381
18882
|
}
|
|
@@ -18386,7 +18887,11 @@ void main() {
|
|
|
18386
18887
|
this.state.moved = true;
|
|
18387
18888
|
if (this.state.handle >= 0) {
|
|
18388
18889
|
const a = d.anchors[this.state.handle];
|
|
18389
|
-
if (a)
|
|
18890
|
+
if (a) {
|
|
18891
|
+
const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
18892
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
|
|
18893
|
+
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
18894
|
+
}
|
|
18390
18895
|
d.constrainHandleDrag(this.state.handle);
|
|
18391
18896
|
} else {
|
|
18392
18897
|
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
@@ -19739,6 +20244,8 @@ void main() {
|
|
|
19739
20244
|
this.eraserActive = false;
|
|
19740
20245
|
this.stayBtn = null;
|
|
19741
20246
|
this.stayActive = false;
|
|
20247
|
+
this.syncBtn = null;
|
|
20248
|
+
this.syncActive = false;
|
|
19742
20249
|
this.collapseBtn = null;
|
|
19743
20250
|
this.collapsed = false;
|
|
19744
20251
|
this.visible = false;
|
|
@@ -19755,6 +20262,7 @@ void main() {
|
|
|
19755
20262
|
this.dock = options.dock ?? "absolute";
|
|
19756
20263
|
this.onCollapse = options.onCollapse ?? (() => {
|
|
19757
20264
|
});
|
|
20265
|
+
this.onDrawingsSync = options.onDrawingsSync ?? null;
|
|
19758
20266
|
ensureStyles4();
|
|
19759
20267
|
this.root = document.createElement("div");
|
|
19760
20268
|
this.root.className = "vela-dtb";
|
|
@@ -19853,6 +20361,10 @@ void main() {
|
|
|
19853
20361
|
this.root.appendChild(this.makeMagnetCell());
|
|
19854
20362
|
this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
|
|
19855
20363
|
this.root.appendChild(this.stayBtn);
|
|
20364
|
+
if (this.onDrawingsSync) {
|
|
20365
|
+
this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
|
|
20366
|
+
this.root.appendChild(this.syncBtn);
|
|
20367
|
+
}
|
|
19856
20368
|
this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
|
|
19857
20369
|
this.collapseBtn.classList.add("vela-dtb-collapse");
|
|
19858
20370
|
this.root.appendChild(this.collapseBtn);
|
|
@@ -19860,6 +20372,7 @@ void main() {
|
|
|
19860
20372
|
this.paintEraser();
|
|
19861
20373
|
this.paintMagnet();
|
|
19862
20374
|
this.paintStay();
|
|
20375
|
+
this.paintDrawingsSync();
|
|
19863
20376
|
this.paintCollapse();
|
|
19864
20377
|
this.highlight();
|
|
19865
20378
|
}
|
|
@@ -19910,7 +20423,7 @@ void main() {
|
|
|
19910
20423
|
cell.appendChild(arrow);
|
|
19911
20424
|
}
|
|
19912
20425
|
this.tipDisposers.push(
|
|
19913
|
-
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right"
|
|
20426
|
+
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
|
|
19914
20427
|
);
|
|
19915
20428
|
return { cell, icon: icon2, arrow };
|
|
19916
20429
|
}
|
|
@@ -20020,6 +20533,23 @@ void main() {
|
|
|
20020
20533
|
b.dataset.active = this.stayActive ? "1" : "";
|
|
20021
20534
|
b.style.opacity = this.stayActive ? "1" : "0.5";
|
|
20022
20535
|
}
|
|
20536
|
+
/** Toggle drawings sync on/off and notify the host. */
|
|
20537
|
+
toggleDrawingsSync() {
|
|
20538
|
+
this.syncActive = !this.syncActive;
|
|
20539
|
+
this.onDrawingsSync?.(this.syncActive);
|
|
20540
|
+
this.paintDrawingsSync();
|
|
20541
|
+
}
|
|
20542
|
+
/** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
|
|
20543
|
+
setDrawingsSyncMode(on) {
|
|
20544
|
+
this.syncActive = on;
|
|
20545
|
+
this.paintDrawingsSync();
|
|
20546
|
+
}
|
|
20547
|
+
paintDrawingsSync() {
|
|
20548
|
+
const b = this.syncBtn;
|
|
20549
|
+
if (!b) return;
|
|
20550
|
+
b.dataset.active = this.syncActive ? "1" : "";
|
|
20551
|
+
b.style.opacity = this.syncActive ? "1" : "0.5";
|
|
20552
|
+
}
|
|
20023
20553
|
// ── flyout ──
|
|
20024
20554
|
/** Create the positioned flyout panel anchored to a cell, register it as open, and start the
|
|
20025
20555
|
* outside-dismiss. Callers fill it with items. */
|
|
@@ -20179,6 +20709,8 @@ void main() {
|
|
|
20179
20709
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
20180
20710
|
const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
|
|
20181
20711
|
iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
|
|
20712
|
+
const cell = this.groupCells.get(group.id);
|
|
20713
|
+
if (cell) this.tipText.set(cell, tool?.label ?? group.label);
|
|
20182
20714
|
}
|
|
20183
20715
|
/** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
|
|
20184
20716
|
* `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
|
|
@@ -20202,7 +20734,7 @@ void main() {
|
|
|
20202
20734
|
this.tipText.set(btn, title);
|
|
20203
20735
|
if (icon2) btn.innerHTML = hitHtml(icon2);
|
|
20204
20736
|
this.tipDisposers.push(
|
|
20205
|
-
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right"
|
|
20737
|
+
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
|
|
20206
20738
|
);
|
|
20207
20739
|
btn.addEventListener("click", onClick);
|
|
20208
20740
|
return btn;
|
|
@@ -20268,6 +20800,7 @@ void main() {
|
|
|
20268
20800
|
var EXPAND_ICON = icon("chevrons-right");
|
|
20269
20801
|
var MAGNET_ICON = icon("magnet");
|
|
20270
20802
|
var STAY_ICON = icon("pen-lock");
|
|
20803
|
+
var SYNC_ICON = icon("pen-sync");
|
|
20271
20804
|
var ERASER_ICON = icon("eraser");
|
|
20272
20805
|
var ARROW_ICON2 = icon("chevron-right");
|
|
20273
20806
|
var CHECK_ICON = icon("check");
|
|
@@ -20491,6 +21024,10 @@ void main() {
|
|
|
20491
21024
|
this.activeTool = null;
|
|
20492
21025
|
// last-used style for the armed tool (seeds the placement ghost)
|
|
20493
21026
|
this.intentCb = null;
|
|
21027
|
+
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
21028
|
+
this.externalGhost = null;
|
|
21029
|
+
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
21030
|
+
this.lastDraftKey = null;
|
|
20494
21031
|
this.measure = new MeasureOverlay();
|
|
20495
21032
|
// transient ruler — not a persistent drawing
|
|
20496
21033
|
this.toolbarVisible = false;
|
|
@@ -20562,6 +21099,11 @@ void main() {
|
|
|
20562
21099
|
syncToolbarGutter() {
|
|
20563
21100
|
this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
20564
21101
|
}
|
|
21102
|
+
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
21103
|
+
setExternalGhost(doc) {
|
|
21104
|
+
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
21105
|
+
this.render();
|
|
21106
|
+
}
|
|
20565
21107
|
syncDrawings(docs) {
|
|
20566
21108
|
this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
|
|
20567
21109
|
if (this.textEditor && !this.editedDrawing(this.textEditor.id)) this.closeTextEditor();
|
|
@@ -20720,7 +21262,7 @@ void main() {
|
|
|
20720
21262
|
}
|
|
20721
21263
|
this.interaction.down(x, y, snap, shift);
|
|
20722
21264
|
}
|
|
20723
|
-
pointerMove(x, y, snap = "off") {
|
|
21265
|
+
pointerMove(x, y, snap = "off", shift = false) {
|
|
20724
21266
|
if (this.eraserMode) {
|
|
20725
21267
|
if (this.erasing) this.deleteAt(x, y);
|
|
20726
21268
|
return;
|
|
@@ -20730,7 +21272,7 @@ void main() {
|
|
|
20730
21272
|
this.render();
|
|
20731
21273
|
return;
|
|
20732
21274
|
}
|
|
20733
|
-
this.interaction.move(x, y, snap);
|
|
21275
|
+
this.interaction.move(x, y, snap, shift);
|
|
20734
21276
|
this.updateHover(x, y);
|
|
20735
21277
|
}
|
|
20736
21278
|
/** Track which drawing is hovered so its handles appear on hover (and only then). */
|
|
@@ -21116,6 +21658,8 @@ void main() {
|
|
|
21116
21658
|
this.layoutTextEditor();
|
|
21117
21659
|
const ghost = this.interaction.ghost();
|
|
21118
21660
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
21661
|
+
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
21662
|
+
this.emitDraft(ghost);
|
|
21119
21663
|
const markers = this.interaction.placingMarkers(proj);
|
|
21120
21664
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
21121
21665
|
const m = this.interaction.snapMarker();
|
|
@@ -21177,6 +21721,15 @@ void main() {
|
|
|
21177
21721
|
}
|
|
21178
21722
|
}, () => this.clearSelection());
|
|
21179
21723
|
}
|
|
21724
|
+
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
21725
|
+
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
21726
|
+
* shape changes emit, and the end of a placement emits one `null`. */
|
|
21727
|
+
emitDraft(ghost) {
|
|
21728
|
+
const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
|
|
21729
|
+
if (key === this.lastDraftKey) return;
|
|
21730
|
+
this.lastDraftKey = key;
|
|
21731
|
+
this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
|
|
21732
|
+
}
|
|
21180
21733
|
emit(i) {
|
|
21181
21734
|
if (i.kind === "create") {
|
|
21182
21735
|
if (i.doc.text && i.doc.text.color === void 0) {
|
|
@@ -21720,10 +22273,10 @@ void main() {
|
|
|
21720
22273
|
var NativeRenderer = class {
|
|
21721
22274
|
constructor(opts) {
|
|
21722
22275
|
this.capabilities = NATIVE_CAPABILITIES;
|
|
21723
|
-
// The app "chrome" surface (drawing toolbar +
|
|
22276
|
+
// The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
|
|
21724
22277
|
// Owned by the app theme alone (mount/setTheme) — config-level background/text edits
|
|
21725
|
-
// (settings dialog, persisted configs) recolor
|
|
21726
|
-
//
|
|
22278
|
+
// (settings dialog, persisted configs) recolor the plot AND the axis scales (see
|
|
22279
|
+
// axisSurface), but never the toolbar/dialog chrome.
|
|
21727
22280
|
this.surfaceBackground = DARK_THEME.background;
|
|
21728
22281
|
this.surfaceTextColor = DARK_THEME.textColor;
|
|
21729
22282
|
// the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
|
|
@@ -21923,6 +22476,7 @@ void main() {
|
|
|
21923
22476
|
this.candleDown = opts.downColor;
|
|
21924
22477
|
this.scene.priceStyle = opts.priceStyle;
|
|
21925
22478
|
this.scene.basePainting = basePaintingOf(opts.priceStyle);
|
|
22479
|
+
this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
|
|
21926
22480
|
}
|
|
21927
22481
|
this.theme = this.deriveTheme(DARK_THEME);
|
|
21928
22482
|
}
|
|
@@ -22400,6 +22954,7 @@ void main() {
|
|
|
22400
22954
|
baselineLevel: next.baseline.baselineLevel
|
|
22401
22955
|
};
|
|
22402
22956
|
this.setPriceStyle(next.series.style);
|
|
22957
|
+
this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
|
|
22403
22958
|
this.scene.baselineValue = next.series.baseline;
|
|
22404
22959
|
this.coords.setPitchScale(next.series.spacing);
|
|
22405
22960
|
this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
|
|
@@ -22505,7 +23060,7 @@ void main() {
|
|
|
22505
23060
|
(patch) => this.applyConfig(patch),
|
|
22506
23061
|
(json) => this.applyConfig(json),
|
|
22507
23062
|
() => {
|
|
22508
|
-
if (this.factoryConfig) this.applyConfig(this.factoryConfig);
|
|
23063
|
+
if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
|
|
22509
23064
|
this.settingsDialog?.close();
|
|
22510
23065
|
this.openSettingsDialog();
|
|
22511
23066
|
},
|
|
@@ -22706,10 +23261,16 @@ void main() {
|
|
|
22706
23261
|
borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
|
|
22707
23262
|
};
|
|
22708
23263
|
}
|
|
23264
|
+
/** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
|
|
23265
|
+
* (`layout.background`) and its contrast-corrected text, so the scales read as part of the
|
|
23266
|
+
* plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
|
|
23267
|
+
axisSurface() {
|
|
23268
|
+
return { background: this.theme.background, textColor: this.theme.textColor };
|
|
23269
|
+
}
|
|
22709
23270
|
// ── lifecycle ──
|
|
22710
23271
|
mount(container, theme) {
|
|
22711
23272
|
this.mountContainer = container;
|
|
22712
|
-
this.
|
|
23273
|
+
this.publishGutters();
|
|
22713
23274
|
this.theme = this.deriveTheme(theme);
|
|
22714
23275
|
this.surfaceBackground = theme.background;
|
|
22715
23276
|
this.surfaceTextColor = theme.textColor;
|
|
@@ -22781,7 +23342,7 @@ void main() {
|
|
|
22781
23342
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
22782
23343
|
drawingsSnapMode: () => this.snapMode,
|
|
22783
23344
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
22784
|
-
drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
|
|
23345
|
+
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
22785
23346
|
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
22786
23347
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
22787
23348
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -23018,6 +23579,7 @@ void main() {
|
|
|
23018
23579
|
this.attributionEl?.remove();
|
|
23019
23580
|
this.attributionEl = null;
|
|
23020
23581
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
23582
|
+
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
23021
23583
|
this.mountContainer = null;
|
|
23022
23584
|
this.wrapper?.remove();
|
|
23023
23585
|
}
|
|
@@ -23186,6 +23748,7 @@ void main() {
|
|
|
23186
23748
|
const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
|
|
23187
23749
|
if (w === this.rightAxisW) return false;
|
|
23188
23750
|
this.rightAxisW = w;
|
|
23751
|
+
this.publishGutters();
|
|
23189
23752
|
if (this.coords.width > 0) this.syncSize();
|
|
23190
23753
|
return true;
|
|
23191
23754
|
}
|
|
@@ -23345,6 +23908,7 @@ void main() {
|
|
|
23345
23908
|
if (style === this.scene.priceStyle) return;
|
|
23346
23909
|
this.scene.priceStyle = style;
|
|
23347
23910
|
this.scene.basePainting = basePaintingOf(style);
|
|
23911
|
+
this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
|
|
23348
23912
|
for (const cb of this.priceStyleCbs) cb(style);
|
|
23349
23913
|
}
|
|
23350
23914
|
onRemoveIndicator(cb) {
|
|
@@ -23506,7 +24070,10 @@ void main() {
|
|
|
23506
24070
|
active = true;
|
|
23507
24071
|
}
|
|
23508
24072
|
}
|
|
23509
|
-
this.
|
|
24073
|
+
const clamped = this.clampViewport(barSpacing, rightOffset);
|
|
24074
|
+
if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
|
|
24075
|
+
if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
|
|
24076
|
+
this.coords.setViewport(clamped);
|
|
23510
24077
|
this.computeScales();
|
|
23511
24078
|
if (this.easeScales(dtMs)) active = true;
|
|
23512
24079
|
if (this.easeLiveBar(dtMs)) active = true;
|
|
@@ -23958,9 +24525,37 @@ void main() {
|
|
|
23958
24525
|
this.paintData();
|
|
23959
24526
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
23960
24527
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
23961
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
24528
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
23962
24529
|
}
|
|
23963
24530
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
24531
|
+
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
24532
|
+
}
|
|
24533
|
+
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
24534
|
+
repaintCursorLayers() {
|
|
24535
|
+
const pane = this.scene.panes.get(PRICE_PANE_ID);
|
|
24536
|
+
if (!pane) return;
|
|
24537
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
24538
|
+
for (const l of this.extLayers) {
|
|
24539
|
+
if (!l.def.repaintOnCursor) continue;
|
|
24540
|
+
l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
|
|
24541
|
+
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
24542
|
+
}
|
|
24543
|
+
}
|
|
24544
|
+
/** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
|
|
24545
|
+
extLayerArgs(id, scale, bounds, nowMs) {
|
|
24546
|
+
return {
|
|
24547
|
+
bars: this.scene.bars,
|
|
24548
|
+
data: this.scene.nativeData.get(id),
|
|
24549
|
+
settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
|
|
24550
|
+
pending: this.scene.nativePending.get(id) ?? [],
|
|
24551
|
+
coords: this.coords,
|
|
24552
|
+
scale,
|
|
24553
|
+
bounds,
|
|
24554
|
+
theme: this.theme,
|
|
24555
|
+
priceStyle: this.scene.priceStyle,
|
|
24556
|
+
nowMs,
|
|
24557
|
+
cursor: this.lastPointer
|
|
24558
|
+
};
|
|
23964
24559
|
}
|
|
23965
24560
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
23966
24561
|
paintData() {
|
|
@@ -23993,18 +24588,16 @@ void main() {
|
|
|
23993
24588
|
});
|
|
23994
24589
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
23995
24590
|
for (const l of this.extLayers) {
|
|
23996
|
-
l.
|
|
23997
|
-
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
nowMs
|
|
24007
|
-
});
|
|
24591
|
+
const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
|
|
24592
|
+
l.instance.render(args);
|
|
24593
|
+
if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
|
|
24594
|
+
const mod = l.instance.modulateBase(args);
|
|
24595
|
+
if (mod) {
|
|
24596
|
+
if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
|
|
24597
|
+
if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
|
|
24598
|
+
if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
|
|
24599
|
+
}
|
|
24600
|
+
}
|
|
24008
24601
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
24009
24602
|
}
|
|
24010
24603
|
}
|
|
@@ -24014,7 +24607,7 @@ void main() {
|
|
|
24014
24607
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
24015
24608
|
this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
|
|
24016
24609
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
24017
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
24610
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
24018
24611
|
this.userDrawings?.render();
|
|
24019
24612
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
24020
24613
|
this.paintedData = true;
|
|
@@ -24448,15 +25041,18 @@ void main() {
|
|
|
24448
25041
|
setToolbarGutter(px) {
|
|
24449
25042
|
if (px === this.toolbarGutter) return;
|
|
24450
25043
|
this.toolbarGutter = px;
|
|
24451
|
-
this.
|
|
25044
|
+
this.publishGutters();
|
|
24452
25045
|
this.positionAttribution();
|
|
24453
25046
|
this.syncSize();
|
|
24454
25047
|
}
|
|
24455
|
-
/** Publish the
|
|
24456
|
-
*
|
|
24457
|
-
*
|
|
24458
|
-
|
|
25048
|
+
/** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
|
|
25049
|
+
* drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
|
|
25050
|
+
* incl. merged own-scale columns), so host overlays sharing that container (a
|
|
25051
|
+
* status line, a watermark, a custom legend) can anchor to the plot's edges
|
|
25052
|
+
* without reaching into the renderer's DOM. */
|
|
25053
|
+
publishGutters() {
|
|
24459
25054
|
this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
|
|
25055
|
+
this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
|
|
24460
25056
|
}
|
|
24461
25057
|
/** The built-in mark, or the host's own when one is set. */
|
|
24462
25058
|
buildAttributionEl() {
|
|
@@ -24526,7 +25122,13 @@ void main() {
|
|
|
24526
25122
|
this.fitContent();
|
|
24527
25123
|
this.didInitialFit = true;
|
|
24528
25124
|
}
|
|
24529
|
-
this.
|
|
25125
|
+
if (this.animator.active) {
|
|
25126
|
+
this.computeScales();
|
|
25127
|
+
this.paintData();
|
|
25128
|
+
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
25129
|
+
} else {
|
|
25130
|
+
this.scheduler.flushNow(4 /* Full */);
|
|
25131
|
+
}
|
|
24530
25132
|
}
|
|
24531
25133
|
applyBackground() {
|
|
24532
25134
|
this.wrapper.style.background = this.theme.background;
|
|
@@ -24595,6 +25197,9 @@ void main() {
|
|
|
24595
25197
|
}
|
|
24596
25198
|
return out.sort((a, b) => a.from - b.from);
|
|
24597
25199
|
}
|
|
25200
|
+
function clamp012(v) {
|
|
25201
|
+
return Math.max(0, Math.min(1, v));
|
|
25202
|
+
}
|
|
24598
25203
|
function isPriceStyle2(v) {
|
|
24599
25204
|
return typeof v === "string" && priceStyleIds().includes(v);
|
|
24600
25205
|
}
|