@luxalgo/vela 0.5.0 → 0.5.2
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-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
- package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
- package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
- package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
- package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
- package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
- package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
- package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
- package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
- package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
- package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
- package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
- package/dist/index.cjs +2010 -397
- package/dist/index.d.cts +83 -25
- package/dist/index.d.ts +83 -25
- package/dist/index.js +4 -4
- package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
- package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
- package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
- package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
- package/dist/plugin.cjs +58 -7
- 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 +342 -9
- package/dist/ui.d.cts +64 -2
- package/dist/ui.d.ts +64 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +2010 -397
- package/dist/vela.global.min.js +115 -34
- package/dist/widget.cjs +5987 -2242
- package/dist/widget.d.cts +89 -17
- package/dist/widget.d.ts +89 -17
- package/dist/widget.js +169 -18
- package/dist/workspace.cjs +5627 -1587
- package/dist/workspace.d.cts +106 -12
- package/dist/workspace.d.ts +106 -12
- package/dist/workspace.js +494 -52
- 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, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, 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-B5TIKQJ5.js';
|
|
2
|
+
import { themeTokens, STATIC_TOKENS } from './chunk-ATPU5CI6.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-LJLZR44Y.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -428,6 +428,28 @@ var RendererControl = class {
|
|
|
428
428
|
setLegendActions(provider) {
|
|
429
429
|
this.renderer.setLegendActions?.(provider);
|
|
430
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
433
|
+
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
434
|
+
* unfolding rows in place. Silent no-op on a renderer without a foldable legend.
|
|
435
|
+
*/
|
|
436
|
+
setLegendOverviewAction(action) {
|
|
437
|
+
this.renderer.setLegendOverviewAction?.(action);
|
|
438
|
+
return this;
|
|
439
|
+
}
|
|
440
|
+
/** Whether the active renderer can open a per-indicator settings dialog. */
|
|
441
|
+
get supportsIndicatorSettings() {
|
|
442
|
+
return typeof this.renderer.openIndicatorSettings === "function";
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Open one indicator's settings dialog — the programmatic twin of the legend gear
|
|
446
|
+
* (see {@link supportsIndicatorSettings}). Silent no-op without the seam or for an
|
|
447
|
+
* unknown id.
|
|
448
|
+
*/
|
|
449
|
+
openIndicatorSettings(indicatorId) {
|
|
450
|
+
this.renderer.openIndicatorSettings?.(indicatorId);
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
431
453
|
/**
|
|
432
454
|
* Export the current chart as a PNG data URL, or null if the active renderer
|
|
433
455
|
* doesn't support it (warns). DOM overlays (tables, legend) are not included.
|
|
@@ -474,6 +496,10 @@ var RendererControl = class {
|
|
|
474
496
|
onCrosshairMove(cb) {
|
|
475
497
|
return this.renderer.onCrosshairMove(cb);
|
|
476
498
|
}
|
|
499
|
+
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
500
|
+
onAxisLongPress(cb) {
|
|
501
|
+
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
502
|
+
}
|
|
477
503
|
/**
|
|
478
504
|
* The current data-window readout — the hovered bar's date/time and OHLCV plus every
|
|
479
505
|
* indicator's value there, each already formatted on its pane's scale (the latest bar when
|
|
@@ -539,6 +565,13 @@ var RendererControl = class {
|
|
|
539
565
|
this.renderer.setSettingsSections?.(sections);
|
|
540
566
|
return this;
|
|
541
567
|
}
|
|
568
|
+
/** Tell the renderer's own chrome which size class the host shell is in —
|
|
569
|
+
* `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
|
|
570
|
+
* Silent no-op on a renderer without adaptive chrome. */
|
|
571
|
+
setLayoutMode(mode) {
|
|
572
|
+
this.renderer.setLayoutMode?.(mode);
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
542
575
|
};
|
|
543
576
|
|
|
544
577
|
// src/data/BarStore.ts
|
|
@@ -1300,6 +1333,14 @@ var DrawingsControl = class {
|
|
|
1300
1333
|
if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
|
|
1301
1334
|
return this;
|
|
1302
1335
|
}
|
|
1336
|
+
/** Display another chart's in-progress placement as a GHOST at reduced opacity
|
|
1337
|
+
* (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
|
|
1338
|
+
* charts. Never a real drawing: no store entry, no selection, no persistence.
|
|
1339
|
+
* Silently inert on a renderer without the optional seam. */
|
|
1340
|
+
setExternalGhost(doc) {
|
|
1341
|
+
this.ctrl.setExternalGhost(doc);
|
|
1342
|
+
return this;
|
|
1343
|
+
}
|
|
1303
1344
|
/** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
|
|
1304
1345
|
add(type, init = {}) {
|
|
1305
1346
|
if (!this.ok("add")) return null;
|
|
@@ -1510,13 +1551,9 @@ function tzMenuLabel(zone, location) {
|
|
|
1510
1551
|
if (normalizeTimezone(zone) === "Etc/UTC") return location;
|
|
1511
1552
|
return `(${tzOffset(zone)}) ${location}`;
|
|
1512
1553
|
}
|
|
1513
|
-
function tzLocation(zone) {
|
|
1514
|
-
const entry = TIMEZONES.find((t) => t.value === normalizeTimezone(zone));
|
|
1515
|
-
return entry?.label ?? (zone.split("/").pop() ?? zone).replace(/_/g, " ");
|
|
1516
|
-
}
|
|
1517
1554
|
function tzButtonLabel(zone) {
|
|
1518
1555
|
if (normalizeTimezone(zone) === "Etc/UTC") return "UTC";
|
|
1519
|
-
return
|
|
1556
|
+
return tzOffset(zone);
|
|
1520
1557
|
}
|
|
1521
1558
|
|
|
1522
1559
|
// src/core/drawings/effectiveColor.ts
|
|
@@ -1727,6 +1764,11 @@ var DrawingController = class {
|
|
|
1727
1764
|
setToolbar(option) {
|
|
1728
1765
|
this.port?.setToolbar(buildToolbar(option).definition);
|
|
1729
1766
|
}
|
|
1767
|
+
/** Display (or clear) another chart's in-progress placement as a ghost — silently
|
|
1768
|
+
* inert on a renderer without the optional `setExternalGhost` seam. */
|
|
1769
|
+
setExternalGhost(doc) {
|
|
1770
|
+
this.port?.setExternalGhost?.(doc);
|
|
1771
|
+
}
|
|
1730
1772
|
/** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
|
|
1731
1773
|
setToolShortcuts(map) {
|
|
1732
1774
|
this.port?.setToolShortcuts?.(map);
|
|
@@ -1957,6 +1999,9 @@ var DrawingController = class {
|
|
|
1957
1999
|
case "arm":
|
|
1958
2000
|
this.setTool(i.type);
|
|
1959
2001
|
break;
|
|
2002
|
+
case "draft":
|
|
2003
|
+
this.events.emit("drawing:draft", { doc: i.doc });
|
|
2004
|
+
break;
|
|
1960
2005
|
case "create": {
|
|
1961
2006
|
const before = this.store.serialize();
|
|
1962
2007
|
const last = this.lastStyle.get(i.doc.type);
|
|
@@ -2073,7 +2118,7 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
2073
2118
|
const doc = anchor.ownerDocument;
|
|
2074
2119
|
tip = doc.createElement("div");
|
|
2075
2120
|
tip.textContent = text;
|
|
2076
|
-
tip.style.cssText = "position:absolute;z-index:
|
|
2121
|
+
tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);" + (opts.wrap ? "max-width:260px;" : "white-space:nowrap;");
|
|
2077
2122
|
applyChromeTokens(tip, opts.theme());
|
|
2078
2123
|
opts.host.appendChild(tip);
|
|
2079
2124
|
const a = anchor.getBoundingClientRect();
|
|
@@ -2087,16 +2132,17 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
2087
2132
|
tip.style.top = `${a.bottom - h.top + 6}px`;
|
|
2088
2133
|
}
|
|
2089
2134
|
};
|
|
2090
|
-
const arm = () => {
|
|
2135
|
+
const arm = (e) => {
|
|
2136
|
+
if (e.pointerType !== "mouse") return;
|
|
2091
2137
|
clear();
|
|
2092
2138
|
timer = window.setTimeout(show, opts.delayMs ?? 700);
|
|
2093
2139
|
};
|
|
2094
|
-
anchor.addEventListener("
|
|
2095
|
-
anchor.addEventListener("
|
|
2140
|
+
anchor.addEventListener("pointerenter", arm);
|
|
2141
|
+
anchor.addEventListener("pointerleave", clear);
|
|
2096
2142
|
anchor.addEventListener("pointerdown", clear);
|
|
2097
2143
|
return () => {
|
|
2098
|
-
anchor.removeEventListener("
|
|
2099
|
-
anchor.removeEventListener("
|
|
2144
|
+
anchor.removeEventListener("pointerenter", arm);
|
|
2145
|
+
anchor.removeEventListener("pointerleave", clear);
|
|
2100
2146
|
anchor.removeEventListener("pointerdown", clear);
|
|
2101
2147
|
clear();
|
|
2102
2148
|
};
|
|
@@ -2150,6 +2196,8 @@ var DrawingToolbar = class {
|
|
|
2150
2196
|
this.eraserActive = false;
|
|
2151
2197
|
this.stayBtn = null;
|
|
2152
2198
|
this.stayActive = false;
|
|
2199
|
+
this.syncBtn = null;
|
|
2200
|
+
this.syncActive = false;
|
|
2153
2201
|
this.collapseBtn = null;
|
|
2154
2202
|
this.collapsed = false;
|
|
2155
2203
|
this.visible = false;
|
|
@@ -2166,6 +2214,7 @@ var DrawingToolbar = class {
|
|
|
2166
2214
|
this.dock = options.dock ?? "absolute";
|
|
2167
2215
|
this.onCollapse = options.onCollapse ?? (() => {
|
|
2168
2216
|
});
|
|
2217
|
+
this.onDrawingsSync = options.onDrawingsSync ?? null;
|
|
2169
2218
|
ensureStyles();
|
|
2170
2219
|
this.root = document.createElement("div");
|
|
2171
2220
|
this.root.className = "vela-dtb";
|
|
@@ -2264,6 +2313,10 @@ var DrawingToolbar = class {
|
|
|
2264
2313
|
this.root.appendChild(this.makeMagnetCell());
|
|
2265
2314
|
this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
|
|
2266
2315
|
this.root.appendChild(this.stayBtn);
|
|
2316
|
+
if (this.onDrawingsSync) {
|
|
2317
|
+
this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
|
|
2318
|
+
this.root.appendChild(this.syncBtn);
|
|
2319
|
+
}
|
|
2267
2320
|
this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
|
|
2268
2321
|
this.collapseBtn.classList.add("vela-dtb-collapse");
|
|
2269
2322
|
this.root.appendChild(this.collapseBtn);
|
|
@@ -2271,6 +2324,7 @@ var DrawingToolbar = class {
|
|
|
2271
2324
|
this.paintEraser();
|
|
2272
2325
|
this.paintMagnet();
|
|
2273
2326
|
this.paintStay();
|
|
2327
|
+
this.paintDrawingsSync();
|
|
2274
2328
|
this.paintCollapse();
|
|
2275
2329
|
this.highlight();
|
|
2276
2330
|
}
|
|
@@ -2321,7 +2375,7 @@ var DrawingToolbar = class {
|
|
|
2321
2375
|
cell.appendChild(arrow);
|
|
2322
2376
|
}
|
|
2323
2377
|
this.tipDisposers.push(
|
|
2324
|
-
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right"
|
|
2378
|
+
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
|
|
2325
2379
|
);
|
|
2326
2380
|
return { cell, icon: icon2, arrow };
|
|
2327
2381
|
}
|
|
@@ -2431,6 +2485,23 @@ var DrawingToolbar = class {
|
|
|
2431
2485
|
b.dataset.active = this.stayActive ? "1" : "";
|
|
2432
2486
|
b.style.opacity = this.stayActive ? "1" : "0.5";
|
|
2433
2487
|
}
|
|
2488
|
+
/** Toggle drawings sync on/off and notify the host. */
|
|
2489
|
+
toggleDrawingsSync() {
|
|
2490
|
+
this.syncActive = !this.syncActive;
|
|
2491
|
+
this.onDrawingsSync?.(this.syncActive);
|
|
2492
|
+
this.paintDrawingsSync();
|
|
2493
|
+
}
|
|
2494
|
+
/** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
|
|
2495
|
+
setDrawingsSyncMode(on) {
|
|
2496
|
+
this.syncActive = on;
|
|
2497
|
+
this.paintDrawingsSync();
|
|
2498
|
+
}
|
|
2499
|
+
paintDrawingsSync() {
|
|
2500
|
+
const b = this.syncBtn;
|
|
2501
|
+
if (!b) return;
|
|
2502
|
+
b.dataset.active = this.syncActive ? "1" : "";
|
|
2503
|
+
b.style.opacity = this.syncActive ? "1" : "0.5";
|
|
2504
|
+
}
|
|
2434
2505
|
// ── flyout ──
|
|
2435
2506
|
/** Create the positioned flyout panel anchored to a cell, register it as open, and start the
|
|
2436
2507
|
* outside-dismiss. Callers fill it with items. */
|
|
@@ -2590,6 +2661,8 @@ var DrawingToolbar = class {
|
|
|
2590
2661
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
2591
2662
|
const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
|
|
2592
2663
|
iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
|
|
2664
|
+
const cell = this.groupCells.get(group.id);
|
|
2665
|
+
if (cell) this.tipText.set(cell, tool?.label ?? group.label);
|
|
2593
2666
|
}
|
|
2594
2667
|
/** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
|
|
2595
2668
|
* `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
|
|
@@ -2613,7 +2686,7 @@ var DrawingToolbar = class {
|
|
|
2613
2686
|
this.tipText.set(btn, title);
|
|
2614
2687
|
if (icon2) btn.innerHTML = hitHtml(icon2);
|
|
2615
2688
|
this.tipDisposers.push(
|
|
2616
|
-
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right"
|
|
2689
|
+
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
|
|
2617
2690
|
);
|
|
2618
2691
|
btn.addEventListener("click", onClick);
|
|
2619
2692
|
return btn;
|
|
@@ -2679,6 +2752,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
|
|
|
2679
2752
|
var EXPAND_ICON = icon("chevrons-right");
|
|
2680
2753
|
var MAGNET_ICON = icon("magnet");
|
|
2681
2754
|
var STAY_ICON = icon("pen-lock");
|
|
2755
|
+
var SYNC_ICON = icon("pen-sync");
|
|
2682
2756
|
var ERASER_ICON = icon("eraser");
|
|
2683
2757
|
var ARROW_ICON = icon("chevron-right");
|
|
2684
2758
|
var CHECK_ICON = icon("check");
|
|
@@ -2764,14 +2838,22 @@ function makeDialogDraggable(box, header, options) {
|
|
|
2764
2838
|
|
|
2765
2839
|
// src/renderers/shared/InputsUI.ts
|
|
2766
2840
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
2767
|
-
var STATUS_KEYFRAMES = "@keyframes vela-ind-
|
|
2768
|
-
var LEGEND_ICON_PX =
|
|
2841
|
+
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
2842
|
+
var LEGEND_ICON_PX = 16;
|
|
2843
|
+
var LEGEND_CTL_PX = 18;
|
|
2844
|
+
var LEGEND_ROW_PAD_Y = 2;
|
|
2845
|
+
var LEGEND_ROW_PAD_X = 6;
|
|
2846
|
+
var LEGEND_TITLE_VALUES_GAP_PX = 6;
|
|
2847
|
+
var LEGEND_ACTION_GAP_PX = 6;
|
|
2848
|
+
var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
|
|
2849
|
+
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
2769
2850
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
|
|
2770
2851
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
|
|
2771
2852
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
|
|
2772
|
-
var CLOSE_SVG = iconAt("close",
|
|
2853
|
+
var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
|
|
2773
2854
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
|
|
2774
2855
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
|
|
2856
|
+
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
|
|
2775
2857
|
var InputsUI = class {
|
|
2776
2858
|
constructor(container, theme, paneBoundsOf) {
|
|
2777
2859
|
this.container = container;
|
|
@@ -2792,6 +2874,8 @@ var InputsUI = class {
|
|
|
2792
2874
|
this.onToggleVisible = null;
|
|
2793
2875
|
/** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
|
|
2794
2876
|
this.symbolPicker = null;
|
|
2877
|
+
/** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
|
|
2878
|
+
this.mobileLayout = false;
|
|
2795
2879
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
2796
2880
|
this.moveApi = null;
|
|
2797
2881
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
@@ -2813,9 +2897,18 @@ var InputsUI = class {
|
|
|
2813
2897
|
/** Titles switched off entirely (a settings toggle): every pane's legend container
|
|
2814
2898
|
* hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
|
|
2815
2899
|
this.titlesVisible = true;
|
|
2900
|
+
/** Plot values shown beside the titles chart-wide (a settings toggle); a row's own
|
|
2901
|
+
* context-menu choice ({@link LegendRow.showValues}) overrides it per indicator. */
|
|
2902
|
+
this.valuesVisible = true;
|
|
2903
|
+
/** Open legend-row context menu (kept so it can be torn down). */
|
|
2904
|
+
this.rowMenu = null;
|
|
2816
2905
|
/** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
|
|
2817
2906
|
* rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
|
|
2818
2907
|
this.foldToggle = null;
|
|
2908
|
+
/** Host-provided replacement for the fold toggle (multi-chart shells route it to
|
|
2909
|
+
* their indicator overview, e.g. an object tree). While set, the legend stays
|
|
2910
|
+
* folded and the chip runs this instead of unfolding inline. */
|
|
2911
|
+
this.overviewAction = null;
|
|
2819
2912
|
/** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
|
|
2820
2913
|
this.onDialogKey = (e) => {
|
|
2821
2914
|
if (e.key === "Escape") {
|
|
@@ -2839,6 +2932,9 @@ var InputsUI = class {
|
|
|
2839
2932
|
this.onMoveMenuOutside = (e) => {
|
|
2840
2933
|
if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
|
|
2841
2934
|
};
|
|
2935
|
+
this.onRowMenuOutside = (e) => {
|
|
2936
|
+
if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
|
|
2937
|
+
};
|
|
2842
2938
|
if (!container.style.position) container.style.position = "relative";
|
|
2843
2939
|
if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
|
|
2844
2940
|
}
|
|
@@ -2846,6 +2942,36 @@ var InputsUI = class {
|
|
|
2846
2942
|
this.dialogHost = host;
|
|
2847
2943
|
if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
|
|
2848
2944
|
}
|
|
2945
|
+
/** The host shell's chrome size class — mobile makes the inputs dialog fullscreen
|
|
2946
|
+
* (an open dialog re-presents on the flip) and folds the legend behind its chip by
|
|
2947
|
+
* default (a phone-width plot has no room for the rows). */
|
|
2948
|
+
setLayoutMode(mode) {
|
|
2949
|
+
const mobile = mode === "mobile";
|
|
2950
|
+
if (mobile === this.mobileLayout) return;
|
|
2951
|
+
this.mobileLayout = mobile;
|
|
2952
|
+
if (this.overviewAction === null) this.legendFolded = mobile;
|
|
2953
|
+
this.syncFoldToggle();
|
|
2954
|
+
if (this.openId !== null) {
|
|
2955
|
+
const id = this.openId;
|
|
2956
|
+
this.closeOpenDialog();
|
|
2957
|
+
this.openDialog(id);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
/** Replace the fold toggle's behavior with a HOST action (or restore it with null):
|
|
2961
|
+
* the chip stays — objects icon + indicator count — but a press runs the action
|
|
2962
|
+
* (a multi-chart shell opens its indicator overview) instead of unfolding the rows,
|
|
2963
|
+
* which stay hidden while the override is in force. */
|
|
2964
|
+
setLegendOverviewAction(action) {
|
|
2965
|
+
if (action === this.overviewAction) return;
|
|
2966
|
+
this.overviewAction = action;
|
|
2967
|
+
this.legendFolded = action !== null ? true : this.mobileLayout;
|
|
2968
|
+
this.syncFoldToggle();
|
|
2969
|
+
}
|
|
2970
|
+
/** Open the settings dialog of one indicator (the legend gear's programmatic twin) —
|
|
2971
|
+
* no-op for an unknown id. */
|
|
2972
|
+
openSettingsFor(id) {
|
|
2973
|
+
if (this.rows.has(id)) this.openDialog(id);
|
|
2974
|
+
}
|
|
2849
2975
|
/** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
|
|
2850
2976
|
tip(store, id, anchor, text, wrap = false) {
|
|
2851
2977
|
const list = store.get(id) ?? [];
|
|
@@ -2873,12 +2999,12 @@ var InputsUI = class {
|
|
|
2873
2999
|
}
|
|
2874
3000
|
setTheme(theme) {
|
|
2875
3001
|
this.theme = theme;
|
|
3002
|
+
for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
|
|
2876
3003
|
for (const row of this.rows.values()) {
|
|
2877
|
-
|
|
2878
|
-
row.el.style.background = open ? theme.background : this.idleRowFill();
|
|
3004
|
+
row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
|
|
2879
3005
|
row.el.style.color = theme.textColor;
|
|
2880
3006
|
}
|
|
2881
|
-
if (this.foldToggle) this.
|
|
3007
|
+
if (this.foldToggle) this.syncFoldToggle();
|
|
2882
3008
|
}
|
|
2883
3009
|
/** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
|
|
2884
3010
|
setSymbolPicker(picker) {
|
|
@@ -2909,7 +3035,7 @@ var InputsUI = class {
|
|
|
2909
3035
|
btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
|
|
2910
3036
|
btn.className = "vela-ind-ctl";
|
|
2911
3037
|
btn.dataset.legendAction = action.id;
|
|
2912
|
-
btn.style.cssText =
|
|
3038
|
+
btn.style.cssText = LEGEND_CTL_CSS;
|
|
2913
3039
|
btn.addEventListener("click", (e) => {
|
|
2914
3040
|
e.stopPropagation();
|
|
2915
3041
|
action.run();
|
|
@@ -2927,6 +3053,54 @@ var InputsUI = class {
|
|
|
2927
3053
|
this.titlesVisible = visible;
|
|
2928
3054
|
this.reposition();
|
|
2929
3055
|
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Show/hide the plot values chart-wide (the settings dialog's Indicators → Values
|
|
3058
|
+
* toggle). "All" is meant literally: per-row context-menu overrides are cleared, so
|
|
3059
|
+
* every legend follows the new state.
|
|
3060
|
+
*/
|
|
3061
|
+
setValuesVisible(visible) {
|
|
3062
|
+
this.valuesVisible = visible;
|
|
3063
|
+
for (const row of this.rows.values()) {
|
|
3064
|
+
row.showValues = null;
|
|
3065
|
+
this.renderRowValues(row);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Push the current plot values of every indicator at once (the renderer calls this per
|
|
3070
|
+
* paint). Rows absent from the map (e.g. a hidden indicator) show no values. Cheap on
|
|
3071
|
+
* unchanged frames: each row diffs against its last rendered values before touching DOM.
|
|
3072
|
+
*/
|
|
3073
|
+
setPlotValues(values) {
|
|
3074
|
+
for (const row of this.rows.values()) {
|
|
3075
|
+
const next = values.get(row.id) ?? [];
|
|
3076
|
+
const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
|
|
3077
|
+
if (key === row.plotValuesKey) continue;
|
|
3078
|
+
row.plotValues = next;
|
|
3079
|
+
row.plotValuesKey = key;
|
|
3080
|
+
this.renderRowValues(row);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
/** Whether a row currently shows its values: its own override, else the chart-wide flag. */
|
|
3084
|
+
rowValuesShown(row) {
|
|
3085
|
+
return row.showValues ?? this.valuesVisible;
|
|
3086
|
+
}
|
|
3087
|
+
/** (Re)build one row's values readout from its stored plot values + visibility. A
|
|
3088
|
+
* highlighted (hovered/selected) row shows only the title + controls — no values. */
|
|
3089
|
+
renderRowValues(row) {
|
|
3090
|
+
if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
|
|
3091
|
+
row.valuesEl.style.display = "none";
|
|
3092
|
+
row.valuesEl.replaceChildren();
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
row.valuesEl.style.display = "inline-flex";
|
|
3096
|
+
row.valuesEl.replaceChildren();
|
|
3097
|
+
for (const v of row.plotValues) {
|
|
3098
|
+
const span = document.createElement("span");
|
|
3099
|
+
span.textContent = v.value;
|
|
3100
|
+
span.style.color = v.color;
|
|
3101
|
+
row.valuesEl.appendChild(span);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
2930
3104
|
// ── legend move/merge (menu + drag) ─────────────────────────────────────
|
|
2931
3105
|
/** Open the "Move to" menu for a row, anchored under its move button. */
|
|
2932
3106
|
openMoveMenu(id, anchor) {
|
|
@@ -2978,6 +3152,48 @@ var InputsUI = class {
|
|
|
2978
3152
|
this.moveMenu?.remove();
|
|
2979
3153
|
this.moveMenu = null;
|
|
2980
3154
|
}
|
|
3155
|
+
// ── legend row context menu (right-click) ───────────────────────────────
|
|
3156
|
+
/** Open the row's action menu at the pointer. One entry today: the values toggle. */
|
|
3157
|
+
openRowMenu(id, x, y) {
|
|
3158
|
+
this.closeRowMenu();
|
|
3159
|
+
this.closeMoveMenu();
|
|
3160
|
+
const row = this.rows.get(id);
|
|
3161
|
+
if (!row) return;
|
|
3162
|
+
const menu = document.createElement("div");
|
|
3163
|
+
menu.style.cssText = `position:fixed;z-index:var(--vela-z-tooltip);min-width:170px;padding:4px;border-radius:var(--vela-radius-md);background:var(--vela-surface-elev);color:${this.theme.textColor};border:1px solid var(--vela-border);box-shadow:var(--vela-shadow);font-size:var(--vela-font-size-md);`;
|
|
3164
|
+
applyChromeTokens(menu, this.theme);
|
|
3165
|
+
const item = document.createElement("button");
|
|
3166
|
+
item.type = "button";
|
|
3167
|
+
item.className = "vela-ind-menuitem";
|
|
3168
|
+
item.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:6px 10px;border:none;border-radius:var(--vela-radius-sm);color:inherit;cursor:pointer;white-space:nowrap;";
|
|
3169
|
+
const check = document.createElement("span");
|
|
3170
|
+
check.style.cssText = "display:inline-flex;align-items:center;justify-content:center;width:14px;flex:none;line-height:0;";
|
|
3171
|
+
check.innerHTML = this.rowValuesShown(row) ? CHECK_SVG : "";
|
|
3172
|
+
const label = document.createElement("span");
|
|
3173
|
+
label.textContent = "Indicator values";
|
|
3174
|
+
item.append(check, label);
|
|
3175
|
+
item.addEventListener("click", (e) => {
|
|
3176
|
+
e.stopPropagation();
|
|
3177
|
+
this.closeRowMenu();
|
|
3178
|
+
row.showValues = !this.rowValuesShown(row);
|
|
3179
|
+
this.renderRowValues(row);
|
|
3180
|
+
});
|
|
3181
|
+
menu.appendChild(item);
|
|
3182
|
+
document.body.appendChild(menu);
|
|
3183
|
+
const mw = menu.offsetWidth || 180;
|
|
3184
|
+
const mh = menu.offsetHeight || 40;
|
|
3185
|
+
menu.style.left = `${Math.min(x, window.innerWidth - mw - 6)}px`;
|
|
3186
|
+
menu.style.top = `${Math.min(y, window.innerHeight - mh - 6)}px`;
|
|
3187
|
+
this.rowMenu = menu;
|
|
3188
|
+
setTimeout(() => {
|
|
3189
|
+
if (typeof document !== "undefined") document.addEventListener("pointerdown", this.onRowMenuOutside, true);
|
|
3190
|
+
}, 0);
|
|
3191
|
+
}
|
|
3192
|
+
closeRowMenu() {
|
|
3193
|
+
if (typeof document !== "undefined") document.removeEventListener("pointerdown", this.onRowMenuOutside, true);
|
|
3194
|
+
this.rowMenu?.remove();
|
|
3195
|
+
this.rowMenu = null;
|
|
3196
|
+
}
|
|
2981
3197
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
2982
3198
|
setPane(id, paneId) {
|
|
2983
3199
|
const row = this.rows.get(id);
|
|
@@ -2999,7 +3215,8 @@ var InputsUI = class {
|
|
|
2999
3215
|
if (!lg) {
|
|
3000
3216
|
lg = document.createElement("div");
|
|
3001
3217
|
lg.dataset.velaPane = paneId;
|
|
3002
|
-
lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:
|
|
3218
|
+
lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
|
|
3219
|
+
applyChromeTokens(lg, this.theme);
|
|
3003
3220
|
this.positionLegend(lg, paneId);
|
|
3004
3221
|
this.container.appendChild(lg);
|
|
3005
3222
|
this.legends.set(paneId, lg);
|
|
@@ -3035,15 +3252,16 @@ var InputsUI = class {
|
|
|
3035
3252
|
*/
|
|
3036
3253
|
syncFoldToggle() {
|
|
3037
3254
|
const count = this.rows.size;
|
|
3038
|
-
|
|
3255
|
+
const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
|
|
3256
|
+
if (count < minRows) {
|
|
3039
3257
|
const wasFolded = this.legendFolded;
|
|
3040
|
-
this.legendFolded = false;
|
|
3258
|
+
if (count > 0) this.legendFolded = false;
|
|
3041
3259
|
if (this.foldToggle) {
|
|
3042
3260
|
this.foldToggle.remove();
|
|
3043
3261
|
this.foldToggle = null;
|
|
3044
3262
|
this.disposeTips(this.rowTips, "fold");
|
|
3045
3263
|
}
|
|
3046
|
-
if (wasFolded) this.reposition();
|
|
3264
|
+
if (wasFolded && count > 0) this.reposition();
|
|
3047
3265
|
return;
|
|
3048
3266
|
}
|
|
3049
3267
|
let btn = this.foldToggle;
|
|
@@ -3051,25 +3269,30 @@ var InputsUI = class {
|
|
|
3051
3269
|
btn = document.createElement("button");
|
|
3052
3270
|
btn.type = "button";
|
|
3053
3271
|
btn.className = "vela-ind-fold";
|
|
3054
|
-
this.tip(this.rowTips, "fold", btn, () => this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
|
|
3272
|
+
this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
|
|
3055
3273
|
btn.addEventListener("click", () => {
|
|
3274
|
+
if (this.overviewAction !== null) {
|
|
3275
|
+
this.overviewAction();
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3056
3278
|
this.legendFolded = !this.legendFolded;
|
|
3057
3279
|
this.syncFoldToggle();
|
|
3058
3280
|
});
|
|
3059
3281
|
this.foldToggle = btn;
|
|
3060
3282
|
}
|
|
3061
3283
|
const folded = this.legendFolded;
|
|
3062
|
-
btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
|
|
3063
|
-
|
|
3284
|
+
btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
|
|
3285
|
+
const ink = this.theme.textColor;
|
|
3286
|
+
btn.style.cssText = `pointer-events:auto;display:inline-flex;align-items:center;gap:4px;background:${this.theme.background};color:${ink};border:1px solid ${this.theme.borderColor};border-radius:4px;padding:2px 6px;margin-top:6px;cursor:pointer;font:inherit;line-height:0;`;
|
|
3064
3287
|
btn.replaceChildren();
|
|
3065
3288
|
const icon2 = document.createElement("span");
|
|
3066
|
-
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
|
|
3067
|
-
icon2.innerHTML = folded ? UNFOLD_SVG : FOLD_SVG;
|
|
3289
|
+
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
|
|
3290
|
+
icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
|
|
3068
3291
|
btn.appendChild(icon2);
|
|
3069
3292
|
if (folded) {
|
|
3070
3293
|
const label = document.createElement("span");
|
|
3071
3294
|
label.textContent = String(count);
|
|
3072
|
-
label.style.cssText =
|
|
3295
|
+
label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
|
|
3073
3296
|
btn.appendChild(label);
|
|
3074
3297
|
}
|
|
3075
3298
|
this.legendFor("price").appendChild(btn);
|
|
@@ -3077,9 +3300,11 @@ var InputsUI = class {
|
|
|
3077
3300
|
}
|
|
3078
3301
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
3079
3302
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
3303
|
+
const settingsTitle = opts.settingsTitle ?? title;
|
|
3080
3304
|
const existing = this.rows.get(id);
|
|
3081
3305
|
if (existing) {
|
|
3082
3306
|
existing.title = title;
|
|
3307
|
+
existing.settingsTitle = settingsTitle;
|
|
3083
3308
|
existing.inputs = inputs;
|
|
3084
3309
|
existing.values = { ...values };
|
|
3085
3310
|
existing.titleEl.textContent = title;
|
|
@@ -3092,8 +3317,7 @@ var InputsUI = class {
|
|
|
3092
3317
|
}
|
|
3093
3318
|
ensureDialogStyles();
|
|
3094
3319
|
const el = document.createElement("div");
|
|
3095
|
-
el.style.cssText = `pointer-events:auto;display:flex;align-items:center;
|
|
3096
|
-
el.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
|
|
3320
|
+
el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
|
|
3097
3321
|
el.addEventListener("mouseleave", () => {
|
|
3098
3322
|
if (this.selectedId !== id) this.setRowHighlighted(id, false);
|
|
3099
3323
|
});
|
|
@@ -3113,11 +3337,16 @@ var InputsUI = class {
|
|
|
3113
3337
|
e.preventDefault();
|
|
3114
3338
|
this.onRemove?.(id);
|
|
3115
3339
|
});
|
|
3340
|
+
el.addEventListener("contextmenu", (e) => {
|
|
3341
|
+
e.preventDefault();
|
|
3342
|
+
e.stopPropagation();
|
|
3343
|
+
this.openRowMenu(id, e.clientX, e.clientY);
|
|
3344
|
+
});
|
|
3116
3345
|
const statusEl = document.createElement("span");
|
|
3117
3346
|
statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
3118
|
-
el.appendChild(statusEl);
|
|
3119
3347
|
const titleWrap = document.createElement("span");
|
|
3120
3348
|
titleWrap.style.cssText = "white-space:nowrap;";
|
|
3349
|
+
titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
|
|
3121
3350
|
const titleEl = document.createElement("span");
|
|
3122
3351
|
titleEl.textContent = title;
|
|
3123
3352
|
titleEl.style.cssText = "font-weight:600;";
|
|
@@ -3129,6 +3358,11 @@ var InputsUI = class {
|
|
|
3129
3358
|
titleWrap.appendChild(beta);
|
|
3130
3359
|
}
|
|
3131
3360
|
el.appendChild(titleWrap);
|
|
3361
|
+
const valuesEl = document.createElement("span");
|
|
3362
|
+
valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
|
|
3363
|
+
el.appendChild(valuesEl);
|
|
3364
|
+
const controlsEl = document.createElement("span");
|
|
3365
|
+
controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
|
|
3132
3366
|
let eyeEl = null;
|
|
3133
3367
|
if (this.onToggleVisible) {
|
|
3134
3368
|
const eye = document.createElement("button");
|
|
@@ -3137,16 +3371,14 @@ var InputsUI = class {
|
|
|
3137
3371
|
this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
|
|
3138
3372
|
eye.innerHTML = EYE_SVG;
|
|
3139
3373
|
eye.className = "vela-ind-ctl";
|
|
3140
|
-
eye.style.cssText =
|
|
3374
|
+
eye.style.cssText = LEGEND_CTL_CSS;
|
|
3141
3375
|
eye.addEventListener("click", () => {
|
|
3142
3376
|
const row = this.rows.get(id);
|
|
3143
3377
|
this.onToggleVisible?.(id, Boolean(row?.hidden));
|
|
3144
3378
|
});
|
|
3145
|
-
|
|
3379
|
+
controlsEl.appendChild(eye);
|
|
3146
3380
|
eyeEl = eye;
|
|
3147
3381
|
}
|
|
3148
|
-
const controlsEl = document.createElement("span");
|
|
3149
|
-
controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
|
|
3150
3382
|
if (inputs.length > 0) {
|
|
3151
3383
|
const gear = document.createElement("button");
|
|
3152
3384
|
gear.type = "button";
|
|
@@ -3154,7 +3386,7 @@ var InputsUI = class {
|
|
|
3154
3386
|
this.tip(this.rowTips, id, gear, "Settings");
|
|
3155
3387
|
gear.innerHTML = GEAR_SVG;
|
|
3156
3388
|
gear.className = "vela-ind-ctl";
|
|
3157
|
-
gear.style.cssText =
|
|
3389
|
+
gear.style.cssText = LEGEND_CTL_CSS;
|
|
3158
3390
|
gear.addEventListener("click", () => this.openDialog(id));
|
|
3159
3391
|
controlsEl.appendChild(gear);
|
|
3160
3392
|
}
|
|
@@ -3165,7 +3397,7 @@ var InputsUI = class {
|
|
|
3165
3397
|
this.tip(this.rowTips, id, mv, "Move to pane");
|
|
3166
3398
|
mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
|
|
3167
3399
|
mv.className = "vela-ind-ctl";
|
|
3168
|
-
mv.style.cssText =
|
|
3400
|
+
mv.style.cssText = LEGEND_CTL_CSS;
|
|
3169
3401
|
mv.addEventListener("click", (e) => {
|
|
3170
3402
|
e.stopPropagation();
|
|
3171
3403
|
this.openMoveMenu(id, mv);
|
|
@@ -3182,12 +3414,13 @@ var InputsUI = class {
|
|
|
3182
3414
|
this.tip(this.rowTips, id, close, "Remove indicator");
|
|
3183
3415
|
close.innerHTML = CLOSE_SVG;
|
|
3184
3416
|
close.className = "vela-ind-close";
|
|
3185
|
-
close.style.cssText =
|
|
3417
|
+
close.style.cssText = LEGEND_CTL_CSS;
|
|
3186
3418
|
close.addEventListener("click", () => this.onRemove?.(id));
|
|
3187
3419
|
controlsEl.appendChild(close);
|
|
3188
3420
|
el.appendChild(controlsEl);
|
|
3421
|
+
el.appendChild(statusEl);
|
|
3189
3422
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
3190
|
-
this.rows.set(id, { id, title, inputs, values: { ...values }, el, titleEl, statusEl, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
|
|
3423
|
+
this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
|
|
3191
3424
|
this.syncFoldToggle();
|
|
3192
3425
|
}
|
|
3193
3426
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -3201,24 +3434,31 @@ var InputsUI = class {
|
|
|
3201
3434
|
if (row) row.values = { ...row.values, ...values };
|
|
3202
3435
|
}
|
|
3203
3436
|
/**
|
|
3204
|
-
* Reflect an indicator's live status in its legend row: `'loading'` shows
|
|
3205
|
-
* in flight
|
|
3437
|
+
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
3438
|
+
* dots (a fetch is in flight — the same load affordance as the chart's own bar-load dots,
|
|
3439
|
+
* at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
|
|
3206
3440
|
*/
|
|
3207
3441
|
setStatus(id, status) {
|
|
3208
3442
|
const row = this.rows.get(id);
|
|
3209
3443
|
if (!row) return;
|
|
3210
3444
|
const el = row.statusEl;
|
|
3445
|
+
el.replaceChildren();
|
|
3211
3446
|
if (status === "idle") {
|
|
3212
|
-
el.style.
|
|
3213
|
-
el.style.animation = "none";
|
|
3447
|
+
el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
3214
3448
|
return;
|
|
3215
3449
|
}
|
|
3216
|
-
this.ensureStatusKeyframes();
|
|
3217
3450
|
if (status === "loading") {
|
|
3218
|
-
el.style.cssText =
|
|
3219
|
-
|
|
3220
|
-
|
|
3451
|
+
el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
|
|
3452
|
+
for (let i = 0; i < 3; i += 1) {
|
|
3453
|
+
const dot = document.createElement("span");
|
|
3454
|
+
dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
|
|
3455
|
+
dot.animate?.([{ opacity: 0.12 }, { opacity: 0.55 }], { duration: 800, iterations: Infinity, direction: "alternate", easing: "ease-in-out", delay: -i * 260 });
|
|
3456
|
+
el.appendChild(dot);
|
|
3457
|
+
}
|
|
3458
|
+
return;
|
|
3221
3459
|
}
|
|
3460
|
+
this.ensureStatusKeyframes();
|
|
3461
|
+
el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
|
|
3222
3462
|
}
|
|
3223
3463
|
/** Inject the status keyframes once (idempotent). */
|
|
3224
3464
|
ensureStatusKeyframes() {
|
|
@@ -3237,8 +3477,8 @@ var InputsUI = class {
|
|
|
3237
3477
|
if (row.eyeEl) {
|
|
3238
3478
|
row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
|
|
3239
3479
|
row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
|
|
3240
|
-
row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
|
|
3241
3480
|
}
|
|
3481
|
+
this.syncRowActions(row);
|
|
3242
3482
|
}
|
|
3243
3483
|
/**
|
|
3244
3484
|
* Select a legend row (outlining it) and clear any previous selection, so at most one
|
|
@@ -3251,17 +3491,36 @@ var InputsUI = class {
|
|
|
3251
3491
|
this.setRowHighlighted(id, true);
|
|
3252
3492
|
}
|
|
3253
3493
|
/**
|
|
3254
|
-
* Show or hide a row's outline and eye/gear/✕ controls together — driven by
|
|
3255
|
-
*
|
|
3256
|
-
*
|
|
3494
|
+
* Show or hide a row's outline and eye/gear/✕ controls together — driven by title
|
|
3495
|
+
* hover and selection (plot values do not open the chip). An idle unselected row
|
|
3496
|
+
* shows just its title (+ values). Exception: a hidden indicator keeps its eye
|
|
3497
|
+
* visible even when idle, so it can be un-hidden without hovering.
|
|
3257
3498
|
*/
|
|
3258
3499
|
setRowHighlighted(id, highlighted) {
|
|
3259
3500
|
const row = this.rows.get(id);
|
|
3260
3501
|
if (!row) return;
|
|
3502
|
+
row.highlighted = highlighted;
|
|
3503
|
+
this.renderRowValues(row);
|
|
3261
3504
|
row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
|
|
3262
3505
|
row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
|
|
3263
|
-
row
|
|
3264
|
-
|
|
3506
|
+
this.syncRowActions(row);
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
|
|
3510
|
+
* buttons only appear while highlighted; the eye keeps its inline-flex from
|
|
3511
|
+
* construction and is shown/hidden with the cluster container.
|
|
3512
|
+
*/
|
|
3513
|
+
syncRowActions(row) {
|
|
3514
|
+
const open = row.highlighted;
|
|
3515
|
+
row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
|
|
3516
|
+
for (const child of Array.from(row.controlsEl.children)) {
|
|
3517
|
+
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
3518
|
+
if (child === row.extrasEl) {
|
|
3519
|
+
child.style.display = open ? "contents" : "none";
|
|
3520
|
+
continue;
|
|
3521
|
+
}
|
|
3522
|
+
child.style.display = open ? "inline-flex" : "none";
|
|
3523
|
+
}
|
|
3265
3524
|
}
|
|
3266
3525
|
/** Drop the current selection outline, if any. */
|
|
3267
3526
|
clearSelection() {
|
|
@@ -3289,6 +3548,7 @@ var InputsUI = class {
|
|
|
3289
3548
|
destroy() {
|
|
3290
3549
|
this.closeDialog();
|
|
3291
3550
|
this.closeMoveMenu();
|
|
3551
|
+
this.closeRowMenu();
|
|
3292
3552
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
3293
3553
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
3294
3554
|
for (const lg of this.legends.values()) lg.remove();
|
|
@@ -3344,6 +3604,7 @@ var InputsUI = class {
|
|
|
3344
3604
|
const card = document.createElement("div");
|
|
3345
3605
|
card.className = "vela-ind-dialog";
|
|
3346
3606
|
card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(360px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:13px ${font};overflow:hidden;`;
|
|
3607
|
+
if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
|
|
3347
3608
|
applyChromeTokens(card, t);
|
|
3348
3609
|
card.addEventListener("keydown", (e) => {
|
|
3349
3610
|
if (e.key !== "Escape") e.stopPropagation();
|
|
@@ -3351,7 +3612,7 @@ var InputsUI = class {
|
|
|
3351
3612
|
const header = document.createElement("div");
|
|
3352
3613
|
header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
|
|
3353
3614
|
const hTitle = document.createElement("span");
|
|
3354
|
-
hTitle.textContent = row.
|
|
3615
|
+
hTitle.textContent = row.settingsTitle;
|
|
3355
3616
|
hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
|
|
3356
3617
|
const closeBtn = document.createElement("button");
|
|
3357
3618
|
closeBtn.type = "button";
|
|
@@ -3363,7 +3624,7 @@ var InputsUI = class {
|
|
|
3363
3624
|
closeBtn.addEventListener("click", () => this.closeDialog());
|
|
3364
3625
|
header.append(hTitle, closeBtn);
|
|
3365
3626
|
card.appendChild(header);
|
|
3366
|
-
makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
3627
|
+
if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
3367
3628
|
const tabs = document.createElement("div");
|
|
3368
3629
|
tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
3369
3630
|
const tab = document.createElement("div");
|
|
@@ -3383,7 +3644,7 @@ var InputsUI = class {
|
|
|
3383
3644
|
section.appendChild(gh);
|
|
3384
3645
|
}
|
|
3385
3646
|
const grid = document.createElement("div");
|
|
3386
|
-
grid.style.cssText = "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
|
|
3647
|
+
grid.style.cssText = this.mobileLayout ? "display:grid;grid-template-columns:minmax(0,1fr) auto auto max-content;align-items:center;column-gap:12px;row-gap:22px;" : "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
|
|
3387
3648
|
for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
|
|
3388
3649
|
section.appendChild(grid);
|
|
3389
3650
|
body.appendChild(section);
|
|
@@ -3832,10 +4093,14 @@ function groupInputs(inputs) {
|
|
|
3832
4093
|
var CHECK_SVG = iconAt("check", 12);
|
|
3833
4094
|
var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
|
|
3834
4095
|
var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
|
|
4096
|
+
var DIALOG_STYLE_REV = "10";
|
|
3835
4097
|
function ensureDialogStyles() {
|
|
3836
|
-
if (typeof document === "undefined"
|
|
3837
|
-
const
|
|
4098
|
+
if (typeof document === "undefined") return;
|
|
4099
|
+
const existing = document.getElementById(DIALOG_STYLE_ID);
|
|
4100
|
+
if (existing?.dataset.rev === DIALOG_STYLE_REV) return;
|
|
4101
|
+
const s = existing ?? document.createElement("style");
|
|
3838
4102
|
s.id = DIALOG_STYLE_ID;
|
|
4103
|
+
s.dataset.rev = DIALOG_STYLE_REV;
|
|
3839
4104
|
s.textContent = `
|
|
3840
4105
|
.vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
|
|
3841
4106
|
.vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -3849,19 +4114,19 @@ function ensureDialogStyles() {
|
|
|
3849
4114
|
.vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
|
|
3850
4115
|
.vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
3851
4116
|
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
3852
|
-
.vela-ind-ctl{color:var(--vela-
|
|
3853
|
-
.vela-ind-ctl
|
|
3854
|
-
.vela-ind-
|
|
3855
|
-
.vela-ind-close:hover{color:var(--vela-danger) !important;}
|
|
3856
|
-
.vela-ind-fold{
|
|
3857
|
-
.vela-ind-fold:hover{
|
|
4117
|
+
.vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
|
|
4118
|
+
.vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
|
|
4119
|
+
.vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
|
|
4120
|
+
.vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
|
|
4121
|
+
.vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
|
|
4122
|
+
.vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
|
|
3858
4123
|
.vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
3859
4124
|
.vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
|
|
3860
4125
|
.vela-ind-btn{cursor:pointer;padding:7px 14px;border-radius:var(--vela-radius-md);border:1px solid transparent;background:transparent;color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
|
|
3861
4126
|
.vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
3862
4127
|
.vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
3863
4128
|
.vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
|
|
3864
|
-
document.head.appendChild(s);
|
|
4129
|
+
if (!existing) document.head.appendChild(s);
|
|
3865
4130
|
}
|
|
3866
4131
|
var TIMEFRAME_OPTIONS = [
|
|
3867
4132
|
{ value: "", label: "Chart" },
|
|
@@ -4470,6 +4735,44 @@ function basePaintingOf(style) {
|
|
|
4470
4735
|
for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
|
|
4471
4736
|
return "candles";
|
|
4472
4737
|
}
|
|
4738
|
+
function hasOwnCandlePaint(style) {
|
|
4739
|
+
if (BUILTIN_PRICE_STYLES.includes(style)) return false;
|
|
4740
|
+
for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
|
|
4741
|
+
return false;
|
|
4742
|
+
}
|
|
4743
|
+
function candleOverrideFor(style, bags) {
|
|
4744
|
+
if (!hasOwnCandlePaint(style)) return null;
|
|
4745
|
+
const bag = bags[style] ?? {};
|
|
4746
|
+
const color = (v) => isColor(v) ? v : null;
|
|
4747
|
+
const bool = (v) => isBool(v) ? v : null;
|
|
4748
|
+
return {
|
|
4749
|
+
upColor: color(bag.candleUpColor),
|
|
4750
|
+
downColor: color(bag.candleDownColor),
|
|
4751
|
+
bodyVisible: bool(bag.candleBodyVisible),
|
|
4752
|
+
borderVisible: bool(bag.candleBorderVisible),
|
|
4753
|
+
borderUpColor: color(bag.candleBorderUpColor),
|
|
4754
|
+
borderDownColor: color(bag.candleBorderDownColor),
|
|
4755
|
+
wickVisible: bool(bag.candleWickVisible),
|
|
4756
|
+
wickUpColor: color(bag.candleWickUpColor),
|
|
4757
|
+
wickDownColor: color(bag.candleWickDownColor)
|
|
4758
|
+
};
|
|
4759
|
+
}
|
|
4760
|
+
function effectiveCandlePaint(base, override, themeUp, themeDown) {
|
|
4761
|
+
if (!override) return { up: themeUp, down: themeDown, candle: base };
|
|
4762
|
+
return {
|
|
4763
|
+
up: override.upColor ?? themeUp,
|
|
4764
|
+
down: override.downColor ?? themeDown,
|
|
4765
|
+
candle: {
|
|
4766
|
+
bodyVisible: override.bodyVisible ?? base.bodyVisible,
|
|
4767
|
+
borderVisible: override.borderVisible ?? base.borderVisible,
|
|
4768
|
+
borderUpColor: override.borderUpColor ?? base.borderUpColor,
|
|
4769
|
+
borderDownColor: override.borderDownColor ?? base.borderDownColor,
|
|
4770
|
+
wickVisible: override.wickVisible ?? base.wickVisible,
|
|
4771
|
+
wickUpColor: override.wickUpColor ?? base.wickUpColor,
|
|
4772
|
+
wickDownColor: override.wickDownColor ?? base.wickDownColor
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
4775
|
+
}
|
|
4473
4776
|
function priceStyleIds() {
|
|
4474
4777
|
const out = [...BUILTIN_PRICE_STYLES];
|
|
4475
4778
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
@@ -4508,6 +4811,33 @@ function clampLevel(v) {
|
|
|
4508
4811
|
function clampSpacing(v) {
|
|
4509
4812
|
return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
|
|
4510
4813
|
}
|
|
4814
|
+
function factoryResetConfig(factory) {
|
|
4815
|
+
const bag = {};
|
|
4816
|
+
for (const t of chartTypes()) {
|
|
4817
|
+
const section = t.settings;
|
|
4818
|
+
if (!section) continue;
|
|
4819
|
+
const defaults = {};
|
|
4820
|
+
const addRows = (rows) => {
|
|
4821
|
+
for (const r of rows ?? []) {
|
|
4822
|
+
for (const k of settingsRowValueKeys(r)) defaults[k.key] = k.defval;
|
|
4823
|
+
}
|
|
4824
|
+
};
|
|
4825
|
+
for (const inst of section.instances ?? []) {
|
|
4826
|
+
if (inst.enableKey) defaults[inst.enableKey] = false;
|
|
4827
|
+
addRows(inst.rows);
|
|
4828
|
+
}
|
|
4829
|
+
for (const sub of section.subsections ?? []) {
|
|
4830
|
+
if (sub.enableKey) defaults[sub.enableKey] = false;
|
|
4831
|
+
addRows(sub.rows);
|
|
4832
|
+
}
|
|
4833
|
+
if (!section.instances) addRows(section.rows);
|
|
4834
|
+
bag[t.id] = defaults;
|
|
4835
|
+
}
|
|
4836
|
+
for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
|
|
4837
|
+
bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
|
|
4838
|
+
}
|
|
4839
|
+
return { ...factory, chartTypes: bag };
|
|
4840
|
+
}
|
|
4511
4841
|
function mergeConfig(base, patch) {
|
|
4512
4842
|
const p = asObject(patch);
|
|
4513
4843
|
const ctPatch = asObject(p.chartTypes);
|
|
@@ -4822,6 +5152,9 @@ var SceneGraph = class {
|
|
|
4822
5152
|
this.priceStyle = "candles";
|
|
4823
5153
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
4824
5154
|
this.basePainting = "candles";
|
|
5155
|
+
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
5156
|
+
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
5157
|
+
this.candleOverride = null;
|
|
4825
5158
|
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
4826
5159
|
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
4827
5160
|
* range (resolved per frame via `baselinePriceFor`). */
|
|
@@ -6051,14 +6384,15 @@ var WebGL2Backend = class {
|
|
|
6051
6384
|
emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
6052
6385
|
const spacing = coords.bodySpacing();
|
|
6053
6386
|
const tier = candleTier(spacing);
|
|
6387
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
6054
6388
|
if (tier === "aggregate") {
|
|
6055
|
-
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane,
|
|
6389
|
+
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
6056
6390
|
return;
|
|
6057
6391
|
}
|
|
6058
6392
|
const drawBody = tier === "full";
|
|
6059
|
-
const up =
|
|
6060
|
-
const down =
|
|
6061
|
-
const cs =
|
|
6393
|
+
const up = paint.up;
|
|
6394
|
+
const down = paint.down;
|
|
6395
|
+
const cs = paint.candle;
|
|
6062
6396
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
6063
6397
|
for (let i = i0; i <= i1; i += 1) {
|
|
6064
6398
|
const bar = bars[i];
|
|
@@ -6629,6 +6963,11 @@ function easeToward(current, target, dtMs, tauMs) {
|
|
|
6629
6963
|
var FLING_MIN_SPEED = 0.04;
|
|
6630
6964
|
var FLING_STALE_MS = 60;
|
|
6631
6965
|
var DRAG_SLOP = 2;
|
|
6966
|
+
var LONG_PRESS_MS = 350;
|
|
6967
|
+
var TOUCH_SLOP = 8;
|
|
6968
|
+
var TOUCH_TAP_SLOP = 14;
|
|
6969
|
+
var DOUBLE_TAP_MS = 350;
|
|
6970
|
+
var DOUBLE_TAP_SLOP = 30;
|
|
6632
6971
|
var TIME_SCALE_K = 4e-3;
|
|
6633
6972
|
var WHEEL_ZOOM_K = 25e-4;
|
|
6634
6973
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
@@ -6649,8 +6988,13 @@ function wheelPanDelta(deltaX, deltaY, shift) {
|
|
|
6649
6988
|
function effectiveSnapMode(momentaryOverride, sticky) {
|
|
6650
6989
|
return momentaryOverride ? "strong" : sticky;
|
|
6651
6990
|
}
|
|
6991
|
+
function pinchBarSpacing(startBarSpacing, startDist, dist) {
|
|
6992
|
+
return clampBarSpacing(startBarSpacing * (dist / Math.max(1, startDist)));
|
|
6993
|
+
}
|
|
6994
|
+
function pinchPinnedRightOffset(anchorLogical, barCount, width, x, pxPerBar) {
|
|
6995
|
+
return anchorLogical - (barCount - 1) + (width - x) / pxPerBar;
|
|
6996
|
+
}
|
|
6652
6997
|
var InputController = class {
|
|
6653
|
-
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
6654
6998
|
constructor(deps) {
|
|
6655
6999
|
this.deps = deps;
|
|
6656
7000
|
/** When true (the default), wheel-zoom pins the right edge instead of the bar
|
|
@@ -6678,9 +7022,40 @@ var InputController = class {
|
|
|
6678
7022
|
this.vx = 0;
|
|
6679
7023
|
// smoothed pointer velocity, px/ms
|
|
6680
7024
|
this.middleDeleted = false;
|
|
7025
|
+
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
7026
|
+
// Last cursor position over the element (NaN once it leaves) — lets a modifier
|
|
7027
|
+
// press/release re-shape the drawings preview with the pointer stationary.
|
|
7028
|
+
this.cursorX = NaN;
|
|
7029
|
+
this.cursorY = NaN;
|
|
7030
|
+
// ── touch state ──
|
|
7031
|
+
/** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
|
|
7032
|
+
this.touches = /* @__PURE__ */ new Map();
|
|
7033
|
+
this.lpTimer = null;
|
|
7034
|
+
this.pinchStartDist = 1;
|
|
7035
|
+
this.pinchStartBarSpacing = 0;
|
|
7036
|
+
this.pinchAnchorLogical = 0;
|
|
7037
|
+
// Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
|
|
7038
|
+
this.lastTapT = 0;
|
|
7039
|
+
this.lastTapX = 0;
|
|
7040
|
+
this.lastTapY = 0;
|
|
7041
|
+
this.onTouchContextMenu = (e) => {
|
|
7042
|
+
if (this.touches.size === 0) return;
|
|
7043
|
+
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
7044
|
+
e.preventDefault();
|
|
7045
|
+
e.stopImmediatePropagation();
|
|
7046
|
+
};
|
|
6681
7047
|
this.onMiddleGuard = (e) => {
|
|
6682
7048
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
6683
7049
|
};
|
|
7050
|
+
/** A modifier press/release with the pointer stationary: re-issue the last cursor
|
|
7051
|
+
* position so the placing ghost / drag preview reflects the new state immediately
|
|
7052
|
+
* (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
|
|
7053
|
+
this.onModifier = (e) => {
|
|
7054
|
+
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
7055
|
+
if (Number.isNaN(this.cursorX)) return;
|
|
7056
|
+
if (this.dragging && this.region !== "drawing") return;
|
|
7057
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
7058
|
+
};
|
|
6684
7059
|
this.onDown = (e) => {
|
|
6685
7060
|
if (e.button === 1) {
|
|
6686
7061
|
const { x: x2, y: y2 } = this.local(e);
|
|
@@ -6690,6 +7065,15 @@ var InputController = class {
|
|
|
6690
7065
|
}
|
|
6691
7066
|
if (e.button !== 0) return;
|
|
6692
7067
|
const { x, y } = this.local(e);
|
|
7068
|
+
if (e.pointerType === "touch") {
|
|
7069
|
+
if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
|
|
7070
|
+
this.touches.set(e.pointerId, { x, y });
|
|
7071
|
+
this.beginPinch(e);
|
|
7072
|
+
return;
|
|
7073
|
+
}
|
|
7074
|
+
if (this.touches.size >= 1) return;
|
|
7075
|
+
this.touches.set(e.pointerId, { x, y });
|
|
7076
|
+
}
|
|
6693
7077
|
this.deps.drawingsClearTransient?.();
|
|
6694
7078
|
this.dragging = true;
|
|
6695
7079
|
this.moved = false;
|
|
@@ -6715,6 +7099,9 @@ var InputController = class {
|
|
|
6715
7099
|
if (this.region === "price") this.deps.beginPriceScale(x, y);
|
|
6716
7100
|
else if (this.region === "separator") this.deps.beginPaneResize(y);
|
|
6717
7101
|
else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
|
|
7102
|
+
if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
|
|
7103
|
+
this.startLongPress(x, y, this.region);
|
|
7104
|
+
}
|
|
6718
7105
|
this.lastX = x;
|
|
6719
7106
|
this.lastT = e.timeStamp;
|
|
6720
7107
|
this.vx = 0;
|
|
@@ -6722,28 +7109,44 @@ var InputController = class {
|
|
|
6722
7109
|
};
|
|
6723
7110
|
this.onMove = (e) => {
|
|
6724
7111
|
const { x, y } = this.local(e);
|
|
7112
|
+
this.cursorX = x;
|
|
7113
|
+
this.cursorY = y;
|
|
7114
|
+
if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
|
|
7115
|
+
if (this.dragging && this.region === "pinch") {
|
|
7116
|
+
this.applyPinch();
|
|
7117
|
+
return;
|
|
7118
|
+
}
|
|
7119
|
+
if (this.dragging && this.region === "crosshair") {
|
|
7120
|
+
this.deps.onPointerMove(x, y);
|
|
7121
|
+
return;
|
|
7122
|
+
}
|
|
7123
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
7124
|
+
return;
|
|
7125
|
+
}
|
|
7126
|
+
if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
|
|
7127
|
+
const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
|
|
6725
7128
|
if (this.dragging) {
|
|
6726
7129
|
if (this.region === "price") {
|
|
6727
|
-
if (Math.abs(y - this.startY) >
|
|
7130
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6728
7131
|
this.deps.priceScaleBy(y - this.startY);
|
|
6729
7132
|
} else if (this.region === "separator") {
|
|
6730
|
-
if (Math.abs(y - this.startY) >
|
|
7133
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6731
7134
|
this.deps.paneResizeBy(y - this.startY);
|
|
6732
7135
|
} else if (this.region === "time") {
|
|
6733
|
-
if (Math.abs(x - this.startX) >
|
|
7136
|
+
if (Math.abs(x - this.startX) > slop) this.moved = true;
|
|
6734
7137
|
const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
|
|
6735
7138
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
6736
7139
|
} else if (this.region === "drawing") {
|
|
6737
|
-
if (Math.abs(x - this.startX) >
|
|
6738
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
7140
|
+
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
7141
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6739
7142
|
} else {
|
|
6740
7143
|
const coords = this.deps.getCoords();
|
|
6741
7144
|
const vp = coords.getViewport();
|
|
6742
7145
|
const dx = x - this.startX;
|
|
6743
|
-
if (Math.abs(dx) >
|
|
7146
|
+
if (Math.abs(dx) > slop) this.moved = true;
|
|
6744
7147
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
|
|
6745
7148
|
if (this.verticalPan) {
|
|
6746
|
-
if (Math.abs(y - this.startY) >
|
|
7149
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6747
7150
|
this.deps.pricePanBy(y - this.startY);
|
|
6748
7151
|
}
|
|
6749
7152
|
const dt = e.timeStamp - this.lastT;
|
|
@@ -6758,16 +7161,46 @@ var InputController = class {
|
|
|
6758
7161
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
6759
7162
|
const r = this.regionAt(x, y);
|
|
6760
7163
|
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));
|
|
7164
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6762
7165
|
}
|
|
6763
|
-
this.deps.onPointerMove(x, y);
|
|
7166
|
+
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
6764
7167
|
};
|
|
6765
7168
|
this.onUp = (e) => {
|
|
7169
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7170
|
+
this.cancelLongPress();
|
|
7171
|
+
if (this.dragging && this.region === "pinch") {
|
|
7172
|
+
if (this.touches.size === 1) {
|
|
7173
|
+
const rest = [...this.touches.values()][0];
|
|
7174
|
+
const vp = this.deps.getCoords().getViewport();
|
|
7175
|
+
this.region = "data";
|
|
7176
|
+
this.verticalPan = false;
|
|
7177
|
+
this.startX = rest.x;
|
|
7178
|
+
this.startY = rest.y;
|
|
7179
|
+
this.startRightOffset = vp.rightOffset;
|
|
7180
|
+
this.startBarSpacing = vp.barSpacing;
|
|
7181
|
+
this.lastX = rest.x;
|
|
7182
|
+
this.lastT = e.timeStamp;
|
|
7183
|
+
this.vx = 0;
|
|
7184
|
+
return;
|
|
7185
|
+
}
|
|
7186
|
+
this.endGesture(e);
|
|
7187
|
+
return;
|
|
7188
|
+
}
|
|
7189
|
+
if (this.dragging && this.region === "crosshair") {
|
|
7190
|
+
this.deps.onPointerMove(null, null);
|
|
7191
|
+
this.endGesture(e);
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
7195
|
+
this.endGesture(e);
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
const { x, y } = this.local(e);
|
|
7199
|
+
const wasTouch = e.pointerType === "touch";
|
|
7200
|
+
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
6766
7201
|
if (this.dragging && this.region === "drawing") {
|
|
6767
|
-
const { x, y } = this.local(e);
|
|
6768
7202
|
this.deps.drawingsPointerUp?.(x, y);
|
|
6769
|
-
} else if (
|
|
6770
|
-
const { x, y } = this.local(e);
|
|
7203
|
+
} else if (tapRelease && this.region === "data") {
|
|
6771
7204
|
this.deps.onClick(x, y);
|
|
6772
7205
|
} else if (this.dragging && this.region === "data") {
|
|
6773
7206
|
const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
|
|
@@ -6776,23 +7209,29 @@ var InputController = class {
|
|
|
6776
7209
|
this.deps.fling(-this.vx / pitch);
|
|
6777
7210
|
}
|
|
6778
7211
|
}
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
7212
|
+
const wasTap = wasTouch && tapRelease;
|
|
7213
|
+
this.endGesture(e);
|
|
7214
|
+
if (wasTouch) this.deps.onPointerMove(null, null);
|
|
7215
|
+
if (wasTap) this.registerTap(e.timeStamp, x, y);
|
|
7216
|
+
};
|
|
7217
|
+
/** The browser took the pointer back (system gesture, palm rejection, tab switch):
|
|
7218
|
+
* drop the gesture where it is — no click, no fling, no stuck `dragging`. */
|
|
7219
|
+
this.onCancel = (e) => {
|
|
7220
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7221
|
+
this.cancelLongPress();
|
|
7222
|
+
if (!this.dragging) return;
|
|
7223
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
7224
|
+
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
7225
|
+
this.endGesture(e);
|
|
6784
7226
|
};
|
|
6785
7227
|
this.onLeave = () => {
|
|
7228
|
+
this.cursorX = NaN;
|
|
7229
|
+
this.cursorY = NaN;
|
|
6786
7230
|
this.deps.onPointerMove(null, null);
|
|
6787
7231
|
};
|
|
6788
7232
|
this.onDblClick = (e) => {
|
|
6789
7233
|
const { x, y } = this.local(e);
|
|
6790
|
-
|
|
6791
|
-
const region = this.regionAt(x, y);
|
|
6792
|
-
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
6793
|
-
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
6794
|
-
else if (region === "time") this.deps.resetView();
|
|
6795
|
-
else this.deps.dataDblClick(x, y);
|
|
7234
|
+
this.doubleActivate(x, y);
|
|
6796
7235
|
};
|
|
6797
7236
|
this.onWheel = (e) => {
|
|
6798
7237
|
e.preventDefault();
|
|
@@ -6813,26 +7252,44 @@ var InputController = class {
|
|
|
6813
7252
|
}
|
|
6814
7253
|
attach(el) {
|
|
6815
7254
|
this.el = el;
|
|
7255
|
+
if (el.style !== void 0) {
|
|
7256
|
+
el.style.touchAction = "none";
|
|
7257
|
+
el.style.userSelect = "none";
|
|
7258
|
+
el.style.setProperty("-webkit-user-select", "none");
|
|
7259
|
+
el.style.setProperty("-webkit-touch-callout", "none");
|
|
7260
|
+
}
|
|
6816
7261
|
el.addEventListener("pointerdown", this.onDown);
|
|
6817
7262
|
el.addEventListener("pointermove", this.onMove);
|
|
6818
7263
|
el.addEventListener("pointerup", this.onUp);
|
|
6819
7264
|
el.addEventListener("pointerleave", this.onLeave);
|
|
7265
|
+
el.addEventListener("pointercancel", this.onCancel);
|
|
6820
7266
|
el.addEventListener("dblclick", this.onDblClick);
|
|
6821
7267
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
6822
7268
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
6823
7269
|
el.addEventListener("auxclick", this.onMiddleGuard);
|
|
7270
|
+
const win = el.ownerDocument?.defaultView;
|
|
7271
|
+
win?.addEventListener("keydown", this.onModifier);
|
|
7272
|
+
win?.addEventListener("keyup", this.onModifier);
|
|
7273
|
+
win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
6824
7274
|
}
|
|
6825
7275
|
detach() {
|
|
6826
7276
|
const el = this.el;
|
|
6827
7277
|
if (!el) return;
|
|
7278
|
+
const win = el.ownerDocument?.defaultView;
|
|
7279
|
+
win?.removeEventListener("keydown", this.onModifier);
|
|
7280
|
+
win?.removeEventListener("keyup", this.onModifier);
|
|
7281
|
+
win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
6828
7282
|
el.removeEventListener("pointerdown", this.onDown);
|
|
6829
7283
|
el.removeEventListener("pointermove", this.onMove);
|
|
6830
7284
|
el.removeEventListener("pointerup", this.onUp);
|
|
6831
7285
|
el.removeEventListener("pointerleave", this.onLeave);
|
|
7286
|
+
el.removeEventListener("pointercancel", this.onCancel);
|
|
6832
7287
|
el.removeEventListener("dblclick", this.onDblClick);
|
|
6833
7288
|
el.removeEventListener("wheel", this.onWheel);
|
|
6834
7289
|
el.removeEventListener("mousedown", this.onMiddleGuard);
|
|
6835
7290
|
el.removeEventListener("auxclick", this.onMiddleGuard);
|
|
7291
|
+
this.cancelLongPress();
|
|
7292
|
+
this.touches.clear();
|
|
6836
7293
|
this.el = null;
|
|
6837
7294
|
}
|
|
6838
7295
|
local(e) {
|
|
@@ -6856,6 +7313,59 @@ var InputController = class {
|
|
|
6856
7313
|
snapMode(e) {
|
|
6857
7314
|
return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
|
|
6858
7315
|
}
|
|
7316
|
+
/** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
|
|
7317
|
+
* movement past TOUCH_SLOP or a second finger cancels it. */
|
|
7318
|
+
startLongPress(x, y, region) {
|
|
7319
|
+
this.cancelLongPress();
|
|
7320
|
+
this.lpTimer = setTimeout(() => {
|
|
7321
|
+
this.lpTimer = null;
|
|
7322
|
+
if (!this.dragging || this.region !== region) return;
|
|
7323
|
+
if (region === "data") {
|
|
7324
|
+
this.region = "crosshair";
|
|
7325
|
+
this.deps.onPointerMove(x, y);
|
|
7326
|
+
return;
|
|
7327
|
+
}
|
|
7328
|
+
this.region = "axis-sheet";
|
|
7329
|
+
this.moved = true;
|
|
7330
|
+
this.deps.onAxisLongPress?.(region, x, y);
|
|
7331
|
+
}, LONG_PRESS_MS);
|
|
7332
|
+
}
|
|
7333
|
+
cancelLongPress() {
|
|
7334
|
+
if (this.lpTimer !== null) clearTimeout(this.lpTimer);
|
|
7335
|
+
this.lpTimer = null;
|
|
7336
|
+
}
|
|
7337
|
+
/** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
|
|
7338
|
+
beginPinch(e) {
|
|
7339
|
+
this.cancelLongPress();
|
|
7340
|
+
if (this.region === "crosshair") this.deps.onPointerMove(null, null);
|
|
7341
|
+
const coords = this.deps.getCoords();
|
|
7342
|
+
const vp = coords.getViewport();
|
|
7343
|
+
this.deps.apply(vp);
|
|
7344
|
+
const [a, b] = [...this.touches.values()];
|
|
7345
|
+
this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
7346
|
+
this.pinchStartBarSpacing = vp.barSpacing;
|
|
7347
|
+
this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
|
|
7348
|
+
this.region = "pinch";
|
|
7349
|
+
this.dragging = true;
|
|
7350
|
+
this.moved = true;
|
|
7351
|
+
this.vx = 0;
|
|
7352
|
+
this.capture(e.pointerId);
|
|
7353
|
+
}
|
|
7354
|
+
/** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
|
|
7355
|
+
applyPinch() {
|
|
7356
|
+
const coords = this.deps.getCoords();
|
|
7357
|
+
const pts = [...this.touches.values()];
|
|
7358
|
+
if (pts.length < 2) return;
|
|
7359
|
+
const [a, b] = pts;
|
|
7360
|
+
const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
|
|
7361
|
+
const vp = coords.getViewport();
|
|
7362
|
+
const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
|
|
7363
|
+
const midX = (a.x + b.x) / 2;
|
|
7364
|
+
this.deps.apply({
|
|
7365
|
+
barSpacing,
|
|
7366
|
+
rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
6859
7369
|
/** Capture defensively: a synthetic/already-released pointer can't be captured, and
|
|
6860
7370
|
* the move/up pair still routes through the element listeners without it. */
|
|
6861
7371
|
capture(pointerId) {
|
|
@@ -6864,6 +7374,35 @@ var InputController = class {
|
|
|
6864
7374
|
} catch {
|
|
6865
7375
|
}
|
|
6866
7376
|
}
|
|
7377
|
+
/** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
|
|
7378
|
+
* routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
|
|
7379
|
+
registerTap(t, x, y) {
|
|
7380
|
+
const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
|
|
7381
|
+
if (paired) {
|
|
7382
|
+
this.lastTapT = 0;
|
|
7383
|
+
this.doubleActivate(this.lastTapX, this.lastTapY);
|
|
7384
|
+
return;
|
|
7385
|
+
}
|
|
7386
|
+
this.lastTapT = t;
|
|
7387
|
+
this.lastTapX = x;
|
|
7388
|
+
this.lastTapY = y;
|
|
7389
|
+
}
|
|
7390
|
+
endGesture(e) {
|
|
7391
|
+
this.dragging = false;
|
|
7392
|
+
try {
|
|
7393
|
+
this.el?.releasePointerCapture(e.pointerId);
|
|
7394
|
+
} catch {
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
/** Shared double-click / double-tap routing, by the region under the point. */
|
|
7398
|
+
doubleActivate(x, y) {
|
|
7399
|
+
if (this.deps.drawingsDblClick?.(x, y)) return;
|
|
7400
|
+
const region = this.regionAt(x, y);
|
|
7401
|
+
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
7402
|
+
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
7403
|
+
else if (region === "time") this.deps.resetView();
|
|
7404
|
+
else this.deps.dataDblClick(x, y);
|
|
7405
|
+
}
|
|
6867
7406
|
};
|
|
6868
7407
|
|
|
6869
7408
|
// src/renderers/native/core/KeyboardController.ts
|
|
@@ -7242,12 +7781,13 @@ var Canvas2dBackend = class {
|
|
|
7242
7781
|
drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
7243
7782
|
const spacing = coords.bodySpacing();
|
|
7244
7783
|
const tier = candleTier(spacing);
|
|
7784
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
7245
7785
|
if (tier === "aggregate") {
|
|
7246
|
-
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane,
|
|
7786
|
+
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
7247
7787
|
return;
|
|
7248
7788
|
}
|
|
7249
7789
|
const drawBody = tier === "full";
|
|
7250
|
-
const cs =
|
|
7790
|
+
const cs = paint.candle;
|
|
7251
7791
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
7252
7792
|
for (let i = i0; i <= i1; i += 1) {
|
|
7253
7793
|
const b = bars[i];
|
|
@@ -7255,7 +7795,7 @@ var Canvas2dBackend = class {
|
|
|
7255
7795
|
const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
|
|
7256
7796
|
const x = g.center;
|
|
7257
7797
|
const up = b.close >= b.open;
|
|
7258
|
-
const dir = up ?
|
|
7798
|
+
const dir = up ? paint.up : paint.down;
|
|
7259
7799
|
const bc = barColors.get(b.time);
|
|
7260
7800
|
const color = bc ?? dir;
|
|
7261
7801
|
let top = 0;
|
|
@@ -8366,7 +8906,7 @@ var ChromeRenderer = class {
|
|
|
8366
8906
|
constructor() {
|
|
8367
8907
|
this.canvas = null;
|
|
8368
8908
|
this.ctx = null;
|
|
8369
|
-
// The color for axis tick labels — the
|
|
8909
|
+
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
8370
8910
|
this.axisTextColor = DARK_THEME.textColor;
|
|
8371
8911
|
// Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
|
|
8372
8912
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
@@ -8398,8 +8938,8 @@ var ChromeRenderer = class {
|
|
|
8398
8938
|
return dr;
|
|
8399
8939
|
}
|
|
8400
8940
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
8401
|
-
* `surface` (background + text) paints the axis-scale gutters
|
|
8402
|
-
*
|
|
8941
|
+
* `surface` (background + text) paints the axis-scale gutters — the host passes the live
|
|
8942
|
+
* chart background so the scales read as part of the plot, with contrast-corrected text.
|
|
8403
8943
|
* Falls back to the theme's own colors when no surface is supplied. */
|
|
8404
8944
|
render(scene, coords, theme, surface) {
|
|
8405
8945
|
const ctx = this.ctx;
|
|
@@ -8676,11 +9216,19 @@ var ChromeRenderer = class {
|
|
|
8676
9216
|
const y = dataH + (fullH - dataH) / 2;
|
|
8677
9217
|
const tr = coords.visibleTimeRange();
|
|
8678
9218
|
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
9219
|
+
const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
|
|
9220
|
+
const ticks = timeTicks(tr.from, tr.to, target, offset).map((tick) => ({ ...tick, x: coords.timeToX(tick.time), half: ctx.measureText(tick.label).width / 2 })).filter((tick) => tick.x >= 20 && tick.x <= dataW - 20);
|
|
9221
|
+
const GAP = 12;
|
|
9222
|
+
const placed = [];
|
|
9223
|
+
const put = (tick) => {
|
|
9224
|
+
const l = tick.x - tick.half;
|
|
9225
|
+
const r = tick.x + tick.half;
|
|
9226
|
+
if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
|
|
9227
|
+
placed.push({ l, r });
|
|
9228
|
+
ctx.fillText(tick.label, tick.x, y);
|
|
9229
|
+
};
|
|
9230
|
+
for (const tick of ticks) if (tick.major) put(tick);
|
|
9231
|
+
for (const tick of ticks) if (!tick.major) put(tick);
|
|
8684
9232
|
ctx.textAlign = "start";
|
|
8685
9233
|
}
|
|
8686
9234
|
};
|
|
@@ -8782,10 +9330,10 @@ var CrosshairRenderer = class {
|
|
|
8782
9330
|
}
|
|
8783
9331
|
/** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
|
|
8784
9332
|
* CONTAINING the foreign time (+ horizontal line when a comparable price came
|
|
8785
|
-
* along), with that bar's time chip in this chart's own timezone
|
|
8786
|
-
*
|
|
8787
|
-
*
|
|
8788
|
-
*
|
|
9333
|
+
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
9334
|
+
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
9335
|
+
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
9336
|
+
* render slightly dimmed so the ghost still reads as foreign. */
|
|
8789
9337
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
8790
9338
|
const cs = scene.style.crosshair;
|
|
8791
9339
|
const dataW = coords.width;
|
|
@@ -8809,7 +9357,20 @@ var CrosshairRenderer = class {
|
|
|
8809
9357
|
ctx.setLineDash([]);
|
|
8810
9358
|
ctx.lineWidth = 1;
|
|
8811
9359
|
ctx.globalAlpha = 0.8;
|
|
8812
|
-
|
|
9360
|
+
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
9361
|
+
if (ext.y != null && ext.price != null) {
|
|
9362
|
+
let pane;
|
|
9363
|
+
for (const p of scene.panes.values()) {
|
|
9364
|
+
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
9365
|
+
pane = p;
|
|
9366
|
+
break;
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
if (pane) {
|
|
9370
|
+
this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
|
|
8813
9374
|
ctx.globalAlpha = 1;
|
|
8814
9375
|
}
|
|
8815
9376
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -9080,6 +9641,7 @@ function colorField(theme, getVal, onVal) {
|
|
|
9080
9641
|
swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
|
|
9081
9642
|
};
|
|
9082
9643
|
paint();
|
|
9644
|
+
trigger.addEventListener("vela-sync", paint);
|
|
9083
9645
|
trigger.addEventListener("click", (e) => {
|
|
9084
9646
|
e.stopPropagation();
|
|
9085
9647
|
if (openPopover && openPopover.trigger === trigger) {
|
|
@@ -9115,6 +9677,101 @@ function closeColorPopover() {
|
|
|
9115
9677
|
closeOpen();
|
|
9116
9678
|
}
|
|
9117
9679
|
|
|
9680
|
+
// src/renderers/native/chrome/WidthField.ts
|
|
9681
|
+
var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
|
|
9682
|
+
var STYLE_ID4 = "vela-width-field";
|
|
9683
|
+
function ensureStyles4() {
|
|
9684
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
|
|
9685
|
+
const st = document.createElement("style");
|
|
9686
|
+
st.id = STYLE_ID4;
|
|
9687
|
+
st.textContent = `
|
|
9688
|
+
.vela-width-field{height:28px;padding:0 8px;border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);background:var(--vela-surface-elev);cursor:pointer;display:inline-flex;align-items:center;gap:8px;flex:none;color:var(--vela-fg);font-family:inherit;outline:none;}
|
|
9689
|
+
.vela-width-field:hover{border-color:var(--vela-fg-muted);}
|
|
9690
|
+
.vela-width-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
|
|
9691
|
+
.vela-width-field-item{display:flex;align-items:center;gap:8px;min-width:96px;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit;}
|
|
9692
|
+
.vela-width-field-item:hover{background:var(--vela-hover-strong);}
|
|
9693
|
+
.vela-width-field-item[data-active='1']{background:var(--vela-active);}
|
|
9694
|
+
`;
|
|
9695
|
+
document.head.appendChild(st);
|
|
9696
|
+
}
|
|
9697
|
+
function lineGlyph(width) {
|
|
9698
|
+
return `<svg width="22" height="14" viewBox="0 0 22 14" fill="none"><line x1="2" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg>`;
|
|
9699
|
+
}
|
|
9700
|
+
var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="#868a96" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
9701
|
+
var openPopover2 = null;
|
|
9702
|
+
function closeOpen2() {
|
|
9703
|
+
if (!openPopover2) return;
|
|
9704
|
+
document.removeEventListener("pointerdown", openPopover2.onOutside, true);
|
|
9705
|
+
window.removeEventListener("resize", openPopover2.reflow, true);
|
|
9706
|
+
openPopover2.el.remove();
|
|
9707
|
+
openPopover2 = null;
|
|
9708
|
+
}
|
|
9709
|
+
function place2(pop, trigger) {
|
|
9710
|
+
const r = trigger.getBoundingClientRect();
|
|
9711
|
+
const w = pop.offsetWidth;
|
|
9712
|
+
const h = pop.offsetHeight;
|
|
9713
|
+
let left = r.right - w;
|
|
9714
|
+
let top = r.bottom + 6;
|
|
9715
|
+
if (left < 6) left = 6;
|
|
9716
|
+
if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
|
|
9717
|
+
if (top + h > window.innerHeight - 6) top = r.top - h - 6;
|
|
9718
|
+
if (top < 6) top = 6;
|
|
9719
|
+
pop.style.left = `${Math.round(left)}px`;
|
|
9720
|
+
pop.style.top = `${Math.round(top)}px`;
|
|
9721
|
+
}
|
|
9722
|
+
function widthField(theme, getVal, onVal) {
|
|
9723
|
+
ensureStyles4();
|
|
9724
|
+
const trigger = document.createElement("button");
|
|
9725
|
+
trigger.type = "button";
|
|
9726
|
+
trigger.className = "vela-width-field";
|
|
9727
|
+
const paint = () => {
|
|
9728
|
+
trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
|
|
9729
|
+
};
|
|
9730
|
+
paint();
|
|
9731
|
+
trigger.addEventListener("click", (e) => {
|
|
9732
|
+
e.stopPropagation();
|
|
9733
|
+
if (openPopover2 && openPopover2.trigger === trigger) {
|
|
9734
|
+
closeOpen2();
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
closeOpen2();
|
|
9738
|
+
const pop = document.createElement("div");
|
|
9739
|
+
pop.className = "vela-width-field-pop";
|
|
9740
|
+
applyChromeTokens(pop, theme);
|
|
9741
|
+
pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
|
|
9742
|
+
pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
|
|
9743
|
+
for (const w of WIDTH_FIELD_OPTIONS) {
|
|
9744
|
+
const item = document.createElement("button");
|
|
9745
|
+
item.type = "button";
|
|
9746
|
+
item.className = "vela-width-field-item";
|
|
9747
|
+
item.dataset.active = w === getVal() ? "1" : "0";
|
|
9748
|
+
item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
|
|
9749
|
+
item.addEventListener("click", (ev) => {
|
|
9750
|
+
ev.stopPropagation();
|
|
9751
|
+
closeOpen2();
|
|
9752
|
+
onVal(w);
|
|
9753
|
+
paint();
|
|
9754
|
+
});
|
|
9755
|
+
pop.appendChild(item);
|
|
9756
|
+
}
|
|
9757
|
+
document.body.appendChild(pop);
|
|
9758
|
+
place2(pop, trigger);
|
|
9759
|
+
const onOutside = (ev) => {
|
|
9760
|
+
const t = ev.target;
|
|
9761
|
+
if (pop.contains(t) || trigger.contains(t)) return;
|
|
9762
|
+
closeOpen2();
|
|
9763
|
+
};
|
|
9764
|
+
const reflow = () => place2(pop, trigger);
|
|
9765
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
|
|
9766
|
+
window.addEventListener("resize", reflow, true);
|
|
9767
|
+
openPopover2 = { el: pop, trigger, onOutside, reflow };
|
|
9768
|
+
});
|
|
9769
|
+
return trigger;
|
|
9770
|
+
}
|
|
9771
|
+
function closeWidthPopover() {
|
|
9772
|
+
closeOpen2();
|
|
9773
|
+
}
|
|
9774
|
+
|
|
9118
9775
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
9119
9776
|
var BUILTIN_STYLE_LABELS = {
|
|
9120
9777
|
candles: "Candles",
|
|
@@ -9166,6 +9823,61 @@ function ensureControlStyles() {
|
|
|
9166
9823
|
.vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
|
|
9167
9824
|
.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
9825
|
.vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9826
|
+
/* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
|
|
9827
|
+
!important: the pane grid rewrites inline display ('contents') AFTER the initial
|
|
9828
|
+
visibility pass, so a class must win. */
|
|
9829
|
+
.vela-sd-hide{display:none !important;}
|
|
9830
|
+
/* Indented rail sub-entry (a chart-type section's subsection tab). */
|
|
9831
|
+
.vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
|
|
9832
|
+
/* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
|
|
9833
|
+
and a dashed + that turns on the next absent instance. The rule under it separates
|
|
9834
|
+
the strip from the instance's TOC + rows area. */
|
|
9835
|
+
.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);}
|
|
9836
|
+
.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;}
|
|
9837
|
+
.vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9838
|
+
.vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
|
|
9839
|
+
.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;}
|
|
9840
|
+
.vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9841
|
+
.vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
|
|
9842
|
+
/* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
|
|
9843
|
+
lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
|
|
9844
|
+
vertical rule sits on the rows column so it spans the full content height. When every
|
|
9845
|
+
group gates out the TOC hides and .no-toc drops the rule with it. */
|
|
9846
|
+
.vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
|
|
9847
|
+
.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;}
|
|
9848
|
+
.vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
|
|
9849
|
+
.vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
|
|
9850
|
+
.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;}
|
|
9851
|
+
.vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
9852
|
+
.vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
9853
|
+
/* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
|
|
9854
|
+
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
9855
|
+
!important beats the inline opacity on labels. */
|
|
9856
|
+
.vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
|
|
9857
|
+
/* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
|
|
9858
|
+
The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
|
|
9859
|
+
row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
|
|
9860
|
+
controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
|
|
9861
|
+
.vela-sd-mobile .vela-sd-burger{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg);line-height:0;width:36px;height:36px;border-radius:var(--vela-radius-sm);margin-right:2px;}
|
|
9862
|
+
.vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
|
|
9863
|
+
.vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
|
|
9864
|
+
.vela-sd-mobile .vela-sd-railscrim.open{display:block;}
|
|
9865
|
+
.vela-sd-mobile .vela-sd-rail{position:absolute;left:0;top:0;bottom:0;z-index:3;width:min(240px,80%);box-sizing:border-box;background:var(--vela-surface);border-right:1px solid var(--vela-border);box-shadow:var(--vela-shadow-dialog);transform:translateX(-105%);transition:transform var(--vela-dur-med) var(--vela-ease);}
|
|
9866
|
+
.vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
|
|
9867
|
+
.vela-sd-mobile .vela-sd-tab{padding:12px;}
|
|
9868
|
+
.vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
|
|
9869
|
+
.vela-sd-mobile .vela-sd-toc{position:sticky;top:0;z-index:1;background:var(--vela-surface);flex-direction:row;overflow-x:auto;scrollbar-width:none;min-width:0;width:100%;box-sizing:border-box;gap:4px;padding:2px 0 8px;}
|
|
9870
|
+
.vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
|
|
9871
|
+
.vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
|
|
9872
|
+
.vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
|
|
9873
|
+
.vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
|
|
9874
|
+
.vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
|
|
9875
|
+
.vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
|
|
9876
|
+
.vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
|
|
9877
|
+
.vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
|
|
9878
|
+
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
9879
|
+
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
9880
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
|
|
9169
9881
|
`;
|
|
9170
9882
|
document.head.appendChild(st);
|
|
9171
9883
|
}
|
|
@@ -9186,8 +9898,14 @@ var SettingsDialog = class {
|
|
|
9186
9898
|
this.themeControl = null;
|
|
9187
9899
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
9188
9900
|
this.tabs = [];
|
|
9901
|
+
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
9902
|
+
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
9903
|
+
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
9904
|
+
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
9189
9905
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
9190
9906
|
this.activeSection = null;
|
|
9907
|
+
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
9908
|
+
this.mobileLayout = false;
|
|
9191
9909
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
9192
9910
|
}
|
|
9193
9911
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
@@ -9206,15 +9924,27 @@ var SettingsDialog = class {
|
|
|
9206
9924
|
}
|
|
9207
9925
|
setTheme(theme) {
|
|
9208
9926
|
this.theme = theme;
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9927
|
+
this.reopenIfLive();
|
|
9928
|
+
}
|
|
9929
|
+
/** The host shell's chrome size class — mobile presents the dialog fullscreen with
|
|
9930
|
+
* the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
|
|
9931
|
+
* re-presents in place, on the same tab. */
|
|
9932
|
+
setLayoutMode(mode) {
|
|
9933
|
+
const mobile = mode === "mobile";
|
|
9934
|
+
if (mobile === this.mobileLayout) return;
|
|
9935
|
+
this.mobileLayout = mobile;
|
|
9936
|
+
this.reopenIfLive();
|
|
9937
|
+
}
|
|
9938
|
+
/** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
|
|
9939
|
+
reopenIfLive() {
|
|
9940
|
+
if (!this.root) return;
|
|
9941
|
+
const cfg = this.config;
|
|
9942
|
+
const oc = this.onChange;
|
|
9943
|
+
const oi = this.onImport;
|
|
9944
|
+
const orst = this.onReset;
|
|
9945
|
+
const section = this.activeSection;
|
|
9946
|
+
this.close();
|
|
9947
|
+
if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
|
|
9218
9948
|
}
|
|
9219
9949
|
isOpen() {
|
|
9220
9950
|
return this.root !== null;
|
|
@@ -9236,18 +9966,37 @@ var SettingsDialog = class {
|
|
|
9236
9966
|
this.onImport = onImport ?? null;
|
|
9237
9967
|
this.onReset = onReset ?? null;
|
|
9238
9968
|
ensureControlStyles();
|
|
9969
|
+
const mobile = this.mobileLayout;
|
|
9239
9970
|
const scrim = document.createElement("div");
|
|
9240
|
-
scrim.style.cssText = "position:absolute;inset:0;z-index:
|
|
9971
|
+
scrim.style.cssText = "position:absolute;inset:0;z-index:var(--vela-z-dialog);display:flex;align-items:flex-start;justify-content:center;background:transparent;padding-top:8vh;pointer-events:auto;";
|
|
9972
|
+
if (mobile) {
|
|
9973
|
+
scrim.classList.add("vela-sd-mobile");
|
|
9974
|
+
scrim.style.paddingTop = "0";
|
|
9975
|
+
scrim.style.alignItems = "stretch";
|
|
9976
|
+
}
|
|
9241
9977
|
scrim.addEventListener("mousedown", (e) => {
|
|
9242
9978
|
if (e.target === scrim) this.close();
|
|
9243
9979
|
});
|
|
9244
9980
|
const dlg = document.createElement("div");
|
|
9245
9981
|
dlg.style.cssText = `width:fit-content;min-width:min(560px,94vw);max-width:min(720px,94vw);max-height:70vh;display:flex;flex-direction:column;background:${SETTINGS_SURFACE};border:1px solid ${SETTINGS_BORDER};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${SETTINGS_TEXT};font:13px var(--vela-font);overflow:hidden;cursor:default;`;
|
|
9982
|
+
if (mobile) dlg.style.cssText += "width:100%;min-width:0;max-width:none;max-height:none;flex:1 1 auto;border:none;border-radius:0;";
|
|
9246
9983
|
const header = document.createElement("div");
|
|
9247
9984
|
header.style.cssText = `display:flex;justify-content:space-between;align-items:center;padding:9px 9px 9px 16px;border-bottom:1px solid ${SETTINGS_BORDER};flex:0 0 auto;user-select:none;`;
|
|
9985
|
+
if (mobile) header.style.paddingLeft = "8px";
|
|
9248
9986
|
const hTitle = document.createElement("span");
|
|
9249
9987
|
hTitle.textContent = "Chart settings";
|
|
9250
9988
|
hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
|
|
9989
|
+
if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
9990
|
+
let toggleRail = null;
|
|
9991
|
+
if (mobile) {
|
|
9992
|
+
const burger = document.createElement("button");
|
|
9993
|
+
burger.type = "button";
|
|
9994
|
+
burger.className = "vela-sd-burger";
|
|
9995
|
+
burger.innerHTML = iconAt("burger", 16);
|
|
9996
|
+
burger.title = "Sections";
|
|
9997
|
+
burger.addEventListener("click", () => toggleRail?.());
|
|
9998
|
+
header.append(burger);
|
|
9999
|
+
}
|
|
9251
10000
|
const closeBtn = document.createElement("button");
|
|
9252
10001
|
closeBtn.type = "button";
|
|
9253
10002
|
closeBtn.innerHTML = iconAt("close", 15);
|
|
@@ -9255,8 +10004,8 @@ var SettingsDialog = class {
|
|
|
9255
10004
|
closeBtn.className = "vela-sd-close";
|
|
9256
10005
|
closeBtn.addEventListener("click", () => this.close());
|
|
9257
10006
|
header.append(hTitle, closeBtn);
|
|
9258
|
-
|
|
9259
|
-
|
|
10007
|
+
if (!mobile) {
|
|
10008
|
+
header.style.cursor = "move";
|
|
9260
10009
|
let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
|
|
9261
10010
|
header.addEventListener("pointerdown", (e) => {
|
|
9262
10011
|
if (e.target?.closest(".vela-sd-close")) return;
|
|
@@ -9347,6 +10096,29 @@ var SettingsDialog = class {
|
|
|
9347
10096
|
baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
|
|
9348
10097
|
groups.baseline = baseline;
|
|
9349
10098
|
body.append(baseline);
|
|
10099
|
+
for (const def of chartTypes()) {
|
|
10100
|
+
if (!hasOwnCandlePaint(def.id)) continue;
|
|
10101
|
+
const bag = config.chartTypes[def.id] ?? {};
|
|
10102
|
+
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
10103
|
+
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
10104
|
+
const g = this.group();
|
|
10105
|
+
g.append(this.sectionTitle("Candles"));
|
|
10106
|
+
g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
10107
|
+
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
10108
|
+
this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
|
|
10109
|
+
]));
|
|
10110
|
+
g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
10111
|
+
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
10112
|
+
this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
|
|
10113
|
+
]));
|
|
10114
|
+
g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
10115
|
+
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
10116
|
+
this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
|
|
10117
|
+
]));
|
|
10118
|
+
g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
10119
|
+
groups[def.id] = g;
|
|
10120
|
+
body.append(g);
|
|
10121
|
+
}
|
|
9350
10122
|
showActive(config.series.style);
|
|
9351
10123
|
body.append(this.sectionTitle("Time zone"));
|
|
9352
10124
|
body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
|
|
@@ -9361,7 +10133,16 @@ var SettingsDialog = class {
|
|
|
9361
10133
|
}
|
|
9362
10134
|
}
|
|
9363
10135
|
};
|
|
10136
|
+
const renderChartTypeSections = (placement) => {
|
|
10137
|
+
for (const def of chartTypes()) {
|
|
10138
|
+
const typeSettings = def.settings;
|
|
10139
|
+
if (!typeSettings) continue;
|
|
10140
|
+
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
10141
|
+
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
10142
|
+
}
|
|
10143
|
+
};
|
|
9364
10144
|
renderHostSections("symbol");
|
|
10145
|
+
renderChartTypeSections("after-symbol");
|
|
9365
10146
|
renderHostSections("after-symbol");
|
|
9366
10147
|
body.append(this.section("Scales and lines"));
|
|
9367
10148
|
body.append(this.sectionTitle("Price scale"));
|
|
@@ -9402,35 +10183,27 @@ var SettingsDialog = class {
|
|
|
9402
10183
|
body.append(this.sectionTitle("Theme"));
|
|
9403
10184
|
body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
|
|
9404
10185
|
}
|
|
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
|
-
}
|
|
10186
|
+
renderChartTypeSections("end");
|
|
9426
10187
|
renderHostSections("end");
|
|
9427
10188
|
const shell = document.createElement("div");
|
|
9428
|
-
shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
10189
|
+
shell.style.cssText = mobile ? "display:flex;min-height:0;flex:1 1 auto;position:relative;overflow:hidden;" : "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
9429
10190
|
const rail = document.createElement("div");
|
|
9430
|
-
rail.
|
|
10191
|
+
rail.className = "vela-sd-rail";
|
|
10192
|
+
rail.style.cssText = mobile ? "display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;" : `flex:0 0 170px;display:flex;flex-direction:column;gap:2px;padding:10px 8px;border-right:1px solid ${SETTINGS_BORDER};overflow-y:auto;`;
|
|
10193
|
+
let railScrim = null;
|
|
10194
|
+
if (mobile) {
|
|
10195
|
+
railScrim = document.createElement("div");
|
|
10196
|
+
railScrim.className = "vela-sd-railscrim";
|
|
10197
|
+
railScrim.addEventListener("click", () => toggleRail?.());
|
|
10198
|
+
toggleRail = (open) => {
|
|
10199
|
+
const on = open ?? !rail.classList.contains("open");
|
|
10200
|
+
rail.classList.toggle("open", on);
|
|
10201
|
+
railScrim?.classList.toggle("open", on);
|
|
10202
|
+
};
|
|
10203
|
+
}
|
|
9431
10204
|
const paneHost = document.createElement("div");
|
|
9432
10205
|
paneHost.className = "vela-sd-pane";
|
|
9433
|
-
paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
|
|
10206
|
+
paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;padding:6px 18px 14px;";
|
|
9434
10207
|
const panes = [];
|
|
9435
10208
|
let current = null;
|
|
9436
10209
|
for (const child of [...body.children]) {
|
|
@@ -9441,7 +10214,7 @@ var SettingsDialog = class {
|
|
|
9441
10214
|
const tab = document.createElement("button");
|
|
9442
10215
|
tab.type = "button";
|
|
9443
10216
|
tab.textContent = title;
|
|
9444
|
-
tab.className = "vela-sd-tab";
|
|
10217
|
+
tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
|
|
9445
10218
|
panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
|
|
9446
10219
|
current = el;
|
|
9447
10220
|
child.remove();
|
|
@@ -9465,6 +10238,10 @@ var SettingsDialog = class {
|
|
|
9465
10238
|
p.tab.classList.toggle("on", i === idx);
|
|
9466
10239
|
});
|
|
9467
10240
|
this.activeSection = panes[idx]?.title ?? null;
|
|
10241
|
+
if (mobile) {
|
|
10242
|
+
hTitle.textContent = panes[idx]?.title ?? "Chart settings";
|
|
10243
|
+
toggleRail?.(false);
|
|
10244
|
+
}
|
|
9468
10245
|
};
|
|
9469
10246
|
panes.forEach((p, i) => {
|
|
9470
10247
|
p.tab.addEventListener("click", () => activate(i));
|
|
@@ -9472,19 +10249,28 @@ var SettingsDialog = class {
|
|
|
9472
10249
|
paneHost.appendChild(p.el);
|
|
9473
10250
|
});
|
|
9474
10251
|
this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
|
|
9475
|
-
const wanted = section
|
|
10252
|
+
const wanted = section !== void 0 ? panes.findIndex((p) => p.title.toLowerCase() === section.toLowerCase()) : panes.findIndex((p) => p.style === config.series.style && !p.tab.classList.contains("vela-sd-tab-sub"));
|
|
9476
10253
|
activate(wanted >= 0 ? wanted : 0);
|
|
9477
10254
|
this.syncTypeTabs?.(config.series.style);
|
|
9478
10255
|
shell.append(rail, paneHost);
|
|
10256
|
+
if (railScrim) shell.append(railScrim);
|
|
9479
10257
|
dlg.appendChild(shell);
|
|
9480
10258
|
dlg.appendChild(this.footer(config));
|
|
9481
10259
|
scrim.appendChild(dlg);
|
|
9482
10260
|
this.container.appendChild(scrim);
|
|
9483
10261
|
this.root = scrim;
|
|
9484
|
-
for (const p of panes)
|
|
10262
|
+
for (const p of panes) {
|
|
10263
|
+
const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
|
|
10264
|
+
if (hosts.length === 0) {
|
|
10265
|
+
this.layoutSettingsGrids(p.el, dlg);
|
|
10266
|
+
continue;
|
|
10267
|
+
}
|
|
10268
|
+
for (const h of hosts) this.layoutSettingsGrids(h, dlg);
|
|
10269
|
+
}
|
|
9485
10270
|
}
|
|
9486
10271
|
close() {
|
|
9487
10272
|
closeColorPopover();
|
|
10273
|
+
closeWidthPopover();
|
|
9488
10274
|
this.root?.remove();
|
|
9489
10275
|
this.root = null;
|
|
9490
10276
|
this.tabs = [];
|
|
@@ -9511,6 +10297,333 @@ var SettingsDialog = class {
|
|
|
9511
10297
|
el.textContent = text;
|
|
9512
10298
|
return el;
|
|
9513
10299
|
}
|
|
10300
|
+
/**
|
|
10301
|
+
* One chart type's settings tab (and its subsection tabs), appended to the linear
|
|
10302
|
+
* `body` for the pane splitter to file. One live values BAG serves the whole section
|
|
10303
|
+
* — instances and subsections share the per-type store, so a `when` gate anywhere
|
|
10304
|
+
* can reference a key edited anywhere else; every edit runs every registered
|
|
10305
|
+
* refresher.
|
|
10306
|
+
*/
|
|
10307
|
+
chartTypeSection(typeId, section, config, body) {
|
|
10308
|
+
const marker = this.section(section.title);
|
|
10309
|
+
marker.dataset.sdStyle = typeId;
|
|
10310
|
+
marker.dataset.sdVisibility = section.visibility ?? "active";
|
|
10311
|
+
body.append(marker);
|
|
10312
|
+
const values = config.chartTypes[typeId] ?? {};
|
|
10313
|
+
const bag = {};
|
|
10314
|
+
const seed = (rows) => {
|
|
10315
|
+
for (const r of rows) {
|
|
10316
|
+
for (const k of settingsRowValueKeys(r)) {
|
|
10317
|
+
const v = values[k.key];
|
|
10318
|
+
bag[k.key] = typeof v === k.type ? v : k.defval;
|
|
10319
|
+
}
|
|
10320
|
+
}
|
|
10321
|
+
};
|
|
10322
|
+
if (section.rows) seed(section.rows);
|
|
10323
|
+
for (const inst of section.instances ?? []) seed(inst.rows);
|
|
10324
|
+
for (const sub of section.subsections ?? []) seed(sub.rows);
|
|
10325
|
+
for (const inst of section.instances ?? []) {
|
|
10326
|
+
if (!inst.enableKey || inst.enableKey in bag) continue;
|
|
10327
|
+
bag[inst.enableKey] = values[inst.enableKey] === true;
|
|
10328
|
+
}
|
|
10329
|
+
for (const sub of section.subsections ?? []) {
|
|
10330
|
+
if (!sub.enableKey || sub.enableKey in bag) continue;
|
|
10331
|
+
bag[sub.enableKey] = values[sub.enableKey] === true;
|
|
10332
|
+
}
|
|
10333
|
+
const refreshers = [];
|
|
10334
|
+
const put = (key, v) => {
|
|
10335
|
+
bag[key] = v;
|
|
10336
|
+
this.emitType(typeId, key, v);
|
|
10337
|
+
for (const r of refreshers) r();
|
|
10338
|
+
};
|
|
10339
|
+
if (section.instances && section.instances.length > 0) {
|
|
10340
|
+
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
10341
|
+
} else if (section.rows) {
|
|
10342
|
+
if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
|
|
10343
|
+
else this.flatTypeRows(section.rows, bag, put, refreshers, body);
|
|
10344
|
+
}
|
|
10345
|
+
for (const sub of section.subsections ?? []) {
|
|
10346
|
+
const subMarker = this.section(sub.title);
|
|
10347
|
+
subMarker.dataset.sdStyle = typeId;
|
|
10348
|
+
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
10349
|
+
subMarker.dataset.sdSub = "1";
|
|
10350
|
+
body.append(subMarker);
|
|
10351
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
|
|
10352
|
+
}
|
|
10353
|
+
for (const r of refreshers) r();
|
|
10354
|
+
}
|
|
10355
|
+
/** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
|
|
10356
|
+
* them), headings/headers as inline group titles, `when` gates refreshed live. */
|
|
10357
|
+
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
10358
|
+
const entries = [];
|
|
10359
|
+
for (const r of rows) {
|
|
10360
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
|
|
10361
|
+
entries.push({ el, when: r.when });
|
|
10362
|
+
body.append(el);
|
|
10363
|
+
}
|
|
10364
|
+
refreshers.push(() => {
|
|
10365
|
+
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
10366
|
+
});
|
|
10367
|
+
}
|
|
10368
|
+
/**
|
|
10369
|
+
* One value row for a chart-type descriptor, writing through `put`. EVERY kind is
|
|
10370
|
+
* first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
|
|
10371
|
+
* rendered by the ONE path: optional leading toggle, then the ordered inline
|
|
10372
|
+
* controls in the control column. A control carrying its own `when` registers a
|
|
10373
|
+
* visibility refresher — it appears and disappears live as the bag changes,
|
|
10374
|
+
* independent of the row's gate — and is exempt from the toggle-off dim (it may
|
|
10375
|
+
* exist FOR the off state).
|
|
10376
|
+
*/
|
|
10377
|
+
typeRow(r, bag, put, refreshers) {
|
|
10378
|
+
const n = normalizeSettingsRow(r);
|
|
10379
|
+
const controls = [];
|
|
10380
|
+
for (const c of n.controls) {
|
|
10381
|
+
const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
|
|
10382
|
+
if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
10383
|
+
if (c.kind !== "hint" && c.when) {
|
|
10384
|
+
el.dataset.sdSelfGated = "1";
|
|
10385
|
+
const when = c.when;
|
|
10386
|
+
refreshers.push(() => {
|
|
10387
|
+
el.style.display = settingsRowVisible(when, bag) ? "" : "none";
|
|
10388
|
+
});
|
|
10389
|
+
}
|
|
10390
|
+
controls.push(el);
|
|
10391
|
+
}
|
|
10392
|
+
if (n.toggle) {
|
|
10393
|
+
const t = n.toggle;
|
|
10394
|
+
const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
|
|
10395
|
+
refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
10396
|
+
return el;
|
|
10397
|
+
}
|
|
10398
|
+
return this.rowWith(n.label, controls);
|
|
10399
|
+
}
|
|
10400
|
+
/**
|
|
10401
|
+
* Build ONE inline control from its descriptor — the factory behind the composite
|
|
10402
|
+
* row path. `compact` narrows number inputs on toggle rows (the historical inline
|
|
10403
|
+
* width) while standalone rows keep the full-width input.
|
|
10404
|
+
*/
|
|
10405
|
+
inlineControl(c, bag, put, compact) {
|
|
10406
|
+
if (c.kind === "hint") {
|
|
10407
|
+
const s = document.createElement("span");
|
|
10408
|
+
s.textContent = c.text;
|
|
10409
|
+
s.style.cssText = "color:var(--vela-fg-muted);";
|
|
10410
|
+
return s;
|
|
10411
|
+
}
|
|
10412
|
+
if (c.kind === "color") {
|
|
10413
|
+
const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
|
|
10414
|
+
sw.title = c.label;
|
|
10415
|
+
return sw;
|
|
10416
|
+
}
|
|
10417
|
+
if (c.kind === "width") {
|
|
10418
|
+
let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
10419
|
+
const wf = widthField(this.theme, () => cur, (v) => {
|
|
10420
|
+
cur = v;
|
|
10421
|
+
put(c.key, v);
|
|
10422
|
+
});
|
|
10423
|
+
wf.title = c.label;
|
|
10424
|
+
return wf;
|
|
10425
|
+
}
|
|
10426
|
+
if (c.kind === "select") {
|
|
10427
|
+
const sel = document.createElement("select");
|
|
10428
|
+
sel.className = "vela-sd-select";
|
|
10429
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
10430
|
+
sel.title = c.label;
|
|
10431
|
+
const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
10432
|
+
for (const [val, lbl] of normalizeSelectOptions(c.options)) {
|
|
10433
|
+
const o = document.createElement("option");
|
|
10434
|
+
o.value = val;
|
|
10435
|
+
o.textContent = lbl;
|
|
10436
|
+
if (val === current2) o.selected = true;
|
|
10437
|
+
sel.appendChild(o);
|
|
10438
|
+
}
|
|
10439
|
+
sel.addEventListener("change", () => put(c.key, sel.value));
|
|
10440
|
+
sel.addEventListener("vela-sync", () => {
|
|
10441
|
+
sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
10442
|
+
});
|
|
10443
|
+
return sel;
|
|
10444
|
+
}
|
|
10445
|
+
const ni = document.createElement("input");
|
|
10446
|
+
ni.type = "number";
|
|
10447
|
+
ni.className = "vela-sd-number";
|
|
10448
|
+
if (compact) ni.style.width = "56px";
|
|
10449
|
+
if (c.min !== void 0) ni.min = String(c.min);
|
|
10450
|
+
if (c.max !== void 0) ni.max = String(c.max);
|
|
10451
|
+
ni.step = String(c.step ?? 1);
|
|
10452
|
+
ni.title = c.label;
|
|
10453
|
+
const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
10454
|
+
if (c.placeholder !== void 0) {
|
|
10455
|
+
ni.placeholder = c.placeholder;
|
|
10456
|
+
if (current !== c.defval) ni.value = String(current);
|
|
10457
|
+
ni.addEventListener("change", () => {
|
|
10458
|
+
const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
|
|
10459
|
+
const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
|
|
10460
|
+
ni.value = v === c.defval ? "" : String(v);
|
|
10461
|
+
put(c.key, v);
|
|
10462
|
+
});
|
|
10463
|
+
} else {
|
|
10464
|
+
ni.value = String(current);
|
|
10465
|
+
ni.addEventListener("input", () => {
|
|
10466
|
+
const v = Number(ni.value);
|
|
10467
|
+
if (Number.isFinite(v)) put(c.key, v);
|
|
10468
|
+
});
|
|
10469
|
+
}
|
|
10470
|
+
return ni;
|
|
10471
|
+
}
|
|
10472
|
+
/**
|
|
10473
|
+
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
10474
|
+
* removable one), a dashed `+` while an instance is still absent, and one
|
|
10475
|
+
* grouped-rows content per instance below — only the active tab's content shows.
|
|
10476
|
+
* Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
|
|
10477
|
+
* every section edit, so gates elsewhere stay coherent.
|
|
10478
|
+
*/
|
|
10479
|
+
instancesBlock(typeId, instances, bag, put, refreshers) {
|
|
10480
|
+
const wrap = document.createElement("div");
|
|
10481
|
+
const strip = document.createElement("div");
|
|
10482
|
+
strip.className = "vela-sd-itabs";
|
|
10483
|
+
if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
|
|
10484
|
+
const contents = instances.map((inst, i) => {
|
|
10485
|
+
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
10486
|
+
wrap.append(content);
|
|
10487
|
+
return content;
|
|
10488
|
+
});
|
|
10489
|
+
const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
|
|
10490
|
+
const refresh = () => {
|
|
10491
|
+
let active = this.typeActiveInstance.get(typeId) ?? 0;
|
|
10492
|
+
if (!present(instances[active] ?? instances[0])) active = 0;
|
|
10493
|
+
this.typeActiveInstance.set(typeId, active);
|
|
10494
|
+
strip.replaceChildren();
|
|
10495
|
+
instances.forEach((inst, i) => {
|
|
10496
|
+
if (!present(inst)) return;
|
|
10497
|
+
const tab = document.createElement("button");
|
|
10498
|
+
tab.type = "button";
|
|
10499
|
+
tab.className = "vela-sd-itab" + (i === active ? " on" : "");
|
|
10500
|
+
const lbl = document.createElement("span");
|
|
10501
|
+
lbl.textContent = inst.label;
|
|
10502
|
+
tab.append(lbl);
|
|
10503
|
+
if (i === active && inst.enableKey) {
|
|
10504
|
+
const x = document.createElement("span");
|
|
10505
|
+
x.className = "vela-sd-ix";
|
|
10506
|
+
x.textContent = "\u2715";
|
|
10507
|
+
x.title = `Remove ${inst.label}`;
|
|
10508
|
+
x.addEventListener("click", (e) => {
|
|
10509
|
+
e.stopPropagation();
|
|
10510
|
+
this.typeActiveInstance.set(typeId, 0);
|
|
10511
|
+
put(inst.enableKey, false);
|
|
10512
|
+
});
|
|
10513
|
+
tab.append(x);
|
|
10514
|
+
}
|
|
10515
|
+
tab.addEventListener("click", () => {
|
|
10516
|
+
this.typeActiveInstance.set(typeId, i);
|
|
10517
|
+
refresh();
|
|
10518
|
+
});
|
|
10519
|
+
strip.append(tab);
|
|
10520
|
+
});
|
|
10521
|
+
const absent = instances.findIndex((inst) => !present(inst));
|
|
10522
|
+
if (absent >= 0) {
|
|
10523
|
+
const add = document.createElement("button");
|
|
10524
|
+
add.type = "button";
|
|
10525
|
+
add.className = "vela-sd-itab vela-sd-itab-add";
|
|
10526
|
+
add.textContent = "+";
|
|
10527
|
+
add.title = `Add ${instances[absent].label}`;
|
|
10528
|
+
add.addEventListener("click", () => {
|
|
10529
|
+
this.typeActiveInstance.set(typeId, absent);
|
|
10530
|
+
put(instances[absent].enableKey, true);
|
|
10531
|
+
});
|
|
10532
|
+
strip.append(add);
|
|
10533
|
+
}
|
|
10534
|
+
contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
|
|
10535
|
+
};
|
|
10536
|
+
refreshers.push(refresh);
|
|
10537
|
+
return wrap;
|
|
10538
|
+
}
|
|
10539
|
+
/**
|
|
10540
|
+
* A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
|
|
10541
|
+
* ONE rows host holding every row — the active group's rows show, the rest hide.
|
|
10542
|
+
* Rows BEFORE the first heading are the always block, visible above every group.
|
|
10543
|
+
* `header` rows stay in the rows column as in-group subgroup titles (not TOC
|
|
10544
|
+
* entries). A group whose value rows are all gated out (or whose heading's own
|
|
10545
|
+
* `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
|
|
10546
|
+
* rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
|
|
10547
|
+
* across all groups.
|
|
10548
|
+
*
|
|
10549
|
+
* `enableKey` (optional): while that bag boolean is false, every row except the one
|
|
10550
|
+
* whose key matches is soft-disabled (visible but grayed / non-interactive) so the
|
|
10551
|
+
* pane stays browseable with the feature off.
|
|
10552
|
+
*/
|
|
10553
|
+
groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
|
|
10554
|
+
const wrap = document.createElement("div");
|
|
10555
|
+
wrap.className = "vela-sd-struct";
|
|
10556
|
+
const toc = document.createElement("div");
|
|
10557
|
+
toc.className = "vela-sd-toc";
|
|
10558
|
+
const rowsHost = document.createElement("div");
|
|
10559
|
+
rowsHost.dataset.sdRowsHost = "1";
|
|
10560
|
+
rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
10561
|
+
wrap.append(toc, rowsHost);
|
|
10562
|
+
const entries = [];
|
|
10563
|
+
const groups = [];
|
|
10564
|
+
const groupWhens = [];
|
|
10565
|
+
let g = -1;
|
|
10566
|
+
for (const r of rows) {
|
|
10567
|
+
if (r.kind === "heading") {
|
|
10568
|
+
g = groups.length;
|
|
10569
|
+
groups.push(r.label);
|
|
10570
|
+
groupWhens.push(r.when);
|
|
10571
|
+
continue;
|
|
10572
|
+
}
|
|
10573
|
+
if (r.kind === "header") {
|
|
10574
|
+
entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
|
|
10575
|
+
rowsHost.append(entries[entries.length - 1].el);
|
|
10576
|
+
continue;
|
|
10577
|
+
}
|
|
10578
|
+
const el = this.typeRow(r, bag, put, refreshers);
|
|
10579
|
+
const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
|
|
10580
|
+
entries.push({ el, when: r.when, group: g, key });
|
|
10581
|
+
rowsHost.append(el);
|
|
10582
|
+
}
|
|
10583
|
+
const refresh = () => {
|
|
10584
|
+
const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
|
|
10585
|
+
let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
|
|
10586
|
+
if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
|
|
10587
|
+
if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
|
|
10588
|
+
toc.replaceChildren();
|
|
10589
|
+
groups.forEach((label, gi) => {
|
|
10590
|
+
if (!live[gi]) return;
|
|
10591
|
+
const b = document.createElement("button");
|
|
10592
|
+
b.type = "button";
|
|
10593
|
+
b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
|
|
10594
|
+
b.textContent = label;
|
|
10595
|
+
b.addEventListener("click", () => {
|
|
10596
|
+
this.typeActiveGroup.set(paneKey, label);
|
|
10597
|
+
refresh();
|
|
10598
|
+
});
|
|
10599
|
+
toc.append(b);
|
|
10600
|
+
});
|
|
10601
|
+
const anyGroup = live.some(Boolean);
|
|
10602
|
+
toc.classList.toggle("vela-sd-hide", !anyGroup);
|
|
10603
|
+
wrap.classList.toggle("no-toc", !anyGroup);
|
|
10604
|
+
const enabled = !enableKey || bag[enableKey] === true;
|
|
10605
|
+
for (let i = 0; i < entries.length; i++) {
|
|
10606
|
+
const e = entries[i];
|
|
10607
|
+
let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
|
|
10608
|
+
if (visible && e.header) {
|
|
10609
|
+
let hasContent = false;
|
|
10610
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
10611
|
+
const n = entries[j];
|
|
10612
|
+
if (n.group !== e.group || n.header) break;
|
|
10613
|
+
if (settingsRowVisible(n.when, bag)) {
|
|
10614
|
+
hasContent = true;
|
|
10615
|
+
break;
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10618
|
+
visible = hasContent;
|
|
10619
|
+
}
|
|
10620
|
+
e.el.classList.toggle("vela-sd-hide", !visible);
|
|
10621
|
+
e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
|
|
10622
|
+
}
|
|
10623
|
+
};
|
|
10624
|
+
refreshers.push(refresh);
|
|
10625
|
+
return wrap;
|
|
10626
|
+
}
|
|
9514
10627
|
/** Vertical breathing space between row clusters — grouping reads from whitespace,
|
|
9515
10628
|
* so no drawn rule. */
|
|
9516
10629
|
separator() {
|
|
@@ -9529,6 +10642,14 @@ var SettingsDialog = class {
|
|
|
9529
10642
|
*/
|
|
9530
10643
|
layoutSettingsGrids(pane, measureHost) {
|
|
9531
10644
|
if (pane.childElementCount === 0) return;
|
|
10645
|
+
if (this.mobileLayout) {
|
|
10646
|
+
const grid2 = document.createElement("div");
|
|
10647
|
+
grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
|
|
10648
|
+
while (pane.firstChild) grid2.appendChild(pane.firstChild);
|
|
10649
|
+
pane.appendChild(grid2);
|
|
10650
|
+
this.prepareGridItems(grid2);
|
|
10651
|
+
return;
|
|
10652
|
+
}
|
|
9532
10653
|
const rows = [...pane.querySelectorAll(".vela-sd-row")];
|
|
9533
10654
|
const probe2 = document.createElement("div");
|
|
9534
10655
|
probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
|
|
@@ -9565,9 +10686,11 @@ var SettingsDialog = class {
|
|
|
9565
10686
|
}
|
|
9566
10687
|
}
|
|
9567
10688
|
/** A bare color swatch input (for toggle-row right groups / swatch pairs). */
|
|
9568
|
-
|
|
10689
|
+
/** With `get`, the field reads its value live (and repaints on 'vela-sync') —
|
|
10690
|
+
* duplicate-keyed rows stay honest when another row edits the shared key. */
|
|
10691
|
+
swatch(value, onChange, get) {
|
|
9569
10692
|
let current = value;
|
|
9570
|
-
return colorField(this.theme, () => current, (v) => {
|
|
10693
|
+
return colorField(this.theme, () => get ? get() : current, (v) => {
|
|
9571
10694
|
current = v;
|
|
9572
10695
|
onChange(v);
|
|
9573
10696
|
});
|
|
@@ -9619,8 +10742,11 @@ var SettingsDialog = class {
|
|
|
9619
10742
|
}
|
|
9620
10743
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
9621
10744
|
* shared control column; the control group dims and ignores input while the toggle
|
|
9622
|
-
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
9623
|
-
|
|
10745
|
+
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
10746
|
+
* With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
|
|
10747
|
+
* keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
|
|
10748
|
+
* only one visible at a time; see `typeRow`). */
|
|
10749
|
+
toggleRow(label, value, onToggle, controls, get) {
|
|
9624
10750
|
const wrap = document.createElement("div");
|
|
9625
10751
|
const cb = document.createElement("button");
|
|
9626
10752
|
cb.type = "button";
|
|
@@ -9640,6 +10766,12 @@ var SettingsDialog = class {
|
|
|
9640
10766
|
wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
|
|
9641
10767
|
wrap.append(cb, lbl);
|
|
9642
10768
|
cb.addEventListener("click", () => onToggle(cbToggle()));
|
|
10769
|
+
if (get) {
|
|
10770
|
+
wrap.addEventListener("vela-sync", () => {
|
|
10771
|
+
checked = get();
|
|
10772
|
+
cb.classList.toggle("on", checked);
|
|
10773
|
+
});
|
|
10774
|
+
}
|
|
9643
10775
|
return wrap;
|
|
9644
10776
|
}
|
|
9645
10777
|
wrap.className = "vela-sd-row";
|
|
@@ -9650,8 +10782,11 @@ var SettingsDialog = class {
|
|
|
9650
10782
|
box.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
9651
10783
|
for (const c of controls) box.appendChild(c);
|
|
9652
10784
|
const syncDim = (on) => {
|
|
9653
|
-
|
|
9654
|
-
|
|
10785
|
+
for (const c of controls) {
|
|
10786
|
+
if (c.dataset.sdSelfGated === "1") continue;
|
|
10787
|
+
c.style.opacity = on ? "1" : "0.4";
|
|
10788
|
+
c.style.pointerEvents = on ? "" : "none";
|
|
10789
|
+
}
|
|
9655
10790
|
};
|
|
9656
10791
|
syncDim(value);
|
|
9657
10792
|
cb.addEventListener("click", () => {
|
|
@@ -9659,6 +10794,13 @@ var SettingsDialog = class {
|
|
|
9659
10794
|
onToggle(v);
|
|
9660
10795
|
syncDim(v);
|
|
9661
10796
|
});
|
|
10797
|
+
if (get) {
|
|
10798
|
+
wrap.addEventListener("vela-sync", () => {
|
|
10799
|
+
checked = get();
|
|
10800
|
+
cb.classList.toggle("on", checked);
|
|
10801
|
+
syncDim(checked);
|
|
10802
|
+
});
|
|
10803
|
+
}
|
|
9662
10804
|
wrap.append(left, box);
|
|
9663
10805
|
return wrap;
|
|
9664
10806
|
}
|
|
@@ -9678,102 +10820,12 @@ var SettingsDialog = class {
|
|
|
9678
10820
|
wrap.appendChild(ni);
|
|
9679
10821
|
return wrap;
|
|
9680
10822
|
}
|
|
9681
|
-
/** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
|
|
9682
|
-
autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
|
|
9683
|
-
const { wrap } = this.row(label);
|
|
9684
|
-
const box = document.createElement("div");
|
|
9685
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
9686
|
-
const sel = document.createElement("select");
|
|
9687
|
-
sel.className = "vela-sd-select";
|
|
9688
|
-
for (const [val, lbl] of AUTO_MANUAL_OPTS) {
|
|
9689
|
-
const o = document.createElement("option");
|
|
9690
|
-
o.value = val;
|
|
9691
|
-
o.textContent = lbl;
|
|
9692
|
-
if (val === "auto" === isAuto) o.selected = true;
|
|
9693
|
-
sel.appendChild(o);
|
|
9694
|
-
}
|
|
9695
|
-
const ni = document.createElement("input");
|
|
9696
|
-
ni.type = "number";
|
|
9697
|
-
ni.min = String(min);
|
|
9698
|
-
ni.max = String(max);
|
|
9699
|
-
ni.step = "1";
|
|
9700
|
-
ni.value = String(value);
|
|
9701
|
-
ni.className = "vela-sd-number";
|
|
9702
|
-
const unitLbl = document.createElement("span");
|
|
9703
|
-
unitLbl.textContent = unit;
|
|
9704
|
-
unitLbl.style.cssText = "opacity:0.6;";
|
|
9705
|
-
const syncDisabled = (auto) => {
|
|
9706
|
-
ni.disabled = auto;
|
|
9707
|
-
ni.style.opacity = auto ? "0.4" : "1";
|
|
9708
|
-
};
|
|
9709
|
-
syncDisabled(isAuto);
|
|
9710
|
-
sel.addEventListener("change", () => {
|
|
9711
|
-
const auto = sel.value === "auto";
|
|
9712
|
-
onMode(auto);
|
|
9713
|
-
syncDisabled(auto);
|
|
9714
|
-
});
|
|
9715
|
-
ni.addEventListener("input", () => {
|
|
9716
|
-
const n = Number(ni.value);
|
|
9717
|
-
if (Number.isFinite(n)) onValue(n);
|
|
9718
|
-
});
|
|
9719
|
-
box.append(sel, ni, unitLbl);
|
|
9720
|
-
wrap.appendChild(box);
|
|
9721
|
-
return wrap;
|
|
9722
|
-
}
|
|
9723
|
-
/** A row whose control area holds several inline controls (e.g. show + color + width). */
|
|
9724
|
-
inlineRow(label, controls) {
|
|
9725
|
-
const { wrap } = this.row(label);
|
|
9726
|
-
const box = document.createElement("div");
|
|
9727
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
9728
|
-
for (const c of controls) box.appendChild(c);
|
|
9729
|
-
wrap.appendChild(box);
|
|
9730
|
-
return wrap;
|
|
9731
|
-
}
|
|
9732
|
-
/** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
|
|
9733
|
-
hint(text) {
|
|
9734
|
-
const s = document.createElement("span");
|
|
9735
|
-
s.textContent = text;
|
|
9736
|
-
s.style.cssText = "opacity:0.5;font-size:12px;";
|
|
9737
|
-
return s;
|
|
9738
|
-
}
|
|
9739
|
-
/** A bare color input (for inline groups). */
|
|
9740
|
-
colorInput(value, onChange) {
|
|
9741
|
-
const ci = document.createElement("input");
|
|
9742
|
-
ci.type = "color";
|
|
9743
|
-
ci.value = toHex6(value);
|
|
9744
|
-
ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
|
|
9745
|
-
ci.addEventListener("input", () => onChange(ci.value));
|
|
9746
|
-
return ci;
|
|
9747
|
-
}
|
|
9748
|
-
/** A bare compact number input (for inline groups). */
|
|
9749
|
-
numberInput(value, min, max, step, onChange) {
|
|
9750
|
-
const ni = document.createElement("input");
|
|
9751
|
-
ni.type = "number";
|
|
9752
|
-
ni.value = String(value);
|
|
9753
|
-
ni.min = String(min);
|
|
9754
|
-
ni.max = String(max);
|
|
9755
|
-
ni.step = String(step);
|
|
9756
|
-
ni.className = "vela-sd-number";
|
|
9757
|
-
ni.style.flex = "0 0 auto";
|
|
9758
|
-
ni.addEventListener("input", () => {
|
|
9759
|
-
const n = Number(ni.value);
|
|
9760
|
-
if (Number.isFinite(n)) onChange(n);
|
|
9761
|
-
});
|
|
9762
|
-
return ni;
|
|
9763
|
-
}
|
|
9764
|
-
/** A small in-group heading (lighter than a top-level section divider). */
|
|
9765
|
-
subheading(text) {
|
|
9766
|
-
const el = document.createElement("div");
|
|
9767
|
-
el.textContent = text;
|
|
9768
|
-
el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
|
|
9769
|
-
return el;
|
|
9770
|
-
}
|
|
9771
10823
|
/** A dropdown whose option values differ from their display labels. */
|
|
9772
10824
|
selectRowLabeled(label, value, options, onChange) {
|
|
9773
10825
|
const { wrap } = this.row(label);
|
|
9774
10826
|
const sel = document.createElement("select");
|
|
9775
10827
|
sel.className = "vela-sd-select";
|
|
9776
|
-
sel.style.cssText = "max-width:
|
|
10828
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
9777
10829
|
for (const [val, lbl] of options) {
|
|
9778
10830
|
const o = document.createElement("option");
|
|
9779
10831
|
o.value = val;
|
|
@@ -9789,7 +10841,7 @@ var SettingsDialog = class {
|
|
|
9789
10841
|
const { wrap } = this.row(label);
|
|
9790
10842
|
const sel = document.createElement("select");
|
|
9791
10843
|
sel.className = "vela-sd-select";
|
|
9792
|
-
sel.style.cssText = "max-width:
|
|
10844
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
9793
10845
|
for (const opt of options) {
|
|
9794
10846
|
const o = document.createElement("option");
|
|
9795
10847
|
o.value = opt;
|
|
@@ -9813,19 +10865,10 @@ var SettingsDialog = class {
|
|
|
9813
10865
|
foot.appendChild(resetBtn);
|
|
9814
10866
|
return foot;
|
|
9815
10867
|
}
|
|
9816
|
-
button(label, onClick) {
|
|
9817
|
-
const b = document.createElement("button");
|
|
9818
|
-
b.type = "button";
|
|
9819
|
-
b.textContent = label;
|
|
9820
|
-
b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
|
|
9821
|
-
b.addEventListener("click", onClick);
|
|
9822
|
-
return b;
|
|
9823
|
-
}
|
|
9824
|
-
ctrlStyle() {
|
|
9825
|
-
return `background:var(--vela-surface-sunken);border:1px solid ${SETTINGS_BORDER};color:${SETTINGS_TEXT};border-radius:var(--vela-radius-sm);padding:3px 6px;font-size:var(--vela-font-size-md);font-family:inherit;outline:none;`;
|
|
9826
|
-
}
|
|
9827
10868
|
};
|
|
9828
|
-
|
|
10869
|
+
function normalizeSelectOptions(options) {
|
|
10870
|
+
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
10871
|
+
}
|
|
9829
10872
|
function timezoneOptions(current) {
|
|
9830
10873
|
const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
|
|
9831
10874
|
const normalized = normalizeTimezone(current);
|
|
@@ -10025,7 +11068,7 @@ var DrawingPainter = class {
|
|
|
10025
11068
|
return;
|
|
10026
11069
|
}
|
|
10027
11070
|
if (d instanceof FixedRangeVolumeProfile) {
|
|
10028
|
-
this.paintFixedRangeVp(ctx, d, proj);
|
|
11071
|
+
this.paintFixedRangeVp(ctx, d, proj, theme);
|
|
10029
11072
|
this.paintLabel(ctx, d, proj, theme);
|
|
10030
11073
|
return;
|
|
10031
11074
|
}
|
|
@@ -10518,7 +11561,7 @@ var DrawingPainter = class {
|
|
|
10518
11561
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
10519
11562
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
10520
11563
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
10521
|
-
paintFixedRangeVp(ctx, d, proj) {
|
|
11564
|
+
paintFixedRangeVp(ctx, d, proj, theme) {
|
|
10522
11565
|
const L = d.layout(proj);
|
|
10523
11566
|
if (!L) return;
|
|
10524
11567
|
const s = d.frvp;
|
|
@@ -10557,7 +11600,7 @@ var DrawingPainter = class {
|
|
|
10557
11600
|
};
|
|
10558
11601
|
hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
|
|
10559
11602
|
hLine(s.showVal, s.valColor, s.valStyle, L.valY);
|
|
10560
|
-
hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
|
|
11603
|
+
hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
|
|
10561
11604
|
const devW = Math.max(1, w);
|
|
10562
11605
|
if (s.showDevelopingPoc) {
|
|
10563
11606
|
this.strokePolyline(ctx, L.developingPoc, {
|
|
@@ -11184,6 +12227,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
11184
12227
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
11185
12228
|
var DRAG_SLOP2 = 3;
|
|
11186
12229
|
var FREEHAND_SAMPLE_PX = 4;
|
|
12230
|
+
var ANGLE_SNAP_STEP = Math.PI / 4;
|
|
12231
|
+
var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
|
|
11187
12232
|
var DrawingInteraction = class {
|
|
11188
12233
|
// magnet target for the ring
|
|
11189
12234
|
constructor(deps) {
|
|
@@ -11224,6 +12269,27 @@ var DrawingInteraction = class {
|
|
|
11224
12269
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
11225
12270
|
return snapped;
|
|
11226
12271
|
}
|
|
12272
|
+
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
12273
|
+
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
12274
|
+
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
12275
|
+
* the result to a candle afterwards would break the exact angle just enforced. */
|
|
12276
|
+
resolveAngleSnapped(pivot, paneId, x, y) {
|
|
12277
|
+
const proj = this.deps.projector();
|
|
12278
|
+
const px = proj.xOf(pivot.time);
|
|
12279
|
+
const py = proj.yOf(pivot.price, paneId);
|
|
12280
|
+
this.snapAt = null;
|
|
12281
|
+
if (py == null) return proj.pxToPoint(x, y, paneId);
|
|
12282
|
+
const r = Math.hypot(x - px, y - py);
|
|
12283
|
+
if (r < 1) return proj.pxToPoint(x, y, paneId);
|
|
12284
|
+
const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
|
|
12285
|
+
return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
|
|
12286
|
+
}
|
|
12287
|
+
/** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
|
|
12288
|
+
* the tool is not a two-point segment (or no anchor is committed yet). */
|
|
12289
|
+
placingPivot(draft) {
|
|
12290
|
+
if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
|
|
12291
|
+
return draft.anchors[draft.anchors.length - 1] ?? null;
|
|
12292
|
+
}
|
|
11227
12293
|
/** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
|
|
11228
12294
|
claim(x, y) {
|
|
11229
12295
|
if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
|
|
@@ -11245,12 +12311,15 @@ var DrawingInteraction = class {
|
|
|
11245
12311
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
11246
12312
|
}
|
|
11247
12313
|
/** 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
|
|
12314
|
+
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
12315
|
+
* while placing a line tool it locks the segment angle to 45° steps. */
|
|
11249
12316
|
down(x, y, mode = "off", shift = false) {
|
|
11250
12317
|
const proj = this.deps.projector();
|
|
11251
12318
|
if (this.state.kind === "placing") {
|
|
11252
12319
|
if (this.state.draft.placementMode() !== "click") return;
|
|
11253
|
-
this.
|
|
12320
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
12321
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
12322
|
+
this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
|
|
11254
12323
|
if (this.state.draft.anchors.length >= this.state.need) this.finalize();
|
|
11255
12324
|
else this.deps.changed();
|
|
11256
12325
|
return;
|
|
@@ -11275,8 +12344,9 @@ var DrawingInteraction = class {
|
|
|
11275
12344
|
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
12345
|
}
|
|
11277
12346
|
}
|
|
11278
|
-
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
11279
|
-
|
|
12347
|
+
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
12348
|
+
* `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
|
|
12349
|
+
move(x, y, mode = "off", shift = false) {
|
|
11280
12350
|
if (this.state.kind === "idle") {
|
|
11281
12351
|
const tool = this.deps.activeTool();
|
|
11282
12352
|
const had = this.snapAt != null;
|
|
@@ -11290,7 +12360,8 @@ var DrawingInteraction = class {
|
|
|
11290
12360
|
this.captureFreehand(x, y);
|
|
11291
12361
|
return;
|
|
11292
12362
|
}
|
|
11293
|
-
|
|
12363
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
12364
|
+
this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
11294
12365
|
this.deps.changed();
|
|
11295
12366
|
return;
|
|
11296
12367
|
}
|
|
@@ -11301,7 +12372,11 @@ var DrawingInteraction = class {
|
|
|
11301
12372
|
this.state.moved = true;
|
|
11302
12373
|
if (this.state.handle >= 0) {
|
|
11303
12374
|
const a = d.anchors[this.state.handle];
|
|
11304
|
-
if (a)
|
|
12375
|
+
if (a) {
|
|
12376
|
+
const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
12377
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
|
|
12378
|
+
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
12379
|
+
}
|
|
11305
12380
|
d.constrainHandleDrag(this.state.handle);
|
|
11306
12381
|
} else {
|
|
11307
12382
|
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
@@ -11397,7 +12472,11 @@ var DrawingInteraction = class {
|
|
|
11397
12472
|
const last = draft.anchors[draft.anchors.length - 1];
|
|
11398
12473
|
const lx = last ? proj.xOf(last.time) : NaN;
|
|
11399
12474
|
const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
|
|
11400
|
-
if (
|
|
12475
|
+
if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
|
|
12476
|
+
if (draft.anchors.length >= this.state.need) {
|
|
12477
|
+
const lastIdx = draft.anchors.length - 1;
|
|
12478
|
+
draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
|
|
12479
|
+
}
|
|
11401
12480
|
draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
|
|
11402
12481
|
}
|
|
11403
12482
|
this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
|
|
@@ -11470,11 +12549,11 @@ function applyFree(anchor, pt, free) {
|
|
|
11470
12549
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
11471
12550
|
var BTN = 30;
|
|
11472
12551
|
var ICON = 21;
|
|
11473
|
-
var
|
|
11474
|
-
function
|
|
11475
|
-
if (typeof document === "undefined" || document.getElementById(
|
|
12552
|
+
var STYLE_ID5 = "vela-drawing-popup-styles";
|
|
12553
|
+
function ensureStyles5() {
|
|
12554
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID5)) return;
|
|
11476
12555
|
const s = document.createElement("style");
|
|
11477
|
-
s.id =
|
|
12556
|
+
s.id = STYLE_ID5;
|
|
11478
12557
|
s.textContent = `
|
|
11479
12558
|
.vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
|
|
11480
12559
|
.vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -11493,7 +12572,10 @@ function ensureStyles4() {
|
|
|
11493
12572
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
11494
12573
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
11495
12574
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
11496
|
-
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
12575
|
+
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
12576
|
+
/* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
|
|
12577
|
+
devices (the glyph stays centered; only the hit target widens). */
|
|
12578
|
+
@media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
|
|
11497
12579
|
document.head.appendChild(s);
|
|
11498
12580
|
}
|
|
11499
12581
|
var DrawingSettingsPopup = class {
|
|
@@ -11540,7 +12622,7 @@ var DrawingSettingsPopup = class {
|
|
|
11540
12622
|
* `onClose` fires on an outside-click dismissal (not a programmatic close). */
|
|
11541
12623
|
open(drawing, anchor, actions, onClose) {
|
|
11542
12624
|
this.close();
|
|
11543
|
-
|
|
12625
|
+
ensureStyles5();
|
|
11544
12626
|
this.onClose = onClose ?? null;
|
|
11545
12627
|
const t = this.theme;
|
|
11546
12628
|
const schema = drawing.schema();
|
|
@@ -11566,7 +12648,14 @@ var DrawingSettingsPopup = class {
|
|
|
11566
12648
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
11567
12649
|
}
|
|
11568
12650
|
if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.lineColor": v })));
|
|
11569
|
-
if (paths.has("style.lineWidth"))
|
|
12651
|
+
if (paths.has("style.lineWidth")) {
|
|
12652
|
+
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
12653
|
+
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
12654
|
+
bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
12655
|
+
} else {
|
|
12656
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
11570
12659
|
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), drawing.style.lineStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
11571
12660
|
if (paths.has("style.fillColor")) bar.appendChild(this.colorButton("Fill", BUCKET_ICON, effectiveFillColor(drawing, this.theme) ?? drawing.style.fillColor ?? DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.fillColor": v })));
|
|
11572
12661
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
@@ -12034,14 +13123,15 @@ var DrawingSettingsPopup = class {
|
|
|
12034
13123
|
const chk = document.createElement("input");
|
|
12035
13124
|
chk.type = "checkbox";
|
|
12036
13125
|
chk.checked = Boolean(s[showPath]);
|
|
12037
|
-
|
|
13126
|
+
const current = s[colorPath] ?? contrastColor(t.background);
|
|
13127
|
+
chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
|
|
12038
13128
|
chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
|
|
12039
13129
|
const lbl = document.createElement("span");
|
|
12040
13130
|
lbl.textContent = label;
|
|
12041
13131
|
lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
|
|
12042
13132
|
const col = document.createElement("button");
|
|
12043
13133
|
col.type = "button";
|
|
12044
|
-
let cur =
|
|
13134
|
+
let cur = current;
|
|
12045
13135
|
col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
|
|
12046
13136
|
col.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
12047
13137
|
col.addEventListener("click", (e) => {
|
|
@@ -12221,7 +13311,8 @@ var DrawingSettingsPopup = class {
|
|
|
12221
13311
|
dragHandle() {
|
|
12222
13312
|
const b = document.createElement("button");
|
|
12223
13313
|
b.type = "button";
|
|
12224
|
-
b.
|
|
13314
|
+
b.className = "vela-dpop-grip";
|
|
13315
|
+
b.style.cssText = `width:${BTN}px;height:${BTN}px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;padding:0;cursor:grab;touch-action:none;color:var(--vela-fg-faint);`;
|
|
12225
13316
|
b.innerHTML = sized(GRIP_ICON);
|
|
12226
13317
|
b.addEventListener("pointerdown", (e) => {
|
|
12227
13318
|
const el = this.el;
|
|
@@ -12237,6 +13328,10 @@ var DrawingSettingsPopup = class {
|
|
|
12237
13328
|
const origLeft = parseFloat(el.style.left) || 0;
|
|
12238
13329
|
const origTop = parseFloat(el.style.top) || 0;
|
|
12239
13330
|
b.style.cursor = "grabbing";
|
|
13331
|
+
try {
|
|
13332
|
+
b.setPointerCapture(e.pointerId);
|
|
13333
|
+
} catch {
|
|
13334
|
+
}
|
|
12240
13335
|
const move = (ev) => {
|
|
12241
13336
|
const w = el.offsetWidth;
|
|
12242
13337
|
const h = el.offsetHeight;
|
|
@@ -12249,9 +13344,11 @@ var DrawingSettingsPopup = class {
|
|
|
12249
13344
|
b.style.cursor = "grab";
|
|
12250
13345
|
window.removeEventListener("pointermove", move);
|
|
12251
13346
|
window.removeEventListener("pointerup", up);
|
|
13347
|
+
window.removeEventListener("pointercancel", up);
|
|
12252
13348
|
};
|
|
12253
13349
|
window.addEventListener("pointermove", move);
|
|
12254
13350
|
window.addEventListener("pointerup", up);
|
|
13351
|
+
window.addEventListener("pointercancel", up);
|
|
12255
13352
|
});
|
|
12256
13353
|
return b;
|
|
12257
13354
|
}
|
|
@@ -12367,6 +13464,39 @@ var DrawingSettingsPopup = class {
|
|
|
12367
13464
|
});
|
|
12368
13465
|
return b;
|
|
12369
13466
|
}
|
|
13467
|
+
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
13468
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. */
|
|
13469
|
+
widthInput(tip, value, min, max, step, onChange) {
|
|
13470
|
+
const input = document.createElement("input");
|
|
13471
|
+
input.type = "number";
|
|
13472
|
+
input.dataset.tip = tip;
|
|
13473
|
+
input.min = String(min);
|
|
13474
|
+
input.max = String(max);
|
|
13475
|
+
input.step = String(step);
|
|
13476
|
+
input.value = String(value);
|
|
13477
|
+
input.style.cssText = `width:52px;height:${BTN}px;box-sizing:border-box;flex:none;background:transparent;color:inherit;border:1px solid var(--vela-border);border-radius:5px;padding:0 4px;font:12px ${this.theme.fontFamily};font-variant-numeric:tabular-nums;outline:none;`;
|
|
13478
|
+
const commit = () => {
|
|
13479
|
+
const n = parseFloat(input.value);
|
|
13480
|
+
if (!Number.isFinite(n)) {
|
|
13481
|
+
input.value = String(value);
|
|
13482
|
+
return;
|
|
13483
|
+
}
|
|
13484
|
+
const v = Math.min(max, Math.max(min, n));
|
|
13485
|
+
input.value = String(v);
|
|
13486
|
+
value = v;
|
|
13487
|
+
onChange(v);
|
|
13488
|
+
};
|
|
13489
|
+
input.addEventListener("change", commit);
|
|
13490
|
+
input.addEventListener("keydown", (e) => {
|
|
13491
|
+
e.stopPropagation();
|
|
13492
|
+
if (e.key === "Enter") {
|
|
13493
|
+
e.preventDefault();
|
|
13494
|
+
commit();
|
|
13495
|
+
input.blur();
|
|
13496
|
+
}
|
|
13497
|
+
});
|
|
13498
|
+
return input;
|
|
13499
|
+
}
|
|
12370
13500
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
12371
13501
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
12372
13502
|
* opens a floating list of every option so a value is one click away (no cycling through). */
|
|
@@ -12810,12 +13940,19 @@ var UserDrawingController = class {
|
|
|
12810
13940
|
this.activeTool = null;
|
|
12811
13941
|
// last-used style for the armed tool (seeds the placement ghost)
|
|
12812
13942
|
this.intentCb = null;
|
|
13943
|
+
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
13944
|
+
this.externalGhost = null;
|
|
13945
|
+
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
13946
|
+
this.lastDraftKey = null;
|
|
12813
13947
|
this.measure = new MeasureOverlay();
|
|
12814
13948
|
// transient ruler — not a persistent drawing
|
|
12815
13949
|
this.toolbarVisible = false;
|
|
12816
13950
|
// mirrors showToolbar (drives the gutter reservation)
|
|
12817
13951
|
this.toolbarCollapsed = false;
|
|
12818
13952
|
// the bar is a slim expand-strip (narrower gutter)
|
|
13953
|
+
/** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
|
|
13954
|
+
* picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
|
|
13955
|
+
this.mobileLayout = false;
|
|
12819
13956
|
/** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
|
|
12820
13957
|
* history (drawings + app ops in one timeline) turns this off so the chords
|
|
12821
13958
|
* bubble to its keymap instead — see the renderer's `historyChords` feature. */
|
|
@@ -12874,12 +14011,26 @@ var UserDrawingController = class {
|
|
|
12874
14011
|
}
|
|
12875
14012
|
showToolbar(visible) {
|
|
12876
14013
|
this.toolbarVisible = visible;
|
|
12877
|
-
this.toolbar.setVisible(visible);
|
|
14014
|
+
this.toolbar.setVisible(visible && !this.mobileLayout);
|
|
14015
|
+
this.syncToolbarGutter();
|
|
14016
|
+
}
|
|
14017
|
+
/** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
|
|
14018
|
+
* shell's own drawer picks tools instead) and releases its gutter; flipping back
|
|
14019
|
+
* to desktop restores whatever `showToolbar` last asked for. */
|
|
14020
|
+
setLayoutMode(mode) {
|
|
14021
|
+
this.mobileLayout = mode === "mobile";
|
|
14022
|
+
this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
|
|
12878
14023
|
this.syncToolbarGutter();
|
|
12879
14024
|
}
|
|
12880
14025
|
/** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
|
|
12881
14026
|
syncToolbarGutter() {
|
|
12882
|
-
this.
|
|
14027
|
+
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
14028
|
+
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
14029
|
+
}
|
|
14030
|
+
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
14031
|
+
setExternalGhost(doc) {
|
|
14032
|
+
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
14033
|
+
this.render();
|
|
12883
14034
|
}
|
|
12884
14035
|
syncDrawings(docs) {
|
|
12885
14036
|
this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
|
|
@@ -13039,7 +14190,7 @@ var UserDrawingController = class {
|
|
|
13039
14190
|
}
|
|
13040
14191
|
this.interaction.down(x, y, snap, shift);
|
|
13041
14192
|
}
|
|
13042
|
-
pointerMove(x, y, snap = "off") {
|
|
14193
|
+
pointerMove(x, y, snap = "off", shift = false) {
|
|
13043
14194
|
if (this.eraserMode) {
|
|
13044
14195
|
if (this.erasing) this.deleteAt(x, y);
|
|
13045
14196
|
return;
|
|
@@ -13049,7 +14200,7 @@ var UserDrawingController = class {
|
|
|
13049
14200
|
this.render();
|
|
13050
14201
|
return;
|
|
13051
14202
|
}
|
|
13052
|
-
this.interaction.move(x, y, snap);
|
|
14203
|
+
this.interaction.move(x, y, snap, shift);
|
|
13053
14204
|
this.updateHover(x, y);
|
|
13054
14205
|
}
|
|
13055
14206
|
/** Track which drawing is hovered so its handles appear on hover (and only then). */
|
|
@@ -13435,6 +14586,8 @@ var UserDrawingController = class {
|
|
|
13435
14586
|
this.layoutTextEditor();
|
|
13436
14587
|
const ghost = this.interaction.ghost();
|
|
13437
14588
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
14589
|
+
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
14590
|
+
this.emitDraft(ghost);
|
|
13438
14591
|
const markers = this.interaction.placingMarkers(proj);
|
|
13439
14592
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
13440
14593
|
const m = this.interaction.snapMarker();
|
|
@@ -13496,6 +14649,15 @@ var UserDrawingController = class {
|
|
|
13496
14649
|
}
|
|
13497
14650
|
}, () => this.clearSelection());
|
|
13498
14651
|
}
|
|
14652
|
+
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
14653
|
+
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
14654
|
+
* shape changes emit, and the end of a placement emits one `null`. */
|
|
14655
|
+
emitDraft(ghost) {
|
|
14656
|
+
const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
|
|
14657
|
+
if (key === this.lastDraftKey) return;
|
|
14658
|
+
this.lastDraftKey = key;
|
|
14659
|
+
this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
|
|
14660
|
+
}
|
|
13499
14661
|
emit(i) {
|
|
13500
14662
|
if (i.kind === "create") {
|
|
13501
14663
|
if (i.doc.text && i.doc.text.color === void 0) {
|
|
@@ -13732,7 +14894,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
|
|
|
13732
14894
|
|
|
13733
14895
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
13734
14896
|
var ATTRIBUTION_URL = "https://luxalgo.com/vela";
|
|
13735
|
-
var
|
|
14897
|
+
var STYLE_ID6 = "vela-attribution-styles";
|
|
13736
14898
|
var CSS = `
|
|
13737
14899
|
.vela-attribution {
|
|
13738
14900
|
text-decoration: none;
|
|
@@ -13778,12 +14940,28 @@ var CSS = `
|
|
|
13778
14940
|
max-width: 240px;
|
|
13779
14941
|
opacity: 1;
|
|
13780
14942
|
transform: translateX(0);
|
|
13781
|
-
}
|
|
13782
|
-
|
|
13783
|
-
|
|
14943
|
+
}
|
|
14944
|
+
/* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
|
|
14945
|
+
renderer's own container attribute (data-vela-layout) AND the shell root's
|
|
14946
|
+
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
14947
|
+
container, so only the shell attribute reaches it. */
|
|
14948
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
14949
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
|
|
14950
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
14951
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
14952
|
+
top: 2px;
|
|
14953
|
+
transform: translateX(-6px);
|
|
14954
|
+
}
|
|
14955
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
14956
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
|
|
14957
|
+
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
14958
|
+
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
14959
|
+
`;
|
|
14960
|
+
function ensureStyles6(doc) {
|
|
14961
|
+
let s = doc.getElementById(STYLE_ID6);
|
|
13784
14962
|
if (!s) {
|
|
13785
14963
|
s = doc.createElement("style");
|
|
13786
|
-
s.id =
|
|
14964
|
+
s.id = STYLE_ID6;
|
|
13787
14965
|
doc.head.appendChild(s);
|
|
13788
14966
|
}
|
|
13789
14967
|
s.textContent = CSS;
|
|
@@ -13812,7 +14990,7 @@ function createCustomMark(doc, html, background) {
|
|
|
13812
14990
|
return el;
|
|
13813
14991
|
}
|
|
13814
14992
|
function createAttributionMark(doc, background) {
|
|
13815
|
-
|
|
14993
|
+
ensureStyles6(doc);
|
|
13816
14994
|
const a = doc.createElement("a");
|
|
13817
14995
|
a.className = "vela-attribution";
|
|
13818
14996
|
a.href = ATTRIBUTION_URL;
|
|
@@ -13838,6 +15016,174 @@ function createAttributionMark(doc, background) {
|
|
|
13838
15016
|
return a;
|
|
13839
15017
|
}
|
|
13840
15018
|
|
|
15019
|
+
// src/renderers/shared/dom-raster.ts
|
|
15020
|
+
function hasInk(color) {
|
|
15021
|
+
if (!color || color === "transparent") return false;
|
|
15022
|
+
const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
|
|
15023
|
+
return m ? parseFloat(m[1]) > 1e-3 : true;
|
|
15024
|
+
}
|
|
15025
|
+
function cornerRadius(cs, w, h) {
|
|
15026
|
+
const raw = cs.borderTopLeftRadius;
|
|
15027
|
+
const v = parseFloat(raw) || 0;
|
|
15028
|
+
const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
|
|
15029
|
+
return Math.max(0, Math.min(px, Math.min(w, h) / 2));
|
|
15030
|
+
}
|
|
15031
|
+
function tracePath(ctx, x, y, w, h, r) {
|
|
15032
|
+
ctx.beginPath();
|
|
15033
|
+
if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
|
|
15034
|
+
else ctx.rect(x, y, w, h);
|
|
15035
|
+
}
|
|
15036
|
+
function canDrawUntainted(img) {
|
|
15037
|
+
try {
|
|
15038
|
+
const probe2 = img.ownerDocument.createElement("canvas");
|
|
15039
|
+
probe2.width = 1;
|
|
15040
|
+
probe2.height = 1;
|
|
15041
|
+
const ctx = probe2.getContext("2d");
|
|
15042
|
+
if (!ctx) return false;
|
|
15043
|
+
ctx.drawImage(img, 0, 0, 1, 1);
|
|
15044
|
+
probe2.toDataURL();
|
|
15045
|
+
return true;
|
|
15046
|
+
} catch {
|
|
15047
|
+
return false;
|
|
15048
|
+
}
|
|
15049
|
+
}
|
|
15050
|
+
function rasterizeOverlay(ctx, root, frame) {
|
|
15051
|
+
const win = root.ownerDocument.defaultView;
|
|
15052
|
+
if (!win) return;
|
|
15053
|
+
ctx.save();
|
|
15054
|
+
ctx.scale(frame.dpr, frame.dpr);
|
|
15055
|
+
const drawText = (node, cs, alpha) => {
|
|
15056
|
+
const text = node.textContent?.replace(/\s+/g, " ").trim();
|
|
15057
|
+
if (!text) return;
|
|
15058
|
+
const range = node.ownerDocument.createRange();
|
|
15059
|
+
range.selectNodeContents(node);
|
|
15060
|
+
const r = range.getBoundingClientRect();
|
|
15061
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15062
|
+
ctx.globalAlpha = alpha;
|
|
15063
|
+
ctx.fillStyle = cs.color;
|
|
15064
|
+
ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
|
|
15065
|
+
ctx.textBaseline = "middle";
|
|
15066
|
+
ctx.textAlign = "left";
|
|
15067
|
+
ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
|
|
15068
|
+
};
|
|
15069
|
+
const drawImg = (img, cs, alpha) => {
|
|
15070
|
+
if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
|
|
15071
|
+
const r = img.getBoundingClientRect();
|
|
15072
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15073
|
+
const x = r.left - frame.left;
|
|
15074
|
+
const y = r.top - frame.top;
|
|
15075
|
+
ctx.save();
|
|
15076
|
+
ctx.globalAlpha = alpha;
|
|
15077
|
+
tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
|
|
15078
|
+
ctx.clip();
|
|
15079
|
+
ctx.drawImage(img, x, y, r.width, r.height);
|
|
15080
|
+
ctx.restore();
|
|
15081
|
+
};
|
|
15082
|
+
const drawSvgShapes = (parent, alpha) => {
|
|
15083
|
+
for (const el of parent.children) {
|
|
15084
|
+
if (el.hasAttribute("transform")) continue;
|
|
15085
|
+
const tag = el.tagName.toLowerCase();
|
|
15086
|
+
if (tag === "g") {
|
|
15087
|
+
drawSvgShapes(el, alpha);
|
|
15088
|
+
continue;
|
|
15089
|
+
}
|
|
15090
|
+
const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
|
|
15091
|
+
let path = null;
|
|
15092
|
+
if (tag === "path") {
|
|
15093
|
+
const d = el.getAttribute("d");
|
|
15094
|
+
if (d) path = new Path2D(d);
|
|
15095
|
+
} else if (tag === "circle") {
|
|
15096
|
+
path = new Path2D();
|
|
15097
|
+
path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
|
|
15098
|
+
} else if (tag === "ellipse") {
|
|
15099
|
+
path = new Path2D();
|
|
15100
|
+
path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
|
|
15101
|
+
} else if (tag === "rect") {
|
|
15102
|
+
path = new Path2D();
|
|
15103
|
+
path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
|
|
15104
|
+
} else if (tag === "line") {
|
|
15105
|
+
path = new Path2D();
|
|
15106
|
+
path.moveTo(num3("x1"), num3("y1"));
|
|
15107
|
+
path.lineTo(num3("x2"), num3("y2"));
|
|
15108
|
+
} else if (tag === "polyline" || tag === "polygon") {
|
|
15109
|
+
const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
|
|
15110
|
+
if (pts.length < 4 || pts.some(Number.isNaN)) continue;
|
|
15111
|
+
path = new Path2D();
|
|
15112
|
+
path.moveTo(pts[0], pts[1]);
|
|
15113
|
+
for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
|
|
15114
|
+
if (tag === "polygon") path.closePath();
|
|
15115
|
+
}
|
|
15116
|
+
if (!path) continue;
|
|
15117
|
+
const cs = win.getComputedStyle(el);
|
|
15118
|
+
if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
|
|
15119
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
|
|
15120
|
+
ctx.fillStyle = cs.fill;
|
|
15121
|
+
ctx.fill(path);
|
|
15122
|
+
}
|
|
15123
|
+
const sw = parseFloat(cs.strokeWidth) || 0;
|
|
15124
|
+
if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
|
|
15125
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
|
|
15126
|
+
ctx.strokeStyle = cs.stroke;
|
|
15127
|
+
ctx.lineWidth = sw;
|
|
15128
|
+
if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
|
|
15129
|
+
if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
|
|
15130
|
+
ctx.stroke(path);
|
|
15131
|
+
}
|
|
15132
|
+
}
|
|
15133
|
+
};
|
|
15134
|
+
const drawSvg = (svg, parentAlpha) => {
|
|
15135
|
+
const r = svg.getBoundingClientRect();
|
|
15136
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15137
|
+
const cs = win.getComputedStyle(svg);
|
|
15138
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
15139
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
15140
|
+
if (alpha <= 1e-3) return;
|
|
15141
|
+
const vb = svg.viewBox.baseVal;
|
|
15142
|
+
const vw = vb && vb.width > 0 ? vb.width : r.width;
|
|
15143
|
+
const vh = vb && vb.height > 0 ? vb.height : r.height;
|
|
15144
|
+
ctx.save();
|
|
15145
|
+
ctx.translate(r.left - frame.left, r.top - frame.top);
|
|
15146
|
+
ctx.scale(r.width / vw, r.height / vh);
|
|
15147
|
+
if (vb) ctx.translate(-vb.x, -vb.y);
|
|
15148
|
+
drawSvgShapes(svg, alpha);
|
|
15149
|
+
ctx.restore();
|
|
15150
|
+
};
|
|
15151
|
+
const drawElement = (el, parentAlpha) => {
|
|
15152
|
+
const cs = win.getComputedStyle(el);
|
|
15153
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
15154
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
15155
|
+
if (alpha <= 1e-3) return;
|
|
15156
|
+
const r = el.getBoundingClientRect();
|
|
15157
|
+
if (r.width > 0 && r.height > 0) {
|
|
15158
|
+
const x = r.left - frame.left;
|
|
15159
|
+
const y = r.top - frame.top;
|
|
15160
|
+
const radius = cornerRadius(cs, r.width, r.height);
|
|
15161
|
+
if (hasInk(cs.backgroundColor)) {
|
|
15162
|
+
ctx.globalAlpha = alpha;
|
|
15163
|
+
ctx.fillStyle = cs.backgroundColor;
|
|
15164
|
+
tracePath(ctx, x, y, r.width, r.height, radius);
|
|
15165
|
+
ctx.fill();
|
|
15166
|
+
}
|
|
15167
|
+
const bw = parseFloat(cs.borderTopWidth) || 0;
|
|
15168
|
+
if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
|
|
15169
|
+
ctx.globalAlpha = alpha;
|
|
15170
|
+
ctx.strokeStyle = cs.borderTopColor;
|
|
15171
|
+
ctx.lineWidth = bw;
|
|
15172
|
+
tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
|
|
15173
|
+
ctx.stroke();
|
|
15174
|
+
}
|
|
15175
|
+
}
|
|
15176
|
+
for (const child of el.childNodes) {
|
|
15177
|
+
if (child.nodeType === 3) drawText(child, cs, alpha);
|
|
15178
|
+
else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
|
|
15179
|
+
else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
|
|
15180
|
+
else if (child instanceof win.HTMLElement) drawElement(child, alpha);
|
|
15181
|
+
}
|
|
15182
|
+
};
|
|
15183
|
+
if (root instanceof win.HTMLElement) drawElement(root, 1);
|
|
15184
|
+
ctx.restore();
|
|
15185
|
+
}
|
|
15186
|
+
|
|
13841
15187
|
// src/renderers/native/vpvr/profile.ts
|
|
13842
15188
|
function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
|
|
13843
15189
|
const from = Math.max(0, i0);
|
|
@@ -14006,12 +15352,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
|
|
|
14006
15352
|
var COLLAPSED_PANE_H = 26;
|
|
14007
15353
|
var TIME_AXIS_H = 22;
|
|
14008
15354
|
var SCROLL_BTN_SIZE = 26;
|
|
15355
|
+
var SCROLL_BTN_SIZE_TOUCH = 24;
|
|
14009
15356
|
var SCROLL_BTN_RADIUS = 5;
|
|
14010
15357
|
var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
|
|
14011
15358
|
var SCROLL_BTN_ICON_PX = 12;
|
|
14012
|
-
var SCROLL_BTN_RIGHT_INSET =
|
|
15359
|
+
var SCROLL_BTN_RIGHT_INSET = 12;
|
|
14013
15360
|
var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
|
|
14014
|
-
var SCROLL_BTN_BOTTOM = TIME_AXIS_H +
|
|
15361
|
+
var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
14015
15362
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
14016
15363
|
var MIN_VISIBLE_BARS = 2;
|
|
14017
15364
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
@@ -14027,10 +15374,10 @@ var SEPARATOR_HIT_PX = 4;
|
|
|
14027
15374
|
var NativeRenderer = class {
|
|
14028
15375
|
constructor(opts) {
|
|
14029
15376
|
this.capabilities = NATIVE_CAPABILITIES;
|
|
14030
|
-
// The app "chrome" surface (drawing toolbar +
|
|
15377
|
+
// The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
|
|
14031
15378
|
// Owned by the app theme alone (mount/setTheme) — config-level background/text edits
|
|
14032
|
-
// (settings dialog, persisted configs) recolor
|
|
14033
|
-
//
|
|
15379
|
+
// (settings dialog, persisted configs) recolor the plot AND the axis scales (see
|
|
15380
|
+
// axisSurface), but never the toolbar/dialog chrome.
|
|
14034
15381
|
this.surfaceBackground = DARK_THEME.background;
|
|
14035
15382
|
this.surfaceTextColor = DARK_THEME.textColor;
|
|
14036
15383
|
// the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
|
|
@@ -14063,8 +15410,12 @@ var NativeRenderer = class {
|
|
|
14063
15410
|
this.symbolPicker = null;
|
|
14064
15411
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
14065
15412
|
this.indicatorTitlesOn = true;
|
|
15413
|
+
/** Plot values beside the legend titles shown — held here so a remount re-applies it. */
|
|
15414
|
+
this.indicatorValuesOn = true;
|
|
14066
15415
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
14067
15416
|
this.legendActionsProvider = null;
|
|
15417
|
+
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
15418
|
+
this.legendOverviewAction = null;
|
|
14068
15419
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
14069
15420
|
this.keyboard = null;
|
|
14070
15421
|
this.keyboardEnabled = true;
|
|
@@ -14174,6 +15525,8 @@ var NativeRenderer = class {
|
|
|
14174
15525
|
// Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
|
|
14175
15526
|
// pane carries merged (own-scale) columns; the constant only clears a single-column scale.
|
|
14176
15527
|
this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
|
|
15528
|
+
// ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
|
|
15529
|
+
this.layoutMode = "desktop";
|
|
14177
15530
|
this.viewportCbs = /* @__PURE__ */ new Set();
|
|
14178
15531
|
this.crosshairCbs = /* @__PURE__ */ new Set();
|
|
14179
15532
|
this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
|
|
@@ -14184,6 +15537,7 @@ var NativeRenderer = class {
|
|
|
14184
15537
|
this.factoryConfig = null;
|
|
14185
15538
|
this.hostSettingsSections = [];
|
|
14186
15539
|
this.clickCbs = /* @__PURE__ */ new Set();
|
|
15540
|
+
this.axisLongPressCbs = /* @__PURE__ */ new Set();
|
|
14187
15541
|
this.inputChangeCbs = /* @__PURE__ */ new Set();
|
|
14188
15542
|
this.removeIndicatorCbs = /* @__PURE__ */ new Set();
|
|
14189
15543
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
@@ -14191,7 +15545,7 @@ var NativeRenderer = class {
|
|
|
14191
15545
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
14192
15546
|
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
14193
15547
|
this.name = "native";
|
|
14194
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles"];
|
|
15548
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
14195
15549
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
14196
15550
|
* so moving onto the button doesn't count as leaving). */
|
|
14197
15551
|
this.onScrollProximityMove = (e) => {
|
|
@@ -14230,6 +15584,7 @@ var NativeRenderer = class {
|
|
|
14230
15584
|
this.candleDown = opts.downColor;
|
|
14231
15585
|
this.scene.priceStyle = opts.priceStyle;
|
|
14232
15586
|
this.scene.basePainting = basePaintingOf(opts.priceStyle);
|
|
15587
|
+
this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
|
|
14233
15588
|
}
|
|
14234
15589
|
this.theme = this.deriveTheme(DARK_THEME);
|
|
14235
15590
|
}
|
|
@@ -14363,6 +15718,11 @@ var NativeRenderer = class {
|
|
|
14363
15718
|
this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
|
|
14364
15719
|
return;
|
|
14365
15720
|
// own DOM, no repaint needed
|
|
15721
|
+
case "indicatorValues":
|
|
15722
|
+
this.indicatorValuesOn = value !== false;
|
|
15723
|
+
this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
|
|
15724
|
+
return;
|
|
15725
|
+
// own DOM, no repaint needed
|
|
14366
15726
|
case "attribution":
|
|
14367
15727
|
this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
|
|
14368
15728
|
this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
|
|
@@ -14451,6 +15811,8 @@ var NativeRenderer = class {
|
|
|
14451
15811
|
return this.settingsEnabled;
|
|
14452
15812
|
case "indicatorTitles":
|
|
14453
15813
|
return this.indicatorTitlesOn;
|
|
15814
|
+
case "indicatorValues":
|
|
15815
|
+
return this.indicatorValuesOn;
|
|
14454
15816
|
case "attribution":
|
|
14455
15817
|
return this.attributionHtml ?? this.attributionEnabled;
|
|
14456
15818
|
case "dialogHost":
|
|
@@ -14707,6 +16069,7 @@ var NativeRenderer = class {
|
|
|
14707
16069
|
baselineLevel: next.baseline.baselineLevel
|
|
14708
16070
|
};
|
|
14709
16071
|
this.setPriceStyle(next.series.style);
|
|
16072
|
+
this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
|
|
14710
16073
|
this.scene.baselineValue = next.series.baseline;
|
|
14711
16074
|
this.coords.setPitchScale(next.series.spacing);
|
|
14712
16075
|
this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
|
|
@@ -14786,7 +16149,10 @@ var NativeRenderer = class {
|
|
|
14786
16149
|
/** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
|
|
14787
16150
|
* in-chart gear — created on demand, independent of the gear feature being enabled. */
|
|
14788
16151
|
openSettingsDialog(section) {
|
|
14789
|
-
if (!this.settingsDialog && this.plot)
|
|
16152
|
+
if (!this.settingsDialog && this.plot) {
|
|
16153
|
+
this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
|
|
16154
|
+
this.settingsDialog.setLayoutMode(this.layoutMode);
|
|
16155
|
+
}
|
|
14790
16156
|
this.toggleSettingsDialog(section);
|
|
14791
16157
|
}
|
|
14792
16158
|
/** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
|
|
@@ -14812,7 +16178,7 @@ var NativeRenderer = class {
|
|
|
14812
16178
|
(patch) => this.applyConfig(patch),
|
|
14813
16179
|
(json) => this.applyConfig(json),
|
|
14814
16180
|
() => {
|
|
14815
|
-
if (this.factoryConfig) this.applyConfig(this.factoryConfig);
|
|
16181
|
+
if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
|
|
14816
16182
|
this.settingsDialog?.close();
|
|
14817
16183
|
this.openSettingsDialog();
|
|
14818
16184
|
},
|
|
@@ -14875,11 +16241,18 @@ var NativeRenderer = class {
|
|
|
14875
16241
|
this.scrollButton.style.color = this.theme.textColor;
|
|
14876
16242
|
this.scrollButton.style.borderColor = this.theme.borderColor;
|
|
14877
16243
|
}
|
|
14878
|
-
/** Show the button only when the latest bar is scrolled off the right edge
|
|
16244
|
+
/** Show the button only when the latest bar is scrolled off the right edge. On desktop the
|
|
16245
|
+
* cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
|
|
16246
|
+
* appears whenever the latest bars are off-screen and hides again once the view is back
|
|
16247
|
+
* at the right edge. */
|
|
14879
16248
|
updateScrollToRealtimeButton() {
|
|
14880
16249
|
const btn = this.scrollButton;
|
|
14881
16250
|
if (!btn) return;
|
|
14882
16251
|
const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
|
|
16252
|
+
if (this.layoutMode === "mobile") {
|
|
16253
|
+
btn.style.display = latestOffScreen ? "flex" : "none";
|
|
16254
|
+
return;
|
|
16255
|
+
}
|
|
14883
16256
|
btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
|
|
14884
16257
|
}
|
|
14885
16258
|
/** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
|
|
@@ -14901,17 +16274,20 @@ var NativeRenderer = class {
|
|
|
14901
16274
|
this.animator.start();
|
|
14902
16275
|
}
|
|
14903
16276
|
/**
|
|
14904
|
-
* Export the current chart as a PNG data URL by compositing
|
|
14905
|
-
*
|
|
14906
|
-
*
|
|
14907
|
-
*
|
|
14908
|
-
*
|
|
14909
|
-
*
|
|
14910
|
-
*
|
|
14911
|
-
*
|
|
14912
|
-
*
|
|
14913
|
-
*
|
|
14914
|
-
*
|
|
16277
|
+
* Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
|
|
16278
|
+
* an offscreen canvas, in the same stacking order the DOM shows: plugin layers
|
|
16279
|
+
* below the data, geometry (L0), volume columns (L0.25), the visible-range volume
|
|
16280
|
+
* profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
|
|
16281
|
+
* (L1.5). The background is filled first (the canvas2d data layer is transparent —
|
|
16282
|
+
* its bg lives on the wrapper), and a fresh synchronous paint runs first so the
|
|
16283
|
+
* WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
|
|
16284
|
+
* back this tick — the same paint also repaints the drawings layers, so they're
|
|
16285
|
+
* current. DOM chrome joins as a best-effort text/chip raster (see
|
|
16286
|
+
* `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
|
|
16287
|
+
* opts in with a `data-vela-screenshot` attribute on the mount container's
|
|
16288
|
+
* subtree (the widget marks its status line, and its symbol watermark with
|
|
16289
|
+
* `"under"` — drawn beneath the canvases, where it sits on screen). Only the
|
|
16290
|
+
* crosshair (L2) is intentionally excluded.
|
|
14915
16291
|
*/
|
|
14916
16292
|
screenshot() {
|
|
14917
16293
|
if (!this.dataCanvas) return null;
|
|
@@ -14924,9 +16300,25 @@ var NativeRenderer = class {
|
|
|
14924
16300
|
if (!ctx) return null;
|
|
14925
16301
|
ctx.fillStyle = this.theme.background;
|
|
14926
16302
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
16303
|
+
const plotRect = this.plot?.getBoundingClientRect();
|
|
16304
|
+
const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
|
|
16305
|
+
const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
|
|
16306
|
+
if (frame) {
|
|
16307
|
+
for (const el of hostMarked) {
|
|
16308
|
+
if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
16311
|
+
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
16312
|
+
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
16313
|
+
for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
|
|
16314
|
+
if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
|
|
16315
|
+
}
|
|
16316
|
+
if (frame) {
|
|
16317
|
+
for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
|
|
16318
|
+
for (const el of hostMarked) {
|
|
16319
|
+
if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
|
|
16320
|
+
}
|
|
16321
|
+
}
|
|
14930
16322
|
return out.toDataURL("image/png");
|
|
14931
16323
|
}
|
|
14932
16324
|
/**
|
|
@@ -15013,10 +16405,16 @@ var NativeRenderer = class {
|
|
|
15013
16405
|
borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
|
|
15014
16406
|
};
|
|
15015
16407
|
}
|
|
16408
|
+
/** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
|
|
16409
|
+
* (`layout.background`) and its contrast-corrected text, so the scales read as part of the
|
|
16410
|
+
* plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
|
|
16411
|
+
axisSurface() {
|
|
16412
|
+
return { background: this.theme.background, textColor: this.theme.textColor };
|
|
16413
|
+
}
|
|
15016
16414
|
// ── lifecycle ──
|
|
15017
16415
|
mount(container, theme) {
|
|
15018
16416
|
this.mountContainer = container;
|
|
15019
|
-
this.
|
|
16417
|
+
this.publishGutters();
|
|
15020
16418
|
this.theme = this.deriveTheme(theme);
|
|
15021
16419
|
this.surfaceBackground = theme.background;
|
|
15022
16420
|
this.surfaceTextColor = theme.textColor;
|
|
@@ -15071,6 +16469,9 @@ var NativeRenderer = class {
|
|
|
15071
16469
|
fling: (v) => this.fling(v),
|
|
15072
16470
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
15073
16471
|
onClick: (x) => this.handleClick(x),
|
|
16472
|
+
onAxisLongPress: (axis, x, y) => {
|
|
16473
|
+
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
16474
|
+
},
|
|
15074
16475
|
beginPriceScale: (x, y) => this.beginPriceScale(x, y),
|
|
15075
16476
|
priceScaleBy: (dy) => this.priceScaleBy(dy),
|
|
15076
16477
|
beginPricePan: (y) => this.beginPricePan(y),
|
|
@@ -15088,7 +16489,7 @@ var NativeRenderer = class {
|
|
|
15088
16489
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
15089
16490
|
drawingsSnapMode: () => this.snapMode,
|
|
15090
16491
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
15091
|
-
drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
|
|
16492
|
+
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
15092
16493
|
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
15093
16494
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
15094
16495
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -15117,6 +16518,14 @@ var NativeRenderer = class {
|
|
|
15117
16518
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
15118
16519
|
});
|
|
15119
16520
|
this.userDrawings.historyChords = this.historyChordsEnabled;
|
|
16521
|
+
if (this.layoutMode === "mobile") {
|
|
16522
|
+
this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
|
|
16523
|
+
this.userDrawings.setLayoutMode(this.layoutMode);
|
|
16524
|
+
if (this.scrollButton) {
|
|
16525
|
+
this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
16526
|
+
this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
16527
|
+
}
|
|
16528
|
+
}
|
|
15120
16529
|
this.keyboard = new KeyboardController({
|
|
15121
16530
|
panByBars: (bars) => this.panByBars(bars),
|
|
15122
16531
|
zoomByStep: (dir) => this.zoomByStep(dir),
|
|
@@ -15129,10 +16538,13 @@ var NativeRenderer = class {
|
|
|
15129
16538
|
});
|
|
15130
16539
|
this.setKeyboardEnabled(this.keyboardEnabled);
|
|
15131
16540
|
this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
|
|
16541
|
+
this.inputsUI.setLayoutMode(this.layoutMode);
|
|
15132
16542
|
this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
|
|
16543
|
+
this.inputsUI.setValuesVisible(this.indicatorValuesOn);
|
|
15133
16544
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
15134
16545
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
15135
16546
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
16547
|
+
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
15136
16548
|
this.inputsUI.setOnChange((c) => {
|
|
15137
16549
|
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
15138
16550
|
});
|
|
@@ -15325,6 +16737,7 @@ var NativeRenderer = class {
|
|
|
15325
16737
|
this.attributionEl?.remove();
|
|
15326
16738
|
this.attributionEl = null;
|
|
15327
16739
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
16740
|
+
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
15328
16741
|
this.mountContainer = null;
|
|
15329
16742
|
this.wrapper?.remove();
|
|
15330
16743
|
}
|
|
@@ -15493,6 +16906,7 @@ var NativeRenderer = class {
|
|
|
15493
16906
|
const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
|
|
15494
16907
|
if (w === this.rightAxisW) return false;
|
|
15495
16908
|
this.rightAxisW = w;
|
|
16909
|
+
this.publishGutters();
|
|
15496
16910
|
if (this.coords.width > 0) this.syncSize();
|
|
15497
16911
|
return true;
|
|
15498
16912
|
}
|
|
@@ -15545,7 +16959,10 @@ var NativeRenderer = class {
|
|
|
15545
16959
|
this.scene.indicators.set(model.id, model);
|
|
15546
16960
|
this.refreshAnchorOffset(model);
|
|
15547
16961
|
this.scene.assignIndicatorZ(model.id);
|
|
15548
|
-
this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, {
|
|
16962
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
16963
|
+
native: !!model.native,
|
|
16964
|
+
...model.shorttitle ? { settingsTitle: model.title } : {}
|
|
16965
|
+
});
|
|
15549
16966
|
this.syncTables(model);
|
|
15550
16967
|
if (model.native?.type === "volume") {
|
|
15551
16968
|
this.volumeActive = true;
|
|
@@ -15606,6 +17023,13 @@ var NativeRenderer = class {
|
|
|
15606
17023
|
this.legendActionsProvider = provider;
|
|
15607
17024
|
this.inputsUI?.setLegendActions(provider);
|
|
15608
17025
|
}
|
|
17026
|
+
setLegendOverviewAction(action) {
|
|
17027
|
+
this.legendOverviewAction = action;
|
|
17028
|
+
this.inputsUI?.setLegendOverviewAction(action);
|
|
17029
|
+
}
|
|
17030
|
+
openIndicatorSettings(indicatorId) {
|
|
17031
|
+
this.inputsUI?.openSettingsFor(indicatorId);
|
|
17032
|
+
}
|
|
15609
17033
|
/**
|
|
15610
17034
|
* Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
|
|
15611
17035
|
* series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
|
|
@@ -15652,12 +17076,31 @@ var NativeRenderer = class {
|
|
|
15652
17076
|
if (style === this.scene.priceStyle) return;
|
|
15653
17077
|
this.scene.priceStyle = style;
|
|
15654
17078
|
this.scene.basePainting = basePaintingOf(style);
|
|
17079
|
+
this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
|
|
15655
17080
|
for (const cb of this.priceStyleCbs) cb(style);
|
|
15656
17081
|
}
|
|
15657
17082
|
onRemoveIndicator(cb) {
|
|
15658
17083
|
this.removeIndicatorCbs.add(cb);
|
|
15659
17084
|
return () => this.removeIndicatorCbs.delete(cb);
|
|
15660
17085
|
}
|
|
17086
|
+
/** Port surface: the host shell's chrome size class. Mobile switches the renderer
|
|
17087
|
+
* chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
|
|
17088
|
+
* toolbar (the shell provides its own picker), the scroll-to-latest button shown
|
|
17089
|
+
* whenever the latest bars are off-screen (touch has no cursor proximity). */
|
|
17090
|
+
setLayoutMode(mode) {
|
|
17091
|
+
if (mode === this.layoutMode) return;
|
|
17092
|
+
this.layoutMode = mode;
|
|
17093
|
+
this.mountContainer?.setAttribute("data-vela-layout", mode);
|
|
17094
|
+
this.userDrawings?.setLayoutMode(mode);
|
|
17095
|
+
this.settingsDialog?.setLayoutMode(mode);
|
|
17096
|
+
this.inputsUI?.setLayoutMode(mode);
|
|
17097
|
+
if (this.scrollButton) {
|
|
17098
|
+
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
17099
|
+
this.scrollButton.style.width = `${px}px`;
|
|
17100
|
+
this.scrollButton.style.height = `${px}px`;
|
|
17101
|
+
}
|
|
17102
|
+
this.updateScrollToRealtimeButton();
|
|
17103
|
+
}
|
|
15661
17104
|
setSettingsSections(sections) {
|
|
15662
17105
|
this.hostSettingsSections = sections;
|
|
15663
17106
|
this.settingsDialog?.setHostSections(sections);
|
|
@@ -15682,6 +17125,10 @@ var NativeRenderer = class {
|
|
|
15682
17125
|
this.clickCbs.add(cb);
|
|
15683
17126
|
return () => this.clickCbs.delete(cb);
|
|
15684
17127
|
}
|
|
17128
|
+
onAxisLongPress(cb) {
|
|
17129
|
+
this.axisLongPressCbs.add(cb);
|
|
17130
|
+
return () => this.axisLongPressCbs.delete(cb);
|
|
17131
|
+
}
|
|
15685
17132
|
onViewportChange(cb) {
|
|
15686
17133
|
this.viewportCbs.add(cb);
|
|
15687
17134
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -15813,13 +17260,17 @@ var NativeRenderer = class {
|
|
|
15813
17260
|
active = true;
|
|
15814
17261
|
}
|
|
15815
17262
|
}
|
|
15816
|
-
this.
|
|
17263
|
+
const clamped = this.clampViewport(barSpacing, rightOffset);
|
|
17264
|
+
if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
|
|
17265
|
+
if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
|
|
17266
|
+
this.coords.setViewport(clamped);
|
|
15817
17267
|
this.computeScales();
|
|
15818
17268
|
if (this.easeScales(dtMs)) active = true;
|
|
15819
17269
|
if (this.easeLiveBar(dtMs)) active = true;
|
|
15820
17270
|
this.skeletonClockMs += dtMs;
|
|
15821
17271
|
this.paintData();
|
|
15822
17272
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
17273
|
+
this.updateLegendValues();
|
|
15823
17274
|
this.emitViewportChange();
|
|
15824
17275
|
return active;
|
|
15825
17276
|
}
|
|
@@ -15884,7 +17335,7 @@ var NativeRenderer = class {
|
|
|
15884
17335
|
this.lastPointer = null;
|
|
15885
17336
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
15886
17337
|
this.hoverLogical = null;
|
|
15887
|
-
const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
17338
|
+
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
15888
17339
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
15889
17340
|
return;
|
|
15890
17341
|
}
|
|
@@ -15896,7 +17347,7 @@ var NativeRenderer = class {
|
|
|
15896
17347
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
15897
17348
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
15898
17349
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
15899
|
-
const pane = this.
|
|
17350
|
+
const pane = this.paneNodeAtY(y);
|
|
15900
17351
|
const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
|
|
15901
17352
|
const values = /* @__PURE__ */ new Map();
|
|
15902
17353
|
if (onBar) {
|
|
@@ -15913,7 +17364,7 @@ var NativeRenderer = class {
|
|
|
15913
17364
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
15914
17365
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
15915
17366
|
this.hoverLogical = onBar ? logical : null;
|
|
15916
|
-
const event = { time, price, values, ohlc };
|
|
17367
|
+
const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
|
|
15917
17368
|
for (const cb of this.crosshairCbs) cb(event);
|
|
15918
17369
|
}
|
|
15919
17370
|
handleClick(x) {
|
|
@@ -15932,6 +17383,20 @@ var NativeRenderer = class {
|
|
|
15932
17383
|
}
|
|
15933
17384
|
return null;
|
|
15934
17385
|
}
|
|
17386
|
+
/** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
|
|
17387
|
+
* fall BETWEEN bounds (separator gaps, the strip under the last pane). */
|
|
17388
|
+
nearestPaneToY(y) {
|
|
17389
|
+
let best = null;
|
|
17390
|
+
let bestDist = Infinity;
|
|
17391
|
+
for (const pane of this.scene.panes.values()) {
|
|
17392
|
+
const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
|
|
17393
|
+
if (d < bestDist) {
|
|
17394
|
+
bestDist = d;
|
|
17395
|
+
best = pane;
|
|
17396
|
+
}
|
|
17397
|
+
}
|
|
17398
|
+
return best;
|
|
17399
|
+
}
|
|
15935
17400
|
// ── manual vertical scaling (price-axis drag + vertical price pan) ──
|
|
15936
17401
|
/** The scale window a gesture targets: a pane's master scale, or a merged indicator's
|
|
15937
17402
|
* own scale column (resolved from the grabbed x). Both share the four scale fields. */
|
|
@@ -15994,7 +17459,7 @@ var NativeRenderer = class {
|
|
|
15994
17459
|
* still fits the view.)
|
|
15995
17460
|
*/
|
|
15996
17461
|
dataDblClick(_x, y) {
|
|
15997
|
-
const pane = this.paneNodeAtY(y);
|
|
17462
|
+
const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
|
|
15998
17463
|
if (!pane) return;
|
|
15999
17464
|
const maximized = this.maximizedPaneId !== pane.id;
|
|
16000
17465
|
this.maximizedPaneId = maximized ? pane.id : null;
|
|
@@ -16235,29 +17700,63 @@ var NativeRenderer = class {
|
|
|
16235
17700
|
dataWindowGroups(idx, pricePane) {
|
|
16236
17701
|
const groups = [];
|
|
16237
17702
|
for (const model of this.scene.indicators.values()) {
|
|
16238
|
-
const
|
|
16239
|
-
const off = this.scene.offsetOf(model.id);
|
|
16240
|
-
const rows = [];
|
|
16241
|
-
for (const s of model.series) {
|
|
16242
|
-
let value;
|
|
16243
|
-
let color;
|
|
16244
|
-
if (s.kind === "candle" || s.kind === "bar") {
|
|
16245
|
-
value = s.bars[idx - off]?.close;
|
|
16246
|
-
color = s.style?.up ?? this.theme.upColor;
|
|
16247
|
-
} else if (isLineLikeSeries(s)) {
|
|
16248
|
-
if (s.visible === false) continue;
|
|
16249
|
-
value = s.points[idx - off]?.value;
|
|
16250
|
-
color = s.points[idx - off]?.color ?? s.style.color;
|
|
16251
|
-
} else {
|
|
16252
|
-
continue;
|
|
16253
|
-
}
|
|
16254
|
-
if (value == null || !Number.isFinite(value)) continue;
|
|
16255
|
-
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
16256
|
-
}
|
|
17703
|
+
const rows = this.dataWindowRowsFor(model, idx, pricePane);
|
|
16257
17704
|
if (rows.length) groups.push({ name: model.title, rows });
|
|
16258
17705
|
}
|
|
16259
17706
|
return groups;
|
|
16260
17707
|
}
|
|
17708
|
+
/** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
|
|
17709
|
+
dataWindowRowsFor(model, idx, pricePane) {
|
|
17710
|
+
if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
|
|
17711
|
+
const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
|
|
17712
|
+
const off = this.scene.offsetOf(model.id);
|
|
17713
|
+
const rows = [];
|
|
17714
|
+
for (const s of model.series) {
|
|
17715
|
+
let value;
|
|
17716
|
+
let color;
|
|
17717
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
17718
|
+
value = s.bars[idx - off]?.close;
|
|
17719
|
+
color = s.style?.up ?? this.theme.upColor;
|
|
17720
|
+
} else if (isLineLikeSeries(s)) {
|
|
17721
|
+
if (s.visible === false) continue;
|
|
17722
|
+
value = s.points[idx - off]?.value;
|
|
17723
|
+
color = s.points[idx - off]?.color ?? s.style.color;
|
|
17724
|
+
} else {
|
|
17725
|
+
continue;
|
|
17726
|
+
}
|
|
17727
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
17728
|
+
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
17729
|
+
}
|
|
17730
|
+
return rows;
|
|
17731
|
+
}
|
|
17732
|
+
/** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
|
|
17733
|
+
* colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
|
|
17734
|
+
* `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
|
|
17735
|
+
volumeReadoutRows(model, idx) {
|
|
17736
|
+
if (this.volumeHidden) return [];
|
|
17737
|
+
const bar = this.bars[idx];
|
|
17738
|
+
const vol = bar?.volume;
|
|
17739
|
+
if (bar == null || vol == null || !Number.isFinite(vol)) return [];
|
|
17740
|
+
const cfg = this.scene.volumeLayer;
|
|
17741
|
+
const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
|
|
17742
|
+
return [{ label: model.title, value: formatVolume(vol), color }];
|
|
17743
|
+
}
|
|
17744
|
+
/** Refresh the plot values beside every legend title — the same readout the data window
|
|
17745
|
+
* shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
|
|
17746
|
+
* no scene model, so its row is absent from the map and its readout clears. */
|
|
17747
|
+
updateLegendValues() {
|
|
17748
|
+
if (!this.inputsUI) return;
|
|
17749
|
+
const n = this.bars.length;
|
|
17750
|
+
const values = /* @__PURE__ */ new Map();
|
|
17751
|
+
if (n > 0) {
|
|
17752
|
+
const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
|
|
17753
|
+
const pricePane = this.dataWindowPricePane();
|
|
17754
|
+
for (const model of this.scene.indicators.values()) {
|
|
17755
|
+
values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
|
|
17756
|
+
}
|
|
17757
|
+
}
|
|
17758
|
+
this.inputsUI.setPlotValues(values);
|
|
17759
|
+
}
|
|
16261
17760
|
renderFrame(level) {
|
|
16262
17761
|
if (this.animator.active) return;
|
|
16263
17762
|
if (repaintsData(level)) {
|
|
@@ -16265,9 +17764,38 @@ var NativeRenderer = class {
|
|
|
16265
17764
|
this.paintData();
|
|
16266
17765
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
16267
17766
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
16268
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
17767
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
16269
17768
|
}
|
|
16270
17769
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
17770
|
+
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
17771
|
+
this.updateLegendValues();
|
|
17772
|
+
}
|
|
17773
|
+
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
17774
|
+
repaintCursorLayers() {
|
|
17775
|
+
const pane = this.scene.panes.get(PRICE_PANE_ID);
|
|
17776
|
+
if (!pane) return;
|
|
17777
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
17778
|
+
for (const l of this.extLayers) {
|
|
17779
|
+
if (!l.def.repaintOnCursor) continue;
|
|
17780
|
+
l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
|
|
17781
|
+
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
17782
|
+
}
|
|
17783
|
+
}
|
|
17784
|
+
/** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
|
|
17785
|
+
extLayerArgs(id, scale, bounds, nowMs) {
|
|
17786
|
+
return {
|
|
17787
|
+
bars: this.scene.bars,
|
|
17788
|
+
data: this.scene.nativeData.get(id),
|
|
17789
|
+
settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
|
|
17790
|
+
pending: this.scene.nativePending.get(id) ?? [],
|
|
17791
|
+
coords: this.coords,
|
|
17792
|
+
scale,
|
|
17793
|
+
bounds,
|
|
17794
|
+
theme: this.theme,
|
|
17795
|
+
priceStyle: this.scene.priceStyle,
|
|
17796
|
+
nowMs,
|
|
17797
|
+
cursor: this.lastPointer
|
|
17798
|
+
};
|
|
16271
17799
|
}
|
|
16272
17800
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
16273
17801
|
paintData() {
|
|
@@ -16300,18 +17828,16 @@ var NativeRenderer = class {
|
|
|
16300
17828
|
});
|
|
16301
17829
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
16302
17830
|
for (const l of this.extLayers) {
|
|
16303
|
-
l.
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16308
|
-
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16312
|
-
|
|
16313
|
-
nowMs
|
|
16314
|
-
});
|
|
17831
|
+
const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
|
|
17832
|
+
l.instance.render(args);
|
|
17833
|
+
if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
|
|
17834
|
+
const mod = l.instance.modulateBase(args);
|
|
17835
|
+
if (mod) {
|
|
17836
|
+
if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
|
|
17837
|
+
if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
|
|
17838
|
+
if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
|
|
17839
|
+
}
|
|
17840
|
+
}
|
|
16315
17841
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
16316
17842
|
}
|
|
16317
17843
|
}
|
|
@@ -16321,7 +17847,7 @@ var NativeRenderer = class {
|
|
|
16321
17847
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
16322
17848
|
this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
|
|
16323
17849
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
16324
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
17850
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
16325
17851
|
this.userDrawings?.render();
|
|
16326
17852
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
16327
17853
|
this.paintedData = true;
|
|
@@ -16391,7 +17917,7 @@ var NativeRenderer = class {
|
|
|
16391
17917
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
16392
17918
|
}
|
|
16393
17919
|
}
|
|
16394
|
-
return { x, y, time: this.coords.logicalToTime(logical) };
|
|
17920
|
+
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
16395
17921
|
}
|
|
16396
17922
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
16397
17923
|
setSnapMode(mode) {
|
|
@@ -16755,15 +18281,18 @@ var NativeRenderer = class {
|
|
|
16755
18281
|
setToolbarGutter(px) {
|
|
16756
18282
|
if (px === this.toolbarGutter) return;
|
|
16757
18283
|
this.toolbarGutter = px;
|
|
16758
|
-
this.
|
|
18284
|
+
this.publishGutters();
|
|
16759
18285
|
this.positionAttribution();
|
|
16760
18286
|
this.syncSize();
|
|
16761
18287
|
}
|
|
16762
|
-
/** Publish the
|
|
16763
|
-
*
|
|
16764
|
-
*
|
|
16765
|
-
|
|
18288
|
+
/** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
|
|
18289
|
+
* drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
|
|
18290
|
+
* incl. merged own-scale columns), so host overlays sharing that container (a
|
|
18291
|
+
* status line, a watermark, a custom legend) can anchor to the plot's edges
|
|
18292
|
+
* without reaching into the renderer's DOM. */
|
|
18293
|
+
publishGutters() {
|
|
16766
18294
|
this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
|
|
18295
|
+
this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
|
|
16767
18296
|
}
|
|
16768
18297
|
/** The built-in mark, or the host's own when one is set. */
|
|
16769
18298
|
buildAttributionEl() {
|
|
@@ -16833,7 +18362,13 @@ var NativeRenderer = class {
|
|
|
16833
18362
|
this.fitContent();
|
|
16834
18363
|
this.didInitialFit = true;
|
|
16835
18364
|
}
|
|
16836
|
-
this.
|
|
18365
|
+
if (this.animator.active) {
|
|
18366
|
+
this.computeScales();
|
|
18367
|
+
this.paintData();
|
|
18368
|
+
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
18369
|
+
} else {
|
|
18370
|
+
this.scheduler.flushNow(4 /* Full */);
|
|
18371
|
+
}
|
|
16837
18372
|
}
|
|
16838
18373
|
applyBackground() {
|
|
16839
18374
|
this.wrapper.style.background = this.theme.background;
|
|
@@ -16902,6 +18437,9 @@ function sanitizeHighlights(value) {
|
|
|
16902
18437
|
}
|
|
16903
18438
|
return out.sort((a, b) => a.from - b.from);
|
|
16904
18439
|
}
|
|
18440
|
+
function clamp012(v) {
|
|
18441
|
+
return Math.max(0, Math.min(1, v));
|
|
18442
|
+
}
|
|
16905
18443
|
function isPriceStyle2(v) {
|
|
16906
18444
|
return typeof v === "string" && priceStyleIds().includes(v);
|
|
16907
18445
|
}
|
|
@@ -17149,6 +18687,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17149
18687
|
this.barTransform = null;
|
|
17150
18688
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
17151
18689
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
18690
|
+
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
18691
|
+
* replayed into a data engine whenever one is (re)created. */
|
|
18692
|
+
this.typeSettings = /* @__PURE__ */ new Map();
|
|
17152
18693
|
/** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
|
|
17153
18694
|
this.activeEngineStyle = null;
|
|
17154
18695
|
/** The chart's current price style (tracked from the renderer's change events + initial read). */
|
|
@@ -17211,7 +18752,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17211
18752
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17212
18753
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17213
18754
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
17214
|
-
this.renderer.onChartTypeSettingsChange?.((typeId, values) =>
|
|
18755
|
+
this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
|
|
18756
|
+
this.typeSettings.set(typeId, values);
|
|
18757
|
+
this.typeEngines.get(typeId)?.onSettings?.(values);
|
|
18758
|
+
});
|
|
17215
18759
|
this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
|
|
17216
18760
|
this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
|
|
17217
18761
|
this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
|
|
@@ -17699,6 +19243,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17699
19243
|
if (!def?.dataEngine) return;
|
|
17700
19244
|
const engine = def.dataEngine();
|
|
17701
19245
|
this.typeEngines.set(id, engine);
|
|
19246
|
+
const stored = this.typeSettings.get(id);
|
|
19247
|
+
if (stored) engine.onSettings?.(stored);
|
|
17702
19248
|
engine.start({
|
|
17703
19249
|
symbol: this.config.market.symbol ?? "TEST",
|
|
17704
19250
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -18170,6 +19716,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18170
19716
|
return {
|
|
18171
19717
|
id: record.id,
|
|
18172
19718
|
title: record.title,
|
|
19719
|
+
...d.shortTitle ? { shorttitle: d.shortTitle } : {},
|
|
18173
19720
|
overlay: d.overlay,
|
|
18174
19721
|
paneHint: d.paneHint,
|
|
18175
19722
|
native: { type: record.native.type },
|
|
@@ -18777,7 +20324,8 @@ var VpvrIndicator = class {
|
|
|
18777
20324
|
};
|
|
18778
20325
|
var vpvrDescriptor = {
|
|
18779
20326
|
type: "vpvr",
|
|
18780
|
-
title: "
|
|
20327
|
+
title: "Visible Range Volume Profile",
|
|
20328
|
+
shortTitle: "VRVP",
|
|
18781
20329
|
paneHint: "price",
|
|
18782
20330
|
overlay: true,
|
|
18783
20331
|
inputsSchema: () => [
|
|
@@ -19134,4 +20682,4 @@ function resolveElement(container) {
|
|
|
19134
20682
|
return element;
|
|
19135
20683
|
}
|
|
19136
20684
|
|
|
19137
|
-
export { BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|
|
20685
|
+
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|