@luxalgo/vela 0.5.1 → 0.5.3
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-DGesjOJT.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
- package/dist/{DataProvider-BBQ3Hr21.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
- package/dist/{chunk-ULIX6AE2.js → chunk-ATPU5CI6.js} +1 -1
- package/dist/{chunk-5SE4PL3H.js → chunk-EY63XBFZ.js} +1370 -55
- package/dist/{chunk-FNOMKHEZ.js → chunk-L5NTMWPP.js} +1383 -376
- package/dist/{chunk-EP6NDJYK.js → chunk-LJLZR44Y.js} +4 -2
- package/dist/{chunk-AIW3EJUE.js → chunk-P556H6EA.js} +329 -10
- package/dist/{chunk-S6F6SQ2L.js → chunk-WJKLBYRM.js} +74 -7
- package/dist/{contributions-BlV3kb0O.d.cts → contributions-Ci_i3IN2.d.cts} +33 -2
- package/dist/{contributions-6N0rbrPC.d.ts → contributions-b6AVrqqG.d.ts} +33 -2
- package/dist/{history-DX3Zne3a.d.cts → history-Cf1lVsap.d.cts} +15 -3
- package/dist/{history-BTd52YMA.d.ts → history-Ci3M0xLV.d.ts} +15 -3
- package/dist/index.cjs +1450 -379
- package/dist/index.d.cts +60 -21
- package/dist/index.d.ts +60 -21
- package/dist/index.js +4 -4
- package/dist/{options-DbQholFV.d.cts → options-XTBpbx0s.d.cts} +47 -1
- package/dist/{options-DbQholFV.d.ts → options-XTBpbx0s.d.ts} +47 -1
- package/dist/{plugin-TN3n-9YN.d.ts → plugin-DP6A4t4o.d.ts} +177 -29
- package/dist/{plugin-O34MbXf-.d.cts → plugin-DlidwMj6.d.cts} +177 -29
- package/dist/plugin.cjs +53 -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 +332 -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 +1450 -379
- package/dist/vela.global.min.js +84 -34
- package/dist/widget.cjs +3430 -569
- package/dist/widget.d.cts +66 -8
- package/dist/widget.d.ts +66 -8
- package/dist/widget.js +169 -18
- package/dist/workspace.cjs +4332 -1418
- package/dist/workspace.d.cts +45 -9
- package/dist/workspace.d.ts +45 -9
- package/dist/workspace.js +226 -22
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-
|
|
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, foldBaseModulation, 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-WJKLBYRM.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
|
|
@@ -1518,13 +1551,9 @@ function tzMenuLabel(zone, location) {
|
|
|
1518
1551
|
if (normalizeTimezone(zone) === "Etc/UTC") return location;
|
|
1519
1552
|
return `(${tzOffset(zone)}) ${location}`;
|
|
1520
1553
|
}
|
|
1521
|
-
function tzLocation(zone) {
|
|
1522
|
-
const entry = TIMEZONES.find((t) => t.value === normalizeTimezone(zone));
|
|
1523
|
-
return entry?.label ?? (zone.split("/").pop() ?? zone).replace(/_/g, " ");
|
|
1524
|
-
}
|
|
1525
1554
|
function tzButtonLabel(zone) {
|
|
1526
1555
|
if (normalizeTimezone(zone) === "Etc/UTC") return "UTC";
|
|
1527
|
-
return
|
|
1556
|
+
return tzOffset(zone);
|
|
1528
1557
|
}
|
|
1529
1558
|
|
|
1530
1559
|
// src/core/drawings/effectiveColor.ts
|
|
@@ -2089,7 +2118,7 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
2089
2118
|
const doc = anchor.ownerDocument;
|
|
2090
2119
|
tip = doc.createElement("div");
|
|
2091
2120
|
tip.textContent = text;
|
|
2092
|
-
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;");
|
|
2093
2122
|
applyChromeTokens(tip, opts.theme());
|
|
2094
2123
|
opts.host.appendChild(tip);
|
|
2095
2124
|
const a = anchor.getBoundingClientRect();
|
|
@@ -2103,16 +2132,17 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
2103
2132
|
tip.style.top = `${a.bottom - h.top + 6}px`;
|
|
2104
2133
|
}
|
|
2105
2134
|
};
|
|
2106
|
-
const arm = () => {
|
|
2135
|
+
const arm = (e) => {
|
|
2136
|
+
if (e.pointerType !== "mouse") return;
|
|
2107
2137
|
clear();
|
|
2108
2138
|
timer = window.setTimeout(show, opts.delayMs ?? 700);
|
|
2109
2139
|
};
|
|
2110
|
-
anchor.addEventListener("
|
|
2111
|
-
anchor.addEventListener("
|
|
2140
|
+
anchor.addEventListener("pointerenter", arm);
|
|
2141
|
+
anchor.addEventListener("pointerleave", clear);
|
|
2112
2142
|
anchor.addEventListener("pointerdown", clear);
|
|
2113
2143
|
return () => {
|
|
2114
|
-
anchor.removeEventListener("
|
|
2115
|
-
anchor.removeEventListener("
|
|
2144
|
+
anchor.removeEventListener("pointerenter", arm);
|
|
2145
|
+
anchor.removeEventListener("pointerleave", clear);
|
|
2116
2146
|
anchor.removeEventListener("pointerdown", clear);
|
|
2117
2147
|
clear();
|
|
2118
2148
|
};
|
|
@@ -2808,14 +2838,22 @@ function makeDialogDraggable(box, header, options) {
|
|
|
2808
2838
|
|
|
2809
2839
|
// src/renderers/shared/InputsUI.ts
|
|
2810
2840
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
2811
|
-
var STATUS_KEYFRAMES = "@keyframes vela-ind-
|
|
2812
|
-
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;
|
|
2813
2850
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
|
|
2814
2851
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
|
|
2815
2852
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
|
|
2816
|
-
var CLOSE_SVG = iconAt("close",
|
|
2853
|
+
var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
|
|
2817
2854
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
|
|
2818
2855
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
|
|
2856
|
+
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
|
|
2819
2857
|
var InputsUI = class {
|
|
2820
2858
|
constructor(container, theme, paneBoundsOf) {
|
|
2821
2859
|
this.container = container;
|
|
@@ -2836,6 +2874,8 @@ var InputsUI = class {
|
|
|
2836
2874
|
this.onToggleVisible = null;
|
|
2837
2875
|
/** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
|
|
2838
2876
|
this.symbolPicker = null;
|
|
2877
|
+
/** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
|
|
2878
|
+
this.mobileLayout = false;
|
|
2839
2879
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
2840
2880
|
this.moveApi = null;
|
|
2841
2881
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
@@ -2857,9 +2897,18 @@ var InputsUI = class {
|
|
|
2857
2897
|
/** Titles switched off entirely (a settings toggle): every pane's legend container
|
|
2858
2898
|
* hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
|
|
2859
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;
|
|
2860
2905
|
/** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
|
|
2861
2906
|
* rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
|
|
2862
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;
|
|
2863
2912
|
/** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
|
|
2864
2913
|
this.onDialogKey = (e) => {
|
|
2865
2914
|
if (e.key === "Escape") {
|
|
@@ -2883,6 +2932,9 @@ var InputsUI = class {
|
|
|
2883
2932
|
this.onMoveMenuOutside = (e) => {
|
|
2884
2933
|
if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
|
|
2885
2934
|
};
|
|
2935
|
+
this.onRowMenuOutside = (e) => {
|
|
2936
|
+
if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
|
|
2937
|
+
};
|
|
2886
2938
|
if (!container.style.position) container.style.position = "relative";
|
|
2887
2939
|
if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
|
|
2888
2940
|
}
|
|
@@ -2890,6 +2942,36 @@ var InputsUI = class {
|
|
|
2890
2942
|
this.dialogHost = host;
|
|
2891
2943
|
if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
|
|
2892
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
|
+
}
|
|
2893
2975
|
/** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
|
|
2894
2976
|
tip(store, id, anchor, text, wrap = false) {
|
|
2895
2977
|
const list = store.get(id) ?? [];
|
|
@@ -2917,12 +2999,12 @@ var InputsUI = class {
|
|
|
2917
2999
|
}
|
|
2918
3000
|
setTheme(theme) {
|
|
2919
3001
|
this.theme = theme;
|
|
3002
|
+
for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
|
|
2920
3003
|
for (const row of this.rows.values()) {
|
|
2921
|
-
|
|
2922
|
-
row.el.style.background = open ? theme.background : this.idleRowFill();
|
|
3004
|
+
row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
|
|
2923
3005
|
row.el.style.color = theme.textColor;
|
|
2924
3006
|
}
|
|
2925
|
-
if (this.foldToggle) this.
|
|
3007
|
+
if (this.foldToggle) this.syncFoldToggle();
|
|
2926
3008
|
}
|
|
2927
3009
|
/** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
|
|
2928
3010
|
setSymbolPicker(picker) {
|
|
@@ -2953,7 +3035,7 @@ var InputsUI = class {
|
|
|
2953
3035
|
btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
|
|
2954
3036
|
btn.className = "vela-ind-ctl";
|
|
2955
3037
|
btn.dataset.legendAction = action.id;
|
|
2956
|
-
btn.style.cssText =
|
|
3038
|
+
btn.style.cssText = LEGEND_CTL_CSS;
|
|
2957
3039
|
btn.addEventListener("click", (e) => {
|
|
2958
3040
|
e.stopPropagation();
|
|
2959
3041
|
action.run();
|
|
@@ -2971,6 +3053,54 @@ var InputsUI = class {
|
|
|
2971
3053
|
this.titlesVisible = visible;
|
|
2972
3054
|
this.reposition();
|
|
2973
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
|
+
}
|
|
2974
3104
|
// ── legend move/merge (menu + drag) ─────────────────────────────────────
|
|
2975
3105
|
/** Open the "Move to" menu for a row, anchored under its move button. */
|
|
2976
3106
|
openMoveMenu(id, anchor) {
|
|
@@ -3022,6 +3152,48 @@ var InputsUI = class {
|
|
|
3022
3152
|
this.moveMenu?.remove();
|
|
3023
3153
|
this.moveMenu = null;
|
|
3024
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
|
+
}
|
|
3025
3197
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
3026
3198
|
setPane(id, paneId) {
|
|
3027
3199
|
const row = this.rows.get(id);
|
|
@@ -3043,7 +3215,8 @@ var InputsUI = class {
|
|
|
3043
3215
|
if (!lg) {
|
|
3044
3216
|
lg = document.createElement("div");
|
|
3045
3217
|
lg.dataset.velaPane = paneId;
|
|
3046
|
-
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);
|
|
3047
3220
|
this.positionLegend(lg, paneId);
|
|
3048
3221
|
this.container.appendChild(lg);
|
|
3049
3222
|
this.legends.set(paneId, lg);
|
|
@@ -3079,15 +3252,16 @@ var InputsUI = class {
|
|
|
3079
3252
|
*/
|
|
3080
3253
|
syncFoldToggle() {
|
|
3081
3254
|
const count = this.rows.size;
|
|
3082
|
-
|
|
3255
|
+
const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
|
|
3256
|
+
if (count < minRows) {
|
|
3083
3257
|
const wasFolded = this.legendFolded;
|
|
3084
|
-
this.legendFolded = false;
|
|
3258
|
+
if (count > 0) this.legendFolded = false;
|
|
3085
3259
|
if (this.foldToggle) {
|
|
3086
3260
|
this.foldToggle.remove();
|
|
3087
3261
|
this.foldToggle = null;
|
|
3088
3262
|
this.disposeTips(this.rowTips, "fold");
|
|
3089
3263
|
}
|
|
3090
|
-
if (wasFolded) this.reposition();
|
|
3264
|
+
if (wasFolded && count > 0) this.reposition();
|
|
3091
3265
|
return;
|
|
3092
3266
|
}
|
|
3093
3267
|
let btn = this.foldToggle;
|
|
@@ -3095,25 +3269,30 @@ var InputsUI = class {
|
|
|
3095
3269
|
btn = document.createElement("button");
|
|
3096
3270
|
btn.type = "button";
|
|
3097
3271
|
btn.className = "vela-ind-fold";
|
|
3098
|
-
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");
|
|
3099
3273
|
btn.addEventListener("click", () => {
|
|
3274
|
+
if (this.overviewAction !== null) {
|
|
3275
|
+
this.overviewAction();
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3100
3278
|
this.legendFolded = !this.legendFolded;
|
|
3101
3279
|
this.syncFoldToggle();
|
|
3102
3280
|
});
|
|
3103
3281
|
this.foldToggle = btn;
|
|
3104
3282
|
}
|
|
3105
3283
|
const folded = this.legendFolded;
|
|
3106
|
-
btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
|
|
3107
|
-
|
|
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;`;
|
|
3108
3287
|
btn.replaceChildren();
|
|
3109
3288
|
const icon2 = document.createElement("span");
|
|
3110
|
-
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
|
|
3111
|
-
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;
|
|
3112
3291
|
btn.appendChild(icon2);
|
|
3113
3292
|
if (folded) {
|
|
3114
3293
|
const label = document.createElement("span");
|
|
3115
3294
|
label.textContent = String(count);
|
|
3116
|
-
label.style.cssText =
|
|
3295
|
+
label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
|
|
3117
3296
|
btn.appendChild(label);
|
|
3118
3297
|
}
|
|
3119
3298
|
this.legendFor("price").appendChild(btn);
|
|
@@ -3121,9 +3300,11 @@ var InputsUI = class {
|
|
|
3121
3300
|
}
|
|
3122
3301
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
3123
3302
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
3303
|
+
const settingsTitle = opts.settingsTitle ?? title;
|
|
3124
3304
|
const existing = this.rows.get(id);
|
|
3125
3305
|
if (existing) {
|
|
3126
3306
|
existing.title = title;
|
|
3307
|
+
existing.settingsTitle = settingsTitle;
|
|
3127
3308
|
existing.inputs = inputs;
|
|
3128
3309
|
existing.values = { ...values };
|
|
3129
3310
|
existing.titleEl.textContent = title;
|
|
@@ -3136,8 +3317,7 @@ var InputsUI = class {
|
|
|
3136
3317
|
}
|
|
3137
3318
|
ensureDialogStyles();
|
|
3138
3319
|
const el = document.createElement("div");
|
|
3139
|
-
el.style.cssText = `pointer-events:auto;display:flex;align-items:center;
|
|
3140
|
-
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;`;
|
|
3141
3321
|
el.addEventListener("mouseleave", () => {
|
|
3142
3322
|
if (this.selectedId !== id) this.setRowHighlighted(id, false);
|
|
3143
3323
|
});
|
|
@@ -3157,11 +3337,16 @@ var InputsUI = class {
|
|
|
3157
3337
|
e.preventDefault();
|
|
3158
3338
|
this.onRemove?.(id);
|
|
3159
3339
|
});
|
|
3340
|
+
el.addEventListener("contextmenu", (e) => {
|
|
3341
|
+
e.preventDefault();
|
|
3342
|
+
e.stopPropagation();
|
|
3343
|
+
this.openRowMenu(id, e.clientX, e.clientY);
|
|
3344
|
+
});
|
|
3160
3345
|
const statusEl = document.createElement("span");
|
|
3161
3346
|
statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
3162
|
-
el.appendChild(statusEl);
|
|
3163
3347
|
const titleWrap = document.createElement("span");
|
|
3164
3348
|
titleWrap.style.cssText = "white-space:nowrap;";
|
|
3349
|
+
titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
|
|
3165
3350
|
const titleEl = document.createElement("span");
|
|
3166
3351
|
titleEl.textContent = title;
|
|
3167
3352
|
titleEl.style.cssText = "font-weight:600;";
|
|
@@ -3173,6 +3358,11 @@ var InputsUI = class {
|
|
|
3173
3358
|
titleWrap.appendChild(beta);
|
|
3174
3359
|
}
|
|
3175
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;`;
|
|
3176
3366
|
let eyeEl = null;
|
|
3177
3367
|
if (this.onToggleVisible) {
|
|
3178
3368
|
const eye = document.createElement("button");
|
|
@@ -3181,16 +3371,14 @@ var InputsUI = class {
|
|
|
3181
3371
|
this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
|
|
3182
3372
|
eye.innerHTML = EYE_SVG;
|
|
3183
3373
|
eye.className = "vela-ind-ctl";
|
|
3184
|
-
eye.style.cssText =
|
|
3374
|
+
eye.style.cssText = LEGEND_CTL_CSS;
|
|
3185
3375
|
eye.addEventListener("click", () => {
|
|
3186
3376
|
const row = this.rows.get(id);
|
|
3187
3377
|
this.onToggleVisible?.(id, Boolean(row?.hidden));
|
|
3188
3378
|
});
|
|
3189
|
-
|
|
3379
|
+
controlsEl.appendChild(eye);
|
|
3190
3380
|
eyeEl = eye;
|
|
3191
3381
|
}
|
|
3192
|
-
const controlsEl = document.createElement("span");
|
|
3193
|
-
controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
|
|
3194
3382
|
if (inputs.length > 0) {
|
|
3195
3383
|
const gear = document.createElement("button");
|
|
3196
3384
|
gear.type = "button";
|
|
@@ -3198,7 +3386,7 @@ var InputsUI = class {
|
|
|
3198
3386
|
this.tip(this.rowTips, id, gear, "Settings");
|
|
3199
3387
|
gear.innerHTML = GEAR_SVG;
|
|
3200
3388
|
gear.className = "vela-ind-ctl";
|
|
3201
|
-
gear.style.cssText =
|
|
3389
|
+
gear.style.cssText = LEGEND_CTL_CSS;
|
|
3202
3390
|
gear.addEventListener("click", () => this.openDialog(id));
|
|
3203
3391
|
controlsEl.appendChild(gear);
|
|
3204
3392
|
}
|
|
@@ -3209,7 +3397,7 @@ var InputsUI = class {
|
|
|
3209
3397
|
this.tip(this.rowTips, id, mv, "Move to pane");
|
|
3210
3398
|
mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
|
|
3211
3399
|
mv.className = "vela-ind-ctl";
|
|
3212
|
-
mv.style.cssText =
|
|
3400
|
+
mv.style.cssText = LEGEND_CTL_CSS;
|
|
3213
3401
|
mv.addEventListener("click", (e) => {
|
|
3214
3402
|
e.stopPropagation();
|
|
3215
3403
|
this.openMoveMenu(id, mv);
|
|
@@ -3226,12 +3414,13 @@ var InputsUI = class {
|
|
|
3226
3414
|
this.tip(this.rowTips, id, close, "Remove indicator");
|
|
3227
3415
|
close.innerHTML = CLOSE_SVG;
|
|
3228
3416
|
close.className = "vela-ind-close";
|
|
3229
|
-
close.style.cssText =
|
|
3417
|
+
close.style.cssText = LEGEND_CTL_CSS;
|
|
3230
3418
|
close.addEventListener("click", () => this.onRemove?.(id));
|
|
3231
3419
|
controlsEl.appendChild(close);
|
|
3232
3420
|
el.appendChild(controlsEl);
|
|
3421
|
+
el.appendChild(statusEl);
|
|
3233
3422
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
3234
|
-
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 });
|
|
3235
3424
|
this.syncFoldToggle();
|
|
3236
3425
|
}
|
|
3237
3426
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -3245,24 +3434,31 @@ var InputsUI = class {
|
|
|
3245
3434
|
if (row) row.values = { ...row.values, ...values };
|
|
3246
3435
|
}
|
|
3247
3436
|
/**
|
|
3248
|
-
* Reflect an indicator's live status in its legend row: `'loading'` shows
|
|
3249
|
-
* 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.
|
|
3250
3440
|
*/
|
|
3251
3441
|
setStatus(id, status) {
|
|
3252
3442
|
const row = this.rows.get(id);
|
|
3253
3443
|
if (!row) return;
|
|
3254
3444
|
const el = row.statusEl;
|
|
3445
|
+
el.replaceChildren();
|
|
3255
3446
|
if (status === "idle") {
|
|
3256
|
-
el.style.
|
|
3257
|
-
el.style.animation = "none";
|
|
3447
|
+
el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
3258
3448
|
return;
|
|
3259
3449
|
}
|
|
3260
|
-
this.ensureStatusKeyframes();
|
|
3261
3450
|
if (status === "loading") {
|
|
3262
|
-
el.style.cssText =
|
|
3263
|
-
|
|
3264
|
-
|
|
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;
|
|
3265
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;`;
|
|
3266
3462
|
}
|
|
3267
3463
|
/** Inject the status keyframes once (idempotent). */
|
|
3268
3464
|
ensureStatusKeyframes() {
|
|
@@ -3281,8 +3477,8 @@ var InputsUI = class {
|
|
|
3281
3477
|
if (row.eyeEl) {
|
|
3282
3478
|
row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
|
|
3283
3479
|
row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
|
|
3284
|
-
row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
|
|
3285
3480
|
}
|
|
3481
|
+
this.syncRowActions(row);
|
|
3286
3482
|
}
|
|
3287
3483
|
/**
|
|
3288
3484
|
* Select a legend row (outlining it) and clear any previous selection, so at most one
|
|
@@ -3295,17 +3491,36 @@ var InputsUI = class {
|
|
|
3295
3491
|
this.setRowHighlighted(id, true);
|
|
3296
3492
|
}
|
|
3297
3493
|
/**
|
|
3298
|
-
* Show or hide a row's outline and eye/gear/✕ controls together — driven by
|
|
3299
|
-
*
|
|
3300
|
-
*
|
|
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.
|
|
3301
3498
|
*/
|
|
3302
3499
|
setRowHighlighted(id, highlighted) {
|
|
3303
3500
|
const row = this.rows.get(id);
|
|
3304
3501
|
if (!row) return;
|
|
3502
|
+
row.highlighted = highlighted;
|
|
3503
|
+
this.renderRowValues(row);
|
|
3305
3504
|
row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
|
|
3306
3505
|
row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
|
|
3307
|
-
row
|
|
3308
|
-
|
|
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
|
+
}
|
|
3309
3524
|
}
|
|
3310
3525
|
/** Drop the current selection outline, if any. */
|
|
3311
3526
|
clearSelection() {
|
|
@@ -3333,6 +3548,7 @@ var InputsUI = class {
|
|
|
3333
3548
|
destroy() {
|
|
3334
3549
|
this.closeDialog();
|
|
3335
3550
|
this.closeMoveMenu();
|
|
3551
|
+
this.closeRowMenu();
|
|
3336
3552
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
3337
3553
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
3338
3554
|
for (const lg of this.legends.values()) lg.remove();
|
|
@@ -3388,6 +3604,7 @@ var InputsUI = class {
|
|
|
3388
3604
|
const card = document.createElement("div");
|
|
3389
3605
|
card.className = "vela-ind-dialog";
|
|
3390
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;";
|
|
3391
3608
|
applyChromeTokens(card, t);
|
|
3392
3609
|
card.addEventListener("keydown", (e) => {
|
|
3393
3610
|
if (e.key !== "Escape") e.stopPropagation();
|
|
@@ -3395,7 +3612,7 @@ var InputsUI = class {
|
|
|
3395
3612
|
const header = document.createElement("div");
|
|
3396
3613
|
header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
|
|
3397
3614
|
const hTitle = document.createElement("span");
|
|
3398
|
-
hTitle.textContent = row.
|
|
3615
|
+
hTitle.textContent = row.settingsTitle;
|
|
3399
3616
|
hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
|
|
3400
3617
|
const closeBtn = document.createElement("button");
|
|
3401
3618
|
closeBtn.type = "button";
|
|
@@ -3407,7 +3624,7 @@ var InputsUI = class {
|
|
|
3407
3624
|
closeBtn.addEventListener("click", () => this.closeDialog());
|
|
3408
3625
|
header.append(hTitle, closeBtn);
|
|
3409
3626
|
card.appendChild(header);
|
|
3410
|
-
makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
3627
|
+
if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
3411
3628
|
const tabs = document.createElement("div");
|
|
3412
3629
|
tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
3413
3630
|
const tab = document.createElement("div");
|
|
@@ -3427,7 +3644,7 @@ var InputsUI = class {
|
|
|
3427
3644
|
section.appendChild(gh);
|
|
3428
3645
|
}
|
|
3429
3646
|
const grid = document.createElement("div");
|
|
3430
|
-
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;";
|
|
3431
3648
|
for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
|
|
3432
3649
|
section.appendChild(grid);
|
|
3433
3650
|
body.appendChild(section);
|
|
@@ -3876,10 +4093,14 @@ function groupInputs(inputs) {
|
|
|
3876
4093
|
var CHECK_SVG = iconAt("check", 12);
|
|
3877
4094
|
var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
|
|
3878
4095
|
var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
|
|
4096
|
+
var DIALOG_STYLE_REV = "10";
|
|
3879
4097
|
function ensureDialogStyles() {
|
|
3880
|
-
if (typeof document === "undefined"
|
|
3881
|
-
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");
|
|
3882
4102
|
s.id = DIALOG_STYLE_ID;
|
|
4103
|
+
s.dataset.rev = DIALOG_STYLE_REV;
|
|
3883
4104
|
s.textContent = `
|
|
3884
4105
|
.vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
|
|
3885
4106
|
.vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -3893,19 +4114,19 @@ function ensureDialogStyles() {
|
|
|
3893
4114
|
.vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
|
|
3894
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;}
|
|
3895
4116
|
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
3896
|
-
.vela-ind-ctl{color:var(--vela-
|
|
3897
|
-
.vela-ind-ctl
|
|
3898
|
-
.vela-ind-
|
|
3899
|
-
.vela-ind-close:hover{color:var(--vela-danger) !important;}
|
|
3900
|
-
.vela-ind-fold{
|
|
3901
|
-
.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;}
|
|
3902
4123
|
.vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
3903
4124
|
.vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
|
|
3904
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;}
|
|
3905
4126
|
.vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
3906
4127
|
.vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
3907
4128
|
.vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
|
|
3908
|
-
document.head.appendChild(s);
|
|
4129
|
+
if (!existing) document.head.appendChild(s);
|
|
3909
4130
|
}
|
|
3910
4131
|
var TIMEFRAME_OPTIONS = [
|
|
3911
4132
|
{ value: "", label: "Chart" },
|
|
@@ -4598,14 +4819,7 @@ function factoryResetConfig(factory) {
|
|
|
4598
4819
|
const defaults = {};
|
|
4599
4820
|
const addRows = (rows) => {
|
|
4600
4821
|
for (const r of rows ?? []) {
|
|
4601
|
-
|
|
4602
|
-
if (r.kind === "range") {
|
|
4603
|
-
defaults[r.minKey] = r.defval;
|
|
4604
|
-
defaults[r.maxKey] = r.defval;
|
|
4605
|
-
continue;
|
|
4606
|
-
}
|
|
4607
|
-
defaults[r.key] = r.defval;
|
|
4608
|
-
if (r.kind === "toggle") for (const c of r.colors ?? []) defaults[c.key] = c.defval;
|
|
4822
|
+
for (const k of settingsRowValueKeys(r)) defaults[k.key] = k.defval;
|
|
4609
4823
|
}
|
|
4610
4824
|
};
|
|
4611
4825
|
for (const inst of section.instances ?? []) {
|
|
@@ -6749,8 +6963,13 @@ function easeToward(current, target, dtMs, tauMs) {
|
|
|
6749
6963
|
var FLING_MIN_SPEED = 0.04;
|
|
6750
6964
|
var FLING_STALE_MS = 60;
|
|
6751
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;
|
|
6752
6971
|
var TIME_SCALE_K = 4e-3;
|
|
6753
|
-
var WHEEL_ZOOM_K =
|
|
6972
|
+
var WHEEL_ZOOM_K = 4e-3;
|
|
6754
6973
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
6755
6974
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
6756
6975
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -6769,6 +6988,12 @@ function wheelPanDelta(deltaX, deltaY, shift) {
|
|
|
6769
6988
|
function effectiveSnapMode(momentaryOverride, sticky) {
|
|
6770
6989
|
return momentaryOverride ? "strong" : sticky;
|
|
6771
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
|
+
}
|
|
6772
6997
|
var InputController = class {
|
|
6773
6998
|
constructor(deps) {
|
|
6774
6999
|
this.deps = deps;
|
|
@@ -6798,13 +7023,40 @@ var InputController = class {
|
|
|
6798
7023
|
// smoothed pointer velocity, px/ms
|
|
6799
7024
|
this.middleDeleted = false;
|
|
6800
7025
|
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
7026
|
+
this.rightCancelled = false;
|
|
7027
|
+
// the last right press cancelled a placement (suppress the context menu)
|
|
6801
7028
|
// Last cursor position over the element (NaN once it leaves) — lets a modifier
|
|
6802
7029
|
// press/release re-shape the drawings preview with the pointer stationary.
|
|
6803
7030
|
this.cursorX = NaN;
|
|
6804
7031
|
this.cursorY = NaN;
|
|
7032
|
+
// ── touch state ──
|
|
7033
|
+
/** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
|
|
7034
|
+
this.touches = /* @__PURE__ */ new Map();
|
|
7035
|
+
this.lpTimer = null;
|
|
7036
|
+
this.pinchStartDist = 1;
|
|
7037
|
+
this.pinchStartBarSpacing = 0;
|
|
7038
|
+
this.pinchAnchorLogical = 0;
|
|
7039
|
+
// Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
|
|
7040
|
+
this.lastTapT = 0;
|
|
7041
|
+
this.lastTapX = 0;
|
|
7042
|
+
this.lastTapY = 0;
|
|
7043
|
+
this.onTouchContextMenu = (e) => {
|
|
7044
|
+
if (this.touches.size === 0) return;
|
|
7045
|
+
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
7046
|
+
e.preventDefault();
|
|
7047
|
+
e.stopImmediatePropagation();
|
|
7048
|
+
};
|
|
6805
7049
|
this.onMiddleGuard = (e) => {
|
|
6806
7050
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
6807
7051
|
};
|
|
7052
|
+
/** A right press that cancelled a placement must not ALSO open the host's chart
|
|
7053
|
+
* context menu — swallow its companion event before it bubbles to the host. */
|
|
7054
|
+
this.onContextGuard = (e) => {
|
|
7055
|
+
if (!this.rightCancelled) return;
|
|
7056
|
+
this.rightCancelled = false;
|
|
7057
|
+
e.preventDefault();
|
|
7058
|
+
e.stopPropagation();
|
|
7059
|
+
};
|
|
6808
7060
|
/** A modifier press/release with the pointer stationary: re-issue the last cursor
|
|
6809
7061
|
* position so the placing ghost / drag preview reflects the new state immediately
|
|
6810
7062
|
* (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
|
|
@@ -6821,8 +7073,22 @@ var InputController = class {
|
|
|
6821
7073
|
if (this.middleDeleted) e.preventDefault();
|
|
6822
7074
|
return;
|
|
6823
7075
|
}
|
|
7076
|
+
if (e.button === 2) {
|
|
7077
|
+
this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
|
|
7078
|
+
if (this.rightCancelled) e.preventDefault();
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
6824
7081
|
if (e.button !== 0) return;
|
|
6825
7082
|
const { x, y } = this.local(e);
|
|
7083
|
+
if (e.pointerType === "touch") {
|
|
7084
|
+
if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
|
|
7085
|
+
this.touches.set(e.pointerId, { x, y });
|
|
7086
|
+
this.beginPinch(e);
|
|
7087
|
+
return;
|
|
7088
|
+
}
|
|
7089
|
+
if (this.touches.size >= 1) return;
|
|
7090
|
+
this.touches.set(e.pointerId, { x, y });
|
|
7091
|
+
}
|
|
6826
7092
|
this.deps.drawingsClearTransient?.();
|
|
6827
7093
|
this.dragging = true;
|
|
6828
7094
|
this.moved = false;
|
|
@@ -6848,6 +7114,9 @@ var InputController = class {
|
|
|
6848
7114
|
if (this.region === "price") this.deps.beginPriceScale(x, y);
|
|
6849
7115
|
else if (this.region === "separator") this.deps.beginPaneResize(y);
|
|
6850
7116
|
else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
|
|
7117
|
+
if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
|
|
7118
|
+
this.startLongPress(x, y, this.region);
|
|
7119
|
+
}
|
|
6851
7120
|
this.lastX = x;
|
|
6852
7121
|
this.lastT = e.timeStamp;
|
|
6853
7122
|
this.vx = 0;
|
|
@@ -6857,28 +7126,42 @@ var InputController = class {
|
|
|
6857
7126
|
const { x, y } = this.local(e);
|
|
6858
7127
|
this.cursorX = x;
|
|
6859
7128
|
this.cursorY = y;
|
|
7129
|
+
if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
|
|
7130
|
+
if (this.dragging && this.region === "pinch") {
|
|
7131
|
+
this.applyPinch();
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
if (this.dragging && this.region === "crosshair") {
|
|
7135
|
+
this.deps.onPointerMove(x, y);
|
|
7136
|
+
return;
|
|
7137
|
+
}
|
|
7138
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
7139
|
+
return;
|
|
7140
|
+
}
|
|
7141
|
+
if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
|
|
7142
|
+
const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
|
|
6860
7143
|
if (this.dragging) {
|
|
6861
7144
|
if (this.region === "price") {
|
|
6862
|
-
if (Math.abs(y - this.startY) >
|
|
7145
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6863
7146
|
this.deps.priceScaleBy(y - this.startY);
|
|
6864
7147
|
} else if (this.region === "separator") {
|
|
6865
|
-
if (Math.abs(y - this.startY) >
|
|
7148
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6866
7149
|
this.deps.paneResizeBy(y - this.startY);
|
|
6867
7150
|
} else if (this.region === "time") {
|
|
6868
|
-
if (Math.abs(x - this.startX) >
|
|
7151
|
+
if (Math.abs(x - this.startX) > slop) this.moved = true;
|
|
6869
7152
|
const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
|
|
6870
7153
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
6871
7154
|
} else if (this.region === "drawing") {
|
|
6872
|
-
if (Math.abs(x - this.startX) >
|
|
7155
|
+
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6873
7156
|
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6874
7157
|
} else {
|
|
6875
7158
|
const coords = this.deps.getCoords();
|
|
6876
7159
|
const vp = coords.getViewport();
|
|
6877
7160
|
const dx = x - this.startX;
|
|
6878
|
-
if (Math.abs(dx) >
|
|
7161
|
+
if (Math.abs(dx) > slop) this.moved = true;
|
|
6879
7162
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
|
|
6880
7163
|
if (this.verticalPan) {
|
|
6881
|
-
if (Math.abs(y - this.startY) >
|
|
7164
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6882
7165
|
this.deps.pricePanBy(y - this.startY);
|
|
6883
7166
|
}
|
|
6884
7167
|
const dt = e.timeStamp - this.lastT;
|
|
@@ -6895,14 +7178,44 @@ var InputController = class {
|
|
|
6895
7178
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
6896
7179
|
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6897
7180
|
}
|
|
6898
|
-
this.deps.onPointerMove(x, y);
|
|
7181
|
+
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
6899
7182
|
};
|
|
6900
7183
|
this.onUp = (e) => {
|
|
7184
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7185
|
+
this.cancelLongPress();
|
|
7186
|
+
if (this.dragging && this.region === "pinch") {
|
|
7187
|
+
if (this.touches.size === 1) {
|
|
7188
|
+
const rest = [...this.touches.values()][0];
|
|
7189
|
+
const vp = this.deps.getCoords().getViewport();
|
|
7190
|
+
this.region = "data";
|
|
7191
|
+
this.verticalPan = false;
|
|
7192
|
+
this.startX = rest.x;
|
|
7193
|
+
this.startY = rest.y;
|
|
7194
|
+
this.startRightOffset = vp.rightOffset;
|
|
7195
|
+
this.startBarSpacing = vp.barSpacing;
|
|
7196
|
+
this.lastX = rest.x;
|
|
7197
|
+
this.lastT = e.timeStamp;
|
|
7198
|
+
this.vx = 0;
|
|
7199
|
+
return;
|
|
7200
|
+
}
|
|
7201
|
+
this.endGesture(e);
|
|
7202
|
+
return;
|
|
7203
|
+
}
|
|
7204
|
+
if (this.dragging && this.region === "crosshair") {
|
|
7205
|
+
this.deps.onPointerMove(null, null);
|
|
7206
|
+
this.endGesture(e);
|
|
7207
|
+
return;
|
|
7208
|
+
}
|
|
7209
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
7210
|
+
this.endGesture(e);
|
|
7211
|
+
return;
|
|
7212
|
+
}
|
|
7213
|
+
const { x, y } = this.local(e);
|
|
7214
|
+
const wasTouch = e.pointerType === "touch";
|
|
7215
|
+
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
6901
7216
|
if (this.dragging && this.region === "drawing") {
|
|
6902
|
-
const { x, y } = this.local(e);
|
|
6903
7217
|
this.deps.drawingsPointerUp?.(x, y);
|
|
6904
|
-
} else if (
|
|
6905
|
-
const { x, y } = this.local(e);
|
|
7218
|
+
} else if (tapRelease && this.region === "data") {
|
|
6906
7219
|
this.deps.onClick(x, y);
|
|
6907
7220
|
} else if (this.dragging && this.region === "data") {
|
|
6908
7221
|
const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
|
|
@@ -6911,11 +7224,20 @@ var InputController = class {
|
|
|
6911
7224
|
this.deps.fling(-this.vx / pitch);
|
|
6912
7225
|
}
|
|
6913
7226
|
}
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
7227
|
+
const wasTap = wasTouch && tapRelease;
|
|
7228
|
+
this.endGesture(e);
|
|
7229
|
+
if (wasTouch) this.deps.onPointerMove(null, null);
|
|
7230
|
+
if (wasTap) this.registerTap(e.timeStamp, x, y);
|
|
7231
|
+
};
|
|
7232
|
+
/** The browser took the pointer back (system gesture, palm rejection, tab switch):
|
|
7233
|
+
* drop the gesture where it is — no click, no fling, no stuck `dragging`. */
|
|
7234
|
+
this.onCancel = (e) => {
|
|
7235
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7236
|
+
this.cancelLongPress();
|
|
7237
|
+
if (!this.dragging) return;
|
|
7238
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
7239
|
+
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
7240
|
+
this.endGesture(e);
|
|
6919
7241
|
};
|
|
6920
7242
|
this.onLeave = () => {
|
|
6921
7243
|
this.cursorX = NaN;
|
|
@@ -6924,12 +7246,7 @@ var InputController = class {
|
|
|
6924
7246
|
};
|
|
6925
7247
|
this.onDblClick = (e) => {
|
|
6926
7248
|
const { x, y } = this.local(e);
|
|
6927
|
-
|
|
6928
|
-
const region = this.regionAt(x, y);
|
|
6929
|
-
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
6930
|
-
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
6931
|
-
else if (region === "time") this.deps.resetView();
|
|
6932
|
-
else this.deps.dataDblClick(x, y);
|
|
7249
|
+
this.doubleActivate(x, y);
|
|
6933
7250
|
};
|
|
6934
7251
|
this.onWheel = (e) => {
|
|
6935
7252
|
e.preventDefault();
|
|
@@ -6950,17 +7267,26 @@ var InputController = class {
|
|
|
6950
7267
|
}
|
|
6951
7268
|
attach(el) {
|
|
6952
7269
|
this.el = el;
|
|
7270
|
+
if (el.style !== void 0) {
|
|
7271
|
+
el.style.touchAction = "none";
|
|
7272
|
+
el.style.userSelect = "none";
|
|
7273
|
+
el.style.setProperty("-webkit-user-select", "none");
|
|
7274
|
+
el.style.setProperty("-webkit-touch-callout", "none");
|
|
7275
|
+
}
|
|
6953
7276
|
el.addEventListener("pointerdown", this.onDown);
|
|
6954
7277
|
el.addEventListener("pointermove", this.onMove);
|
|
6955
7278
|
el.addEventListener("pointerup", this.onUp);
|
|
6956
7279
|
el.addEventListener("pointerleave", this.onLeave);
|
|
7280
|
+
el.addEventListener("pointercancel", this.onCancel);
|
|
6957
7281
|
el.addEventListener("dblclick", this.onDblClick);
|
|
6958
7282
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
6959
7283
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
6960
7284
|
el.addEventListener("auxclick", this.onMiddleGuard);
|
|
7285
|
+
el.addEventListener("contextmenu", this.onContextGuard);
|
|
6961
7286
|
const win = el.ownerDocument?.defaultView;
|
|
6962
7287
|
win?.addEventListener("keydown", this.onModifier);
|
|
6963
7288
|
win?.addEventListener("keyup", this.onModifier);
|
|
7289
|
+
win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
6964
7290
|
}
|
|
6965
7291
|
detach() {
|
|
6966
7292
|
const el = this.el;
|
|
@@ -6968,14 +7294,19 @@ var InputController = class {
|
|
|
6968
7294
|
const win = el.ownerDocument?.defaultView;
|
|
6969
7295
|
win?.removeEventListener("keydown", this.onModifier);
|
|
6970
7296
|
win?.removeEventListener("keyup", this.onModifier);
|
|
7297
|
+
win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
6971
7298
|
el.removeEventListener("pointerdown", this.onDown);
|
|
6972
7299
|
el.removeEventListener("pointermove", this.onMove);
|
|
6973
7300
|
el.removeEventListener("pointerup", this.onUp);
|
|
6974
7301
|
el.removeEventListener("pointerleave", this.onLeave);
|
|
7302
|
+
el.removeEventListener("pointercancel", this.onCancel);
|
|
6975
7303
|
el.removeEventListener("dblclick", this.onDblClick);
|
|
6976
7304
|
el.removeEventListener("wheel", this.onWheel);
|
|
6977
7305
|
el.removeEventListener("mousedown", this.onMiddleGuard);
|
|
6978
7306
|
el.removeEventListener("auxclick", this.onMiddleGuard);
|
|
7307
|
+
el.removeEventListener("contextmenu", this.onContextGuard);
|
|
7308
|
+
this.cancelLongPress();
|
|
7309
|
+
this.touches.clear();
|
|
6979
7310
|
this.el = null;
|
|
6980
7311
|
}
|
|
6981
7312
|
local(e) {
|
|
@@ -6999,6 +7330,59 @@ var InputController = class {
|
|
|
6999
7330
|
snapMode(e) {
|
|
7000
7331
|
return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
|
|
7001
7332
|
}
|
|
7333
|
+
/** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
|
|
7334
|
+
* movement past TOUCH_SLOP or a second finger cancels it. */
|
|
7335
|
+
startLongPress(x, y, region) {
|
|
7336
|
+
this.cancelLongPress();
|
|
7337
|
+
this.lpTimer = setTimeout(() => {
|
|
7338
|
+
this.lpTimer = null;
|
|
7339
|
+
if (!this.dragging || this.region !== region) return;
|
|
7340
|
+
if (region === "data") {
|
|
7341
|
+
this.region = "crosshair";
|
|
7342
|
+
this.deps.onPointerMove(x, y);
|
|
7343
|
+
return;
|
|
7344
|
+
}
|
|
7345
|
+
this.region = "axis-sheet";
|
|
7346
|
+
this.moved = true;
|
|
7347
|
+
this.deps.onAxisLongPress?.(region, x, y);
|
|
7348
|
+
}, LONG_PRESS_MS);
|
|
7349
|
+
}
|
|
7350
|
+
cancelLongPress() {
|
|
7351
|
+
if (this.lpTimer !== null) clearTimeout(this.lpTimer);
|
|
7352
|
+
this.lpTimer = null;
|
|
7353
|
+
}
|
|
7354
|
+
/** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
|
|
7355
|
+
beginPinch(e) {
|
|
7356
|
+
this.cancelLongPress();
|
|
7357
|
+
if (this.region === "crosshair") this.deps.onPointerMove(null, null);
|
|
7358
|
+
const coords = this.deps.getCoords();
|
|
7359
|
+
const vp = coords.getViewport();
|
|
7360
|
+
this.deps.apply(vp);
|
|
7361
|
+
const [a, b] = [...this.touches.values()];
|
|
7362
|
+
this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
7363
|
+
this.pinchStartBarSpacing = vp.barSpacing;
|
|
7364
|
+
this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
|
|
7365
|
+
this.region = "pinch";
|
|
7366
|
+
this.dragging = true;
|
|
7367
|
+
this.moved = true;
|
|
7368
|
+
this.vx = 0;
|
|
7369
|
+
this.capture(e.pointerId);
|
|
7370
|
+
}
|
|
7371
|
+
/** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
|
|
7372
|
+
applyPinch() {
|
|
7373
|
+
const coords = this.deps.getCoords();
|
|
7374
|
+
const pts = [...this.touches.values()];
|
|
7375
|
+
if (pts.length < 2) return;
|
|
7376
|
+
const [a, b] = pts;
|
|
7377
|
+
const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
|
|
7378
|
+
const vp = coords.getViewport();
|
|
7379
|
+
const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
|
|
7380
|
+
const midX = (a.x + b.x) / 2;
|
|
7381
|
+
this.deps.apply({
|
|
7382
|
+
barSpacing,
|
|
7383
|
+
rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
|
|
7384
|
+
});
|
|
7385
|
+
}
|
|
7002
7386
|
/** Capture defensively: a synthetic/already-released pointer can't be captured, and
|
|
7003
7387
|
* the move/up pair still routes through the element listeners without it. */
|
|
7004
7388
|
capture(pointerId) {
|
|
@@ -7007,6 +7391,35 @@ var InputController = class {
|
|
|
7007
7391
|
} catch {
|
|
7008
7392
|
}
|
|
7009
7393
|
}
|
|
7394
|
+
/** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
|
|
7395
|
+
* routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
|
|
7396
|
+
registerTap(t, x, y) {
|
|
7397
|
+
const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
|
|
7398
|
+
if (paired) {
|
|
7399
|
+
this.lastTapT = 0;
|
|
7400
|
+
this.doubleActivate(this.lastTapX, this.lastTapY);
|
|
7401
|
+
return;
|
|
7402
|
+
}
|
|
7403
|
+
this.lastTapT = t;
|
|
7404
|
+
this.lastTapX = x;
|
|
7405
|
+
this.lastTapY = y;
|
|
7406
|
+
}
|
|
7407
|
+
endGesture(e) {
|
|
7408
|
+
this.dragging = false;
|
|
7409
|
+
try {
|
|
7410
|
+
this.el?.releasePointerCapture(e.pointerId);
|
|
7411
|
+
} catch {
|
|
7412
|
+
}
|
|
7413
|
+
}
|
|
7414
|
+
/** Shared double-click / double-tap routing, by the region under the point. */
|
|
7415
|
+
doubleActivate(x, y) {
|
|
7416
|
+
if (this.deps.drawingsDblClick?.(x, y)) return;
|
|
7417
|
+
const region = this.regionAt(x, y);
|
|
7418
|
+
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
7419
|
+
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
7420
|
+
else if (region === "time") this.deps.resetView();
|
|
7421
|
+
else this.deps.dataDblClick(x, y);
|
|
7422
|
+
}
|
|
7010
7423
|
};
|
|
7011
7424
|
|
|
7012
7425
|
// src/renderers/native/core/KeyboardController.ts
|
|
@@ -8564,8 +8977,11 @@ var ChromeRenderer = class {
|
|
|
8564
8977
|
}
|
|
8565
8978
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
8566
8979
|
ctx.textBaseline = "middle";
|
|
8567
|
-
if (coords.barCount === 0) return;
|
|
8568
8980
|
const panes = scene.orderedPanes();
|
|
8981
|
+
if (coords.barCount === 0) {
|
|
8982
|
+
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
8983
|
+
return;
|
|
8984
|
+
}
|
|
8569
8985
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
8570
8986
|
for (const pane of panes) {
|
|
8571
8987
|
if (pane.collapsed) continue;
|
|
@@ -8820,11 +9236,19 @@ var ChromeRenderer = class {
|
|
|
8820
9236
|
const y = dataH + (fullH - dataH) / 2;
|
|
8821
9237
|
const tr = coords.visibleTimeRange();
|
|
8822
9238
|
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
9239
|
+
const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
|
|
9240
|
+
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);
|
|
9241
|
+
const GAP = 12;
|
|
9242
|
+
const placed = [];
|
|
9243
|
+
const put = (tick) => {
|
|
9244
|
+
const l = tick.x - tick.half;
|
|
9245
|
+
const r = tick.x + tick.half;
|
|
9246
|
+
if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
|
|
9247
|
+
placed.push({ l, r });
|
|
9248
|
+
ctx.fillText(tick.label, tick.x, y);
|
|
9249
|
+
};
|
|
9250
|
+
for (const tick of ticks) if (tick.major) put(tick);
|
|
9251
|
+
for (const tick of ticks) if (!tick.major) put(tick);
|
|
8828
9252
|
ctx.textAlign = "start";
|
|
8829
9253
|
}
|
|
8830
9254
|
};
|
|
@@ -8926,10 +9350,10 @@ var CrosshairRenderer = class {
|
|
|
8926
9350
|
}
|
|
8927
9351
|
/** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
|
|
8928
9352
|
* CONTAINING the foreign time (+ horizontal line when a comparable price came
|
|
8929
|
-
* along), with that bar's time chip in this chart's own timezone
|
|
8930
|
-
*
|
|
8931
|
-
*
|
|
8932
|
-
*
|
|
9353
|
+
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
9354
|
+
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
9355
|
+
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
9356
|
+
* render slightly dimmed so the ghost still reads as foreign. */
|
|
8933
9357
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
8934
9358
|
const cs = scene.style.crosshair;
|
|
8935
9359
|
const dataW = coords.width;
|
|
@@ -8953,7 +9377,20 @@ var CrosshairRenderer = class {
|
|
|
8953
9377
|
ctx.setLineDash([]);
|
|
8954
9378
|
ctx.lineWidth = 1;
|
|
8955
9379
|
ctx.globalAlpha = 0.8;
|
|
8956
|
-
|
|
9380
|
+
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
9381
|
+
if (ext.y != null && ext.price != null) {
|
|
9382
|
+
let pane;
|
|
9383
|
+
for (const p of scene.panes.values()) {
|
|
9384
|
+
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
9385
|
+
pane = p;
|
|
9386
|
+
break;
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
if (pane) {
|
|
9390
|
+
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);
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
|
|
8957
9394
|
ctx.globalAlpha = 1;
|
|
8958
9395
|
}
|
|
8959
9396
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -9224,6 +9661,7 @@ function colorField(theme, getVal, onVal) {
|
|
|
9224
9661
|
swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
|
|
9225
9662
|
};
|
|
9226
9663
|
paint();
|
|
9664
|
+
trigger.addEventListener("vela-sync", paint);
|
|
9227
9665
|
trigger.addEventListener("click", (e) => {
|
|
9228
9666
|
e.stopPropagation();
|
|
9229
9667
|
if (openPopover && openPopover.trigger === trigger) {
|
|
@@ -9259,6 +9697,101 @@ function closeColorPopover() {
|
|
|
9259
9697
|
closeOpen();
|
|
9260
9698
|
}
|
|
9261
9699
|
|
|
9700
|
+
// src/renderers/native/chrome/WidthField.ts
|
|
9701
|
+
var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
|
|
9702
|
+
var STYLE_ID4 = "vela-width-field";
|
|
9703
|
+
function ensureStyles4() {
|
|
9704
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
|
|
9705
|
+
const st = document.createElement("style");
|
|
9706
|
+
st.id = STYLE_ID4;
|
|
9707
|
+
st.textContent = `
|
|
9708
|
+
.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;}
|
|
9709
|
+
.vela-width-field:hover{border-color:var(--vela-fg-muted);}
|
|
9710
|
+
.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);}
|
|
9711
|
+
.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;}
|
|
9712
|
+
.vela-width-field-item:hover{background:var(--vela-hover-strong);}
|
|
9713
|
+
.vela-width-field-item[data-active='1']{background:var(--vela-active);}
|
|
9714
|
+
`;
|
|
9715
|
+
document.head.appendChild(st);
|
|
9716
|
+
}
|
|
9717
|
+
function lineGlyph(width) {
|
|
9718
|
+
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>`;
|
|
9719
|
+
}
|
|
9720
|
+
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>`;
|
|
9721
|
+
var openPopover2 = null;
|
|
9722
|
+
function closeOpen2() {
|
|
9723
|
+
if (!openPopover2) return;
|
|
9724
|
+
document.removeEventListener("pointerdown", openPopover2.onOutside, true);
|
|
9725
|
+
window.removeEventListener("resize", openPopover2.reflow, true);
|
|
9726
|
+
openPopover2.el.remove();
|
|
9727
|
+
openPopover2 = null;
|
|
9728
|
+
}
|
|
9729
|
+
function place2(pop, trigger) {
|
|
9730
|
+
const r = trigger.getBoundingClientRect();
|
|
9731
|
+
const w = pop.offsetWidth;
|
|
9732
|
+
const h = pop.offsetHeight;
|
|
9733
|
+
let left = r.right - w;
|
|
9734
|
+
let top = r.bottom + 6;
|
|
9735
|
+
if (left < 6) left = 6;
|
|
9736
|
+
if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
|
|
9737
|
+
if (top + h > window.innerHeight - 6) top = r.top - h - 6;
|
|
9738
|
+
if (top < 6) top = 6;
|
|
9739
|
+
pop.style.left = `${Math.round(left)}px`;
|
|
9740
|
+
pop.style.top = `${Math.round(top)}px`;
|
|
9741
|
+
}
|
|
9742
|
+
function widthField(theme, getVal, onVal) {
|
|
9743
|
+
ensureStyles4();
|
|
9744
|
+
const trigger = document.createElement("button");
|
|
9745
|
+
trigger.type = "button";
|
|
9746
|
+
trigger.className = "vela-width-field";
|
|
9747
|
+
const paint = () => {
|
|
9748
|
+
trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
|
|
9749
|
+
};
|
|
9750
|
+
paint();
|
|
9751
|
+
trigger.addEventListener("click", (e) => {
|
|
9752
|
+
e.stopPropagation();
|
|
9753
|
+
if (openPopover2 && openPopover2.trigger === trigger) {
|
|
9754
|
+
closeOpen2();
|
|
9755
|
+
return;
|
|
9756
|
+
}
|
|
9757
|
+
closeOpen2();
|
|
9758
|
+
const pop = document.createElement("div");
|
|
9759
|
+
pop.className = "vela-width-field-pop";
|
|
9760
|
+
applyChromeTokens(pop, theme);
|
|
9761
|
+
pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
|
|
9762
|
+
pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
|
|
9763
|
+
for (const w of WIDTH_FIELD_OPTIONS) {
|
|
9764
|
+
const item = document.createElement("button");
|
|
9765
|
+
item.type = "button";
|
|
9766
|
+
item.className = "vela-width-field-item";
|
|
9767
|
+
item.dataset.active = w === getVal() ? "1" : "0";
|
|
9768
|
+
item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
|
|
9769
|
+
item.addEventListener("click", (ev) => {
|
|
9770
|
+
ev.stopPropagation();
|
|
9771
|
+
closeOpen2();
|
|
9772
|
+
onVal(w);
|
|
9773
|
+
paint();
|
|
9774
|
+
});
|
|
9775
|
+
pop.appendChild(item);
|
|
9776
|
+
}
|
|
9777
|
+
document.body.appendChild(pop);
|
|
9778
|
+
place2(pop, trigger);
|
|
9779
|
+
const onOutside = (ev) => {
|
|
9780
|
+
const t = ev.target;
|
|
9781
|
+
if (pop.contains(t) || trigger.contains(t)) return;
|
|
9782
|
+
closeOpen2();
|
|
9783
|
+
};
|
|
9784
|
+
const reflow = () => place2(pop, trigger);
|
|
9785
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
|
|
9786
|
+
window.addEventListener("resize", reflow, true);
|
|
9787
|
+
openPopover2 = { el: pop, trigger, onOutside, reflow };
|
|
9788
|
+
});
|
|
9789
|
+
return trigger;
|
|
9790
|
+
}
|
|
9791
|
+
function closeWidthPopover() {
|
|
9792
|
+
closeOpen2();
|
|
9793
|
+
}
|
|
9794
|
+
|
|
9262
9795
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
9263
9796
|
var BUILTIN_STYLE_LABELS = {
|
|
9264
9797
|
candles: "Candles",
|
|
@@ -9341,6 +9874,30 @@ function ensureControlStyles() {
|
|
|
9341
9874
|
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
9342
9875
|
!important beats the inline opacity on labels. */
|
|
9343
9876
|
.vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
|
|
9877
|
+
/* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
|
|
9878
|
+
The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
|
|
9879
|
+
row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
|
|
9880
|
+
controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
|
|
9881
|
+
.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;}
|
|
9882
|
+
.vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
|
|
9883
|
+
.vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
|
|
9884
|
+
.vela-sd-mobile .vela-sd-railscrim.open{display:block;}
|
|
9885
|
+
.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);}
|
|
9886
|
+
.vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
|
|
9887
|
+
.vela-sd-mobile .vela-sd-tab{padding:12px;}
|
|
9888
|
+
.vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
|
|
9889
|
+
.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;}
|
|
9890
|
+
.vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
|
|
9891
|
+
.vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
|
|
9892
|
+
.vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
|
|
9893
|
+
.vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
|
|
9894
|
+
.vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
|
|
9895
|
+
.vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
|
|
9896
|
+
.vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
|
|
9897
|
+
.vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
|
|
9898
|
+
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
9899
|
+
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
9900
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
|
|
9344
9901
|
`;
|
|
9345
9902
|
document.head.appendChild(st);
|
|
9346
9903
|
}
|
|
@@ -9367,6 +9924,8 @@ var SettingsDialog = class {
|
|
|
9367
9924
|
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
9368
9925
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
9369
9926
|
this.activeSection = null;
|
|
9927
|
+
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
9928
|
+
this.mobileLayout = false;
|
|
9370
9929
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
9371
9930
|
}
|
|
9372
9931
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
@@ -9385,15 +9944,27 @@ var SettingsDialog = class {
|
|
|
9385
9944
|
}
|
|
9386
9945
|
setTheme(theme) {
|
|
9387
9946
|
this.theme = theme;
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9947
|
+
this.reopenIfLive();
|
|
9948
|
+
}
|
|
9949
|
+
/** The host shell's chrome size class — mobile presents the dialog fullscreen with
|
|
9950
|
+
* the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
|
|
9951
|
+
* re-presents in place, on the same tab. */
|
|
9952
|
+
setLayoutMode(mode) {
|
|
9953
|
+
const mobile = mode === "mobile";
|
|
9954
|
+
if (mobile === this.mobileLayout) return;
|
|
9955
|
+
this.mobileLayout = mobile;
|
|
9956
|
+
this.reopenIfLive();
|
|
9957
|
+
}
|
|
9958
|
+
/** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
|
|
9959
|
+
reopenIfLive() {
|
|
9960
|
+
if (!this.root) return;
|
|
9961
|
+
const cfg = this.config;
|
|
9962
|
+
const oc = this.onChange;
|
|
9963
|
+
const oi = this.onImport;
|
|
9964
|
+
const orst = this.onReset;
|
|
9965
|
+
const section = this.activeSection;
|
|
9966
|
+
this.close();
|
|
9967
|
+
if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
|
|
9397
9968
|
}
|
|
9398
9969
|
isOpen() {
|
|
9399
9970
|
return this.root !== null;
|
|
@@ -9415,18 +9986,37 @@ var SettingsDialog = class {
|
|
|
9415
9986
|
this.onImport = onImport ?? null;
|
|
9416
9987
|
this.onReset = onReset ?? null;
|
|
9417
9988
|
ensureControlStyles();
|
|
9989
|
+
const mobile = this.mobileLayout;
|
|
9418
9990
|
const scrim = document.createElement("div");
|
|
9419
|
-
scrim.style.cssText = "position:absolute;inset:0;z-index:
|
|
9991
|
+
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;";
|
|
9992
|
+
if (mobile) {
|
|
9993
|
+
scrim.classList.add("vela-sd-mobile");
|
|
9994
|
+
scrim.style.paddingTop = "0";
|
|
9995
|
+
scrim.style.alignItems = "stretch";
|
|
9996
|
+
}
|
|
9420
9997
|
scrim.addEventListener("mousedown", (e) => {
|
|
9421
9998
|
if (e.target === scrim) this.close();
|
|
9422
9999
|
});
|
|
9423
10000
|
const dlg = document.createElement("div");
|
|
9424
10001
|
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;`;
|
|
10002
|
+
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;";
|
|
9425
10003
|
const header = document.createElement("div");
|
|
9426
10004
|
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;`;
|
|
10005
|
+
if (mobile) header.style.paddingLeft = "8px";
|
|
9427
10006
|
const hTitle = document.createElement("span");
|
|
9428
10007
|
hTitle.textContent = "Chart settings";
|
|
9429
10008
|
hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
|
|
10009
|
+
if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
10010
|
+
let toggleRail = null;
|
|
10011
|
+
if (mobile) {
|
|
10012
|
+
const burger = document.createElement("button");
|
|
10013
|
+
burger.type = "button";
|
|
10014
|
+
burger.className = "vela-sd-burger";
|
|
10015
|
+
burger.innerHTML = iconAt("burger", 16);
|
|
10016
|
+
burger.title = "Sections";
|
|
10017
|
+
burger.addEventListener("click", () => toggleRail?.());
|
|
10018
|
+
header.append(burger);
|
|
10019
|
+
}
|
|
9430
10020
|
const closeBtn = document.createElement("button");
|
|
9431
10021
|
closeBtn.type = "button";
|
|
9432
10022
|
closeBtn.innerHTML = iconAt("close", 15);
|
|
@@ -9434,8 +10024,8 @@ var SettingsDialog = class {
|
|
|
9434
10024
|
closeBtn.className = "vela-sd-close";
|
|
9435
10025
|
closeBtn.addEventListener("click", () => this.close());
|
|
9436
10026
|
header.append(hTitle, closeBtn);
|
|
9437
|
-
|
|
9438
|
-
|
|
10027
|
+
if (!mobile) {
|
|
10028
|
+
header.style.cursor = "move";
|
|
9439
10029
|
let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
|
|
9440
10030
|
header.addEventListener("pointerdown", (e) => {
|
|
9441
10031
|
if (e.target?.closest(".vela-sd-close")) return;
|
|
@@ -9616,12 +10206,24 @@ var SettingsDialog = class {
|
|
|
9616
10206
|
renderChartTypeSections("end");
|
|
9617
10207
|
renderHostSections("end");
|
|
9618
10208
|
const shell = document.createElement("div");
|
|
9619
|
-
shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
10209
|
+
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;";
|
|
9620
10210
|
const rail = document.createElement("div");
|
|
9621
|
-
rail.
|
|
10211
|
+
rail.className = "vela-sd-rail";
|
|
10212
|
+
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;`;
|
|
10213
|
+
let railScrim = null;
|
|
10214
|
+
if (mobile) {
|
|
10215
|
+
railScrim = document.createElement("div");
|
|
10216
|
+
railScrim.className = "vela-sd-railscrim";
|
|
10217
|
+
railScrim.addEventListener("click", () => toggleRail?.());
|
|
10218
|
+
toggleRail = (open) => {
|
|
10219
|
+
const on = open ?? !rail.classList.contains("open");
|
|
10220
|
+
rail.classList.toggle("open", on);
|
|
10221
|
+
railScrim?.classList.toggle("open", on);
|
|
10222
|
+
};
|
|
10223
|
+
}
|
|
9622
10224
|
const paneHost = document.createElement("div");
|
|
9623
10225
|
paneHost.className = "vela-sd-pane";
|
|
9624
|
-
paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
|
|
10226
|
+
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;";
|
|
9625
10227
|
const panes = [];
|
|
9626
10228
|
let current = null;
|
|
9627
10229
|
for (const child of [...body.children]) {
|
|
@@ -9656,6 +10258,10 @@ var SettingsDialog = class {
|
|
|
9656
10258
|
p.tab.classList.toggle("on", i === idx);
|
|
9657
10259
|
});
|
|
9658
10260
|
this.activeSection = panes[idx]?.title ?? null;
|
|
10261
|
+
if (mobile) {
|
|
10262
|
+
hTitle.textContent = panes[idx]?.title ?? "Chart settings";
|
|
10263
|
+
toggleRail?.(false);
|
|
10264
|
+
}
|
|
9659
10265
|
};
|
|
9660
10266
|
panes.forEach((p, i) => {
|
|
9661
10267
|
p.tab.addEventListener("click", () => activate(i));
|
|
@@ -9663,10 +10269,11 @@ var SettingsDialog = class {
|
|
|
9663
10269
|
paneHost.appendChild(p.el);
|
|
9664
10270
|
});
|
|
9665
10271
|
this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
|
|
9666
|
-
const wanted = section
|
|
10272
|
+
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"));
|
|
9667
10273
|
activate(wanted >= 0 ? wanted : 0);
|
|
9668
10274
|
this.syncTypeTabs?.(config.series.style);
|
|
9669
10275
|
shell.append(rail, paneHost);
|
|
10276
|
+
if (railScrim) shell.append(railScrim);
|
|
9670
10277
|
dlg.appendChild(shell);
|
|
9671
10278
|
dlg.appendChild(this.footer(config));
|
|
9672
10279
|
scrim.appendChild(dlg);
|
|
@@ -9683,6 +10290,7 @@ var SettingsDialog = class {
|
|
|
9683
10290
|
}
|
|
9684
10291
|
close() {
|
|
9685
10292
|
closeColorPopover();
|
|
10293
|
+
closeWidthPopover();
|
|
9686
10294
|
this.root?.remove();
|
|
9687
10295
|
this.root = null;
|
|
9688
10296
|
this.tabs = [];
|
|
@@ -9723,20 +10331,12 @@ var SettingsDialog = class {
|
|
|
9723
10331
|
body.append(marker);
|
|
9724
10332
|
const values = config.chartTypes[typeId] ?? {};
|
|
9725
10333
|
const bag = {};
|
|
9726
|
-
const seedKey = (key, want, defval) => {
|
|
9727
|
-
const v = values[key];
|
|
9728
|
-
bag[key] = typeof v === want ? v : defval;
|
|
9729
|
-
};
|
|
9730
10334
|
const seed = (rows) => {
|
|
9731
10335
|
for (const r of rows) {
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
seedKey(r.maxKey, "number", r.defval);
|
|
9736
|
-
continue;
|
|
10336
|
+
for (const k of settingsRowValueKeys(r)) {
|
|
10337
|
+
const v = values[k.key];
|
|
10338
|
+
bag[k.key] = typeof v === k.type ? v : k.defval;
|
|
9737
10339
|
}
|
|
9738
|
-
seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
|
|
9739
|
-
if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
|
|
9740
10340
|
}
|
|
9741
10341
|
};
|
|
9742
10342
|
if (section.rows) seed(section.rows);
|
|
@@ -9759,7 +10359,8 @@ var SettingsDialog = class {
|
|
|
9759
10359
|
if (section.instances && section.instances.length > 0) {
|
|
9760
10360
|
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
9761
10361
|
} else if (section.rows) {
|
|
9762
|
-
this.
|
|
10362
|
+
if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
|
|
10363
|
+
else this.flatTypeRows(section.rows, bag, put, refreshers, body);
|
|
9763
10364
|
}
|
|
9764
10365
|
for (const sub of section.subsections ?? []) {
|
|
9765
10366
|
const subMarker = this.section(sub.title);
|
|
@@ -9776,7 +10377,7 @@ var SettingsDialog = class {
|
|
|
9776
10377
|
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
9777
10378
|
const entries = [];
|
|
9778
10379
|
for (const r of rows) {
|
|
9779
|
-
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
|
|
10380
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
|
|
9780
10381
|
entries.push({ el, when: r.when });
|
|
9781
10382
|
body.append(el);
|
|
9782
10383
|
}
|
|
@@ -9784,60 +10385,109 @@ var SettingsDialog = class {
|
|
|
9784
10385
|
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
9785
10386
|
});
|
|
9786
10387
|
}
|
|
9787
|
-
/**
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
10388
|
+
/**
|
|
10389
|
+
* One value row for a chart-type descriptor, writing through `put`. EVERY kind is
|
|
10390
|
+
* first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
|
|
10391
|
+
* rendered by the ONE path: optional leading toggle, then the ordered inline
|
|
10392
|
+
* controls in the control column. A control carrying its own `when` registers a
|
|
10393
|
+
* visibility refresher — it appears and disappears live as the bag changes,
|
|
10394
|
+
* independent of the row's gate — and is exempt from the toggle-off dim (it may
|
|
10395
|
+
* exist FOR the off state).
|
|
10396
|
+
*/
|
|
10397
|
+
typeRow(r, bag, put, refreshers) {
|
|
10398
|
+
const n = normalizeSettingsRow(r);
|
|
10399
|
+
const controls = [];
|
|
10400
|
+
for (const c of n.controls) {
|
|
10401
|
+
const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
|
|
10402
|
+
if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
10403
|
+
if (c.kind !== "hint" && c.when) {
|
|
10404
|
+
el.dataset.sdSelfGated = "1";
|
|
10405
|
+
const when = c.when;
|
|
10406
|
+
refreshers.push(() => {
|
|
10407
|
+
el.style.display = settingsRowVisible(when, bag) ? "" : "none";
|
|
10408
|
+
});
|
|
10409
|
+
}
|
|
10410
|
+
controls.push(el);
|
|
10411
|
+
}
|
|
10412
|
+
if (n.toggle) {
|
|
10413
|
+
const t = n.toggle;
|
|
10414
|
+
const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
|
|
10415
|
+
refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
10416
|
+
return el;
|
|
9796
10417
|
}
|
|
9797
|
-
|
|
9798
|
-
if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
|
|
9799
|
-
if (r.kind === "range") return this.rangeRow(r, bag, put);
|
|
9800
|
-
return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
|
|
10418
|
+
return this.rowWith(n.label, controls);
|
|
9801
10419
|
}
|
|
9802
10420
|
/**
|
|
9803
|
-
*
|
|
9804
|
-
* `
|
|
9805
|
-
*
|
|
9806
|
-
* placeholder names the unset state ('Off' for 0-disables bounds).
|
|
10421
|
+
* Build ONE inline control from its descriptor — the factory behind the composite
|
|
10422
|
+
* row path. `compact` narrows number inputs on toggle rows (the historical inline
|
|
10423
|
+
* width) while standalone rows keep the full-width input.
|
|
9807
10424
|
*/
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
10425
|
+
inlineControl(c, bag, put, compact) {
|
|
10426
|
+
if (c.kind === "hint") {
|
|
10427
|
+
const s = document.createElement("span");
|
|
10428
|
+
s.textContent = c.text;
|
|
10429
|
+
s.style.cssText = "color:var(--vela-fg-muted);";
|
|
10430
|
+
return s;
|
|
10431
|
+
}
|
|
10432
|
+
if (c.kind === "color") {
|
|
10433
|
+
const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
|
|
10434
|
+
sw.title = c.label;
|
|
10435
|
+
return sw;
|
|
10436
|
+
}
|
|
10437
|
+
if (c.kind === "width") {
|
|
10438
|
+
let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
10439
|
+
const wf = widthField(this.theme, () => cur, (v) => {
|
|
10440
|
+
cur = v;
|
|
10441
|
+
put(c.key, v);
|
|
10442
|
+
});
|
|
10443
|
+
wf.title = c.label;
|
|
10444
|
+
return wf;
|
|
10445
|
+
}
|
|
10446
|
+
if (c.kind === "select") {
|
|
10447
|
+
const sel = document.createElement("select");
|
|
10448
|
+
sel.className = "vela-sd-select";
|
|
10449
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
10450
|
+
sel.title = c.label;
|
|
10451
|
+
const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
10452
|
+
for (const [val, lbl] of normalizeSelectOptions(c.options)) {
|
|
10453
|
+
const o = document.createElement("option");
|
|
10454
|
+
o.value = val;
|
|
10455
|
+
o.textContent = lbl;
|
|
10456
|
+
if (val === current2) o.selected = true;
|
|
10457
|
+
sel.appendChild(o);
|
|
9824
10458
|
}
|
|
10459
|
+
sel.addEventListener("change", () => put(c.key, sel.value));
|
|
10460
|
+
sel.addEventListener("vela-sync", () => {
|
|
10461
|
+
sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
10462
|
+
});
|
|
10463
|
+
return sel;
|
|
10464
|
+
}
|
|
10465
|
+
const ni = document.createElement("input");
|
|
10466
|
+
ni.type = "number";
|
|
10467
|
+
ni.className = "vela-sd-number";
|
|
10468
|
+
if (compact) ni.style.width = "56px";
|
|
10469
|
+
if (c.min !== void 0) ni.min = String(c.min);
|
|
10470
|
+
if (c.max !== void 0) ni.max = String(c.max);
|
|
10471
|
+
ni.step = String(c.step ?? 1);
|
|
10472
|
+
ni.title = c.label;
|
|
10473
|
+
const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
10474
|
+
if (c.placeholder !== void 0) {
|
|
10475
|
+
ni.placeholder = c.placeholder;
|
|
10476
|
+
if (current !== c.defval) ni.value = String(current);
|
|
9825
10477
|
ni.addEventListener("change", () => {
|
|
9826
|
-
const raw = ni.value.trim() === "" ?
|
|
9827
|
-
const v = Number.isFinite(raw) ? Math.min(
|
|
9828
|
-
ni.value =
|
|
9829
|
-
put(key, v);
|
|
10478
|
+
const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
|
|
10479
|
+
const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
|
|
10480
|
+
ni.value = v === c.defval ? "" : String(v);
|
|
10481
|
+
put(c.key, v);
|
|
9830
10482
|
});
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
wrap.appendChild(box);
|
|
9840
|
-
return wrap;
|
|
10483
|
+
} else {
|
|
10484
|
+
ni.value = String(current);
|
|
10485
|
+
ni.addEventListener("input", () => {
|
|
10486
|
+
const v = Number(ni.value);
|
|
10487
|
+
if (Number.isFinite(v)) put(c.key, v);
|
|
10488
|
+
});
|
|
10489
|
+
}
|
|
10490
|
+
return ni;
|
|
9841
10491
|
}
|
|
9842
10492
|
/**
|
|
9843
10493
|
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
@@ -9850,7 +10500,7 @@ var SettingsDialog = class {
|
|
|
9850
10500
|
const wrap = document.createElement("div");
|
|
9851
10501
|
const strip = document.createElement("div");
|
|
9852
10502
|
strip.className = "vela-sd-itabs";
|
|
9853
|
-
wrap.append(strip);
|
|
10503
|
+
if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
|
|
9854
10504
|
const contents = instances.map((inst, i) => {
|
|
9855
10505
|
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
9856
10506
|
wrap.append(content);
|
|
@@ -9945,8 +10595,8 @@ var SettingsDialog = class {
|
|
|
9945
10595
|
rowsHost.append(entries[entries.length - 1].el);
|
|
9946
10596
|
continue;
|
|
9947
10597
|
}
|
|
9948
|
-
const el = this.typeRow(r, bag, put);
|
|
9949
|
-
const key = r.kind === "range" ? void 0 : r.key;
|
|
10598
|
+
const el = this.typeRow(r, bag, put, refreshers);
|
|
10599
|
+
const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
|
|
9950
10600
|
entries.push({ el, when: r.when, group: g, key });
|
|
9951
10601
|
rowsHost.append(el);
|
|
9952
10602
|
}
|
|
@@ -10012,6 +10662,14 @@ var SettingsDialog = class {
|
|
|
10012
10662
|
*/
|
|
10013
10663
|
layoutSettingsGrids(pane, measureHost) {
|
|
10014
10664
|
if (pane.childElementCount === 0) return;
|
|
10665
|
+
if (this.mobileLayout) {
|
|
10666
|
+
const grid2 = document.createElement("div");
|
|
10667
|
+
grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
|
|
10668
|
+
while (pane.firstChild) grid2.appendChild(pane.firstChild);
|
|
10669
|
+
pane.appendChild(grid2);
|
|
10670
|
+
this.prepareGridItems(grid2);
|
|
10671
|
+
return;
|
|
10672
|
+
}
|
|
10015
10673
|
const rows = [...pane.querySelectorAll(".vela-sd-row")];
|
|
10016
10674
|
const probe2 = document.createElement("div");
|
|
10017
10675
|
probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
|
|
@@ -10048,9 +10706,11 @@ var SettingsDialog = class {
|
|
|
10048
10706
|
}
|
|
10049
10707
|
}
|
|
10050
10708
|
/** A bare color swatch input (for toggle-row right groups / swatch pairs). */
|
|
10051
|
-
|
|
10709
|
+
/** With `get`, the field reads its value live (and repaints on 'vela-sync') —
|
|
10710
|
+
* duplicate-keyed rows stay honest when another row edits the shared key. */
|
|
10711
|
+
swatch(value, onChange, get) {
|
|
10052
10712
|
let current = value;
|
|
10053
|
-
return colorField(this.theme, () => current, (v) => {
|
|
10713
|
+
return colorField(this.theme, () => get ? get() : current, (v) => {
|
|
10054
10714
|
current = v;
|
|
10055
10715
|
onChange(v);
|
|
10056
10716
|
});
|
|
@@ -10102,8 +10762,11 @@ var SettingsDialog = class {
|
|
|
10102
10762
|
}
|
|
10103
10763
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
10104
10764
|
* shared control column; the control group dims and ignores input while the toggle
|
|
10105
|
-
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
10106
|
-
|
|
10765
|
+
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
10766
|
+
* With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
|
|
10767
|
+
* keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
|
|
10768
|
+
* only one visible at a time; see `typeRow`). */
|
|
10769
|
+
toggleRow(label, value, onToggle, controls, get) {
|
|
10107
10770
|
const wrap = document.createElement("div");
|
|
10108
10771
|
const cb = document.createElement("button");
|
|
10109
10772
|
cb.type = "button";
|
|
@@ -10123,6 +10786,12 @@ var SettingsDialog = class {
|
|
|
10123
10786
|
wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
|
|
10124
10787
|
wrap.append(cb, lbl);
|
|
10125
10788
|
cb.addEventListener("click", () => onToggle(cbToggle()));
|
|
10789
|
+
if (get) {
|
|
10790
|
+
wrap.addEventListener("vela-sync", () => {
|
|
10791
|
+
checked = get();
|
|
10792
|
+
cb.classList.toggle("on", checked);
|
|
10793
|
+
});
|
|
10794
|
+
}
|
|
10126
10795
|
return wrap;
|
|
10127
10796
|
}
|
|
10128
10797
|
wrap.className = "vela-sd-row";
|
|
@@ -10133,8 +10802,11 @@ var SettingsDialog = class {
|
|
|
10133
10802
|
box.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
10134
10803
|
for (const c of controls) box.appendChild(c);
|
|
10135
10804
|
const syncDim = (on) => {
|
|
10136
|
-
|
|
10137
|
-
|
|
10805
|
+
for (const c of controls) {
|
|
10806
|
+
if (c.dataset.sdSelfGated === "1") continue;
|
|
10807
|
+
c.style.opacity = on ? "1" : "0.4";
|
|
10808
|
+
c.style.pointerEvents = on ? "" : "none";
|
|
10809
|
+
}
|
|
10138
10810
|
};
|
|
10139
10811
|
syncDim(value);
|
|
10140
10812
|
cb.addEventListener("click", () => {
|
|
@@ -10142,6 +10814,13 @@ var SettingsDialog = class {
|
|
|
10142
10814
|
onToggle(v);
|
|
10143
10815
|
syncDim(v);
|
|
10144
10816
|
});
|
|
10817
|
+
if (get) {
|
|
10818
|
+
wrap.addEventListener("vela-sync", () => {
|
|
10819
|
+
checked = get();
|
|
10820
|
+
cb.classList.toggle("on", checked);
|
|
10821
|
+
syncDim(checked);
|
|
10822
|
+
});
|
|
10823
|
+
}
|
|
10145
10824
|
wrap.append(left, box);
|
|
10146
10825
|
return wrap;
|
|
10147
10826
|
}
|
|
@@ -10161,96 +10840,6 @@ var SettingsDialog = class {
|
|
|
10161
10840
|
wrap.appendChild(ni);
|
|
10162
10841
|
return wrap;
|
|
10163
10842
|
}
|
|
10164
|
-
/** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
|
|
10165
|
-
autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
|
|
10166
|
-
const { wrap } = this.row(label);
|
|
10167
|
-
const box = document.createElement("div");
|
|
10168
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
10169
|
-
const sel = document.createElement("select");
|
|
10170
|
-
sel.className = "vela-sd-select";
|
|
10171
|
-
for (const [val, lbl] of AUTO_MANUAL_OPTS) {
|
|
10172
|
-
const o = document.createElement("option");
|
|
10173
|
-
o.value = val;
|
|
10174
|
-
o.textContent = lbl;
|
|
10175
|
-
if (val === "auto" === isAuto) o.selected = true;
|
|
10176
|
-
sel.appendChild(o);
|
|
10177
|
-
}
|
|
10178
|
-
const ni = document.createElement("input");
|
|
10179
|
-
ni.type = "number";
|
|
10180
|
-
ni.min = String(min);
|
|
10181
|
-
ni.max = String(max);
|
|
10182
|
-
ni.step = "1";
|
|
10183
|
-
ni.value = String(value);
|
|
10184
|
-
ni.className = "vela-sd-number";
|
|
10185
|
-
const unitLbl = document.createElement("span");
|
|
10186
|
-
unitLbl.textContent = unit;
|
|
10187
|
-
unitLbl.style.cssText = "opacity:0.6;";
|
|
10188
|
-
const syncDisabled = (auto) => {
|
|
10189
|
-
ni.disabled = auto;
|
|
10190
|
-
ni.style.opacity = auto ? "0.4" : "1";
|
|
10191
|
-
};
|
|
10192
|
-
syncDisabled(isAuto);
|
|
10193
|
-
sel.addEventListener("change", () => {
|
|
10194
|
-
const auto = sel.value === "auto";
|
|
10195
|
-
onMode(auto);
|
|
10196
|
-
syncDisabled(auto);
|
|
10197
|
-
});
|
|
10198
|
-
ni.addEventListener("input", () => {
|
|
10199
|
-
const n = Number(ni.value);
|
|
10200
|
-
if (Number.isFinite(n)) onValue(n);
|
|
10201
|
-
});
|
|
10202
|
-
box.append(sel, ni, unitLbl);
|
|
10203
|
-
wrap.appendChild(box);
|
|
10204
|
-
return wrap;
|
|
10205
|
-
}
|
|
10206
|
-
/** A row whose control area holds several inline controls (e.g. show + color + width). */
|
|
10207
|
-
inlineRow(label, controls) {
|
|
10208
|
-
const { wrap } = this.row(label);
|
|
10209
|
-
const box = document.createElement("div");
|
|
10210
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
10211
|
-
for (const c of controls) box.appendChild(c);
|
|
10212
|
-
wrap.appendChild(box);
|
|
10213
|
-
return wrap;
|
|
10214
|
-
}
|
|
10215
|
-
/** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
|
|
10216
|
-
hint(text) {
|
|
10217
|
-
const s = document.createElement("span");
|
|
10218
|
-
s.textContent = text;
|
|
10219
|
-
s.style.cssText = "opacity:0.5;font-size:12px;";
|
|
10220
|
-
return s;
|
|
10221
|
-
}
|
|
10222
|
-
/** A bare color input (for inline groups). */
|
|
10223
|
-
colorInput(value, onChange) {
|
|
10224
|
-
const ci = document.createElement("input");
|
|
10225
|
-
ci.type = "color";
|
|
10226
|
-
ci.value = toHex6(value);
|
|
10227
|
-
ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
|
|
10228
|
-
ci.addEventListener("input", () => onChange(ci.value));
|
|
10229
|
-
return ci;
|
|
10230
|
-
}
|
|
10231
|
-
/** A bare compact number input (for inline groups). */
|
|
10232
|
-
numberInput(value, min, max, step, onChange) {
|
|
10233
|
-
const ni = document.createElement("input");
|
|
10234
|
-
ni.type = "number";
|
|
10235
|
-
ni.value = String(value);
|
|
10236
|
-
ni.min = String(min);
|
|
10237
|
-
ni.max = String(max);
|
|
10238
|
-
ni.step = String(step);
|
|
10239
|
-
ni.className = "vela-sd-number";
|
|
10240
|
-
ni.style.flex = "0 0 auto";
|
|
10241
|
-
ni.addEventListener("input", () => {
|
|
10242
|
-
const n = Number(ni.value);
|
|
10243
|
-
if (Number.isFinite(n)) onChange(n);
|
|
10244
|
-
});
|
|
10245
|
-
return ni;
|
|
10246
|
-
}
|
|
10247
|
-
/** A small in-group heading (lighter than a top-level section divider). */
|
|
10248
|
-
subheading(text) {
|
|
10249
|
-
const el = document.createElement("div");
|
|
10250
|
-
el.textContent = text;
|
|
10251
|
-
el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
|
|
10252
|
-
return el;
|
|
10253
|
-
}
|
|
10254
10843
|
/** A dropdown whose option values differ from their display labels. */
|
|
10255
10844
|
selectRowLabeled(label, value, options, onChange) {
|
|
10256
10845
|
const { wrap } = this.row(label);
|
|
@@ -10296,19 +10885,7 @@ var SettingsDialog = class {
|
|
|
10296
10885
|
foot.appendChild(resetBtn);
|
|
10297
10886
|
return foot;
|
|
10298
10887
|
}
|
|
10299
|
-
button(label, onClick) {
|
|
10300
|
-
const b = document.createElement("button");
|
|
10301
|
-
b.type = "button";
|
|
10302
|
-
b.textContent = label;
|
|
10303
|
-
b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
|
|
10304
|
-
b.addEventListener("click", onClick);
|
|
10305
|
-
return b;
|
|
10306
|
-
}
|
|
10307
|
-
ctrlStyle() {
|
|
10308
|
-
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;`;
|
|
10309
|
-
}
|
|
10310
10888
|
};
|
|
10311
|
-
var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
|
|
10312
10889
|
function normalizeSelectOptions(options) {
|
|
10313
10890
|
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
10314
10891
|
}
|
|
@@ -10511,7 +11088,7 @@ var DrawingPainter = class {
|
|
|
10511
11088
|
return;
|
|
10512
11089
|
}
|
|
10513
11090
|
if (d instanceof FixedRangeVolumeProfile) {
|
|
10514
|
-
this.paintFixedRangeVp(ctx, d, proj);
|
|
11091
|
+
this.paintFixedRangeVp(ctx, d, proj, theme);
|
|
10515
11092
|
this.paintLabel(ctx, d, proj, theme);
|
|
10516
11093
|
return;
|
|
10517
11094
|
}
|
|
@@ -11004,7 +11581,7 @@ var DrawingPainter = class {
|
|
|
11004
11581
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
11005
11582
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
11006
11583
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
11007
|
-
paintFixedRangeVp(ctx, d, proj) {
|
|
11584
|
+
paintFixedRangeVp(ctx, d, proj, theme) {
|
|
11008
11585
|
const L = d.layout(proj);
|
|
11009
11586
|
if (!L) return;
|
|
11010
11587
|
const s = d.frvp;
|
|
@@ -11043,7 +11620,7 @@ var DrawingPainter = class {
|
|
|
11043
11620
|
};
|
|
11044
11621
|
hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
|
|
11045
11622
|
hLine(s.showVal, s.valColor, s.valStyle, L.valY);
|
|
11046
|
-
hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
|
|
11623
|
+
hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
|
|
11047
11624
|
const devW = Math.max(1, w);
|
|
11048
11625
|
if (s.showDevelopingPoc) {
|
|
11049
11626
|
this.strokePolyline(ctx, L.developingPoc, {
|
|
@@ -11915,7 +12492,11 @@ var DrawingInteraction = class {
|
|
|
11915
12492
|
const last = draft.anchors[draft.anchors.length - 1];
|
|
11916
12493
|
const lx = last ? proj.xOf(last.time) : NaN;
|
|
11917
12494
|
const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
|
|
11918
|
-
if (
|
|
12495
|
+
if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
|
|
12496
|
+
if (draft.anchors.length >= this.state.need) {
|
|
12497
|
+
const lastIdx = draft.anchors.length - 1;
|
|
12498
|
+
draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
|
|
12499
|
+
}
|
|
11919
12500
|
draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
|
|
11920
12501
|
}
|
|
11921
12502
|
this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
|
|
@@ -11988,11 +12569,11 @@ function applyFree(anchor, pt, free) {
|
|
|
11988
12569
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
11989
12570
|
var BTN = 30;
|
|
11990
12571
|
var ICON = 21;
|
|
11991
|
-
var
|
|
11992
|
-
function
|
|
11993
|
-
if (typeof document === "undefined" || document.getElementById(
|
|
12572
|
+
var STYLE_ID5 = "vela-drawing-popup-styles";
|
|
12573
|
+
function ensureStyles5() {
|
|
12574
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID5)) return;
|
|
11994
12575
|
const s = document.createElement("style");
|
|
11995
|
-
s.id =
|
|
12576
|
+
s.id = STYLE_ID5;
|
|
11996
12577
|
s.textContent = `
|
|
11997
12578
|
.vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
|
|
11998
12579
|
.vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -12011,7 +12592,10 @@ function ensureStyles4() {
|
|
|
12011
12592
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
12012
12593
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
12013
12594
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
12014
|
-
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
12595
|
+
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
12596
|
+
/* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
|
|
12597
|
+
devices (the glyph stays centered; only the hit target widens). */
|
|
12598
|
+
@media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
|
|
12015
12599
|
document.head.appendChild(s);
|
|
12016
12600
|
}
|
|
12017
12601
|
var DrawingSettingsPopup = class {
|
|
@@ -12058,7 +12642,7 @@ var DrawingSettingsPopup = class {
|
|
|
12058
12642
|
* `onClose` fires on an outside-click dismissal (not a programmatic close). */
|
|
12059
12643
|
open(drawing, anchor, actions, onClose) {
|
|
12060
12644
|
this.close();
|
|
12061
|
-
|
|
12645
|
+
ensureStyles5();
|
|
12062
12646
|
this.onClose = onClose ?? null;
|
|
12063
12647
|
const t = this.theme;
|
|
12064
12648
|
const schema = drawing.schema();
|
|
@@ -12084,7 +12668,14 @@ var DrawingSettingsPopup = class {
|
|
|
12084
12668
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
12085
12669
|
}
|
|
12086
12670
|
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 })));
|
|
12087
|
-
if (paths.has("style.lineWidth"))
|
|
12671
|
+
if (paths.has("style.lineWidth")) {
|
|
12672
|
+
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
12673
|
+
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
12674
|
+
bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
12675
|
+
} else {
|
|
12676
|
+
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 }));
|
|
12677
|
+
}
|
|
12678
|
+
}
|
|
12088
12679
|
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 }));
|
|
12089
12680
|
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 })));
|
|
12090
12681
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
@@ -12552,14 +13143,15 @@ var DrawingSettingsPopup = class {
|
|
|
12552
13143
|
const chk = document.createElement("input");
|
|
12553
13144
|
chk.type = "checkbox";
|
|
12554
13145
|
chk.checked = Boolean(s[showPath]);
|
|
12555
|
-
|
|
13146
|
+
const current = s[colorPath] ?? contrastColor(t.background);
|
|
13147
|
+
chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
|
|
12556
13148
|
chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
|
|
12557
13149
|
const lbl = document.createElement("span");
|
|
12558
13150
|
lbl.textContent = label;
|
|
12559
13151
|
lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
|
|
12560
13152
|
const col = document.createElement("button");
|
|
12561
13153
|
col.type = "button";
|
|
12562
|
-
let cur =
|
|
13154
|
+
let cur = current;
|
|
12563
13155
|
col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
|
|
12564
13156
|
col.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
12565
13157
|
col.addEventListener("click", (e) => {
|
|
@@ -12739,7 +13331,8 @@ var DrawingSettingsPopup = class {
|
|
|
12739
13331
|
dragHandle() {
|
|
12740
13332
|
const b = document.createElement("button");
|
|
12741
13333
|
b.type = "button";
|
|
12742
|
-
b.
|
|
13334
|
+
b.className = "vela-dpop-grip";
|
|
13335
|
+
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);`;
|
|
12743
13336
|
b.innerHTML = sized(GRIP_ICON);
|
|
12744
13337
|
b.addEventListener("pointerdown", (e) => {
|
|
12745
13338
|
const el = this.el;
|
|
@@ -12755,6 +13348,10 @@ var DrawingSettingsPopup = class {
|
|
|
12755
13348
|
const origLeft = parseFloat(el.style.left) || 0;
|
|
12756
13349
|
const origTop = parseFloat(el.style.top) || 0;
|
|
12757
13350
|
b.style.cursor = "grabbing";
|
|
13351
|
+
try {
|
|
13352
|
+
b.setPointerCapture(e.pointerId);
|
|
13353
|
+
} catch {
|
|
13354
|
+
}
|
|
12758
13355
|
const move = (ev) => {
|
|
12759
13356
|
const w = el.offsetWidth;
|
|
12760
13357
|
const h = el.offsetHeight;
|
|
@@ -12767,9 +13364,11 @@ var DrawingSettingsPopup = class {
|
|
|
12767
13364
|
b.style.cursor = "grab";
|
|
12768
13365
|
window.removeEventListener("pointermove", move);
|
|
12769
13366
|
window.removeEventListener("pointerup", up);
|
|
13367
|
+
window.removeEventListener("pointercancel", up);
|
|
12770
13368
|
};
|
|
12771
13369
|
window.addEventListener("pointermove", move);
|
|
12772
13370
|
window.addEventListener("pointerup", up);
|
|
13371
|
+
window.addEventListener("pointercancel", up);
|
|
12773
13372
|
});
|
|
12774
13373
|
return b;
|
|
12775
13374
|
}
|
|
@@ -12885,6 +13484,39 @@ var DrawingSettingsPopup = class {
|
|
|
12885
13484
|
});
|
|
12886
13485
|
return b;
|
|
12887
13486
|
}
|
|
13487
|
+
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
13488
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. */
|
|
13489
|
+
widthInput(tip, value, min, max, step, onChange) {
|
|
13490
|
+
const input = document.createElement("input");
|
|
13491
|
+
input.type = "number";
|
|
13492
|
+
input.dataset.tip = tip;
|
|
13493
|
+
input.min = String(min);
|
|
13494
|
+
input.max = String(max);
|
|
13495
|
+
input.step = String(step);
|
|
13496
|
+
input.value = String(value);
|
|
13497
|
+
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;`;
|
|
13498
|
+
const commit = () => {
|
|
13499
|
+
const n = parseFloat(input.value);
|
|
13500
|
+
if (!Number.isFinite(n)) {
|
|
13501
|
+
input.value = String(value);
|
|
13502
|
+
return;
|
|
13503
|
+
}
|
|
13504
|
+
const v = Math.min(max, Math.max(min, n));
|
|
13505
|
+
input.value = String(v);
|
|
13506
|
+
value = v;
|
|
13507
|
+
onChange(v);
|
|
13508
|
+
};
|
|
13509
|
+
input.addEventListener("change", commit);
|
|
13510
|
+
input.addEventListener("keydown", (e) => {
|
|
13511
|
+
e.stopPropagation();
|
|
13512
|
+
if (e.key === "Enter") {
|
|
13513
|
+
e.preventDefault();
|
|
13514
|
+
commit();
|
|
13515
|
+
input.blur();
|
|
13516
|
+
}
|
|
13517
|
+
});
|
|
13518
|
+
return input;
|
|
13519
|
+
}
|
|
12888
13520
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
12889
13521
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
12890
13522
|
* opens a floating list of every option so a value is one click away (no cycling through). */
|
|
@@ -13338,6 +13970,9 @@ var UserDrawingController = class {
|
|
|
13338
13970
|
// mirrors showToolbar (drives the gutter reservation)
|
|
13339
13971
|
this.toolbarCollapsed = false;
|
|
13340
13972
|
// the bar is a slim expand-strip (narrower gutter)
|
|
13973
|
+
/** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
|
|
13974
|
+
* picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
|
|
13975
|
+
this.mobileLayout = false;
|
|
13341
13976
|
/** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
|
|
13342
13977
|
* history (drawings + app ops in one timeline) turns this off so the chords
|
|
13343
13978
|
* bubble to its keymap instead — see the renderer's `historyChords` feature. */
|
|
@@ -13396,12 +14031,21 @@ var UserDrawingController = class {
|
|
|
13396
14031
|
}
|
|
13397
14032
|
showToolbar(visible) {
|
|
13398
14033
|
this.toolbarVisible = visible;
|
|
13399
|
-
this.toolbar.setVisible(visible);
|
|
14034
|
+
this.toolbar.setVisible(visible && !this.mobileLayout);
|
|
14035
|
+
this.syncToolbarGutter();
|
|
14036
|
+
}
|
|
14037
|
+
/** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
|
|
14038
|
+
* shell's own drawer picks tools instead) and releases its gutter; flipping back
|
|
14039
|
+
* to desktop restores whatever `showToolbar` last asked for. */
|
|
14040
|
+
setLayoutMode(mode) {
|
|
14041
|
+
this.mobileLayout = mode === "mobile";
|
|
14042
|
+
this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
|
|
13400
14043
|
this.syncToolbarGutter();
|
|
13401
14044
|
}
|
|
13402
14045
|
/** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
|
|
13403
14046
|
syncToolbarGutter() {
|
|
13404
|
-
this.
|
|
14047
|
+
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
14048
|
+
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
13405
14049
|
}
|
|
13406
14050
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
13407
14051
|
setExternalGhost(doc) {
|
|
@@ -13781,6 +14425,16 @@ var UserDrawingController = class {
|
|
|
13781
14425
|
if (this.eraserMode) return "pointer";
|
|
13782
14426
|
return this.interaction.cursorAt(x, y);
|
|
13783
14427
|
}
|
|
14428
|
+
/** Right-click while placing: cancel the in-progress drawing and revert to the
|
|
14429
|
+
* pointer — the gesture is an explicit escape, so it disarms even in
|
|
14430
|
+
* stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
|
|
14431
|
+
* the press was consumed; false lets the host's context menu open normally. */
|
|
14432
|
+
cancelPlacement() {
|
|
14433
|
+
if (!this.interaction.isPlacing()) return false;
|
|
14434
|
+
this.interaction.cancel();
|
|
14435
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
14436
|
+
return true;
|
|
14437
|
+
}
|
|
13784
14438
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
13785
14439
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
13786
14440
|
dblClick(x, y) {
|
|
@@ -14270,7 +14924,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
|
|
|
14270
14924
|
|
|
14271
14925
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
14272
14926
|
var ATTRIBUTION_URL = "https://luxalgo.com/vela";
|
|
14273
|
-
var
|
|
14927
|
+
var STYLE_ID6 = "vela-attribution-styles";
|
|
14274
14928
|
var CSS = `
|
|
14275
14929
|
.vela-attribution {
|
|
14276
14930
|
text-decoration: none;
|
|
@@ -14316,12 +14970,28 @@ var CSS = `
|
|
|
14316
14970
|
max-width: 240px;
|
|
14317
14971
|
opacity: 1;
|
|
14318
14972
|
transform: translateX(0);
|
|
14319
|
-
}
|
|
14320
|
-
|
|
14321
|
-
|
|
14973
|
+
}
|
|
14974
|
+
/* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
|
|
14975
|
+
renderer's own container attribute (data-vela-layout) AND the shell root's
|
|
14976
|
+
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
14977
|
+
container, so only the shell attribute reaches it. */
|
|
14978
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
14979
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
|
|
14980
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
14981
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
14982
|
+
top: 2px;
|
|
14983
|
+
transform: translateX(-6px);
|
|
14984
|
+
}
|
|
14985
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
14986
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
|
|
14987
|
+
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
14988
|
+
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
14989
|
+
`;
|
|
14990
|
+
function ensureStyles6(doc) {
|
|
14991
|
+
let s = doc.getElementById(STYLE_ID6);
|
|
14322
14992
|
if (!s) {
|
|
14323
14993
|
s = doc.createElement("style");
|
|
14324
|
-
s.id =
|
|
14994
|
+
s.id = STYLE_ID6;
|
|
14325
14995
|
doc.head.appendChild(s);
|
|
14326
14996
|
}
|
|
14327
14997
|
s.textContent = CSS;
|
|
@@ -14350,7 +15020,7 @@ function createCustomMark(doc, html, background) {
|
|
|
14350
15020
|
return el;
|
|
14351
15021
|
}
|
|
14352
15022
|
function createAttributionMark(doc, background) {
|
|
14353
|
-
|
|
15023
|
+
ensureStyles6(doc);
|
|
14354
15024
|
const a = doc.createElement("a");
|
|
14355
15025
|
a.className = "vela-attribution";
|
|
14356
15026
|
a.href = ATTRIBUTION_URL;
|
|
@@ -14376,6 +15046,174 @@ function createAttributionMark(doc, background) {
|
|
|
14376
15046
|
return a;
|
|
14377
15047
|
}
|
|
14378
15048
|
|
|
15049
|
+
// src/renderers/shared/dom-raster.ts
|
|
15050
|
+
function hasInk(color) {
|
|
15051
|
+
if (!color || color === "transparent") return false;
|
|
15052
|
+
const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
|
|
15053
|
+
return m ? parseFloat(m[1]) > 1e-3 : true;
|
|
15054
|
+
}
|
|
15055
|
+
function cornerRadius(cs, w, h) {
|
|
15056
|
+
const raw = cs.borderTopLeftRadius;
|
|
15057
|
+
const v = parseFloat(raw) || 0;
|
|
15058
|
+
const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
|
|
15059
|
+
return Math.max(0, Math.min(px, Math.min(w, h) / 2));
|
|
15060
|
+
}
|
|
15061
|
+
function tracePath(ctx, x, y, w, h, r) {
|
|
15062
|
+
ctx.beginPath();
|
|
15063
|
+
if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
|
|
15064
|
+
else ctx.rect(x, y, w, h);
|
|
15065
|
+
}
|
|
15066
|
+
function canDrawUntainted(img) {
|
|
15067
|
+
try {
|
|
15068
|
+
const probe2 = img.ownerDocument.createElement("canvas");
|
|
15069
|
+
probe2.width = 1;
|
|
15070
|
+
probe2.height = 1;
|
|
15071
|
+
const ctx = probe2.getContext("2d");
|
|
15072
|
+
if (!ctx) return false;
|
|
15073
|
+
ctx.drawImage(img, 0, 0, 1, 1);
|
|
15074
|
+
probe2.toDataURL();
|
|
15075
|
+
return true;
|
|
15076
|
+
} catch {
|
|
15077
|
+
return false;
|
|
15078
|
+
}
|
|
15079
|
+
}
|
|
15080
|
+
function rasterizeOverlay(ctx, root, frame) {
|
|
15081
|
+
const win = root.ownerDocument.defaultView;
|
|
15082
|
+
if (!win) return;
|
|
15083
|
+
ctx.save();
|
|
15084
|
+
ctx.scale(frame.dpr, frame.dpr);
|
|
15085
|
+
const drawText = (node, cs, alpha) => {
|
|
15086
|
+
const text = node.textContent?.replace(/\s+/g, " ").trim();
|
|
15087
|
+
if (!text) return;
|
|
15088
|
+
const range = node.ownerDocument.createRange();
|
|
15089
|
+
range.selectNodeContents(node);
|
|
15090
|
+
const r = range.getBoundingClientRect();
|
|
15091
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15092
|
+
ctx.globalAlpha = alpha;
|
|
15093
|
+
ctx.fillStyle = cs.color;
|
|
15094
|
+
ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
|
|
15095
|
+
ctx.textBaseline = "middle";
|
|
15096
|
+
ctx.textAlign = "left";
|
|
15097
|
+
ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
|
|
15098
|
+
};
|
|
15099
|
+
const drawImg = (img, cs, alpha) => {
|
|
15100
|
+
if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
|
|
15101
|
+
const r = img.getBoundingClientRect();
|
|
15102
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15103
|
+
const x = r.left - frame.left;
|
|
15104
|
+
const y = r.top - frame.top;
|
|
15105
|
+
ctx.save();
|
|
15106
|
+
ctx.globalAlpha = alpha;
|
|
15107
|
+
tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
|
|
15108
|
+
ctx.clip();
|
|
15109
|
+
ctx.drawImage(img, x, y, r.width, r.height);
|
|
15110
|
+
ctx.restore();
|
|
15111
|
+
};
|
|
15112
|
+
const drawSvgShapes = (parent, alpha) => {
|
|
15113
|
+
for (const el of parent.children) {
|
|
15114
|
+
if (el.hasAttribute("transform")) continue;
|
|
15115
|
+
const tag = el.tagName.toLowerCase();
|
|
15116
|
+
if (tag === "g") {
|
|
15117
|
+
drawSvgShapes(el, alpha);
|
|
15118
|
+
continue;
|
|
15119
|
+
}
|
|
15120
|
+
const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
|
|
15121
|
+
let path = null;
|
|
15122
|
+
if (tag === "path") {
|
|
15123
|
+
const d = el.getAttribute("d");
|
|
15124
|
+
if (d) path = new Path2D(d);
|
|
15125
|
+
} else if (tag === "circle") {
|
|
15126
|
+
path = new Path2D();
|
|
15127
|
+
path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
|
|
15128
|
+
} else if (tag === "ellipse") {
|
|
15129
|
+
path = new Path2D();
|
|
15130
|
+
path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
|
|
15131
|
+
} else if (tag === "rect") {
|
|
15132
|
+
path = new Path2D();
|
|
15133
|
+
path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
|
|
15134
|
+
} else if (tag === "line") {
|
|
15135
|
+
path = new Path2D();
|
|
15136
|
+
path.moveTo(num3("x1"), num3("y1"));
|
|
15137
|
+
path.lineTo(num3("x2"), num3("y2"));
|
|
15138
|
+
} else if (tag === "polyline" || tag === "polygon") {
|
|
15139
|
+
const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
|
|
15140
|
+
if (pts.length < 4 || pts.some(Number.isNaN)) continue;
|
|
15141
|
+
path = new Path2D();
|
|
15142
|
+
path.moveTo(pts[0], pts[1]);
|
|
15143
|
+
for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
|
|
15144
|
+
if (tag === "polygon") path.closePath();
|
|
15145
|
+
}
|
|
15146
|
+
if (!path) continue;
|
|
15147
|
+
const cs = win.getComputedStyle(el);
|
|
15148
|
+
if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
|
|
15149
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
|
|
15150
|
+
ctx.fillStyle = cs.fill;
|
|
15151
|
+
ctx.fill(path);
|
|
15152
|
+
}
|
|
15153
|
+
const sw = parseFloat(cs.strokeWidth) || 0;
|
|
15154
|
+
if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
|
|
15155
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
|
|
15156
|
+
ctx.strokeStyle = cs.stroke;
|
|
15157
|
+
ctx.lineWidth = sw;
|
|
15158
|
+
if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
|
|
15159
|
+
if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
|
|
15160
|
+
ctx.stroke(path);
|
|
15161
|
+
}
|
|
15162
|
+
}
|
|
15163
|
+
};
|
|
15164
|
+
const drawSvg = (svg, parentAlpha) => {
|
|
15165
|
+
const r = svg.getBoundingClientRect();
|
|
15166
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15167
|
+
const cs = win.getComputedStyle(svg);
|
|
15168
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
15169
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
15170
|
+
if (alpha <= 1e-3) return;
|
|
15171
|
+
const vb = svg.viewBox.baseVal;
|
|
15172
|
+
const vw = vb && vb.width > 0 ? vb.width : r.width;
|
|
15173
|
+
const vh = vb && vb.height > 0 ? vb.height : r.height;
|
|
15174
|
+
ctx.save();
|
|
15175
|
+
ctx.translate(r.left - frame.left, r.top - frame.top);
|
|
15176
|
+
ctx.scale(r.width / vw, r.height / vh);
|
|
15177
|
+
if (vb) ctx.translate(-vb.x, -vb.y);
|
|
15178
|
+
drawSvgShapes(svg, alpha);
|
|
15179
|
+
ctx.restore();
|
|
15180
|
+
};
|
|
15181
|
+
const drawElement = (el, parentAlpha) => {
|
|
15182
|
+
const cs = win.getComputedStyle(el);
|
|
15183
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
15184
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
15185
|
+
if (alpha <= 1e-3) return;
|
|
15186
|
+
const r = el.getBoundingClientRect();
|
|
15187
|
+
if (r.width > 0 && r.height > 0) {
|
|
15188
|
+
const x = r.left - frame.left;
|
|
15189
|
+
const y = r.top - frame.top;
|
|
15190
|
+
const radius = cornerRadius(cs, r.width, r.height);
|
|
15191
|
+
if (hasInk(cs.backgroundColor)) {
|
|
15192
|
+
ctx.globalAlpha = alpha;
|
|
15193
|
+
ctx.fillStyle = cs.backgroundColor;
|
|
15194
|
+
tracePath(ctx, x, y, r.width, r.height, radius);
|
|
15195
|
+
ctx.fill();
|
|
15196
|
+
}
|
|
15197
|
+
const bw = parseFloat(cs.borderTopWidth) || 0;
|
|
15198
|
+
if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
|
|
15199
|
+
ctx.globalAlpha = alpha;
|
|
15200
|
+
ctx.strokeStyle = cs.borderTopColor;
|
|
15201
|
+
ctx.lineWidth = bw;
|
|
15202
|
+
tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
|
|
15203
|
+
ctx.stroke();
|
|
15204
|
+
}
|
|
15205
|
+
}
|
|
15206
|
+
for (const child of el.childNodes) {
|
|
15207
|
+
if (child.nodeType === 3) drawText(child, cs, alpha);
|
|
15208
|
+
else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
|
|
15209
|
+
else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
|
|
15210
|
+
else if (child instanceof win.HTMLElement) drawElement(child, alpha);
|
|
15211
|
+
}
|
|
15212
|
+
};
|
|
15213
|
+
if (root instanceof win.HTMLElement) drawElement(root, 1);
|
|
15214
|
+
ctx.restore();
|
|
15215
|
+
}
|
|
15216
|
+
|
|
14379
15217
|
// src/renderers/native/vpvr/profile.ts
|
|
14380
15218
|
function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
|
|
14381
15219
|
const from = Math.max(0, i0);
|
|
@@ -14544,12 +15382,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
|
|
|
14544
15382
|
var COLLAPSED_PANE_H = 26;
|
|
14545
15383
|
var TIME_AXIS_H = 22;
|
|
14546
15384
|
var SCROLL_BTN_SIZE = 26;
|
|
15385
|
+
var SCROLL_BTN_SIZE_TOUCH = 24;
|
|
14547
15386
|
var SCROLL_BTN_RADIUS = 5;
|
|
14548
15387
|
var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
|
|
14549
15388
|
var SCROLL_BTN_ICON_PX = 12;
|
|
14550
|
-
var SCROLL_BTN_RIGHT_INSET =
|
|
15389
|
+
var SCROLL_BTN_RIGHT_INSET = 12;
|
|
14551
15390
|
var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
|
|
14552
|
-
var SCROLL_BTN_BOTTOM = TIME_AXIS_H +
|
|
15391
|
+
var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
14553
15392
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
14554
15393
|
var MIN_VISIBLE_BARS = 2;
|
|
14555
15394
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
@@ -14601,8 +15440,12 @@ var NativeRenderer = class {
|
|
|
14601
15440
|
this.symbolPicker = null;
|
|
14602
15441
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
14603
15442
|
this.indicatorTitlesOn = true;
|
|
15443
|
+
/** Plot values beside the legend titles shown — held here so a remount re-applies it. */
|
|
15444
|
+
this.indicatorValuesOn = true;
|
|
14604
15445
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
14605
15446
|
this.legendActionsProvider = null;
|
|
15447
|
+
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
15448
|
+
this.legendOverviewAction = null;
|
|
14606
15449
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
14607
15450
|
this.keyboard = null;
|
|
14608
15451
|
this.keyboardEnabled = true;
|
|
@@ -14707,11 +15550,13 @@ var NativeRenderer = class {
|
|
|
14707
15550
|
this.scrollTargetRO = null;
|
|
14708
15551
|
// eased rightOffset target while gliding back to the latest bars
|
|
14709
15552
|
// Distance (px) from the plot's bottom edge to the button — tracks the bottom-most EXPANDED
|
|
14710
|
-
// pane
|
|
15553
|
+
// pane: when the lower sub-panes collapse it rides up into the open pane.
|
|
14711
15554
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM;
|
|
14712
15555
|
// Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
|
|
14713
15556
|
// pane carries merged (own-scale) columns; the constant only clears a single-column scale.
|
|
14714
15557
|
this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
|
|
15558
|
+
// ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
|
|
15559
|
+
this.layoutMode = "desktop";
|
|
14715
15560
|
this.viewportCbs = /* @__PURE__ */ new Set();
|
|
14716
15561
|
this.crosshairCbs = /* @__PURE__ */ new Set();
|
|
14717
15562
|
this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
|
|
@@ -14722,6 +15567,7 @@ var NativeRenderer = class {
|
|
|
14722
15567
|
this.factoryConfig = null;
|
|
14723
15568
|
this.hostSettingsSections = [];
|
|
14724
15569
|
this.clickCbs = /* @__PURE__ */ new Set();
|
|
15570
|
+
this.axisLongPressCbs = /* @__PURE__ */ new Set();
|
|
14725
15571
|
this.inputChangeCbs = /* @__PURE__ */ new Set();
|
|
14726
15572
|
this.removeIndicatorCbs = /* @__PURE__ */ new Set();
|
|
14727
15573
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
@@ -14729,7 +15575,7 @@ var NativeRenderer = class {
|
|
|
14729
15575
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
14730
15576
|
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
14731
15577
|
this.name = "native";
|
|
14732
|
-
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"];
|
|
15578
|
+
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"];
|
|
14733
15579
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
14734
15580
|
* so moving onto the button doesn't count as leaving). */
|
|
14735
15581
|
this.onScrollProximityMove = (e) => {
|
|
@@ -14902,6 +15748,11 @@ var NativeRenderer = class {
|
|
|
14902
15748
|
this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
|
|
14903
15749
|
return;
|
|
14904
15750
|
// own DOM, no repaint needed
|
|
15751
|
+
case "indicatorValues":
|
|
15752
|
+
this.indicatorValuesOn = value !== false;
|
|
15753
|
+
this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
|
|
15754
|
+
return;
|
|
15755
|
+
// own DOM, no repaint needed
|
|
14905
15756
|
case "attribution":
|
|
14906
15757
|
this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
|
|
14907
15758
|
this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
|
|
@@ -14990,6 +15841,8 @@ var NativeRenderer = class {
|
|
|
14990
15841
|
return this.settingsEnabled;
|
|
14991
15842
|
case "indicatorTitles":
|
|
14992
15843
|
return this.indicatorTitlesOn;
|
|
15844
|
+
case "indicatorValues":
|
|
15845
|
+
return this.indicatorValuesOn;
|
|
14993
15846
|
case "attribution":
|
|
14994
15847
|
return this.attributionHtml ?? this.attributionEnabled;
|
|
14995
15848
|
case "dialogHost":
|
|
@@ -15326,7 +16179,10 @@ var NativeRenderer = class {
|
|
|
15326
16179
|
/** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
|
|
15327
16180
|
* in-chart gear — created on demand, independent of the gear feature being enabled. */
|
|
15328
16181
|
openSettingsDialog(section) {
|
|
15329
|
-
if (!this.settingsDialog && this.plot)
|
|
16182
|
+
if (!this.settingsDialog && this.plot) {
|
|
16183
|
+
this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
|
|
16184
|
+
this.settingsDialog.setLayoutMode(this.layoutMode);
|
|
16185
|
+
}
|
|
15330
16186
|
this.toggleSettingsDialog(section);
|
|
15331
16187
|
}
|
|
15332
16188
|
/** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
|
|
@@ -15415,11 +16271,18 @@ var NativeRenderer = class {
|
|
|
15415
16271
|
this.scrollButton.style.color = this.theme.textColor;
|
|
15416
16272
|
this.scrollButton.style.borderColor = this.theme.borderColor;
|
|
15417
16273
|
}
|
|
15418
|
-
/** Show the button only when the latest bar is scrolled off the right edge
|
|
16274
|
+
/** Show the button only when the latest bar is scrolled off the right edge. On desktop the
|
|
16275
|
+
* cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
|
|
16276
|
+
* appears whenever the latest bars are off-screen and hides again once the view is back
|
|
16277
|
+
* at the right edge. */
|
|
15419
16278
|
updateScrollToRealtimeButton() {
|
|
15420
16279
|
const btn = this.scrollButton;
|
|
15421
16280
|
if (!btn) return;
|
|
15422
16281
|
const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
|
|
16282
|
+
if (this.layoutMode === "mobile") {
|
|
16283
|
+
btn.style.display = latestOffScreen ? "flex" : "none";
|
|
16284
|
+
return;
|
|
16285
|
+
}
|
|
15423
16286
|
btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
|
|
15424
16287
|
}
|
|
15425
16288
|
/** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
|
|
@@ -15441,17 +16304,20 @@ var NativeRenderer = class {
|
|
|
15441
16304
|
this.animator.start();
|
|
15442
16305
|
}
|
|
15443
16306
|
/**
|
|
15444
|
-
* Export the current chart as a PNG data URL by compositing
|
|
15445
|
-
*
|
|
15446
|
-
*
|
|
15447
|
-
*
|
|
15448
|
-
*
|
|
15449
|
-
*
|
|
15450
|
-
*
|
|
15451
|
-
*
|
|
15452
|
-
*
|
|
15453
|
-
*
|
|
15454
|
-
*
|
|
16307
|
+
* Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
|
|
16308
|
+
* an offscreen canvas, in the same stacking order the DOM shows: plugin layers
|
|
16309
|
+
* below the data, geometry (L0), volume columns (L0.25), the visible-range volume
|
|
16310
|
+
* profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
|
|
16311
|
+
* (L1.5). The background is filled first (the canvas2d data layer is transparent —
|
|
16312
|
+
* its bg lives on the wrapper), and a fresh synchronous paint runs first so the
|
|
16313
|
+
* WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
|
|
16314
|
+
* back this tick — the same paint also repaints the drawings layers, so they're
|
|
16315
|
+
* current. DOM chrome joins as a best-effort text/chip raster (see
|
|
16316
|
+
* `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
|
|
16317
|
+
* opts in with a `data-vela-screenshot` attribute on the mount container's
|
|
16318
|
+
* subtree (the widget marks its status line, and its symbol watermark with
|
|
16319
|
+
* `"under"` — drawn beneath the canvases, where it sits on screen). Only the
|
|
16320
|
+
* crosshair (L2) is intentionally excluded.
|
|
15455
16321
|
*/
|
|
15456
16322
|
screenshot() {
|
|
15457
16323
|
if (!this.dataCanvas) return null;
|
|
@@ -15464,9 +16330,25 @@ var NativeRenderer = class {
|
|
|
15464
16330
|
if (!ctx) return null;
|
|
15465
16331
|
ctx.fillStyle = this.theme.background;
|
|
15466
16332
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
16333
|
+
const plotRect = this.plot?.getBoundingClientRect();
|
|
16334
|
+
const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
|
|
16335
|
+
const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
|
|
16336
|
+
if (frame) {
|
|
16337
|
+
for (const el of hostMarked) {
|
|
16338
|
+
if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
16342
|
+
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
16343
|
+
for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
|
|
16344
|
+
if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
|
|
16345
|
+
}
|
|
16346
|
+
if (frame) {
|
|
16347
|
+
for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
|
|
16348
|
+
for (const el of hostMarked) {
|
|
16349
|
+
if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
|
|
16350
|
+
}
|
|
16351
|
+
}
|
|
15470
16352
|
return out.toDataURL("image/png");
|
|
15471
16353
|
}
|
|
15472
16354
|
/**
|
|
@@ -15617,6 +16499,9 @@ var NativeRenderer = class {
|
|
|
15617
16499
|
fling: (v) => this.fling(v),
|
|
15618
16500
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
15619
16501
|
onClick: (x) => this.handleClick(x),
|
|
16502
|
+
onAxisLongPress: (axis, x, y) => {
|
|
16503
|
+
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
16504
|
+
},
|
|
15620
16505
|
beginPriceScale: (x, y) => this.beginPriceScale(x, y),
|
|
15621
16506
|
priceScaleBy: (dy) => this.priceScaleBy(dy),
|
|
15622
16507
|
beginPricePan: (y) => this.beginPricePan(y),
|
|
@@ -15632,6 +16517,7 @@ var NativeRenderer = class {
|
|
|
15632
16517
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
15633
16518
|
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
15634
16519
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
16520
|
+
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
15635
16521
|
drawingsSnapMode: () => this.snapMode,
|
|
15636
16522
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
15637
16523
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
@@ -15663,6 +16549,14 @@ var NativeRenderer = class {
|
|
|
15663
16549
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
15664
16550
|
});
|
|
15665
16551
|
this.userDrawings.historyChords = this.historyChordsEnabled;
|
|
16552
|
+
if (this.layoutMode === "mobile") {
|
|
16553
|
+
this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
|
|
16554
|
+
this.userDrawings.setLayoutMode(this.layoutMode);
|
|
16555
|
+
if (this.scrollButton) {
|
|
16556
|
+
this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
16557
|
+
this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
16558
|
+
}
|
|
16559
|
+
}
|
|
15666
16560
|
this.keyboard = new KeyboardController({
|
|
15667
16561
|
panByBars: (bars) => this.panByBars(bars),
|
|
15668
16562
|
zoomByStep: (dir) => this.zoomByStep(dir),
|
|
@@ -15675,10 +16569,13 @@ var NativeRenderer = class {
|
|
|
15675
16569
|
});
|
|
15676
16570
|
this.setKeyboardEnabled(this.keyboardEnabled);
|
|
15677
16571
|
this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
|
|
16572
|
+
this.inputsUI.setLayoutMode(this.layoutMode);
|
|
15678
16573
|
this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
|
|
16574
|
+
this.inputsUI.setValuesVisible(this.indicatorValuesOn);
|
|
15679
16575
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
15680
16576
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
15681
16577
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
16578
|
+
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
15682
16579
|
this.inputsUI.setOnChange((c) => {
|
|
15683
16580
|
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
15684
16581
|
});
|
|
@@ -15872,6 +16769,8 @@ var NativeRenderer = class {
|
|
|
15872
16769
|
this.attributionEl = null;
|
|
15873
16770
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
15874
16771
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
16772
|
+
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
16773
|
+
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
15875
16774
|
this.mountContainer = null;
|
|
15876
16775
|
this.wrapper?.remove();
|
|
15877
16776
|
}
|
|
@@ -16093,7 +16992,10 @@ var NativeRenderer = class {
|
|
|
16093
16992
|
this.scene.indicators.set(model.id, model);
|
|
16094
16993
|
this.refreshAnchorOffset(model);
|
|
16095
16994
|
this.scene.assignIndicatorZ(model.id);
|
|
16096
|
-
this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, {
|
|
16995
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
16996
|
+
native: !!model.native,
|
|
16997
|
+
...model.shorttitle ? { settingsTitle: model.title } : {}
|
|
16998
|
+
});
|
|
16097
16999
|
this.syncTables(model);
|
|
16098
17000
|
if (model.native?.type === "volume") {
|
|
16099
17001
|
this.volumeActive = true;
|
|
@@ -16154,6 +17056,13 @@ var NativeRenderer = class {
|
|
|
16154
17056
|
this.legendActionsProvider = provider;
|
|
16155
17057
|
this.inputsUI?.setLegendActions(provider);
|
|
16156
17058
|
}
|
|
17059
|
+
setLegendOverviewAction(action) {
|
|
17060
|
+
this.legendOverviewAction = action;
|
|
17061
|
+
this.inputsUI?.setLegendOverviewAction(action);
|
|
17062
|
+
}
|
|
17063
|
+
openIndicatorSettings(indicatorId) {
|
|
17064
|
+
this.inputsUI?.openSettingsFor(indicatorId);
|
|
17065
|
+
}
|
|
16157
17066
|
/**
|
|
16158
17067
|
* Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
|
|
16159
17068
|
* series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
|
|
@@ -16207,6 +17116,24 @@ var NativeRenderer = class {
|
|
|
16207
17116
|
this.removeIndicatorCbs.add(cb);
|
|
16208
17117
|
return () => this.removeIndicatorCbs.delete(cb);
|
|
16209
17118
|
}
|
|
17119
|
+
/** Port surface: the host shell's chrome size class. Mobile switches the renderer
|
|
17120
|
+
* chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
|
|
17121
|
+
* toolbar (the shell provides its own picker), the scroll-to-latest button shown
|
|
17122
|
+
* whenever the latest bars are off-screen (touch has no cursor proximity). */
|
|
17123
|
+
setLayoutMode(mode) {
|
|
17124
|
+
if (mode === this.layoutMode) return;
|
|
17125
|
+
this.layoutMode = mode;
|
|
17126
|
+
this.mountContainer?.setAttribute("data-vela-layout", mode);
|
|
17127
|
+
this.userDrawings?.setLayoutMode(mode);
|
|
17128
|
+
this.settingsDialog?.setLayoutMode(mode);
|
|
17129
|
+
this.inputsUI?.setLayoutMode(mode);
|
|
17130
|
+
if (this.scrollButton) {
|
|
17131
|
+
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
17132
|
+
this.scrollButton.style.width = `${px}px`;
|
|
17133
|
+
this.scrollButton.style.height = `${px}px`;
|
|
17134
|
+
}
|
|
17135
|
+
this.updateScrollToRealtimeButton();
|
|
17136
|
+
}
|
|
16210
17137
|
setSettingsSections(sections) {
|
|
16211
17138
|
this.hostSettingsSections = sections;
|
|
16212
17139
|
this.settingsDialog?.setHostSections(sections);
|
|
@@ -16231,6 +17158,10 @@ var NativeRenderer = class {
|
|
|
16231
17158
|
this.clickCbs.add(cb);
|
|
16232
17159
|
return () => this.clickCbs.delete(cb);
|
|
16233
17160
|
}
|
|
17161
|
+
onAxisLongPress(cb) {
|
|
17162
|
+
this.axisLongPressCbs.add(cb);
|
|
17163
|
+
return () => this.axisLongPressCbs.delete(cb);
|
|
17164
|
+
}
|
|
16234
17165
|
onViewportChange(cb) {
|
|
16235
17166
|
this.viewportCbs.add(cb);
|
|
16236
17167
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -16372,6 +17303,7 @@ var NativeRenderer = class {
|
|
|
16372
17303
|
this.skeletonClockMs += dtMs;
|
|
16373
17304
|
this.paintData();
|
|
16374
17305
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
17306
|
+
this.updateLegendValues();
|
|
16375
17307
|
this.emitViewportChange();
|
|
16376
17308
|
return active;
|
|
16377
17309
|
}
|
|
@@ -16436,7 +17368,7 @@ var NativeRenderer = class {
|
|
|
16436
17368
|
this.lastPointer = null;
|
|
16437
17369
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
16438
17370
|
this.hoverLogical = null;
|
|
16439
|
-
const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
17371
|
+
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
16440
17372
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
16441
17373
|
return;
|
|
16442
17374
|
}
|
|
@@ -16448,7 +17380,7 @@ var NativeRenderer = class {
|
|
|
16448
17380
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
16449
17381
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
16450
17382
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
16451
|
-
const pane = this.
|
|
17383
|
+
const pane = this.paneNodeAtY(y);
|
|
16452
17384
|
const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
|
|
16453
17385
|
const values = /* @__PURE__ */ new Map();
|
|
16454
17386
|
if (onBar) {
|
|
@@ -16465,7 +17397,7 @@ var NativeRenderer = class {
|
|
|
16465
17397
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
16466
17398
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
16467
17399
|
this.hoverLogical = onBar ? logical : null;
|
|
16468
|
-
const event = { time, price, values, ohlc };
|
|
17400
|
+
const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
|
|
16469
17401
|
for (const cb of this.crosshairCbs) cb(event);
|
|
16470
17402
|
}
|
|
16471
17403
|
handleClick(x) {
|
|
@@ -16484,6 +17416,20 @@ var NativeRenderer = class {
|
|
|
16484
17416
|
}
|
|
16485
17417
|
return null;
|
|
16486
17418
|
}
|
|
17419
|
+
/** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
|
|
17420
|
+
* fall BETWEEN bounds (separator gaps, the strip under the last pane). */
|
|
17421
|
+
nearestPaneToY(y) {
|
|
17422
|
+
let best = null;
|
|
17423
|
+
let bestDist = Infinity;
|
|
17424
|
+
for (const pane of this.scene.panes.values()) {
|
|
17425
|
+
const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
|
|
17426
|
+
if (d < bestDist) {
|
|
17427
|
+
bestDist = d;
|
|
17428
|
+
best = pane;
|
|
17429
|
+
}
|
|
17430
|
+
}
|
|
17431
|
+
return best;
|
|
17432
|
+
}
|
|
16487
17433
|
// ── manual vertical scaling (price-axis drag + vertical price pan) ──
|
|
16488
17434
|
/** The scale window a gesture targets: a pane's master scale, or a merged indicator's
|
|
16489
17435
|
* own scale column (resolved from the grabbed x). Both share the four scale fields. */
|
|
@@ -16546,7 +17492,7 @@ var NativeRenderer = class {
|
|
|
16546
17492
|
* still fits the view.)
|
|
16547
17493
|
*/
|
|
16548
17494
|
dataDblClick(_x, y) {
|
|
16549
|
-
const pane = this.paneNodeAtY(y);
|
|
17495
|
+
const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
|
|
16550
17496
|
if (!pane) return;
|
|
16551
17497
|
const maximized = this.maximizedPaneId !== pane.id;
|
|
16552
17498
|
this.maximizedPaneId = maximized ? pane.id : null;
|
|
@@ -16787,29 +17733,63 @@ var NativeRenderer = class {
|
|
|
16787
17733
|
dataWindowGroups(idx, pricePane) {
|
|
16788
17734
|
const groups = [];
|
|
16789
17735
|
for (const model of this.scene.indicators.values()) {
|
|
16790
|
-
const
|
|
16791
|
-
const off = this.scene.offsetOf(model.id);
|
|
16792
|
-
const rows = [];
|
|
16793
|
-
for (const s of model.series) {
|
|
16794
|
-
let value;
|
|
16795
|
-
let color;
|
|
16796
|
-
if (s.kind === "candle" || s.kind === "bar") {
|
|
16797
|
-
value = s.bars[idx - off]?.close;
|
|
16798
|
-
color = s.style?.up ?? this.theme.upColor;
|
|
16799
|
-
} else if (isLineLikeSeries(s)) {
|
|
16800
|
-
if (s.visible === false) continue;
|
|
16801
|
-
value = s.points[idx - off]?.value;
|
|
16802
|
-
color = s.points[idx - off]?.color ?? s.style.color;
|
|
16803
|
-
} else {
|
|
16804
|
-
continue;
|
|
16805
|
-
}
|
|
16806
|
-
if (value == null || !Number.isFinite(value)) continue;
|
|
16807
|
-
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
16808
|
-
}
|
|
17736
|
+
const rows = this.dataWindowRowsFor(model, idx, pricePane);
|
|
16809
17737
|
if (rows.length) groups.push({ name: model.title, rows });
|
|
16810
17738
|
}
|
|
16811
17739
|
return groups;
|
|
16812
17740
|
}
|
|
17741
|
+
/** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
|
|
17742
|
+
dataWindowRowsFor(model, idx, pricePane) {
|
|
17743
|
+
if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
|
|
17744
|
+
const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
|
|
17745
|
+
const off = this.scene.offsetOf(model.id);
|
|
17746
|
+
const rows = [];
|
|
17747
|
+
for (const s of model.series) {
|
|
17748
|
+
let value;
|
|
17749
|
+
let color;
|
|
17750
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
17751
|
+
value = s.bars[idx - off]?.close;
|
|
17752
|
+
color = s.style?.up ?? this.theme.upColor;
|
|
17753
|
+
} else if (isLineLikeSeries(s)) {
|
|
17754
|
+
if (s.visible === false) continue;
|
|
17755
|
+
value = s.points[idx - off]?.value;
|
|
17756
|
+
color = s.points[idx - off]?.color ?? s.style.color;
|
|
17757
|
+
} else {
|
|
17758
|
+
continue;
|
|
17759
|
+
}
|
|
17760
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
17761
|
+
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
17762
|
+
}
|
|
17763
|
+
return rows;
|
|
17764
|
+
}
|
|
17765
|
+
/** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
|
|
17766
|
+
* colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
|
|
17767
|
+
* `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
|
|
17768
|
+
volumeReadoutRows(model, idx) {
|
|
17769
|
+
if (this.volumeHidden) return [];
|
|
17770
|
+
const bar = this.bars[idx];
|
|
17771
|
+
const vol = bar?.volume;
|
|
17772
|
+
if (bar == null || vol == null || !Number.isFinite(vol)) return [];
|
|
17773
|
+
const cfg = this.scene.volumeLayer;
|
|
17774
|
+
const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
|
|
17775
|
+
return [{ label: model.title, value: formatVolume(vol), color }];
|
|
17776
|
+
}
|
|
17777
|
+
/** Refresh the plot values beside every legend title — the same readout the data window
|
|
17778
|
+
* shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
|
|
17779
|
+
* no scene model, so its row is absent from the map and its readout clears. */
|
|
17780
|
+
updateLegendValues() {
|
|
17781
|
+
if (!this.inputsUI) return;
|
|
17782
|
+
const n = this.bars.length;
|
|
17783
|
+
const values = /* @__PURE__ */ new Map();
|
|
17784
|
+
if (n > 0) {
|
|
17785
|
+
const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
|
|
17786
|
+
const pricePane = this.dataWindowPricePane();
|
|
17787
|
+
for (const model of this.scene.indicators.values()) {
|
|
17788
|
+
values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
|
|
17789
|
+
}
|
|
17790
|
+
}
|
|
17791
|
+
this.inputsUI.setPlotValues(values);
|
|
17792
|
+
}
|
|
16813
17793
|
renderFrame(level) {
|
|
16814
17794
|
if (this.animator.active) return;
|
|
16815
17795
|
if (repaintsData(level)) {
|
|
@@ -16821,6 +17801,7 @@ var NativeRenderer = class {
|
|
|
16821
17801
|
}
|
|
16822
17802
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
16823
17803
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
17804
|
+
this.updateLegendValues();
|
|
16824
17805
|
}
|
|
16825
17806
|
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
16826
17807
|
repaintCursorLayers() {
|
|
@@ -16879,19 +17860,18 @@ var NativeRenderer = class {
|
|
|
16879
17860
|
theme: this.theme
|
|
16880
17861
|
});
|
|
16881
17862
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
17863
|
+
let folded = null;
|
|
16882
17864
|
for (const l of this.extLayers) {
|
|
16883
17865
|
const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
|
|
16884
17866
|
l.instance.render(args);
|
|
16885
|
-
|
|
16886
|
-
const mod = l.instance.modulateBase(args);
|
|
16887
|
-
if (mod) {
|
|
16888
|
-
if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
|
|
16889
|
-
if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
|
|
16890
|
-
if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
|
|
16891
|
-
}
|
|
16892
|
-
}
|
|
17867
|
+
folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
16893
17868
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
16894
17869
|
}
|
|
17870
|
+
if (folded) {
|
|
17871
|
+
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
17872
|
+
if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
|
|
17873
|
+
if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
|
|
17874
|
+
}
|
|
16895
17875
|
}
|
|
16896
17876
|
const li = this.bars.length - 1;
|
|
16897
17877
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
@@ -16969,7 +17949,7 @@ var NativeRenderer = class {
|
|
|
16969
17949
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
16970
17950
|
}
|
|
16971
17951
|
}
|
|
16972
|
-
return { x, y, time: this.coords.logicalToTime(logical) };
|
|
17952
|
+
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
16973
17953
|
}
|
|
16974
17954
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
16975
17955
|
setSnapMode(mode) {
|
|
@@ -17283,6 +18263,7 @@ var NativeRenderer = class {
|
|
|
17283
18263
|
this.paneControls?.reposition();
|
|
17284
18264
|
this.repositionScrollButton();
|
|
17285
18265
|
this.positionAttribution();
|
|
18266
|
+
this.publishPricePaneBounds();
|
|
17286
18267
|
return;
|
|
17287
18268
|
}
|
|
17288
18269
|
const collapsed = panes.filter((p) => p.collapsed);
|
|
@@ -17300,9 +18281,10 @@ var NativeRenderer = class {
|
|
|
17300
18281
|
this.paneControls?.reposition();
|
|
17301
18282
|
this.repositionScrollButton();
|
|
17302
18283
|
this.positionAttribution();
|
|
18284
|
+
this.publishPricePaneBounds();
|
|
17303
18285
|
}
|
|
17304
|
-
/** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area
|
|
17305
|
-
*
|
|
18286
|
+
/** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area:
|
|
18287
|
+
* with all lower sub-panes collapsed it settles into the lowest open pane. */
|
|
17306
18288
|
repositionScrollButton() {
|
|
17307
18289
|
const dataHeight = this.coords.height;
|
|
17308
18290
|
if (dataHeight <= 0) return;
|
|
@@ -17346,6 +18328,21 @@ var NativeRenderer = class {
|
|
|
17346
18328
|
this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
|
|
17347
18329
|
this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
|
|
17348
18330
|
}
|
|
18331
|
+
/** Publish the price pane's vertical insets as `--vela-price-pane-top` /
|
|
18332
|
+
* `--vela-price-pane-bottom` on the mount container, so the symbol watermark
|
|
18333
|
+
* (and any other host overlay) can clip to the price pane instead of spanning
|
|
18334
|
+
* study panes and the time axis. A maximized study pane collapses the box to
|
|
18335
|
+
* zero height — the mark does not appear on a study. */
|
|
18336
|
+
publishPricePaneBounds() {
|
|
18337
|
+
if (!this.mountContainer) return;
|
|
18338
|
+
const plotH = this.coords.height + TIME_AXIS_H;
|
|
18339
|
+
const price = this.scene.panes.get(PRICE_PANE_ID);
|
|
18340
|
+
const top = price ? price.bounds.top : 0;
|
|
18341
|
+
const height = price ? price.bounds.height : this.coords.height;
|
|
18342
|
+
const bottom = Math.max(0, plotH - (top + height));
|
|
18343
|
+
this.mountContainer.style.setProperty("--vela-price-pane-top", `${top}px`);
|
|
18344
|
+
this.mountContainer.style.setProperty("--vela-price-pane-bottom", `${bottom}px`);
|
|
18345
|
+
}
|
|
17349
18346
|
/** The built-in mark, or the host's own when one is set. */
|
|
17350
18347
|
buildAttributionEl() {
|
|
17351
18348
|
return this.attributionHtml !== null ? createCustomMark(document, this.attributionHtml, this.theme.background) : createAttributionMark(document, this.theme.background);
|
|
@@ -17739,6 +18736,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17739
18736
|
this.barTransform = null;
|
|
17740
18737
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
17741
18738
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
18739
|
+
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
18740
|
+
* replayed into a data engine whenever one is (re)created. */
|
|
18741
|
+
this.typeSettings = /* @__PURE__ */ new Map();
|
|
17742
18742
|
/** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
|
|
17743
18743
|
this.activeEngineStyle = null;
|
|
17744
18744
|
/** The chart's current price style (tracked from the renderer's change events + initial read). */
|
|
@@ -17801,7 +18801,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17801
18801
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17802
18802
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17803
18803
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
17804
|
-
this.renderer.onChartTypeSettingsChange?.((typeId, values) =>
|
|
18804
|
+
this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
|
|
18805
|
+
this.typeSettings.set(typeId, values);
|
|
18806
|
+
this.typeEngines.get(typeId)?.onSettings?.(values);
|
|
18807
|
+
});
|
|
17805
18808
|
this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
|
|
17806
18809
|
this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
|
|
17807
18810
|
this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
|
|
@@ -18289,6 +19292,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18289
19292
|
if (!def?.dataEngine) return;
|
|
18290
19293
|
const engine = def.dataEngine();
|
|
18291
19294
|
this.typeEngines.set(id, engine);
|
|
19295
|
+
const stored = this.typeSettings.get(id);
|
|
19296
|
+
if (stored) engine.onSettings?.(stored);
|
|
18292
19297
|
engine.start({
|
|
18293
19298
|
symbol: this.config.market.symbol ?? "TEST",
|
|
18294
19299
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -18760,6 +19765,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18760
19765
|
return {
|
|
18761
19766
|
id: record.id,
|
|
18762
19767
|
title: record.title,
|
|
19768
|
+
...d.shortTitle ? { shorttitle: d.shortTitle } : {},
|
|
18763
19769
|
overlay: d.overlay,
|
|
18764
19770
|
paneHint: d.paneHint,
|
|
18765
19771
|
native: { type: record.native.type },
|
|
@@ -19367,7 +20373,8 @@ var VpvrIndicator = class {
|
|
|
19367
20373
|
};
|
|
19368
20374
|
var vpvrDescriptor = {
|
|
19369
20375
|
type: "vpvr",
|
|
19370
|
-
title: "
|
|
20376
|
+
title: "Visible Range Volume Profile",
|
|
20377
|
+
shortTitle: "VRVP",
|
|
19371
20378
|
paneHint: "price",
|
|
19372
20379
|
overlay: true,
|
|
19373
20380
|
inputsSchema: () => [
|
|
@@ -19724,4 +20731,4 @@ function resolveElement(container) {
|
|
|
19724
20731
|
return element;
|
|
19725
20732
|
}
|
|
19726
20733
|
|
|
19727
|
-
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 };
|
|
20734
|
+
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 };
|