@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-
|
|
2
|
-
import { themeTokens, STATIC_TOKENS } from './chunk-
|
|
3
|
-
import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-
|
|
1
|
+
import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-S6F6SQ2L.js';
|
|
2
|
+
import { themeTokens, STATIC_TOKENS } from './chunk-ULIX6AE2.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-EP6NDJYK.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -1300,6 +1300,14 @@ var DrawingsControl = class {
|
|
|
1300
1300
|
if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
|
|
1301
1301
|
return this;
|
|
1302
1302
|
}
|
|
1303
|
+
/** Display another chart's in-progress placement as a GHOST at reduced opacity
|
|
1304
|
+
* (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
|
|
1305
|
+
* charts. Never a real drawing: no store entry, no selection, no persistence.
|
|
1306
|
+
* Silently inert on a renderer without the optional seam. */
|
|
1307
|
+
setExternalGhost(doc) {
|
|
1308
|
+
this.ctrl.setExternalGhost(doc);
|
|
1309
|
+
return this;
|
|
1310
|
+
}
|
|
1303
1311
|
/** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
|
|
1304
1312
|
add(type, init = {}) {
|
|
1305
1313
|
if (!this.ok("add")) return null;
|
|
@@ -1727,6 +1735,11 @@ var DrawingController = class {
|
|
|
1727
1735
|
setToolbar(option) {
|
|
1728
1736
|
this.port?.setToolbar(buildToolbar(option).definition);
|
|
1729
1737
|
}
|
|
1738
|
+
/** Display (or clear) another chart's in-progress placement as a ghost — silently
|
|
1739
|
+
* inert on a renderer without the optional `setExternalGhost` seam. */
|
|
1740
|
+
setExternalGhost(doc) {
|
|
1741
|
+
this.port?.setExternalGhost?.(doc);
|
|
1742
|
+
}
|
|
1730
1743
|
/** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
|
|
1731
1744
|
setToolShortcuts(map) {
|
|
1732
1745
|
this.port?.setToolShortcuts?.(map);
|
|
@@ -1957,6 +1970,9 @@ var DrawingController = class {
|
|
|
1957
1970
|
case "arm":
|
|
1958
1971
|
this.setTool(i.type);
|
|
1959
1972
|
break;
|
|
1973
|
+
case "draft":
|
|
1974
|
+
this.events.emit("drawing:draft", { doc: i.doc });
|
|
1975
|
+
break;
|
|
1960
1976
|
case "create": {
|
|
1961
1977
|
const before = this.store.serialize();
|
|
1962
1978
|
const last = this.lastStyle.get(i.doc.type);
|
|
@@ -2150,6 +2166,8 @@ var DrawingToolbar = class {
|
|
|
2150
2166
|
this.eraserActive = false;
|
|
2151
2167
|
this.stayBtn = null;
|
|
2152
2168
|
this.stayActive = false;
|
|
2169
|
+
this.syncBtn = null;
|
|
2170
|
+
this.syncActive = false;
|
|
2153
2171
|
this.collapseBtn = null;
|
|
2154
2172
|
this.collapsed = false;
|
|
2155
2173
|
this.visible = false;
|
|
@@ -2166,6 +2184,7 @@ var DrawingToolbar = class {
|
|
|
2166
2184
|
this.dock = options.dock ?? "absolute";
|
|
2167
2185
|
this.onCollapse = options.onCollapse ?? (() => {
|
|
2168
2186
|
});
|
|
2187
|
+
this.onDrawingsSync = options.onDrawingsSync ?? null;
|
|
2169
2188
|
ensureStyles();
|
|
2170
2189
|
this.root = document.createElement("div");
|
|
2171
2190
|
this.root.className = "vela-dtb";
|
|
@@ -2264,6 +2283,10 @@ var DrawingToolbar = class {
|
|
|
2264
2283
|
this.root.appendChild(this.makeMagnetCell());
|
|
2265
2284
|
this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
|
|
2266
2285
|
this.root.appendChild(this.stayBtn);
|
|
2286
|
+
if (this.onDrawingsSync) {
|
|
2287
|
+
this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
|
|
2288
|
+
this.root.appendChild(this.syncBtn);
|
|
2289
|
+
}
|
|
2267
2290
|
this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
|
|
2268
2291
|
this.collapseBtn.classList.add("vela-dtb-collapse");
|
|
2269
2292
|
this.root.appendChild(this.collapseBtn);
|
|
@@ -2271,6 +2294,7 @@ var DrawingToolbar = class {
|
|
|
2271
2294
|
this.paintEraser();
|
|
2272
2295
|
this.paintMagnet();
|
|
2273
2296
|
this.paintStay();
|
|
2297
|
+
this.paintDrawingsSync();
|
|
2274
2298
|
this.paintCollapse();
|
|
2275
2299
|
this.highlight();
|
|
2276
2300
|
}
|
|
@@ -2321,7 +2345,7 @@ var DrawingToolbar = class {
|
|
|
2321
2345
|
cell.appendChild(arrow);
|
|
2322
2346
|
}
|
|
2323
2347
|
this.tipDisposers.push(
|
|
2324
|
-
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right"
|
|
2348
|
+
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
|
|
2325
2349
|
);
|
|
2326
2350
|
return { cell, icon: icon2, arrow };
|
|
2327
2351
|
}
|
|
@@ -2431,6 +2455,23 @@ var DrawingToolbar = class {
|
|
|
2431
2455
|
b.dataset.active = this.stayActive ? "1" : "";
|
|
2432
2456
|
b.style.opacity = this.stayActive ? "1" : "0.5";
|
|
2433
2457
|
}
|
|
2458
|
+
/** Toggle drawings sync on/off and notify the host. */
|
|
2459
|
+
toggleDrawingsSync() {
|
|
2460
|
+
this.syncActive = !this.syncActive;
|
|
2461
|
+
this.onDrawingsSync?.(this.syncActive);
|
|
2462
|
+
this.paintDrawingsSync();
|
|
2463
|
+
}
|
|
2464
|
+
/** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
|
|
2465
|
+
setDrawingsSyncMode(on) {
|
|
2466
|
+
this.syncActive = on;
|
|
2467
|
+
this.paintDrawingsSync();
|
|
2468
|
+
}
|
|
2469
|
+
paintDrawingsSync() {
|
|
2470
|
+
const b = this.syncBtn;
|
|
2471
|
+
if (!b) return;
|
|
2472
|
+
b.dataset.active = this.syncActive ? "1" : "";
|
|
2473
|
+
b.style.opacity = this.syncActive ? "1" : "0.5";
|
|
2474
|
+
}
|
|
2434
2475
|
// ── flyout ──
|
|
2435
2476
|
/** Create the positioned flyout panel anchored to a cell, register it as open, and start the
|
|
2436
2477
|
* outside-dismiss. Callers fill it with items. */
|
|
@@ -2590,6 +2631,8 @@ var DrawingToolbar = class {
|
|
|
2590
2631
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
2591
2632
|
const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
|
|
2592
2633
|
iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
|
|
2634
|
+
const cell = this.groupCells.get(group.id);
|
|
2635
|
+
if (cell) this.tipText.set(cell, tool?.label ?? group.label);
|
|
2593
2636
|
}
|
|
2594
2637
|
/** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
|
|
2595
2638
|
* `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
|
|
@@ -2613,7 +2656,7 @@ var DrawingToolbar = class {
|
|
|
2613
2656
|
this.tipText.set(btn, title);
|
|
2614
2657
|
if (icon2) btn.innerHTML = hitHtml(icon2);
|
|
2615
2658
|
this.tipDisposers.push(
|
|
2616
|
-
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right"
|
|
2659
|
+
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
|
|
2617
2660
|
);
|
|
2618
2661
|
btn.addEventListener("click", onClick);
|
|
2619
2662
|
return btn;
|
|
@@ -2679,6 +2722,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
|
|
|
2679
2722
|
var EXPAND_ICON = icon("chevrons-right");
|
|
2680
2723
|
var MAGNET_ICON = icon("magnet");
|
|
2681
2724
|
var STAY_ICON = icon("pen-lock");
|
|
2725
|
+
var SYNC_ICON = icon("pen-sync");
|
|
2682
2726
|
var ERASER_ICON = icon("eraser");
|
|
2683
2727
|
var ARROW_ICON = icon("chevron-right");
|
|
2684
2728
|
var CHECK_ICON = icon("check");
|
|
@@ -4470,6 +4514,44 @@ function basePaintingOf(style) {
|
|
|
4470
4514
|
for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
|
|
4471
4515
|
return "candles";
|
|
4472
4516
|
}
|
|
4517
|
+
function hasOwnCandlePaint(style) {
|
|
4518
|
+
if (BUILTIN_PRICE_STYLES.includes(style)) return false;
|
|
4519
|
+
for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
|
|
4520
|
+
return false;
|
|
4521
|
+
}
|
|
4522
|
+
function candleOverrideFor(style, bags) {
|
|
4523
|
+
if (!hasOwnCandlePaint(style)) return null;
|
|
4524
|
+
const bag = bags[style] ?? {};
|
|
4525
|
+
const color = (v) => isColor(v) ? v : null;
|
|
4526
|
+
const bool = (v) => isBool(v) ? v : null;
|
|
4527
|
+
return {
|
|
4528
|
+
upColor: color(bag.candleUpColor),
|
|
4529
|
+
downColor: color(bag.candleDownColor),
|
|
4530
|
+
bodyVisible: bool(bag.candleBodyVisible),
|
|
4531
|
+
borderVisible: bool(bag.candleBorderVisible),
|
|
4532
|
+
borderUpColor: color(bag.candleBorderUpColor),
|
|
4533
|
+
borderDownColor: color(bag.candleBorderDownColor),
|
|
4534
|
+
wickVisible: bool(bag.candleWickVisible),
|
|
4535
|
+
wickUpColor: color(bag.candleWickUpColor),
|
|
4536
|
+
wickDownColor: color(bag.candleWickDownColor)
|
|
4537
|
+
};
|
|
4538
|
+
}
|
|
4539
|
+
function effectiveCandlePaint(base, override, themeUp, themeDown) {
|
|
4540
|
+
if (!override) return { up: themeUp, down: themeDown, candle: base };
|
|
4541
|
+
return {
|
|
4542
|
+
up: override.upColor ?? themeUp,
|
|
4543
|
+
down: override.downColor ?? themeDown,
|
|
4544
|
+
candle: {
|
|
4545
|
+
bodyVisible: override.bodyVisible ?? base.bodyVisible,
|
|
4546
|
+
borderVisible: override.borderVisible ?? base.borderVisible,
|
|
4547
|
+
borderUpColor: override.borderUpColor ?? base.borderUpColor,
|
|
4548
|
+
borderDownColor: override.borderDownColor ?? base.borderDownColor,
|
|
4549
|
+
wickVisible: override.wickVisible ?? base.wickVisible,
|
|
4550
|
+
wickUpColor: override.wickUpColor ?? base.wickUpColor,
|
|
4551
|
+
wickDownColor: override.wickDownColor ?? base.wickDownColor
|
|
4552
|
+
}
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4473
4555
|
function priceStyleIds() {
|
|
4474
4556
|
const out = [...BUILTIN_PRICE_STYLES];
|
|
4475
4557
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
@@ -4508,6 +4590,40 @@ function clampLevel(v) {
|
|
|
4508
4590
|
function clampSpacing(v) {
|
|
4509
4591
|
return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
|
|
4510
4592
|
}
|
|
4593
|
+
function factoryResetConfig(factory) {
|
|
4594
|
+
const bag = {};
|
|
4595
|
+
for (const t of chartTypes()) {
|
|
4596
|
+
const section = t.settings;
|
|
4597
|
+
if (!section) continue;
|
|
4598
|
+
const defaults = {};
|
|
4599
|
+
const addRows = (rows) => {
|
|
4600
|
+
for (const r of rows ?? []) {
|
|
4601
|
+
if (r.kind === "heading" || r.kind === "header") continue;
|
|
4602
|
+
if (r.kind === "range") {
|
|
4603
|
+
defaults[r.minKey] = r.defval;
|
|
4604
|
+
defaults[r.maxKey] = r.defval;
|
|
4605
|
+
continue;
|
|
4606
|
+
}
|
|
4607
|
+
defaults[r.key] = r.defval;
|
|
4608
|
+
if (r.kind === "toggle") for (const c of r.colors ?? []) defaults[c.key] = c.defval;
|
|
4609
|
+
}
|
|
4610
|
+
};
|
|
4611
|
+
for (const inst of section.instances ?? []) {
|
|
4612
|
+
if (inst.enableKey) defaults[inst.enableKey] = false;
|
|
4613
|
+
addRows(inst.rows);
|
|
4614
|
+
}
|
|
4615
|
+
for (const sub of section.subsections ?? []) {
|
|
4616
|
+
if (sub.enableKey) defaults[sub.enableKey] = false;
|
|
4617
|
+
addRows(sub.rows);
|
|
4618
|
+
}
|
|
4619
|
+
if (!section.instances) addRows(section.rows);
|
|
4620
|
+
bag[t.id] = defaults;
|
|
4621
|
+
}
|
|
4622
|
+
for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
|
|
4623
|
+
bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
|
|
4624
|
+
}
|
|
4625
|
+
return { ...factory, chartTypes: bag };
|
|
4626
|
+
}
|
|
4511
4627
|
function mergeConfig(base, patch) {
|
|
4512
4628
|
const p = asObject(patch);
|
|
4513
4629
|
const ctPatch = asObject(p.chartTypes);
|
|
@@ -4822,6 +4938,9 @@ var SceneGraph = class {
|
|
|
4822
4938
|
this.priceStyle = "candles";
|
|
4823
4939
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
4824
4940
|
this.basePainting = "candles";
|
|
4941
|
+
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
4942
|
+
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
4943
|
+
this.candleOverride = null;
|
|
4825
4944
|
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
4826
4945
|
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
4827
4946
|
* range (resolved per frame via `baselinePriceFor`). */
|
|
@@ -6051,14 +6170,15 @@ var WebGL2Backend = class {
|
|
|
6051
6170
|
emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
6052
6171
|
const spacing = coords.bodySpacing();
|
|
6053
6172
|
const tier = candleTier(spacing);
|
|
6173
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
6054
6174
|
if (tier === "aggregate") {
|
|
6055
|
-
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane,
|
|
6175
|
+
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
6056
6176
|
return;
|
|
6057
6177
|
}
|
|
6058
6178
|
const drawBody = tier === "full";
|
|
6059
|
-
const up =
|
|
6060
|
-
const down =
|
|
6061
|
-
const cs =
|
|
6179
|
+
const up = paint.up;
|
|
6180
|
+
const down = paint.down;
|
|
6181
|
+
const cs = paint.candle;
|
|
6062
6182
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
6063
6183
|
for (let i = i0; i <= i1; i += 1) {
|
|
6064
6184
|
const bar = bars[i];
|
|
@@ -6650,7 +6770,6 @@ function effectiveSnapMode(momentaryOverride, sticky) {
|
|
|
6650
6770
|
return momentaryOverride ? "strong" : sticky;
|
|
6651
6771
|
}
|
|
6652
6772
|
var InputController = class {
|
|
6653
|
-
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
6654
6773
|
constructor(deps) {
|
|
6655
6774
|
this.deps = deps;
|
|
6656
6775
|
/** When true (the default), wheel-zoom pins the right edge instead of the bar
|
|
@@ -6678,9 +6797,23 @@ var InputController = class {
|
|
|
6678
6797
|
this.vx = 0;
|
|
6679
6798
|
// smoothed pointer velocity, px/ms
|
|
6680
6799
|
this.middleDeleted = false;
|
|
6800
|
+
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
6801
|
+
// Last cursor position over the element (NaN once it leaves) — lets a modifier
|
|
6802
|
+
// press/release re-shape the drawings preview with the pointer stationary.
|
|
6803
|
+
this.cursorX = NaN;
|
|
6804
|
+
this.cursorY = NaN;
|
|
6681
6805
|
this.onMiddleGuard = (e) => {
|
|
6682
6806
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
6683
6807
|
};
|
|
6808
|
+
/** A modifier press/release with the pointer stationary: re-issue the last cursor
|
|
6809
|
+
* position so the placing ghost / drag preview reflects the new state immediately
|
|
6810
|
+
* (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
|
|
6811
|
+
this.onModifier = (e) => {
|
|
6812
|
+
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
6813
|
+
if (Number.isNaN(this.cursorX)) return;
|
|
6814
|
+
if (this.dragging && this.region !== "drawing") return;
|
|
6815
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
6816
|
+
};
|
|
6684
6817
|
this.onDown = (e) => {
|
|
6685
6818
|
if (e.button === 1) {
|
|
6686
6819
|
const { x: x2, y: y2 } = this.local(e);
|
|
@@ -6722,6 +6855,8 @@ var InputController = class {
|
|
|
6722
6855
|
};
|
|
6723
6856
|
this.onMove = (e) => {
|
|
6724
6857
|
const { x, y } = this.local(e);
|
|
6858
|
+
this.cursorX = x;
|
|
6859
|
+
this.cursorY = y;
|
|
6725
6860
|
if (this.dragging) {
|
|
6726
6861
|
if (this.region === "price") {
|
|
6727
6862
|
if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
|
|
@@ -6735,7 +6870,7 @@ var InputController = class {
|
|
|
6735
6870
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
6736
6871
|
} else if (this.region === "drawing") {
|
|
6737
6872
|
if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
|
|
6738
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
6873
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6739
6874
|
} else {
|
|
6740
6875
|
const coords = this.deps.getCoords();
|
|
6741
6876
|
const vp = coords.getViewport();
|
|
@@ -6758,7 +6893,7 @@ var InputController = class {
|
|
|
6758
6893
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
6759
6894
|
const r = this.regionAt(x, y);
|
|
6760
6895
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
6761
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
6896
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6762
6897
|
}
|
|
6763
6898
|
this.deps.onPointerMove(x, y);
|
|
6764
6899
|
};
|
|
@@ -6783,6 +6918,8 @@ var InputController = class {
|
|
|
6783
6918
|
}
|
|
6784
6919
|
};
|
|
6785
6920
|
this.onLeave = () => {
|
|
6921
|
+
this.cursorX = NaN;
|
|
6922
|
+
this.cursorY = NaN;
|
|
6786
6923
|
this.deps.onPointerMove(null, null);
|
|
6787
6924
|
};
|
|
6788
6925
|
this.onDblClick = (e) => {
|
|
@@ -6821,10 +6958,16 @@ var InputController = class {
|
|
|
6821
6958
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
6822
6959
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
6823
6960
|
el.addEventListener("auxclick", this.onMiddleGuard);
|
|
6961
|
+
const win = el.ownerDocument?.defaultView;
|
|
6962
|
+
win?.addEventListener("keydown", this.onModifier);
|
|
6963
|
+
win?.addEventListener("keyup", this.onModifier);
|
|
6824
6964
|
}
|
|
6825
6965
|
detach() {
|
|
6826
6966
|
const el = this.el;
|
|
6827
6967
|
if (!el) return;
|
|
6968
|
+
const win = el.ownerDocument?.defaultView;
|
|
6969
|
+
win?.removeEventListener("keydown", this.onModifier);
|
|
6970
|
+
win?.removeEventListener("keyup", this.onModifier);
|
|
6828
6971
|
el.removeEventListener("pointerdown", this.onDown);
|
|
6829
6972
|
el.removeEventListener("pointermove", this.onMove);
|
|
6830
6973
|
el.removeEventListener("pointerup", this.onUp);
|
|
@@ -7242,12 +7385,13 @@ var Canvas2dBackend = class {
|
|
|
7242
7385
|
drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
7243
7386
|
const spacing = coords.bodySpacing();
|
|
7244
7387
|
const tier = candleTier(spacing);
|
|
7388
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
7245
7389
|
if (tier === "aggregate") {
|
|
7246
|
-
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane,
|
|
7390
|
+
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
7247
7391
|
return;
|
|
7248
7392
|
}
|
|
7249
7393
|
const drawBody = tier === "full";
|
|
7250
|
-
const cs =
|
|
7394
|
+
const cs = paint.candle;
|
|
7251
7395
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
7252
7396
|
for (let i = i0; i <= i1; i += 1) {
|
|
7253
7397
|
const b = bars[i];
|
|
@@ -7255,7 +7399,7 @@ var Canvas2dBackend = class {
|
|
|
7255
7399
|
const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
|
|
7256
7400
|
const x = g.center;
|
|
7257
7401
|
const up = b.close >= b.open;
|
|
7258
|
-
const dir = up ?
|
|
7402
|
+
const dir = up ? paint.up : paint.down;
|
|
7259
7403
|
const bc = barColors.get(b.time);
|
|
7260
7404
|
const color = bc ?? dir;
|
|
7261
7405
|
let top = 0;
|
|
@@ -8366,7 +8510,7 @@ var ChromeRenderer = class {
|
|
|
8366
8510
|
constructor() {
|
|
8367
8511
|
this.canvas = null;
|
|
8368
8512
|
this.ctx = null;
|
|
8369
|
-
// The color for axis tick labels — the
|
|
8513
|
+
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
8370
8514
|
this.axisTextColor = DARK_THEME.textColor;
|
|
8371
8515
|
// Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
|
|
8372
8516
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
@@ -8398,8 +8542,8 @@ var ChromeRenderer = class {
|
|
|
8398
8542
|
return dr;
|
|
8399
8543
|
}
|
|
8400
8544
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
8401
|
-
* `surface` (background + text) paints the axis-scale gutters
|
|
8402
|
-
*
|
|
8545
|
+
* `surface` (background + text) paints the axis-scale gutters — the host passes the live
|
|
8546
|
+
* chart background so the scales read as part of the plot, with contrast-corrected text.
|
|
8403
8547
|
* Falls back to the theme's own colors when no surface is supplied. */
|
|
8404
8548
|
render(scene, coords, theme, surface) {
|
|
8405
8549
|
const ctx = this.ctx;
|
|
@@ -9166,6 +9310,37 @@ function ensureControlStyles() {
|
|
|
9166
9310
|
.vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
|
|
9167
9311
|
.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;}
|
|
9168
9312
|
.vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9313
|
+
/* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
|
|
9314
|
+
!important: the pane grid rewrites inline display ('contents') AFTER the initial
|
|
9315
|
+
visibility pass, so a class must win. */
|
|
9316
|
+
.vela-sd-hide{display:none !important;}
|
|
9317
|
+
/* Indented rail sub-entry (a chart-type section's subsection tab). */
|
|
9318
|
+
.vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
|
|
9319
|
+
/* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
|
|
9320
|
+
and a dashed + that turns on the next absent instance. The rule under it separates
|
|
9321
|
+
the strip from the instance's TOC + rows area. */
|
|
9322
|
+
.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);}
|
|
9323
|
+
.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;}
|
|
9324
|
+
.vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9325
|
+
.vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
|
|
9326
|
+
.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;}
|
|
9327
|
+
.vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9328
|
+
.vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
|
|
9329
|
+
/* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
|
|
9330
|
+
lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
|
|
9331
|
+
vertical rule sits on the rows column so it spans the full content height. When every
|
|
9332
|
+
group gates out the TOC hides and .no-toc drops the rule with it. */
|
|
9333
|
+
.vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
|
|
9334
|
+
.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;}
|
|
9335
|
+
.vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
|
|
9336
|
+
.vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
|
|
9337
|
+
.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;}
|
|
9338
|
+
.vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9339
|
+
.vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
9340
|
+
/* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
|
|
9341
|
+
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
9342
|
+
!important beats the inline opacity on labels. */
|
|
9343
|
+
.vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
|
|
9169
9344
|
`;
|
|
9170
9345
|
document.head.appendChild(st);
|
|
9171
9346
|
}
|
|
@@ -9186,6 +9361,10 @@ var SettingsDialog = class {
|
|
|
9186
9361
|
this.themeControl = null;
|
|
9187
9362
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
9188
9363
|
this.tabs = [];
|
|
9364
|
+
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
9365
|
+
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
9366
|
+
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
9367
|
+
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
9189
9368
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
9190
9369
|
this.activeSection = null;
|
|
9191
9370
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
@@ -9347,6 +9526,29 @@ var SettingsDialog = class {
|
|
|
9347
9526
|
baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
|
|
9348
9527
|
groups.baseline = baseline;
|
|
9349
9528
|
body.append(baseline);
|
|
9529
|
+
for (const def of chartTypes()) {
|
|
9530
|
+
if (!hasOwnCandlePaint(def.id)) continue;
|
|
9531
|
+
const bag = config.chartTypes[def.id] ?? {};
|
|
9532
|
+
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
9533
|
+
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
9534
|
+
const g = this.group();
|
|
9535
|
+
g.append(this.sectionTitle("Candles"));
|
|
9536
|
+
g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
9537
|
+
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
9538
|
+
this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
|
|
9539
|
+
]));
|
|
9540
|
+
g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
9541
|
+
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
9542
|
+
this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
|
|
9543
|
+
]));
|
|
9544
|
+
g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
9545
|
+
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
9546
|
+
this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
|
|
9547
|
+
]));
|
|
9548
|
+
g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
9549
|
+
groups[def.id] = g;
|
|
9550
|
+
body.append(g);
|
|
9551
|
+
}
|
|
9350
9552
|
showActive(config.series.style);
|
|
9351
9553
|
body.append(this.sectionTitle("Time zone"));
|
|
9352
9554
|
body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
|
|
@@ -9361,7 +9563,16 @@ var SettingsDialog = class {
|
|
|
9361
9563
|
}
|
|
9362
9564
|
}
|
|
9363
9565
|
};
|
|
9566
|
+
const renderChartTypeSections = (placement) => {
|
|
9567
|
+
for (const def of chartTypes()) {
|
|
9568
|
+
const typeSettings = def.settings;
|
|
9569
|
+
if (!typeSettings) continue;
|
|
9570
|
+
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
9571
|
+
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
9572
|
+
}
|
|
9573
|
+
};
|
|
9364
9574
|
renderHostSections("symbol");
|
|
9575
|
+
renderChartTypeSections("after-symbol");
|
|
9365
9576
|
renderHostSections("after-symbol");
|
|
9366
9577
|
body.append(this.section("Scales and lines"));
|
|
9367
9578
|
body.append(this.sectionTitle("Price scale"));
|
|
@@ -9402,27 +9613,7 @@ var SettingsDialog = class {
|
|
|
9402
9613
|
body.append(this.sectionTitle("Theme"));
|
|
9403
9614
|
body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
|
|
9404
9615
|
}
|
|
9405
|
-
|
|
9406
|
-
const typeSettings = def.settings;
|
|
9407
|
-
if (!typeSettings) continue;
|
|
9408
|
-
const marker = this.section(typeSettings.title);
|
|
9409
|
-
marker.dataset.sdStyle = def.id;
|
|
9410
|
-
marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
|
|
9411
|
-
body.append(marker);
|
|
9412
|
-
const values = config.chartTypes[def.id] ?? {};
|
|
9413
|
-
for (const r of typeSettings.rows) {
|
|
9414
|
-
const current2 = values[r.key];
|
|
9415
|
-
if (r.kind === "toggle") {
|
|
9416
|
-
body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
|
|
9417
|
-
} else if (r.kind === "number") {
|
|
9418
|
-
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)));
|
|
9419
|
-
} else if (r.kind === "color") {
|
|
9420
|
-
body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
|
|
9421
|
-
} else {
|
|
9422
|
-
body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
|
|
9423
|
-
}
|
|
9424
|
-
}
|
|
9425
|
-
}
|
|
9616
|
+
renderChartTypeSections("end");
|
|
9426
9617
|
renderHostSections("end");
|
|
9427
9618
|
const shell = document.createElement("div");
|
|
9428
9619
|
shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
@@ -9441,7 +9632,7 @@ var SettingsDialog = class {
|
|
|
9441
9632
|
const tab = document.createElement("button");
|
|
9442
9633
|
tab.type = "button";
|
|
9443
9634
|
tab.textContent = title;
|
|
9444
|
-
tab.className = "vela-sd-tab";
|
|
9635
|
+
tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
|
|
9445
9636
|
panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
|
|
9446
9637
|
current = el;
|
|
9447
9638
|
child.remove();
|
|
@@ -9481,7 +9672,14 @@ var SettingsDialog = class {
|
|
|
9481
9672
|
scrim.appendChild(dlg);
|
|
9482
9673
|
this.container.appendChild(scrim);
|
|
9483
9674
|
this.root = scrim;
|
|
9484
|
-
for (const p of panes)
|
|
9675
|
+
for (const p of panes) {
|
|
9676
|
+
const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
|
|
9677
|
+
if (hosts.length === 0) {
|
|
9678
|
+
this.layoutSettingsGrids(p.el, dlg);
|
|
9679
|
+
continue;
|
|
9680
|
+
}
|
|
9681
|
+
for (const h of hosts) this.layoutSettingsGrids(h, dlg);
|
|
9682
|
+
}
|
|
9485
9683
|
}
|
|
9486
9684
|
close() {
|
|
9487
9685
|
closeColorPopover();
|
|
@@ -9511,6 +9709,291 @@ var SettingsDialog = class {
|
|
|
9511
9709
|
el.textContent = text;
|
|
9512
9710
|
return el;
|
|
9513
9711
|
}
|
|
9712
|
+
/**
|
|
9713
|
+
* One chart type's settings tab (and its subsection tabs), appended to the linear
|
|
9714
|
+
* `body` for the pane splitter to file. One live values BAG serves the whole section
|
|
9715
|
+
* — instances and subsections share the per-type store, so a `when` gate anywhere
|
|
9716
|
+
* can reference a key edited anywhere else; every edit runs every registered
|
|
9717
|
+
* refresher.
|
|
9718
|
+
*/
|
|
9719
|
+
chartTypeSection(typeId, section, config, body) {
|
|
9720
|
+
const marker = this.section(section.title);
|
|
9721
|
+
marker.dataset.sdStyle = typeId;
|
|
9722
|
+
marker.dataset.sdVisibility = section.visibility ?? "active";
|
|
9723
|
+
body.append(marker);
|
|
9724
|
+
const values = config.chartTypes[typeId] ?? {};
|
|
9725
|
+
const bag = {};
|
|
9726
|
+
const seedKey = (key, want, defval) => {
|
|
9727
|
+
const v = values[key];
|
|
9728
|
+
bag[key] = typeof v === want ? v : defval;
|
|
9729
|
+
};
|
|
9730
|
+
const seed = (rows) => {
|
|
9731
|
+
for (const r of rows) {
|
|
9732
|
+
if (r.kind === "heading" || r.kind === "header") continue;
|
|
9733
|
+
if (r.kind === "range") {
|
|
9734
|
+
seedKey(r.minKey, "number", r.defval);
|
|
9735
|
+
seedKey(r.maxKey, "number", r.defval);
|
|
9736
|
+
continue;
|
|
9737
|
+
}
|
|
9738
|
+
seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
|
|
9739
|
+
if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
|
|
9740
|
+
}
|
|
9741
|
+
};
|
|
9742
|
+
if (section.rows) seed(section.rows);
|
|
9743
|
+
for (const inst of section.instances ?? []) seed(inst.rows);
|
|
9744
|
+
for (const sub of section.subsections ?? []) seed(sub.rows);
|
|
9745
|
+
for (const inst of section.instances ?? []) {
|
|
9746
|
+
if (!inst.enableKey || inst.enableKey in bag) continue;
|
|
9747
|
+
bag[inst.enableKey] = values[inst.enableKey] === true;
|
|
9748
|
+
}
|
|
9749
|
+
for (const sub of section.subsections ?? []) {
|
|
9750
|
+
if (!sub.enableKey || sub.enableKey in bag) continue;
|
|
9751
|
+
bag[sub.enableKey] = values[sub.enableKey] === true;
|
|
9752
|
+
}
|
|
9753
|
+
const refreshers = [];
|
|
9754
|
+
const put = (key, v) => {
|
|
9755
|
+
bag[key] = v;
|
|
9756
|
+
this.emitType(typeId, key, v);
|
|
9757
|
+
for (const r of refreshers) r();
|
|
9758
|
+
};
|
|
9759
|
+
if (section.instances && section.instances.length > 0) {
|
|
9760
|
+
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
9761
|
+
} else if (section.rows) {
|
|
9762
|
+
this.flatTypeRows(section.rows, bag, put, refreshers, body);
|
|
9763
|
+
}
|
|
9764
|
+
for (const sub of section.subsections ?? []) {
|
|
9765
|
+
const subMarker = this.section(sub.title);
|
|
9766
|
+
subMarker.dataset.sdStyle = typeId;
|
|
9767
|
+
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
9768
|
+
subMarker.dataset.sdSub = "1";
|
|
9769
|
+
body.append(subMarker);
|
|
9770
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
|
|
9771
|
+
}
|
|
9772
|
+
for (const r of refreshers) r();
|
|
9773
|
+
}
|
|
9774
|
+
/** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
|
|
9775
|
+
* them), headings/headers as inline group titles, `when` gates refreshed live. */
|
|
9776
|
+
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
9777
|
+
const entries = [];
|
|
9778
|
+
for (const r of rows) {
|
|
9779
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
|
|
9780
|
+
entries.push({ el, when: r.when });
|
|
9781
|
+
body.append(el);
|
|
9782
|
+
}
|
|
9783
|
+
refreshers.push(() => {
|
|
9784
|
+
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
9785
|
+
});
|
|
9786
|
+
}
|
|
9787
|
+
/** One value row for a chart-type descriptor, writing through `put`. */
|
|
9788
|
+
typeRow(r, bag, put) {
|
|
9789
|
+
if (r.kind === "toggle") {
|
|
9790
|
+
const swatches = (r.colors ?? []).map((c) => {
|
|
9791
|
+
const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
|
|
9792
|
+
sw.title = c.label;
|
|
9793
|
+
return sw;
|
|
9794
|
+
});
|
|
9795
|
+
return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
|
|
9796
|
+
}
|
|
9797
|
+
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));
|
|
9798
|
+
if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
|
|
9799
|
+
if (r.kind === "range") return this.rangeRow(r, bag, put);
|
|
9800
|
+
return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
|
|
9801
|
+
}
|
|
9802
|
+
/**
|
|
9803
|
+
* A MIN–MAX row: two number inputs on one row (stored under the descriptor's
|
|
9804
|
+
* `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
|
|
9805
|
+
* empty showing it, and clearing an input stores the default back — the
|
|
9806
|
+
* placeholder names the unset state ('Off' for 0-disables bounds).
|
|
9807
|
+
*/
|
|
9808
|
+
rangeRow(r, bag, put) {
|
|
9809
|
+
const { wrap } = this.row(r.label);
|
|
9810
|
+
const input = (key, title) => {
|
|
9811
|
+
const ni = document.createElement("input");
|
|
9812
|
+
ni.type = "number";
|
|
9813
|
+
ni.className = "vela-sd-number";
|
|
9814
|
+
ni.min = String(r.min ?? 0);
|
|
9815
|
+
ni.max = String(r.max ?? 1e6);
|
|
9816
|
+
ni.step = String(r.step ?? 1);
|
|
9817
|
+
ni.title = title;
|
|
9818
|
+
const current = bag[key];
|
|
9819
|
+
if (r.placeholder !== void 0) {
|
|
9820
|
+
ni.placeholder = r.placeholder;
|
|
9821
|
+
if (current !== r.defval) ni.value = String(current);
|
|
9822
|
+
} else {
|
|
9823
|
+
ni.value = String(current);
|
|
9824
|
+
}
|
|
9825
|
+
ni.addEventListener("change", () => {
|
|
9826
|
+
const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
|
|
9827
|
+
const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
|
|
9828
|
+
ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
|
|
9829
|
+
put(key, v);
|
|
9830
|
+
});
|
|
9831
|
+
return ni;
|
|
9832
|
+
};
|
|
9833
|
+
const box = document.createElement("div");
|
|
9834
|
+
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
9835
|
+
const dash = document.createElement("span");
|
|
9836
|
+
dash.textContent = "\u2013";
|
|
9837
|
+
dash.style.cssText = "color:var(--vela-fg-muted);";
|
|
9838
|
+
box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
|
|
9839
|
+
wrap.appendChild(box);
|
|
9840
|
+
return wrap;
|
|
9841
|
+
}
|
|
9842
|
+
/**
|
|
9843
|
+
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
9844
|
+
* removable one), a dashed `+` while an instance is still absent, and one
|
|
9845
|
+
* grouped-rows content per instance below — only the active tab's content shows.
|
|
9846
|
+
* Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
|
|
9847
|
+
* every section edit, so gates elsewhere stay coherent.
|
|
9848
|
+
*/
|
|
9849
|
+
instancesBlock(typeId, instances, bag, put, refreshers) {
|
|
9850
|
+
const wrap = document.createElement("div");
|
|
9851
|
+
const strip = document.createElement("div");
|
|
9852
|
+
strip.className = "vela-sd-itabs";
|
|
9853
|
+
wrap.append(strip);
|
|
9854
|
+
const contents = instances.map((inst, i) => {
|
|
9855
|
+
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
9856
|
+
wrap.append(content);
|
|
9857
|
+
return content;
|
|
9858
|
+
});
|
|
9859
|
+
const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
|
|
9860
|
+
const refresh = () => {
|
|
9861
|
+
let active = this.typeActiveInstance.get(typeId) ?? 0;
|
|
9862
|
+
if (!present(instances[active] ?? instances[0])) active = 0;
|
|
9863
|
+
this.typeActiveInstance.set(typeId, active);
|
|
9864
|
+
strip.replaceChildren();
|
|
9865
|
+
instances.forEach((inst, i) => {
|
|
9866
|
+
if (!present(inst)) return;
|
|
9867
|
+
const tab = document.createElement("button");
|
|
9868
|
+
tab.type = "button";
|
|
9869
|
+
tab.className = "vela-sd-itab" + (i === active ? " on" : "");
|
|
9870
|
+
const lbl = document.createElement("span");
|
|
9871
|
+
lbl.textContent = inst.label;
|
|
9872
|
+
tab.append(lbl);
|
|
9873
|
+
if (i === active && inst.enableKey) {
|
|
9874
|
+
const x = document.createElement("span");
|
|
9875
|
+
x.className = "vela-sd-ix";
|
|
9876
|
+
x.textContent = "\u2715";
|
|
9877
|
+
x.title = `Remove ${inst.label}`;
|
|
9878
|
+
x.addEventListener("click", (e) => {
|
|
9879
|
+
e.stopPropagation();
|
|
9880
|
+
this.typeActiveInstance.set(typeId, 0);
|
|
9881
|
+
put(inst.enableKey, false);
|
|
9882
|
+
});
|
|
9883
|
+
tab.append(x);
|
|
9884
|
+
}
|
|
9885
|
+
tab.addEventListener("click", () => {
|
|
9886
|
+
this.typeActiveInstance.set(typeId, i);
|
|
9887
|
+
refresh();
|
|
9888
|
+
});
|
|
9889
|
+
strip.append(tab);
|
|
9890
|
+
});
|
|
9891
|
+
const absent = instances.findIndex((inst) => !present(inst));
|
|
9892
|
+
if (absent >= 0) {
|
|
9893
|
+
const add = document.createElement("button");
|
|
9894
|
+
add.type = "button";
|
|
9895
|
+
add.className = "vela-sd-itab vela-sd-itab-add";
|
|
9896
|
+
add.textContent = "+";
|
|
9897
|
+
add.title = `Add ${instances[absent].label}`;
|
|
9898
|
+
add.addEventListener("click", () => {
|
|
9899
|
+
this.typeActiveInstance.set(typeId, absent);
|
|
9900
|
+
put(instances[absent].enableKey, true);
|
|
9901
|
+
});
|
|
9902
|
+
strip.append(add);
|
|
9903
|
+
}
|
|
9904
|
+
contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
|
|
9905
|
+
};
|
|
9906
|
+
refreshers.push(refresh);
|
|
9907
|
+
return wrap;
|
|
9908
|
+
}
|
|
9909
|
+
/**
|
|
9910
|
+
* A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
|
|
9911
|
+
* ONE rows host holding every row — the active group's rows show, the rest hide.
|
|
9912
|
+
* Rows BEFORE the first heading are the always block, visible above every group.
|
|
9913
|
+
* `header` rows stay in the rows column as in-group subgroup titles (not TOC
|
|
9914
|
+
* entries). A group whose value rows are all gated out (or whose heading's own
|
|
9915
|
+
* `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
|
|
9916
|
+
* rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
|
|
9917
|
+
* across all groups.
|
|
9918
|
+
*
|
|
9919
|
+
* `enableKey` (optional): while that bag boolean is false, every row except the one
|
|
9920
|
+
* whose key matches is soft-disabled (visible but grayed / non-interactive) so the
|
|
9921
|
+
* pane stays browseable with the feature off.
|
|
9922
|
+
*/
|
|
9923
|
+
groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
|
|
9924
|
+
const wrap = document.createElement("div");
|
|
9925
|
+
wrap.className = "vela-sd-struct";
|
|
9926
|
+
const toc = document.createElement("div");
|
|
9927
|
+
toc.className = "vela-sd-toc";
|
|
9928
|
+
const rowsHost = document.createElement("div");
|
|
9929
|
+
rowsHost.dataset.sdRowsHost = "1";
|
|
9930
|
+
rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
9931
|
+
wrap.append(toc, rowsHost);
|
|
9932
|
+
const entries = [];
|
|
9933
|
+
const groups = [];
|
|
9934
|
+
const groupWhens = [];
|
|
9935
|
+
let g = -1;
|
|
9936
|
+
for (const r of rows) {
|
|
9937
|
+
if (r.kind === "heading") {
|
|
9938
|
+
g = groups.length;
|
|
9939
|
+
groups.push(r.label);
|
|
9940
|
+
groupWhens.push(r.when);
|
|
9941
|
+
continue;
|
|
9942
|
+
}
|
|
9943
|
+
if (r.kind === "header") {
|
|
9944
|
+
entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
|
|
9945
|
+
rowsHost.append(entries[entries.length - 1].el);
|
|
9946
|
+
continue;
|
|
9947
|
+
}
|
|
9948
|
+
const el = this.typeRow(r, bag, put);
|
|
9949
|
+
const key = r.kind === "range" ? void 0 : r.key;
|
|
9950
|
+
entries.push({ el, when: r.when, group: g, key });
|
|
9951
|
+
rowsHost.append(el);
|
|
9952
|
+
}
|
|
9953
|
+
const refresh = () => {
|
|
9954
|
+
const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
|
|
9955
|
+
let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
|
|
9956
|
+
if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
|
|
9957
|
+
if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
|
|
9958
|
+
toc.replaceChildren();
|
|
9959
|
+
groups.forEach((label, gi) => {
|
|
9960
|
+
if (!live[gi]) return;
|
|
9961
|
+
const b = document.createElement("button");
|
|
9962
|
+
b.type = "button";
|
|
9963
|
+
b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
|
|
9964
|
+
b.textContent = label;
|
|
9965
|
+
b.addEventListener("click", () => {
|
|
9966
|
+
this.typeActiveGroup.set(paneKey, label);
|
|
9967
|
+
refresh();
|
|
9968
|
+
});
|
|
9969
|
+
toc.append(b);
|
|
9970
|
+
});
|
|
9971
|
+
const anyGroup = live.some(Boolean);
|
|
9972
|
+
toc.classList.toggle("vela-sd-hide", !anyGroup);
|
|
9973
|
+
wrap.classList.toggle("no-toc", !anyGroup);
|
|
9974
|
+
const enabled = !enableKey || bag[enableKey] === true;
|
|
9975
|
+
for (let i = 0; i < entries.length; i++) {
|
|
9976
|
+
const e = entries[i];
|
|
9977
|
+
let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
|
|
9978
|
+
if (visible && e.header) {
|
|
9979
|
+
let hasContent = false;
|
|
9980
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
9981
|
+
const n = entries[j];
|
|
9982
|
+
if (n.group !== e.group || n.header) break;
|
|
9983
|
+
if (settingsRowVisible(n.when, bag)) {
|
|
9984
|
+
hasContent = true;
|
|
9985
|
+
break;
|
|
9986
|
+
}
|
|
9987
|
+
}
|
|
9988
|
+
visible = hasContent;
|
|
9989
|
+
}
|
|
9990
|
+
e.el.classList.toggle("vela-sd-hide", !visible);
|
|
9991
|
+
e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
|
|
9992
|
+
}
|
|
9993
|
+
};
|
|
9994
|
+
refreshers.push(refresh);
|
|
9995
|
+
return wrap;
|
|
9996
|
+
}
|
|
9514
9997
|
/** Vertical breathing space between row clusters — grouping reads from whitespace,
|
|
9515
9998
|
* so no drawn rule. */
|
|
9516
9999
|
separator() {
|
|
@@ -9773,7 +10256,7 @@ var SettingsDialog = class {
|
|
|
9773
10256
|
const { wrap } = this.row(label);
|
|
9774
10257
|
const sel = document.createElement("select");
|
|
9775
10258
|
sel.className = "vela-sd-select";
|
|
9776
|
-
sel.style.cssText = "max-width:
|
|
10259
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
9777
10260
|
for (const [val, lbl] of options) {
|
|
9778
10261
|
const o = document.createElement("option");
|
|
9779
10262
|
o.value = val;
|
|
@@ -9789,7 +10272,7 @@ var SettingsDialog = class {
|
|
|
9789
10272
|
const { wrap } = this.row(label);
|
|
9790
10273
|
const sel = document.createElement("select");
|
|
9791
10274
|
sel.className = "vela-sd-select";
|
|
9792
|
-
sel.style.cssText = "max-width:
|
|
10275
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
9793
10276
|
for (const opt of options) {
|
|
9794
10277
|
const o = document.createElement("option");
|
|
9795
10278
|
o.value = opt;
|
|
@@ -9826,6 +10309,9 @@ var SettingsDialog = class {
|
|
|
9826
10309
|
}
|
|
9827
10310
|
};
|
|
9828
10311
|
var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
|
|
10312
|
+
function normalizeSelectOptions(options) {
|
|
10313
|
+
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
10314
|
+
}
|
|
9829
10315
|
function timezoneOptions(current) {
|
|
9830
10316
|
const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
|
|
9831
10317
|
const normalized = normalizeTimezone(current);
|
|
@@ -11184,6 +11670,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
11184
11670
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
11185
11671
|
var DRAG_SLOP2 = 3;
|
|
11186
11672
|
var FREEHAND_SAMPLE_PX = 4;
|
|
11673
|
+
var ANGLE_SNAP_STEP = Math.PI / 4;
|
|
11674
|
+
var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
|
|
11187
11675
|
var DrawingInteraction = class {
|
|
11188
11676
|
// magnet target for the ring
|
|
11189
11677
|
constructor(deps) {
|
|
@@ -11224,6 +11712,27 @@ var DrawingInteraction = class {
|
|
|
11224
11712
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
11225
11713
|
return snapped;
|
|
11226
11714
|
}
|
|
11715
|
+
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
11716
|
+
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
11717
|
+
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
11718
|
+
* the result to a candle afterwards would break the exact angle just enforced. */
|
|
11719
|
+
resolveAngleSnapped(pivot, paneId, x, y) {
|
|
11720
|
+
const proj = this.deps.projector();
|
|
11721
|
+
const px = proj.xOf(pivot.time);
|
|
11722
|
+
const py = proj.yOf(pivot.price, paneId);
|
|
11723
|
+
this.snapAt = null;
|
|
11724
|
+
if (py == null) return proj.pxToPoint(x, y, paneId);
|
|
11725
|
+
const r = Math.hypot(x - px, y - py);
|
|
11726
|
+
if (r < 1) return proj.pxToPoint(x, y, paneId);
|
|
11727
|
+
const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
|
|
11728
|
+
return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
|
|
11729
|
+
}
|
|
11730
|
+
/** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
|
|
11731
|
+
* the tool is not a two-point segment (or no anchor is committed yet). */
|
|
11732
|
+
placingPivot(draft) {
|
|
11733
|
+
if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
|
|
11734
|
+
return draft.anchors[draft.anchors.length - 1] ?? null;
|
|
11735
|
+
}
|
|
11227
11736
|
/** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
|
|
11228
11737
|
claim(x, y) {
|
|
11229
11738
|
if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
|
|
@@ -11245,12 +11754,15 @@ var DrawingInteraction = class {
|
|
|
11245
11754
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
11246
11755
|
}
|
|
11247
11756
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
11248
|
-
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging
|
|
11757
|
+
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
11758
|
+
* while placing a line tool it locks the segment angle to 45° steps. */
|
|
11249
11759
|
down(x, y, mode = "off", shift = false) {
|
|
11250
11760
|
const proj = this.deps.projector();
|
|
11251
11761
|
if (this.state.kind === "placing") {
|
|
11252
11762
|
if (this.state.draft.placementMode() !== "click") return;
|
|
11253
|
-
this.
|
|
11763
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
11764
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
11765
|
+
this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
|
|
11254
11766
|
if (this.state.draft.anchors.length >= this.state.need) this.finalize();
|
|
11255
11767
|
else this.deps.changed();
|
|
11256
11768
|
return;
|
|
@@ -11275,8 +11787,9 @@ var DrawingInteraction = class {
|
|
|
11275
11787
|
this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
|
|
11276
11788
|
}
|
|
11277
11789
|
}
|
|
11278
|
-
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
11279
|
-
|
|
11790
|
+
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
11791
|
+
* `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
|
|
11792
|
+
move(x, y, mode = "off", shift = false) {
|
|
11280
11793
|
if (this.state.kind === "idle") {
|
|
11281
11794
|
const tool = this.deps.activeTool();
|
|
11282
11795
|
const had = this.snapAt != null;
|
|
@@ -11290,7 +11803,8 @@ var DrawingInteraction = class {
|
|
|
11290
11803
|
this.captureFreehand(x, y);
|
|
11291
11804
|
return;
|
|
11292
11805
|
}
|
|
11293
|
-
|
|
11806
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
11807
|
+
this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
11294
11808
|
this.deps.changed();
|
|
11295
11809
|
return;
|
|
11296
11810
|
}
|
|
@@ -11301,7 +11815,11 @@ var DrawingInteraction = class {
|
|
|
11301
11815
|
this.state.moved = true;
|
|
11302
11816
|
if (this.state.handle >= 0) {
|
|
11303
11817
|
const a = d.anchors[this.state.handle];
|
|
11304
|
-
if (a)
|
|
11818
|
+
if (a) {
|
|
11819
|
+
const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
11820
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
|
|
11821
|
+
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
11822
|
+
}
|
|
11305
11823
|
d.constrainHandleDrag(this.state.handle);
|
|
11306
11824
|
} else {
|
|
11307
11825
|
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
@@ -12810,6 +13328,10 @@ var UserDrawingController = class {
|
|
|
12810
13328
|
this.activeTool = null;
|
|
12811
13329
|
// last-used style for the armed tool (seeds the placement ghost)
|
|
12812
13330
|
this.intentCb = null;
|
|
13331
|
+
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
13332
|
+
this.externalGhost = null;
|
|
13333
|
+
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
13334
|
+
this.lastDraftKey = null;
|
|
12813
13335
|
this.measure = new MeasureOverlay();
|
|
12814
13336
|
// transient ruler — not a persistent drawing
|
|
12815
13337
|
this.toolbarVisible = false;
|
|
@@ -12881,6 +13403,11 @@ var UserDrawingController = class {
|
|
|
12881
13403
|
syncToolbarGutter() {
|
|
12882
13404
|
this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
12883
13405
|
}
|
|
13406
|
+
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
13407
|
+
setExternalGhost(doc) {
|
|
13408
|
+
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
13409
|
+
this.render();
|
|
13410
|
+
}
|
|
12884
13411
|
syncDrawings(docs) {
|
|
12885
13412
|
this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
|
|
12886
13413
|
if (this.textEditor && !this.editedDrawing(this.textEditor.id)) this.closeTextEditor();
|
|
@@ -13039,7 +13566,7 @@ var UserDrawingController = class {
|
|
|
13039
13566
|
}
|
|
13040
13567
|
this.interaction.down(x, y, snap, shift);
|
|
13041
13568
|
}
|
|
13042
|
-
pointerMove(x, y, snap = "off") {
|
|
13569
|
+
pointerMove(x, y, snap = "off", shift = false) {
|
|
13043
13570
|
if (this.eraserMode) {
|
|
13044
13571
|
if (this.erasing) this.deleteAt(x, y);
|
|
13045
13572
|
return;
|
|
@@ -13049,7 +13576,7 @@ var UserDrawingController = class {
|
|
|
13049
13576
|
this.render();
|
|
13050
13577
|
return;
|
|
13051
13578
|
}
|
|
13052
|
-
this.interaction.move(x, y, snap);
|
|
13579
|
+
this.interaction.move(x, y, snap, shift);
|
|
13053
13580
|
this.updateHover(x, y);
|
|
13054
13581
|
}
|
|
13055
13582
|
/** Track which drawing is hovered so its handles appear on hover (and only then). */
|
|
@@ -13435,6 +13962,8 @@ var UserDrawingController = class {
|
|
|
13435
13962
|
this.layoutTextEditor();
|
|
13436
13963
|
const ghost = this.interaction.ghost();
|
|
13437
13964
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
13965
|
+
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
13966
|
+
this.emitDraft(ghost);
|
|
13438
13967
|
const markers = this.interaction.placingMarkers(proj);
|
|
13439
13968
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
13440
13969
|
const m = this.interaction.snapMarker();
|
|
@@ -13496,6 +14025,15 @@ var UserDrawingController = class {
|
|
|
13496
14025
|
}
|
|
13497
14026
|
}, () => this.clearSelection());
|
|
13498
14027
|
}
|
|
14028
|
+
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
14029
|
+
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
14030
|
+
* shape changes emit, and the end of a placement emits one `null`. */
|
|
14031
|
+
emitDraft(ghost) {
|
|
14032
|
+
const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
|
|
14033
|
+
if (key === this.lastDraftKey) return;
|
|
14034
|
+
this.lastDraftKey = key;
|
|
14035
|
+
this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
|
|
14036
|
+
}
|
|
13499
14037
|
emit(i) {
|
|
13500
14038
|
if (i.kind === "create") {
|
|
13501
14039
|
if (i.doc.text && i.doc.text.color === void 0) {
|
|
@@ -14027,10 +14565,10 @@ var SEPARATOR_HIT_PX = 4;
|
|
|
14027
14565
|
var NativeRenderer = class {
|
|
14028
14566
|
constructor(opts) {
|
|
14029
14567
|
this.capabilities = NATIVE_CAPABILITIES;
|
|
14030
|
-
// The app "chrome" surface (drawing toolbar +
|
|
14568
|
+
// The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
|
|
14031
14569
|
// Owned by the app theme alone (mount/setTheme) — config-level background/text edits
|
|
14032
|
-
// (settings dialog, persisted configs) recolor
|
|
14033
|
-
//
|
|
14570
|
+
// (settings dialog, persisted configs) recolor the plot AND the axis scales (see
|
|
14571
|
+
// axisSurface), but never the toolbar/dialog chrome.
|
|
14034
14572
|
this.surfaceBackground = DARK_THEME.background;
|
|
14035
14573
|
this.surfaceTextColor = DARK_THEME.textColor;
|
|
14036
14574
|
// the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
|
|
@@ -14230,6 +14768,7 @@ var NativeRenderer = class {
|
|
|
14230
14768
|
this.candleDown = opts.downColor;
|
|
14231
14769
|
this.scene.priceStyle = opts.priceStyle;
|
|
14232
14770
|
this.scene.basePainting = basePaintingOf(opts.priceStyle);
|
|
14771
|
+
this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
|
|
14233
14772
|
}
|
|
14234
14773
|
this.theme = this.deriveTheme(DARK_THEME);
|
|
14235
14774
|
}
|
|
@@ -14707,6 +15246,7 @@ var NativeRenderer = class {
|
|
|
14707
15246
|
baselineLevel: next.baseline.baselineLevel
|
|
14708
15247
|
};
|
|
14709
15248
|
this.setPriceStyle(next.series.style);
|
|
15249
|
+
this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
|
|
14710
15250
|
this.scene.baselineValue = next.series.baseline;
|
|
14711
15251
|
this.coords.setPitchScale(next.series.spacing);
|
|
14712
15252
|
this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
|
|
@@ -14812,7 +15352,7 @@ var NativeRenderer = class {
|
|
|
14812
15352
|
(patch) => this.applyConfig(patch),
|
|
14813
15353
|
(json) => this.applyConfig(json),
|
|
14814
15354
|
() => {
|
|
14815
|
-
if (this.factoryConfig) this.applyConfig(this.factoryConfig);
|
|
15355
|
+
if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
|
|
14816
15356
|
this.settingsDialog?.close();
|
|
14817
15357
|
this.openSettingsDialog();
|
|
14818
15358
|
},
|
|
@@ -15013,10 +15553,16 @@ var NativeRenderer = class {
|
|
|
15013
15553
|
borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
|
|
15014
15554
|
};
|
|
15015
15555
|
}
|
|
15556
|
+
/** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
|
|
15557
|
+
* (`layout.background`) and its contrast-corrected text, so the scales read as part of the
|
|
15558
|
+
* plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
|
|
15559
|
+
axisSurface() {
|
|
15560
|
+
return { background: this.theme.background, textColor: this.theme.textColor };
|
|
15561
|
+
}
|
|
15016
15562
|
// ── lifecycle ──
|
|
15017
15563
|
mount(container, theme) {
|
|
15018
15564
|
this.mountContainer = container;
|
|
15019
|
-
this.
|
|
15565
|
+
this.publishGutters();
|
|
15020
15566
|
this.theme = this.deriveTheme(theme);
|
|
15021
15567
|
this.surfaceBackground = theme.background;
|
|
15022
15568
|
this.surfaceTextColor = theme.textColor;
|
|
@@ -15088,7 +15634,7 @@ var NativeRenderer = class {
|
|
|
15088
15634
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
15089
15635
|
drawingsSnapMode: () => this.snapMode,
|
|
15090
15636
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
15091
|
-
drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
|
|
15637
|
+
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
15092
15638
|
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
15093
15639
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
15094
15640
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -15325,6 +15871,7 @@ var NativeRenderer = class {
|
|
|
15325
15871
|
this.attributionEl?.remove();
|
|
15326
15872
|
this.attributionEl = null;
|
|
15327
15873
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
15874
|
+
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
15328
15875
|
this.mountContainer = null;
|
|
15329
15876
|
this.wrapper?.remove();
|
|
15330
15877
|
}
|
|
@@ -15493,6 +16040,7 @@ var NativeRenderer = class {
|
|
|
15493
16040
|
const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
|
|
15494
16041
|
if (w === this.rightAxisW) return false;
|
|
15495
16042
|
this.rightAxisW = w;
|
|
16043
|
+
this.publishGutters();
|
|
15496
16044
|
if (this.coords.width > 0) this.syncSize();
|
|
15497
16045
|
return true;
|
|
15498
16046
|
}
|
|
@@ -15652,6 +16200,7 @@ var NativeRenderer = class {
|
|
|
15652
16200
|
if (style === this.scene.priceStyle) return;
|
|
15653
16201
|
this.scene.priceStyle = style;
|
|
15654
16202
|
this.scene.basePainting = basePaintingOf(style);
|
|
16203
|
+
this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
|
|
15655
16204
|
for (const cb of this.priceStyleCbs) cb(style);
|
|
15656
16205
|
}
|
|
15657
16206
|
onRemoveIndicator(cb) {
|
|
@@ -15813,7 +16362,10 @@ var NativeRenderer = class {
|
|
|
15813
16362
|
active = true;
|
|
15814
16363
|
}
|
|
15815
16364
|
}
|
|
15816
|
-
this.
|
|
16365
|
+
const clamped = this.clampViewport(barSpacing, rightOffset);
|
|
16366
|
+
if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
|
|
16367
|
+
if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
|
|
16368
|
+
this.coords.setViewport(clamped);
|
|
15817
16369
|
this.computeScales();
|
|
15818
16370
|
if (this.easeScales(dtMs)) active = true;
|
|
15819
16371
|
if (this.easeLiveBar(dtMs)) active = true;
|
|
@@ -16265,9 +16817,37 @@ var NativeRenderer = class {
|
|
|
16265
16817
|
this.paintData();
|
|
16266
16818
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
16267
16819
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
16268
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
16820
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
16269
16821
|
}
|
|
16270
16822
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
16823
|
+
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
16824
|
+
}
|
|
16825
|
+
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
16826
|
+
repaintCursorLayers() {
|
|
16827
|
+
const pane = this.scene.panes.get(PRICE_PANE_ID);
|
|
16828
|
+
if (!pane) return;
|
|
16829
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
16830
|
+
for (const l of this.extLayers) {
|
|
16831
|
+
if (!l.def.repaintOnCursor) continue;
|
|
16832
|
+
l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
|
|
16833
|
+
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
16834
|
+
}
|
|
16835
|
+
}
|
|
16836
|
+
/** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
|
|
16837
|
+
extLayerArgs(id, scale, bounds, nowMs) {
|
|
16838
|
+
return {
|
|
16839
|
+
bars: this.scene.bars,
|
|
16840
|
+
data: this.scene.nativeData.get(id),
|
|
16841
|
+
settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
|
|
16842
|
+
pending: this.scene.nativePending.get(id) ?? [],
|
|
16843
|
+
coords: this.coords,
|
|
16844
|
+
scale,
|
|
16845
|
+
bounds,
|
|
16846
|
+
theme: this.theme,
|
|
16847
|
+
priceStyle: this.scene.priceStyle,
|
|
16848
|
+
nowMs,
|
|
16849
|
+
cursor: this.lastPointer
|
|
16850
|
+
};
|
|
16271
16851
|
}
|
|
16272
16852
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
16273
16853
|
paintData() {
|
|
@@ -16300,18 +16880,16 @@ var NativeRenderer = class {
|
|
|
16300
16880
|
});
|
|
16301
16881
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
16302
16882
|
for (const l of this.extLayers) {
|
|
16303
|
-
l.
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16308
|
-
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16312
|
-
|
|
16313
|
-
nowMs
|
|
16314
|
-
});
|
|
16883
|
+
const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
|
|
16884
|
+
l.instance.render(args);
|
|
16885
|
+
if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
|
|
16886
|
+
const mod = l.instance.modulateBase(args);
|
|
16887
|
+
if (mod) {
|
|
16888
|
+
if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
|
|
16889
|
+
if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
|
|
16890
|
+
if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
|
|
16891
|
+
}
|
|
16892
|
+
}
|
|
16315
16893
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
16316
16894
|
}
|
|
16317
16895
|
}
|
|
@@ -16321,7 +16899,7 @@ var NativeRenderer = class {
|
|
|
16321
16899
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
16322
16900
|
this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
|
|
16323
16901
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
16324
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
16902
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
16325
16903
|
this.userDrawings?.render();
|
|
16326
16904
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
16327
16905
|
this.paintedData = true;
|
|
@@ -16755,15 +17333,18 @@ var NativeRenderer = class {
|
|
|
16755
17333
|
setToolbarGutter(px) {
|
|
16756
17334
|
if (px === this.toolbarGutter) return;
|
|
16757
17335
|
this.toolbarGutter = px;
|
|
16758
|
-
this.
|
|
17336
|
+
this.publishGutters();
|
|
16759
17337
|
this.positionAttribution();
|
|
16760
17338
|
this.syncSize();
|
|
16761
17339
|
}
|
|
16762
|
-
/** Publish the
|
|
16763
|
-
*
|
|
16764
|
-
*
|
|
16765
|
-
|
|
17340
|
+
/** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
|
|
17341
|
+
* drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
|
|
17342
|
+
* incl. merged own-scale columns), so host overlays sharing that container (a
|
|
17343
|
+
* status line, a watermark, a custom legend) can anchor to the plot's edges
|
|
17344
|
+
* without reaching into the renderer's DOM. */
|
|
17345
|
+
publishGutters() {
|
|
16766
17346
|
this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
|
|
17347
|
+
this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
|
|
16767
17348
|
}
|
|
16768
17349
|
/** The built-in mark, or the host's own when one is set. */
|
|
16769
17350
|
buildAttributionEl() {
|
|
@@ -16833,7 +17414,13 @@ var NativeRenderer = class {
|
|
|
16833
17414
|
this.fitContent();
|
|
16834
17415
|
this.didInitialFit = true;
|
|
16835
17416
|
}
|
|
16836
|
-
this.
|
|
17417
|
+
if (this.animator.active) {
|
|
17418
|
+
this.computeScales();
|
|
17419
|
+
this.paintData();
|
|
17420
|
+
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
17421
|
+
} else {
|
|
17422
|
+
this.scheduler.flushNow(4 /* Full */);
|
|
17423
|
+
}
|
|
16837
17424
|
}
|
|
16838
17425
|
applyBackground() {
|
|
16839
17426
|
this.wrapper.style.background = this.theme.background;
|
|
@@ -16902,6 +17489,9 @@ function sanitizeHighlights(value) {
|
|
|
16902
17489
|
}
|
|
16903
17490
|
return out.sort((a, b) => a.from - b.from);
|
|
16904
17491
|
}
|
|
17492
|
+
function clamp012(v) {
|
|
17493
|
+
return Math.max(0, Math.min(1, v));
|
|
17494
|
+
}
|
|
16905
17495
|
function isPriceStyle2(v) {
|
|
16906
17496
|
return typeof v === "string" && priceStyleIds().includes(v);
|
|
16907
17497
|
}
|