@luxalgo/vela 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-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-S6F6SQ2L.js → chunk-B5TIKQJ5.js} +60 -7
- package/dist/{chunk-EP6NDJYK.js → chunk-LJLZR44Y.js} +4 -2
- package/dist/{chunk-AIW3EJUE.js → chunk-P556H6EA.js} +329 -10
- package/dist/{chunk-FNOMKHEZ.js → chunk-RRFGWZNJ.js} +1321 -363
- package/dist/{chunk-5SE4PL3H.js → chunk-Y7TK4ZWK.js} +1363 -48
- 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 +1374 -366
- package/dist/index.d.cts +52 -19
- package/dist/index.d.ts +52 -19
- 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-CnNd7cuF.d.ts} +172 -25
- package/dist/{plugin-O34MbXf-.d.cts → plugin-DfH4Y-Om.d.cts} +172 -25
- 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 +1374 -366
- package/dist/vela.global.min.js +84 -34
- package/dist/widget.cjs +3348 -550
- package/dist/widget.d.cts +65 -7
- package/dist/widget.d.ts +65 -7
- package/dist/widget.js +169 -18
- package/dist/workspace.cjs +4251 -1400
- 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, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-B5TIKQJ5.js';
|
|
2
|
+
import { themeTokens, STATIC_TOKENS } from './chunk-ATPU5CI6.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-LJLZR44Y.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -428,6 +428,28 @@ var RendererControl = class {
|
|
|
428
428
|
setLegendActions(provider) {
|
|
429
429
|
this.renderer.setLegendActions?.(provider);
|
|
430
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
433
|
+
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
434
|
+
* unfolding rows in place. Silent no-op on a renderer without a foldable legend.
|
|
435
|
+
*/
|
|
436
|
+
setLegendOverviewAction(action) {
|
|
437
|
+
this.renderer.setLegendOverviewAction?.(action);
|
|
438
|
+
return this;
|
|
439
|
+
}
|
|
440
|
+
/** Whether the active renderer can open a per-indicator settings dialog. */
|
|
441
|
+
get supportsIndicatorSettings() {
|
|
442
|
+
return typeof this.renderer.openIndicatorSettings === "function";
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Open one indicator's settings dialog — the programmatic twin of the legend gear
|
|
446
|
+
* (see {@link supportsIndicatorSettings}). Silent no-op without the seam or for an
|
|
447
|
+
* unknown id.
|
|
448
|
+
*/
|
|
449
|
+
openIndicatorSettings(indicatorId) {
|
|
450
|
+
this.renderer.openIndicatorSettings?.(indicatorId);
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
431
453
|
/**
|
|
432
454
|
* Export the current chart as a PNG data URL, or null if the active renderer
|
|
433
455
|
* doesn't support it (warns). DOM overlays (tables, legend) are not included.
|
|
@@ -474,6 +496,10 @@ var RendererControl = class {
|
|
|
474
496
|
onCrosshairMove(cb) {
|
|
475
497
|
return this.renderer.onCrosshairMove(cb);
|
|
476
498
|
}
|
|
499
|
+
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
500
|
+
onAxisLongPress(cb) {
|
|
501
|
+
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
502
|
+
}
|
|
477
503
|
/**
|
|
478
504
|
* The current data-window readout — the hovered bar's date/time and OHLCV plus every
|
|
479
505
|
* indicator's value there, each already formatted on its pane's scale (the latest bar when
|
|
@@ -539,6 +565,13 @@ var RendererControl = class {
|
|
|
539
565
|
this.renderer.setSettingsSections?.(sections);
|
|
540
566
|
return this;
|
|
541
567
|
}
|
|
568
|
+
/** Tell the renderer's own chrome which size class the host shell is in —
|
|
569
|
+
* `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
|
|
570
|
+
* Silent no-op on a renderer without adaptive chrome. */
|
|
571
|
+
setLayoutMode(mode) {
|
|
572
|
+
this.renderer.setLayoutMode?.(mode);
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
542
575
|
};
|
|
543
576
|
|
|
544
577
|
// src/data/BarStore.ts
|
|
@@ -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,6 +6963,11 @@ 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
6972
|
var WHEEL_ZOOM_K = 25e-4;
|
|
6754
6973
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
@@ -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;
|
|
@@ -6802,6 +7027,23 @@ var InputController = class {
|
|
|
6802
7027
|
// press/release re-shape the drawings preview with the pointer stationary.
|
|
6803
7028
|
this.cursorX = NaN;
|
|
6804
7029
|
this.cursorY = NaN;
|
|
7030
|
+
// ── touch state ──
|
|
7031
|
+
/** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
|
|
7032
|
+
this.touches = /* @__PURE__ */ new Map();
|
|
7033
|
+
this.lpTimer = null;
|
|
7034
|
+
this.pinchStartDist = 1;
|
|
7035
|
+
this.pinchStartBarSpacing = 0;
|
|
7036
|
+
this.pinchAnchorLogical = 0;
|
|
7037
|
+
// Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
|
|
7038
|
+
this.lastTapT = 0;
|
|
7039
|
+
this.lastTapX = 0;
|
|
7040
|
+
this.lastTapY = 0;
|
|
7041
|
+
this.onTouchContextMenu = (e) => {
|
|
7042
|
+
if (this.touches.size === 0) return;
|
|
7043
|
+
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
7044
|
+
e.preventDefault();
|
|
7045
|
+
e.stopImmediatePropagation();
|
|
7046
|
+
};
|
|
6805
7047
|
this.onMiddleGuard = (e) => {
|
|
6806
7048
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
6807
7049
|
};
|
|
@@ -6823,6 +7065,15 @@ var InputController = class {
|
|
|
6823
7065
|
}
|
|
6824
7066
|
if (e.button !== 0) return;
|
|
6825
7067
|
const { x, y } = this.local(e);
|
|
7068
|
+
if (e.pointerType === "touch") {
|
|
7069
|
+
if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
|
|
7070
|
+
this.touches.set(e.pointerId, { x, y });
|
|
7071
|
+
this.beginPinch(e);
|
|
7072
|
+
return;
|
|
7073
|
+
}
|
|
7074
|
+
if (this.touches.size >= 1) return;
|
|
7075
|
+
this.touches.set(e.pointerId, { x, y });
|
|
7076
|
+
}
|
|
6826
7077
|
this.deps.drawingsClearTransient?.();
|
|
6827
7078
|
this.dragging = true;
|
|
6828
7079
|
this.moved = false;
|
|
@@ -6848,6 +7099,9 @@ var InputController = class {
|
|
|
6848
7099
|
if (this.region === "price") this.deps.beginPriceScale(x, y);
|
|
6849
7100
|
else if (this.region === "separator") this.deps.beginPaneResize(y);
|
|
6850
7101
|
else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
|
|
7102
|
+
if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
|
|
7103
|
+
this.startLongPress(x, y, this.region);
|
|
7104
|
+
}
|
|
6851
7105
|
this.lastX = x;
|
|
6852
7106
|
this.lastT = e.timeStamp;
|
|
6853
7107
|
this.vx = 0;
|
|
@@ -6857,28 +7111,42 @@ var InputController = class {
|
|
|
6857
7111
|
const { x, y } = this.local(e);
|
|
6858
7112
|
this.cursorX = x;
|
|
6859
7113
|
this.cursorY = y;
|
|
7114
|
+
if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
|
|
7115
|
+
if (this.dragging && this.region === "pinch") {
|
|
7116
|
+
this.applyPinch();
|
|
7117
|
+
return;
|
|
7118
|
+
}
|
|
7119
|
+
if (this.dragging && this.region === "crosshair") {
|
|
7120
|
+
this.deps.onPointerMove(x, y);
|
|
7121
|
+
return;
|
|
7122
|
+
}
|
|
7123
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
7124
|
+
return;
|
|
7125
|
+
}
|
|
7126
|
+
if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
|
|
7127
|
+
const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
|
|
6860
7128
|
if (this.dragging) {
|
|
6861
7129
|
if (this.region === "price") {
|
|
6862
|
-
if (Math.abs(y - this.startY) >
|
|
7130
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6863
7131
|
this.deps.priceScaleBy(y - this.startY);
|
|
6864
7132
|
} else if (this.region === "separator") {
|
|
6865
|
-
if (Math.abs(y - this.startY) >
|
|
7133
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6866
7134
|
this.deps.paneResizeBy(y - this.startY);
|
|
6867
7135
|
} else if (this.region === "time") {
|
|
6868
|
-
if (Math.abs(x - this.startX) >
|
|
7136
|
+
if (Math.abs(x - this.startX) > slop) this.moved = true;
|
|
6869
7137
|
const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
|
|
6870
7138
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
6871
7139
|
} else if (this.region === "drawing") {
|
|
6872
|
-
if (Math.abs(x - this.startX) >
|
|
7140
|
+
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6873
7141
|
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6874
7142
|
} else {
|
|
6875
7143
|
const coords = this.deps.getCoords();
|
|
6876
7144
|
const vp = coords.getViewport();
|
|
6877
7145
|
const dx = x - this.startX;
|
|
6878
|
-
if (Math.abs(dx) >
|
|
7146
|
+
if (Math.abs(dx) > slop) this.moved = true;
|
|
6879
7147
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
|
|
6880
7148
|
if (this.verticalPan) {
|
|
6881
|
-
if (Math.abs(y - this.startY) >
|
|
7149
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
6882
7150
|
this.deps.pricePanBy(y - this.startY);
|
|
6883
7151
|
}
|
|
6884
7152
|
const dt = e.timeStamp - this.lastT;
|
|
@@ -6895,14 +7163,44 @@ var InputController = class {
|
|
|
6895
7163
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
6896
7164
|
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
6897
7165
|
}
|
|
6898
|
-
this.deps.onPointerMove(x, y);
|
|
7166
|
+
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
6899
7167
|
};
|
|
6900
7168
|
this.onUp = (e) => {
|
|
7169
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7170
|
+
this.cancelLongPress();
|
|
7171
|
+
if (this.dragging && this.region === "pinch") {
|
|
7172
|
+
if (this.touches.size === 1) {
|
|
7173
|
+
const rest = [...this.touches.values()][0];
|
|
7174
|
+
const vp = this.deps.getCoords().getViewport();
|
|
7175
|
+
this.region = "data";
|
|
7176
|
+
this.verticalPan = false;
|
|
7177
|
+
this.startX = rest.x;
|
|
7178
|
+
this.startY = rest.y;
|
|
7179
|
+
this.startRightOffset = vp.rightOffset;
|
|
7180
|
+
this.startBarSpacing = vp.barSpacing;
|
|
7181
|
+
this.lastX = rest.x;
|
|
7182
|
+
this.lastT = e.timeStamp;
|
|
7183
|
+
this.vx = 0;
|
|
7184
|
+
return;
|
|
7185
|
+
}
|
|
7186
|
+
this.endGesture(e);
|
|
7187
|
+
return;
|
|
7188
|
+
}
|
|
7189
|
+
if (this.dragging && this.region === "crosshair") {
|
|
7190
|
+
this.deps.onPointerMove(null, null);
|
|
7191
|
+
this.endGesture(e);
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
7195
|
+
this.endGesture(e);
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
const { x, y } = this.local(e);
|
|
7199
|
+
const wasTouch = e.pointerType === "touch";
|
|
7200
|
+
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
6901
7201
|
if (this.dragging && this.region === "drawing") {
|
|
6902
|
-
const { x, y } = this.local(e);
|
|
6903
7202
|
this.deps.drawingsPointerUp?.(x, y);
|
|
6904
|
-
} else if (
|
|
6905
|
-
const { x, y } = this.local(e);
|
|
7203
|
+
} else if (tapRelease && this.region === "data") {
|
|
6906
7204
|
this.deps.onClick(x, y);
|
|
6907
7205
|
} else if (this.dragging && this.region === "data") {
|
|
6908
7206
|
const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
|
|
@@ -6911,11 +7209,20 @@ var InputController = class {
|
|
|
6911
7209
|
this.deps.fling(-this.vx / pitch);
|
|
6912
7210
|
}
|
|
6913
7211
|
}
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
7212
|
+
const wasTap = wasTouch && tapRelease;
|
|
7213
|
+
this.endGesture(e);
|
|
7214
|
+
if (wasTouch) this.deps.onPointerMove(null, null);
|
|
7215
|
+
if (wasTap) this.registerTap(e.timeStamp, x, y);
|
|
7216
|
+
};
|
|
7217
|
+
/** The browser took the pointer back (system gesture, palm rejection, tab switch):
|
|
7218
|
+
* drop the gesture where it is — no click, no fling, no stuck `dragging`. */
|
|
7219
|
+
this.onCancel = (e) => {
|
|
7220
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7221
|
+
this.cancelLongPress();
|
|
7222
|
+
if (!this.dragging) return;
|
|
7223
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
7224
|
+
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
7225
|
+
this.endGesture(e);
|
|
6919
7226
|
};
|
|
6920
7227
|
this.onLeave = () => {
|
|
6921
7228
|
this.cursorX = NaN;
|
|
@@ -6924,12 +7231,7 @@ var InputController = class {
|
|
|
6924
7231
|
};
|
|
6925
7232
|
this.onDblClick = (e) => {
|
|
6926
7233
|
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);
|
|
7234
|
+
this.doubleActivate(x, y);
|
|
6933
7235
|
};
|
|
6934
7236
|
this.onWheel = (e) => {
|
|
6935
7237
|
e.preventDefault();
|
|
@@ -6950,10 +7252,17 @@ var InputController = class {
|
|
|
6950
7252
|
}
|
|
6951
7253
|
attach(el) {
|
|
6952
7254
|
this.el = el;
|
|
7255
|
+
if (el.style !== void 0) {
|
|
7256
|
+
el.style.touchAction = "none";
|
|
7257
|
+
el.style.userSelect = "none";
|
|
7258
|
+
el.style.setProperty("-webkit-user-select", "none");
|
|
7259
|
+
el.style.setProperty("-webkit-touch-callout", "none");
|
|
7260
|
+
}
|
|
6953
7261
|
el.addEventListener("pointerdown", this.onDown);
|
|
6954
7262
|
el.addEventListener("pointermove", this.onMove);
|
|
6955
7263
|
el.addEventListener("pointerup", this.onUp);
|
|
6956
7264
|
el.addEventListener("pointerleave", this.onLeave);
|
|
7265
|
+
el.addEventListener("pointercancel", this.onCancel);
|
|
6957
7266
|
el.addEventListener("dblclick", this.onDblClick);
|
|
6958
7267
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
6959
7268
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
@@ -6961,6 +7270,7 @@ var InputController = class {
|
|
|
6961
7270
|
const win = el.ownerDocument?.defaultView;
|
|
6962
7271
|
win?.addEventListener("keydown", this.onModifier);
|
|
6963
7272
|
win?.addEventListener("keyup", this.onModifier);
|
|
7273
|
+
win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
6964
7274
|
}
|
|
6965
7275
|
detach() {
|
|
6966
7276
|
const el = this.el;
|
|
@@ -6968,14 +7278,18 @@ var InputController = class {
|
|
|
6968
7278
|
const win = el.ownerDocument?.defaultView;
|
|
6969
7279
|
win?.removeEventListener("keydown", this.onModifier);
|
|
6970
7280
|
win?.removeEventListener("keyup", this.onModifier);
|
|
7281
|
+
win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
6971
7282
|
el.removeEventListener("pointerdown", this.onDown);
|
|
6972
7283
|
el.removeEventListener("pointermove", this.onMove);
|
|
6973
7284
|
el.removeEventListener("pointerup", this.onUp);
|
|
6974
7285
|
el.removeEventListener("pointerleave", this.onLeave);
|
|
7286
|
+
el.removeEventListener("pointercancel", this.onCancel);
|
|
6975
7287
|
el.removeEventListener("dblclick", this.onDblClick);
|
|
6976
7288
|
el.removeEventListener("wheel", this.onWheel);
|
|
6977
7289
|
el.removeEventListener("mousedown", this.onMiddleGuard);
|
|
6978
7290
|
el.removeEventListener("auxclick", this.onMiddleGuard);
|
|
7291
|
+
this.cancelLongPress();
|
|
7292
|
+
this.touches.clear();
|
|
6979
7293
|
this.el = null;
|
|
6980
7294
|
}
|
|
6981
7295
|
local(e) {
|
|
@@ -6999,6 +7313,59 @@ var InputController = class {
|
|
|
6999
7313
|
snapMode(e) {
|
|
7000
7314
|
return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
|
|
7001
7315
|
}
|
|
7316
|
+
/** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
|
|
7317
|
+
* movement past TOUCH_SLOP or a second finger cancels it. */
|
|
7318
|
+
startLongPress(x, y, region) {
|
|
7319
|
+
this.cancelLongPress();
|
|
7320
|
+
this.lpTimer = setTimeout(() => {
|
|
7321
|
+
this.lpTimer = null;
|
|
7322
|
+
if (!this.dragging || this.region !== region) return;
|
|
7323
|
+
if (region === "data") {
|
|
7324
|
+
this.region = "crosshair";
|
|
7325
|
+
this.deps.onPointerMove(x, y);
|
|
7326
|
+
return;
|
|
7327
|
+
}
|
|
7328
|
+
this.region = "axis-sheet";
|
|
7329
|
+
this.moved = true;
|
|
7330
|
+
this.deps.onAxisLongPress?.(region, x, y);
|
|
7331
|
+
}, LONG_PRESS_MS);
|
|
7332
|
+
}
|
|
7333
|
+
cancelLongPress() {
|
|
7334
|
+
if (this.lpTimer !== null) clearTimeout(this.lpTimer);
|
|
7335
|
+
this.lpTimer = null;
|
|
7336
|
+
}
|
|
7337
|
+
/** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
|
|
7338
|
+
beginPinch(e) {
|
|
7339
|
+
this.cancelLongPress();
|
|
7340
|
+
if (this.region === "crosshair") this.deps.onPointerMove(null, null);
|
|
7341
|
+
const coords = this.deps.getCoords();
|
|
7342
|
+
const vp = coords.getViewport();
|
|
7343
|
+
this.deps.apply(vp);
|
|
7344
|
+
const [a, b] = [...this.touches.values()];
|
|
7345
|
+
this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
7346
|
+
this.pinchStartBarSpacing = vp.barSpacing;
|
|
7347
|
+
this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
|
|
7348
|
+
this.region = "pinch";
|
|
7349
|
+
this.dragging = true;
|
|
7350
|
+
this.moved = true;
|
|
7351
|
+
this.vx = 0;
|
|
7352
|
+
this.capture(e.pointerId);
|
|
7353
|
+
}
|
|
7354
|
+
/** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
|
|
7355
|
+
applyPinch() {
|
|
7356
|
+
const coords = this.deps.getCoords();
|
|
7357
|
+
const pts = [...this.touches.values()];
|
|
7358
|
+
if (pts.length < 2) return;
|
|
7359
|
+
const [a, b] = pts;
|
|
7360
|
+
const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
|
|
7361
|
+
const vp = coords.getViewport();
|
|
7362
|
+
const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
|
|
7363
|
+
const midX = (a.x + b.x) / 2;
|
|
7364
|
+
this.deps.apply({
|
|
7365
|
+
barSpacing,
|
|
7366
|
+
rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
7002
7369
|
/** Capture defensively: a synthetic/already-released pointer can't be captured, and
|
|
7003
7370
|
* the move/up pair still routes through the element listeners without it. */
|
|
7004
7371
|
capture(pointerId) {
|
|
@@ -7007,6 +7374,35 @@ var InputController = class {
|
|
|
7007
7374
|
} catch {
|
|
7008
7375
|
}
|
|
7009
7376
|
}
|
|
7377
|
+
/** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
|
|
7378
|
+
* routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
|
|
7379
|
+
registerTap(t, x, y) {
|
|
7380
|
+
const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
|
|
7381
|
+
if (paired) {
|
|
7382
|
+
this.lastTapT = 0;
|
|
7383
|
+
this.doubleActivate(this.lastTapX, this.lastTapY);
|
|
7384
|
+
return;
|
|
7385
|
+
}
|
|
7386
|
+
this.lastTapT = t;
|
|
7387
|
+
this.lastTapX = x;
|
|
7388
|
+
this.lastTapY = y;
|
|
7389
|
+
}
|
|
7390
|
+
endGesture(e) {
|
|
7391
|
+
this.dragging = false;
|
|
7392
|
+
try {
|
|
7393
|
+
this.el?.releasePointerCapture(e.pointerId);
|
|
7394
|
+
} catch {
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
/** Shared double-click / double-tap routing, by the region under the point. */
|
|
7398
|
+
doubleActivate(x, y) {
|
|
7399
|
+
if (this.deps.drawingsDblClick?.(x, y)) return;
|
|
7400
|
+
const region = this.regionAt(x, y);
|
|
7401
|
+
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
7402
|
+
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
7403
|
+
else if (region === "time") this.deps.resetView();
|
|
7404
|
+
else this.deps.dataDblClick(x, y);
|
|
7405
|
+
}
|
|
7010
7406
|
};
|
|
7011
7407
|
|
|
7012
7408
|
// src/renderers/native/core/KeyboardController.ts
|
|
@@ -8820,11 +9216,19 @@ var ChromeRenderer = class {
|
|
|
8820
9216
|
const y = dataH + (fullH - dataH) / 2;
|
|
8821
9217
|
const tr = coords.visibleTimeRange();
|
|
8822
9218
|
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
9219
|
+
const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
|
|
9220
|
+
const ticks = timeTicks(tr.from, tr.to, target, offset).map((tick) => ({ ...tick, x: coords.timeToX(tick.time), half: ctx.measureText(tick.label).width / 2 })).filter((tick) => tick.x >= 20 && tick.x <= dataW - 20);
|
|
9221
|
+
const GAP = 12;
|
|
9222
|
+
const placed = [];
|
|
9223
|
+
const put = (tick) => {
|
|
9224
|
+
const l = tick.x - tick.half;
|
|
9225
|
+
const r = tick.x + tick.half;
|
|
9226
|
+
if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
|
|
9227
|
+
placed.push({ l, r });
|
|
9228
|
+
ctx.fillText(tick.label, tick.x, y);
|
|
9229
|
+
};
|
|
9230
|
+
for (const tick of ticks) if (tick.major) put(tick);
|
|
9231
|
+
for (const tick of ticks) if (!tick.major) put(tick);
|
|
8828
9232
|
ctx.textAlign = "start";
|
|
8829
9233
|
}
|
|
8830
9234
|
};
|
|
@@ -8926,10 +9330,10 @@ var CrosshairRenderer = class {
|
|
|
8926
9330
|
}
|
|
8927
9331
|
/** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
|
|
8928
9332
|
* 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
|
-
*
|
|
9333
|
+
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
9334
|
+
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
9335
|
+
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
9336
|
+
* render slightly dimmed so the ghost still reads as foreign. */
|
|
8933
9337
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
8934
9338
|
const cs = scene.style.crosshair;
|
|
8935
9339
|
const dataW = coords.width;
|
|
@@ -8953,7 +9357,20 @@ var CrosshairRenderer = class {
|
|
|
8953
9357
|
ctx.setLineDash([]);
|
|
8954
9358
|
ctx.lineWidth = 1;
|
|
8955
9359
|
ctx.globalAlpha = 0.8;
|
|
8956
|
-
|
|
9360
|
+
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
9361
|
+
if (ext.y != null && ext.price != null) {
|
|
9362
|
+
let pane;
|
|
9363
|
+
for (const p of scene.panes.values()) {
|
|
9364
|
+
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
9365
|
+
pane = p;
|
|
9366
|
+
break;
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
if (pane) {
|
|
9370
|
+
this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
|
|
8957
9374
|
ctx.globalAlpha = 1;
|
|
8958
9375
|
}
|
|
8959
9376
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -9224,6 +9641,7 @@ function colorField(theme, getVal, onVal) {
|
|
|
9224
9641
|
swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
|
|
9225
9642
|
};
|
|
9226
9643
|
paint();
|
|
9644
|
+
trigger.addEventListener("vela-sync", paint);
|
|
9227
9645
|
trigger.addEventListener("click", (e) => {
|
|
9228
9646
|
e.stopPropagation();
|
|
9229
9647
|
if (openPopover && openPopover.trigger === trigger) {
|
|
@@ -9259,6 +9677,101 @@ function closeColorPopover() {
|
|
|
9259
9677
|
closeOpen();
|
|
9260
9678
|
}
|
|
9261
9679
|
|
|
9680
|
+
// src/renderers/native/chrome/WidthField.ts
|
|
9681
|
+
var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
|
|
9682
|
+
var STYLE_ID4 = "vela-width-field";
|
|
9683
|
+
function ensureStyles4() {
|
|
9684
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
|
|
9685
|
+
const st = document.createElement("style");
|
|
9686
|
+
st.id = STYLE_ID4;
|
|
9687
|
+
st.textContent = `
|
|
9688
|
+
.vela-width-field{height:28px;padding:0 8px;border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);background:var(--vela-surface-elev);cursor:pointer;display:inline-flex;align-items:center;gap:8px;flex:none;color:var(--vela-fg);font-family:inherit;outline:none;}
|
|
9689
|
+
.vela-width-field:hover{border-color:var(--vela-fg-muted);}
|
|
9690
|
+
.vela-width-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
|
|
9691
|
+
.vela-width-field-item{display:flex;align-items:center;gap:8px;min-width:96px;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit;}
|
|
9692
|
+
.vela-width-field-item:hover{background:var(--vela-hover-strong);}
|
|
9693
|
+
.vela-width-field-item[data-active='1']{background:var(--vela-active);}
|
|
9694
|
+
`;
|
|
9695
|
+
document.head.appendChild(st);
|
|
9696
|
+
}
|
|
9697
|
+
function lineGlyph(width) {
|
|
9698
|
+
return `<svg width="22" height="14" viewBox="0 0 22 14" fill="none"><line x1="2" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg>`;
|
|
9699
|
+
}
|
|
9700
|
+
var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="#868a96" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
9701
|
+
var openPopover2 = null;
|
|
9702
|
+
function closeOpen2() {
|
|
9703
|
+
if (!openPopover2) return;
|
|
9704
|
+
document.removeEventListener("pointerdown", openPopover2.onOutside, true);
|
|
9705
|
+
window.removeEventListener("resize", openPopover2.reflow, true);
|
|
9706
|
+
openPopover2.el.remove();
|
|
9707
|
+
openPopover2 = null;
|
|
9708
|
+
}
|
|
9709
|
+
function place2(pop, trigger) {
|
|
9710
|
+
const r = trigger.getBoundingClientRect();
|
|
9711
|
+
const w = pop.offsetWidth;
|
|
9712
|
+
const h = pop.offsetHeight;
|
|
9713
|
+
let left = r.right - w;
|
|
9714
|
+
let top = r.bottom + 6;
|
|
9715
|
+
if (left < 6) left = 6;
|
|
9716
|
+
if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
|
|
9717
|
+
if (top + h > window.innerHeight - 6) top = r.top - h - 6;
|
|
9718
|
+
if (top < 6) top = 6;
|
|
9719
|
+
pop.style.left = `${Math.round(left)}px`;
|
|
9720
|
+
pop.style.top = `${Math.round(top)}px`;
|
|
9721
|
+
}
|
|
9722
|
+
function widthField(theme, getVal, onVal) {
|
|
9723
|
+
ensureStyles4();
|
|
9724
|
+
const trigger = document.createElement("button");
|
|
9725
|
+
trigger.type = "button";
|
|
9726
|
+
trigger.className = "vela-width-field";
|
|
9727
|
+
const paint = () => {
|
|
9728
|
+
trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
|
|
9729
|
+
};
|
|
9730
|
+
paint();
|
|
9731
|
+
trigger.addEventListener("click", (e) => {
|
|
9732
|
+
e.stopPropagation();
|
|
9733
|
+
if (openPopover2 && openPopover2.trigger === trigger) {
|
|
9734
|
+
closeOpen2();
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
closeOpen2();
|
|
9738
|
+
const pop = document.createElement("div");
|
|
9739
|
+
pop.className = "vela-width-field-pop";
|
|
9740
|
+
applyChromeTokens(pop, theme);
|
|
9741
|
+
pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
|
|
9742
|
+
pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
|
|
9743
|
+
for (const w of WIDTH_FIELD_OPTIONS) {
|
|
9744
|
+
const item = document.createElement("button");
|
|
9745
|
+
item.type = "button";
|
|
9746
|
+
item.className = "vela-width-field-item";
|
|
9747
|
+
item.dataset.active = w === getVal() ? "1" : "0";
|
|
9748
|
+
item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
|
|
9749
|
+
item.addEventListener("click", (ev) => {
|
|
9750
|
+
ev.stopPropagation();
|
|
9751
|
+
closeOpen2();
|
|
9752
|
+
onVal(w);
|
|
9753
|
+
paint();
|
|
9754
|
+
});
|
|
9755
|
+
pop.appendChild(item);
|
|
9756
|
+
}
|
|
9757
|
+
document.body.appendChild(pop);
|
|
9758
|
+
place2(pop, trigger);
|
|
9759
|
+
const onOutside = (ev) => {
|
|
9760
|
+
const t = ev.target;
|
|
9761
|
+
if (pop.contains(t) || trigger.contains(t)) return;
|
|
9762
|
+
closeOpen2();
|
|
9763
|
+
};
|
|
9764
|
+
const reflow = () => place2(pop, trigger);
|
|
9765
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
|
|
9766
|
+
window.addEventListener("resize", reflow, true);
|
|
9767
|
+
openPopover2 = { el: pop, trigger, onOutside, reflow };
|
|
9768
|
+
});
|
|
9769
|
+
return trigger;
|
|
9770
|
+
}
|
|
9771
|
+
function closeWidthPopover() {
|
|
9772
|
+
closeOpen2();
|
|
9773
|
+
}
|
|
9774
|
+
|
|
9262
9775
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
9263
9776
|
var BUILTIN_STYLE_LABELS = {
|
|
9264
9777
|
candles: "Candles",
|
|
@@ -9341,6 +9854,30 @@ function ensureControlStyles() {
|
|
|
9341
9854
|
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
9342
9855
|
!important beats the inline opacity on labels. */
|
|
9343
9856
|
.vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
|
|
9857
|
+
/* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
|
|
9858
|
+
The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
|
|
9859
|
+
row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
|
|
9860
|
+
controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
|
|
9861
|
+
.vela-sd-mobile .vela-sd-burger{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg);line-height:0;width:36px;height:36px;border-radius:var(--vela-radius-sm);margin-right:2px;}
|
|
9862
|
+
.vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
|
|
9863
|
+
.vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
|
|
9864
|
+
.vela-sd-mobile .vela-sd-railscrim.open{display:block;}
|
|
9865
|
+
.vela-sd-mobile .vela-sd-rail{position:absolute;left:0;top:0;bottom:0;z-index:3;width:min(240px,80%);box-sizing:border-box;background:var(--vela-surface);border-right:1px solid var(--vela-border);box-shadow:var(--vela-shadow-dialog);transform:translateX(-105%);transition:transform var(--vela-dur-med) var(--vela-ease);}
|
|
9866
|
+
.vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
|
|
9867
|
+
.vela-sd-mobile .vela-sd-tab{padding:12px;}
|
|
9868
|
+
.vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
|
|
9869
|
+
.vela-sd-mobile .vela-sd-toc{position:sticky;top:0;z-index:1;background:var(--vela-surface);flex-direction:row;overflow-x:auto;scrollbar-width:none;min-width:0;width:100%;box-sizing:border-box;gap:4px;padding:2px 0 8px;}
|
|
9870
|
+
.vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
|
|
9871
|
+
.vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
|
|
9872
|
+
.vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
|
|
9873
|
+
.vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
|
|
9874
|
+
.vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
|
|
9875
|
+
.vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
|
|
9876
|
+
.vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
|
|
9877
|
+
.vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
|
|
9878
|
+
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
9879
|
+
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
9880
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
|
|
9344
9881
|
`;
|
|
9345
9882
|
document.head.appendChild(st);
|
|
9346
9883
|
}
|
|
@@ -9367,6 +9904,8 @@ var SettingsDialog = class {
|
|
|
9367
9904
|
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
9368
9905
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
9369
9906
|
this.activeSection = null;
|
|
9907
|
+
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
9908
|
+
this.mobileLayout = false;
|
|
9370
9909
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
9371
9910
|
}
|
|
9372
9911
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
@@ -9385,15 +9924,27 @@ var SettingsDialog = class {
|
|
|
9385
9924
|
}
|
|
9386
9925
|
setTheme(theme) {
|
|
9387
9926
|
this.theme = theme;
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9927
|
+
this.reopenIfLive();
|
|
9928
|
+
}
|
|
9929
|
+
/** The host shell's chrome size class — mobile presents the dialog fullscreen with
|
|
9930
|
+
* the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
|
|
9931
|
+
* re-presents in place, on the same tab. */
|
|
9932
|
+
setLayoutMode(mode) {
|
|
9933
|
+
const mobile = mode === "mobile";
|
|
9934
|
+
if (mobile === this.mobileLayout) return;
|
|
9935
|
+
this.mobileLayout = mobile;
|
|
9936
|
+
this.reopenIfLive();
|
|
9937
|
+
}
|
|
9938
|
+
/** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
|
|
9939
|
+
reopenIfLive() {
|
|
9940
|
+
if (!this.root) return;
|
|
9941
|
+
const cfg = this.config;
|
|
9942
|
+
const oc = this.onChange;
|
|
9943
|
+
const oi = this.onImport;
|
|
9944
|
+
const orst = this.onReset;
|
|
9945
|
+
const section = this.activeSection;
|
|
9946
|
+
this.close();
|
|
9947
|
+
if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
|
|
9397
9948
|
}
|
|
9398
9949
|
isOpen() {
|
|
9399
9950
|
return this.root !== null;
|
|
@@ -9415,18 +9966,37 @@ var SettingsDialog = class {
|
|
|
9415
9966
|
this.onImport = onImport ?? null;
|
|
9416
9967
|
this.onReset = onReset ?? null;
|
|
9417
9968
|
ensureControlStyles();
|
|
9969
|
+
const mobile = this.mobileLayout;
|
|
9418
9970
|
const scrim = document.createElement("div");
|
|
9419
|
-
scrim.style.cssText = "position:absolute;inset:0;z-index:
|
|
9971
|
+
scrim.style.cssText = "position:absolute;inset:0;z-index:var(--vela-z-dialog);display:flex;align-items:flex-start;justify-content:center;background:transparent;padding-top:8vh;pointer-events:auto;";
|
|
9972
|
+
if (mobile) {
|
|
9973
|
+
scrim.classList.add("vela-sd-mobile");
|
|
9974
|
+
scrim.style.paddingTop = "0";
|
|
9975
|
+
scrim.style.alignItems = "stretch";
|
|
9976
|
+
}
|
|
9420
9977
|
scrim.addEventListener("mousedown", (e) => {
|
|
9421
9978
|
if (e.target === scrim) this.close();
|
|
9422
9979
|
});
|
|
9423
9980
|
const dlg = document.createElement("div");
|
|
9424
9981
|
dlg.style.cssText = `width:fit-content;min-width:min(560px,94vw);max-width:min(720px,94vw);max-height:70vh;display:flex;flex-direction:column;background:${SETTINGS_SURFACE};border:1px solid ${SETTINGS_BORDER};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${SETTINGS_TEXT};font:13px var(--vela-font);overflow:hidden;cursor:default;`;
|
|
9982
|
+
if (mobile) dlg.style.cssText += "width:100%;min-width:0;max-width:none;max-height:none;flex:1 1 auto;border:none;border-radius:0;";
|
|
9425
9983
|
const header = document.createElement("div");
|
|
9426
9984
|
header.style.cssText = `display:flex;justify-content:space-between;align-items:center;padding:9px 9px 9px 16px;border-bottom:1px solid ${SETTINGS_BORDER};flex:0 0 auto;user-select:none;`;
|
|
9985
|
+
if (mobile) header.style.paddingLeft = "8px";
|
|
9427
9986
|
const hTitle = document.createElement("span");
|
|
9428
9987
|
hTitle.textContent = "Chart settings";
|
|
9429
9988
|
hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
|
|
9989
|
+
if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
9990
|
+
let toggleRail = null;
|
|
9991
|
+
if (mobile) {
|
|
9992
|
+
const burger = document.createElement("button");
|
|
9993
|
+
burger.type = "button";
|
|
9994
|
+
burger.className = "vela-sd-burger";
|
|
9995
|
+
burger.innerHTML = iconAt("burger", 16);
|
|
9996
|
+
burger.title = "Sections";
|
|
9997
|
+
burger.addEventListener("click", () => toggleRail?.());
|
|
9998
|
+
header.append(burger);
|
|
9999
|
+
}
|
|
9430
10000
|
const closeBtn = document.createElement("button");
|
|
9431
10001
|
closeBtn.type = "button";
|
|
9432
10002
|
closeBtn.innerHTML = iconAt("close", 15);
|
|
@@ -9434,8 +10004,8 @@ var SettingsDialog = class {
|
|
|
9434
10004
|
closeBtn.className = "vela-sd-close";
|
|
9435
10005
|
closeBtn.addEventListener("click", () => this.close());
|
|
9436
10006
|
header.append(hTitle, closeBtn);
|
|
9437
|
-
|
|
9438
|
-
|
|
10007
|
+
if (!mobile) {
|
|
10008
|
+
header.style.cursor = "move";
|
|
9439
10009
|
let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
|
|
9440
10010
|
header.addEventListener("pointerdown", (e) => {
|
|
9441
10011
|
if (e.target?.closest(".vela-sd-close")) return;
|
|
@@ -9616,12 +10186,24 @@ var SettingsDialog = class {
|
|
|
9616
10186
|
renderChartTypeSections("end");
|
|
9617
10187
|
renderHostSections("end");
|
|
9618
10188
|
const shell = document.createElement("div");
|
|
9619
|
-
shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
10189
|
+
shell.style.cssText = mobile ? "display:flex;min-height:0;flex:1 1 auto;position:relative;overflow:hidden;" : "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
9620
10190
|
const rail = document.createElement("div");
|
|
9621
|
-
rail.
|
|
10191
|
+
rail.className = "vela-sd-rail";
|
|
10192
|
+
rail.style.cssText = mobile ? "display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;" : `flex:0 0 170px;display:flex;flex-direction:column;gap:2px;padding:10px 8px;border-right:1px solid ${SETTINGS_BORDER};overflow-y:auto;`;
|
|
10193
|
+
let railScrim = null;
|
|
10194
|
+
if (mobile) {
|
|
10195
|
+
railScrim = document.createElement("div");
|
|
10196
|
+
railScrim.className = "vela-sd-railscrim";
|
|
10197
|
+
railScrim.addEventListener("click", () => toggleRail?.());
|
|
10198
|
+
toggleRail = (open) => {
|
|
10199
|
+
const on = open ?? !rail.classList.contains("open");
|
|
10200
|
+
rail.classList.toggle("open", on);
|
|
10201
|
+
railScrim?.classList.toggle("open", on);
|
|
10202
|
+
};
|
|
10203
|
+
}
|
|
9622
10204
|
const paneHost = document.createElement("div");
|
|
9623
10205
|
paneHost.className = "vela-sd-pane";
|
|
9624
|
-
paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
|
|
10206
|
+
paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;padding:6px 18px 14px;";
|
|
9625
10207
|
const panes = [];
|
|
9626
10208
|
let current = null;
|
|
9627
10209
|
for (const child of [...body.children]) {
|
|
@@ -9656,6 +10238,10 @@ var SettingsDialog = class {
|
|
|
9656
10238
|
p.tab.classList.toggle("on", i === idx);
|
|
9657
10239
|
});
|
|
9658
10240
|
this.activeSection = panes[idx]?.title ?? null;
|
|
10241
|
+
if (mobile) {
|
|
10242
|
+
hTitle.textContent = panes[idx]?.title ?? "Chart settings";
|
|
10243
|
+
toggleRail?.(false);
|
|
10244
|
+
}
|
|
9659
10245
|
};
|
|
9660
10246
|
panes.forEach((p, i) => {
|
|
9661
10247
|
p.tab.addEventListener("click", () => activate(i));
|
|
@@ -9663,10 +10249,11 @@ var SettingsDialog = class {
|
|
|
9663
10249
|
paneHost.appendChild(p.el);
|
|
9664
10250
|
});
|
|
9665
10251
|
this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
|
|
9666
|
-
const wanted = section
|
|
10252
|
+
const wanted = section !== void 0 ? panes.findIndex((p) => p.title.toLowerCase() === section.toLowerCase()) : panes.findIndex((p) => p.style === config.series.style && !p.tab.classList.contains("vela-sd-tab-sub"));
|
|
9667
10253
|
activate(wanted >= 0 ? wanted : 0);
|
|
9668
10254
|
this.syncTypeTabs?.(config.series.style);
|
|
9669
10255
|
shell.append(rail, paneHost);
|
|
10256
|
+
if (railScrim) shell.append(railScrim);
|
|
9670
10257
|
dlg.appendChild(shell);
|
|
9671
10258
|
dlg.appendChild(this.footer(config));
|
|
9672
10259
|
scrim.appendChild(dlg);
|
|
@@ -9683,6 +10270,7 @@ var SettingsDialog = class {
|
|
|
9683
10270
|
}
|
|
9684
10271
|
close() {
|
|
9685
10272
|
closeColorPopover();
|
|
10273
|
+
closeWidthPopover();
|
|
9686
10274
|
this.root?.remove();
|
|
9687
10275
|
this.root = null;
|
|
9688
10276
|
this.tabs = [];
|
|
@@ -9723,20 +10311,12 @@ var SettingsDialog = class {
|
|
|
9723
10311
|
body.append(marker);
|
|
9724
10312
|
const values = config.chartTypes[typeId] ?? {};
|
|
9725
10313
|
const bag = {};
|
|
9726
|
-
const seedKey = (key, want, defval) => {
|
|
9727
|
-
const v = values[key];
|
|
9728
|
-
bag[key] = typeof v === want ? v : defval;
|
|
9729
|
-
};
|
|
9730
10314
|
const seed = (rows) => {
|
|
9731
10315
|
for (const r of rows) {
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
seedKey(r.maxKey, "number", r.defval);
|
|
9736
|
-
continue;
|
|
10316
|
+
for (const k of settingsRowValueKeys(r)) {
|
|
10317
|
+
const v = values[k.key];
|
|
10318
|
+
bag[k.key] = typeof v === k.type ? v : k.defval;
|
|
9737
10319
|
}
|
|
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
10320
|
}
|
|
9741
10321
|
};
|
|
9742
10322
|
if (section.rows) seed(section.rows);
|
|
@@ -9759,7 +10339,8 @@ var SettingsDialog = class {
|
|
|
9759
10339
|
if (section.instances && section.instances.length > 0) {
|
|
9760
10340
|
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
9761
10341
|
} else if (section.rows) {
|
|
9762
|
-
this.
|
|
10342
|
+
if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
|
|
10343
|
+
else this.flatTypeRows(section.rows, bag, put, refreshers, body);
|
|
9763
10344
|
}
|
|
9764
10345
|
for (const sub of section.subsections ?? []) {
|
|
9765
10346
|
const subMarker = this.section(sub.title);
|
|
@@ -9776,7 +10357,7 @@ var SettingsDialog = class {
|
|
|
9776
10357
|
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
9777
10358
|
const entries = [];
|
|
9778
10359
|
for (const r of rows) {
|
|
9779
|
-
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
|
|
10360
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
|
|
9780
10361
|
entries.push({ el, when: r.when });
|
|
9781
10362
|
body.append(el);
|
|
9782
10363
|
}
|
|
@@ -9784,60 +10365,109 @@ var SettingsDialog = class {
|
|
|
9784
10365
|
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
9785
10366
|
});
|
|
9786
10367
|
}
|
|
9787
|
-
/**
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
10368
|
+
/**
|
|
10369
|
+
* One value row for a chart-type descriptor, writing through `put`. EVERY kind is
|
|
10370
|
+
* first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
|
|
10371
|
+
* rendered by the ONE path: optional leading toggle, then the ordered inline
|
|
10372
|
+
* controls in the control column. A control carrying its own `when` registers a
|
|
10373
|
+
* visibility refresher — it appears and disappears live as the bag changes,
|
|
10374
|
+
* independent of the row's gate — and is exempt from the toggle-off dim (it may
|
|
10375
|
+
* exist FOR the off state).
|
|
10376
|
+
*/
|
|
10377
|
+
typeRow(r, bag, put, refreshers) {
|
|
10378
|
+
const n = normalizeSettingsRow(r);
|
|
10379
|
+
const controls = [];
|
|
10380
|
+
for (const c of n.controls) {
|
|
10381
|
+
const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
|
|
10382
|
+
if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
10383
|
+
if (c.kind !== "hint" && c.when) {
|
|
10384
|
+
el.dataset.sdSelfGated = "1";
|
|
10385
|
+
const when = c.when;
|
|
10386
|
+
refreshers.push(() => {
|
|
10387
|
+
el.style.display = settingsRowVisible(when, bag) ? "" : "none";
|
|
10388
|
+
});
|
|
10389
|
+
}
|
|
10390
|
+
controls.push(el);
|
|
9796
10391
|
}
|
|
9797
|
-
if (
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
10392
|
+
if (n.toggle) {
|
|
10393
|
+
const t = n.toggle;
|
|
10394
|
+
const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
|
|
10395
|
+
refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
10396
|
+
return el;
|
|
10397
|
+
}
|
|
10398
|
+
return this.rowWith(n.label, controls);
|
|
9801
10399
|
}
|
|
9802
10400
|
/**
|
|
9803
|
-
*
|
|
9804
|
-
* `
|
|
9805
|
-
*
|
|
9806
|
-
* placeholder names the unset state ('Off' for 0-disables bounds).
|
|
10401
|
+
* Build ONE inline control from its descriptor — the factory behind the composite
|
|
10402
|
+
* row path. `compact` narrows number inputs on toggle rows (the historical inline
|
|
10403
|
+
* width) while standalone rows keep the full-width input.
|
|
9807
10404
|
*/
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
10405
|
+
inlineControl(c, bag, put, compact) {
|
|
10406
|
+
if (c.kind === "hint") {
|
|
10407
|
+
const s = document.createElement("span");
|
|
10408
|
+
s.textContent = c.text;
|
|
10409
|
+
s.style.cssText = "color:var(--vela-fg-muted);";
|
|
10410
|
+
return s;
|
|
10411
|
+
}
|
|
10412
|
+
if (c.kind === "color") {
|
|
10413
|
+
const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
|
|
10414
|
+
sw.title = c.label;
|
|
10415
|
+
return sw;
|
|
10416
|
+
}
|
|
10417
|
+
if (c.kind === "width") {
|
|
10418
|
+
let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
10419
|
+
const wf = widthField(this.theme, () => cur, (v) => {
|
|
10420
|
+
cur = v;
|
|
10421
|
+
put(c.key, v);
|
|
10422
|
+
});
|
|
10423
|
+
wf.title = c.label;
|
|
10424
|
+
return wf;
|
|
10425
|
+
}
|
|
10426
|
+
if (c.kind === "select") {
|
|
10427
|
+
const sel = document.createElement("select");
|
|
10428
|
+
sel.className = "vela-sd-select";
|
|
10429
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
10430
|
+
sel.title = c.label;
|
|
10431
|
+
const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
10432
|
+
for (const [val, lbl] of normalizeSelectOptions(c.options)) {
|
|
10433
|
+
const o = document.createElement("option");
|
|
10434
|
+
o.value = val;
|
|
10435
|
+
o.textContent = lbl;
|
|
10436
|
+
if (val === current2) o.selected = true;
|
|
10437
|
+
sel.appendChild(o);
|
|
9824
10438
|
}
|
|
10439
|
+
sel.addEventListener("change", () => put(c.key, sel.value));
|
|
10440
|
+
sel.addEventListener("vela-sync", () => {
|
|
10441
|
+
sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
10442
|
+
});
|
|
10443
|
+
return sel;
|
|
10444
|
+
}
|
|
10445
|
+
const ni = document.createElement("input");
|
|
10446
|
+
ni.type = "number";
|
|
10447
|
+
ni.className = "vela-sd-number";
|
|
10448
|
+
if (compact) ni.style.width = "56px";
|
|
10449
|
+
if (c.min !== void 0) ni.min = String(c.min);
|
|
10450
|
+
if (c.max !== void 0) ni.max = String(c.max);
|
|
10451
|
+
ni.step = String(c.step ?? 1);
|
|
10452
|
+
ni.title = c.label;
|
|
10453
|
+
const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
10454
|
+
if (c.placeholder !== void 0) {
|
|
10455
|
+
ni.placeholder = c.placeholder;
|
|
10456
|
+
if (current !== c.defval) ni.value = String(current);
|
|
9825
10457
|
ni.addEventListener("change", () => {
|
|
9826
|
-
const raw = ni.value.trim() === "" ?
|
|
9827
|
-
const v = Number.isFinite(raw) ? Math.min(
|
|
9828
|
-
ni.value =
|
|
9829
|
-
put(key, v);
|
|
10458
|
+
const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
|
|
10459
|
+
const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
|
|
10460
|
+
ni.value = v === c.defval ? "" : String(v);
|
|
10461
|
+
put(c.key, v);
|
|
9830
10462
|
});
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
wrap.appendChild(box);
|
|
9840
|
-
return wrap;
|
|
10463
|
+
} else {
|
|
10464
|
+
ni.value = String(current);
|
|
10465
|
+
ni.addEventListener("input", () => {
|
|
10466
|
+
const v = Number(ni.value);
|
|
10467
|
+
if (Number.isFinite(v)) put(c.key, v);
|
|
10468
|
+
});
|
|
10469
|
+
}
|
|
10470
|
+
return ni;
|
|
9841
10471
|
}
|
|
9842
10472
|
/**
|
|
9843
10473
|
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
@@ -9850,7 +10480,7 @@ var SettingsDialog = class {
|
|
|
9850
10480
|
const wrap = document.createElement("div");
|
|
9851
10481
|
const strip = document.createElement("div");
|
|
9852
10482
|
strip.className = "vela-sd-itabs";
|
|
9853
|
-
wrap.append(strip);
|
|
10483
|
+
if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
|
|
9854
10484
|
const contents = instances.map((inst, i) => {
|
|
9855
10485
|
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
9856
10486
|
wrap.append(content);
|
|
@@ -9945,8 +10575,8 @@ var SettingsDialog = class {
|
|
|
9945
10575
|
rowsHost.append(entries[entries.length - 1].el);
|
|
9946
10576
|
continue;
|
|
9947
10577
|
}
|
|
9948
|
-
const el = this.typeRow(r, bag, put);
|
|
9949
|
-
const key = r.kind === "range" ? void 0 : r.key;
|
|
10578
|
+
const el = this.typeRow(r, bag, put, refreshers);
|
|
10579
|
+
const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
|
|
9950
10580
|
entries.push({ el, when: r.when, group: g, key });
|
|
9951
10581
|
rowsHost.append(el);
|
|
9952
10582
|
}
|
|
@@ -10012,6 +10642,14 @@ var SettingsDialog = class {
|
|
|
10012
10642
|
*/
|
|
10013
10643
|
layoutSettingsGrids(pane, measureHost) {
|
|
10014
10644
|
if (pane.childElementCount === 0) return;
|
|
10645
|
+
if (this.mobileLayout) {
|
|
10646
|
+
const grid2 = document.createElement("div");
|
|
10647
|
+
grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
|
|
10648
|
+
while (pane.firstChild) grid2.appendChild(pane.firstChild);
|
|
10649
|
+
pane.appendChild(grid2);
|
|
10650
|
+
this.prepareGridItems(grid2);
|
|
10651
|
+
return;
|
|
10652
|
+
}
|
|
10015
10653
|
const rows = [...pane.querySelectorAll(".vela-sd-row")];
|
|
10016
10654
|
const probe2 = document.createElement("div");
|
|
10017
10655
|
probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
|
|
@@ -10048,9 +10686,11 @@ var SettingsDialog = class {
|
|
|
10048
10686
|
}
|
|
10049
10687
|
}
|
|
10050
10688
|
/** A bare color swatch input (for toggle-row right groups / swatch pairs). */
|
|
10051
|
-
|
|
10689
|
+
/** With `get`, the field reads its value live (and repaints on 'vela-sync') —
|
|
10690
|
+
* duplicate-keyed rows stay honest when another row edits the shared key. */
|
|
10691
|
+
swatch(value, onChange, get) {
|
|
10052
10692
|
let current = value;
|
|
10053
|
-
return colorField(this.theme, () => current, (v) => {
|
|
10693
|
+
return colorField(this.theme, () => get ? get() : current, (v) => {
|
|
10054
10694
|
current = v;
|
|
10055
10695
|
onChange(v);
|
|
10056
10696
|
});
|
|
@@ -10102,8 +10742,11 @@ var SettingsDialog = class {
|
|
|
10102
10742
|
}
|
|
10103
10743
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
10104
10744
|
* 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
|
-
|
|
10745
|
+
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
10746
|
+
* With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
|
|
10747
|
+
* keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
|
|
10748
|
+
* only one visible at a time; see `typeRow`). */
|
|
10749
|
+
toggleRow(label, value, onToggle, controls, get) {
|
|
10107
10750
|
const wrap = document.createElement("div");
|
|
10108
10751
|
const cb = document.createElement("button");
|
|
10109
10752
|
cb.type = "button";
|
|
@@ -10123,6 +10766,12 @@ var SettingsDialog = class {
|
|
|
10123
10766
|
wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
|
|
10124
10767
|
wrap.append(cb, lbl);
|
|
10125
10768
|
cb.addEventListener("click", () => onToggle(cbToggle()));
|
|
10769
|
+
if (get) {
|
|
10770
|
+
wrap.addEventListener("vela-sync", () => {
|
|
10771
|
+
checked = get();
|
|
10772
|
+
cb.classList.toggle("on", checked);
|
|
10773
|
+
});
|
|
10774
|
+
}
|
|
10126
10775
|
return wrap;
|
|
10127
10776
|
}
|
|
10128
10777
|
wrap.className = "vela-sd-row";
|
|
@@ -10133,8 +10782,11 @@ var SettingsDialog = class {
|
|
|
10133
10782
|
box.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
10134
10783
|
for (const c of controls) box.appendChild(c);
|
|
10135
10784
|
const syncDim = (on) => {
|
|
10136
|
-
|
|
10137
|
-
|
|
10785
|
+
for (const c of controls) {
|
|
10786
|
+
if (c.dataset.sdSelfGated === "1") continue;
|
|
10787
|
+
c.style.opacity = on ? "1" : "0.4";
|
|
10788
|
+
c.style.pointerEvents = on ? "" : "none";
|
|
10789
|
+
}
|
|
10138
10790
|
};
|
|
10139
10791
|
syncDim(value);
|
|
10140
10792
|
cb.addEventListener("click", () => {
|
|
@@ -10142,6 +10794,13 @@ var SettingsDialog = class {
|
|
|
10142
10794
|
onToggle(v);
|
|
10143
10795
|
syncDim(v);
|
|
10144
10796
|
});
|
|
10797
|
+
if (get) {
|
|
10798
|
+
wrap.addEventListener("vela-sync", () => {
|
|
10799
|
+
checked = get();
|
|
10800
|
+
cb.classList.toggle("on", checked);
|
|
10801
|
+
syncDim(checked);
|
|
10802
|
+
});
|
|
10803
|
+
}
|
|
10145
10804
|
wrap.append(left, box);
|
|
10146
10805
|
return wrap;
|
|
10147
10806
|
}
|
|
@@ -10161,96 +10820,6 @@ var SettingsDialog = class {
|
|
|
10161
10820
|
wrap.appendChild(ni);
|
|
10162
10821
|
return wrap;
|
|
10163
10822
|
}
|
|
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
10823
|
/** A dropdown whose option values differ from their display labels. */
|
|
10255
10824
|
selectRowLabeled(label, value, options, onChange) {
|
|
10256
10825
|
const { wrap } = this.row(label);
|
|
@@ -10296,19 +10865,7 @@ var SettingsDialog = class {
|
|
|
10296
10865
|
foot.appendChild(resetBtn);
|
|
10297
10866
|
return foot;
|
|
10298
10867
|
}
|
|
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
10868
|
};
|
|
10311
|
-
var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
|
|
10312
10869
|
function normalizeSelectOptions(options) {
|
|
10313
10870
|
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
10314
10871
|
}
|
|
@@ -10511,7 +11068,7 @@ var DrawingPainter = class {
|
|
|
10511
11068
|
return;
|
|
10512
11069
|
}
|
|
10513
11070
|
if (d instanceof FixedRangeVolumeProfile) {
|
|
10514
|
-
this.paintFixedRangeVp(ctx, d, proj);
|
|
11071
|
+
this.paintFixedRangeVp(ctx, d, proj, theme);
|
|
10515
11072
|
this.paintLabel(ctx, d, proj, theme);
|
|
10516
11073
|
return;
|
|
10517
11074
|
}
|
|
@@ -11004,7 +11561,7 @@ var DrawingPainter = class {
|
|
|
11004
11561
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
11005
11562
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
11006
11563
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
11007
|
-
paintFixedRangeVp(ctx, d, proj) {
|
|
11564
|
+
paintFixedRangeVp(ctx, d, proj, theme) {
|
|
11008
11565
|
const L = d.layout(proj);
|
|
11009
11566
|
if (!L) return;
|
|
11010
11567
|
const s = d.frvp;
|
|
@@ -11043,7 +11600,7 @@ var DrawingPainter = class {
|
|
|
11043
11600
|
};
|
|
11044
11601
|
hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
|
|
11045
11602
|
hLine(s.showVal, s.valColor, s.valStyle, L.valY);
|
|
11046
|
-
hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
|
|
11603
|
+
hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
|
|
11047
11604
|
const devW = Math.max(1, w);
|
|
11048
11605
|
if (s.showDevelopingPoc) {
|
|
11049
11606
|
this.strokePolyline(ctx, L.developingPoc, {
|
|
@@ -11915,7 +12472,11 @@ var DrawingInteraction = class {
|
|
|
11915
12472
|
const last = draft.anchors[draft.anchors.length - 1];
|
|
11916
12473
|
const lx = last ? proj.xOf(last.time) : NaN;
|
|
11917
12474
|
const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
|
|
11918
|
-
if (
|
|
12475
|
+
if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
|
|
12476
|
+
if (draft.anchors.length >= this.state.need) {
|
|
12477
|
+
const lastIdx = draft.anchors.length - 1;
|
|
12478
|
+
draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
|
|
12479
|
+
}
|
|
11919
12480
|
draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
|
|
11920
12481
|
}
|
|
11921
12482
|
this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
|
|
@@ -11988,11 +12549,11 @@ function applyFree(anchor, pt, free) {
|
|
|
11988
12549
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
11989
12550
|
var BTN = 30;
|
|
11990
12551
|
var ICON = 21;
|
|
11991
|
-
var
|
|
11992
|
-
function
|
|
11993
|
-
if (typeof document === "undefined" || document.getElementById(
|
|
12552
|
+
var STYLE_ID5 = "vela-drawing-popup-styles";
|
|
12553
|
+
function ensureStyles5() {
|
|
12554
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID5)) return;
|
|
11994
12555
|
const s = document.createElement("style");
|
|
11995
|
-
s.id =
|
|
12556
|
+
s.id = STYLE_ID5;
|
|
11996
12557
|
s.textContent = `
|
|
11997
12558
|
.vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
|
|
11998
12559
|
.vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -12011,7 +12572,10 @@ function ensureStyles4() {
|
|
|
12011
12572
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
12012
12573
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
12013
12574
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
12014
|
-
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
12575
|
+
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
12576
|
+
/* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
|
|
12577
|
+
devices (the glyph stays centered; only the hit target widens). */
|
|
12578
|
+
@media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
|
|
12015
12579
|
document.head.appendChild(s);
|
|
12016
12580
|
}
|
|
12017
12581
|
var DrawingSettingsPopup = class {
|
|
@@ -12058,7 +12622,7 @@ var DrawingSettingsPopup = class {
|
|
|
12058
12622
|
* `onClose` fires on an outside-click dismissal (not a programmatic close). */
|
|
12059
12623
|
open(drawing, anchor, actions, onClose) {
|
|
12060
12624
|
this.close();
|
|
12061
|
-
|
|
12625
|
+
ensureStyles5();
|
|
12062
12626
|
this.onClose = onClose ?? null;
|
|
12063
12627
|
const t = this.theme;
|
|
12064
12628
|
const schema = drawing.schema();
|
|
@@ -12084,7 +12648,14 @@ var DrawingSettingsPopup = class {
|
|
|
12084
12648
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
12085
12649
|
}
|
|
12086
12650
|
if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.lineColor": v })));
|
|
12087
|
-
if (paths.has("style.lineWidth"))
|
|
12651
|
+
if (paths.has("style.lineWidth")) {
|
|
12652
|
+
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
12653
|
+
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
12654
|
+
bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
12655
|
+
} else {
|
|
12656
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
12088
12659
|
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), drawing.style.lineStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
12089
12660
|
if (paths.has("style.fillColor")) bar.appendChild(this.colorButton("Fill", BUCKET_ICON, effectiveFillColor(drawing, this.theme) ?? drawing.style.fillColor ?? DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.fillColor": v })));
|
|
12090
12661
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
@@ -12552,14 +13123,15 @@ var DrawingSettingsPopup = class {
|
|
|
12552
13123
|
const chk = document.createElement("input");
|
|
12553
13124
|
chk.type = "checkbox";
|
|
12554
13125
|
chk.checked = Boolean(s[showPath]);
|
|
12555
|
-
|
|
13126
|
+
const current = s[colorPath] ?? contrastColor(t.background);
|
|
13127
|
+
chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
|
|
12556
13128
|
chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
|
|
12557
13129
|
const lbl = document.createElement("span");
|
|
12558
13130
|
lbl.textContent = label;
|
|
12559
13131
|
lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
|
|
12560
13132
|
const col = document.createElement("button");
|
|
12561
13133
|
col.type = "button";
|
|
12562
|
-
let cur =
|
|
13134
|
+
let cur = current;
|
|
12563
13135
|
col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
|
|
12564
13136
|
col.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
12565
13137
|
col.addEventListener("click", (e) => {
|
|
@@ -12739,7 +13311,8 @@ var DrawingSettingsPopup = class {
|
|
|
12739
13311
|
dragHandle() {
|
|
12740
13312
|
const b = document.createElement("button");
|
|
12741
13313
|
b.type = "button";
|
|
12742
|
-
b.
|
|
13314
|
+
b.className = "vela-dpop-grip";
|
|
13315
|
+
b.style.cssText = `width:${BTN}px;height:${BTN}px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;padding:0;cursor:grab;touch-action:none;color:var(--vela-fg-faint);`;
|
|
12743
13316
|
b.innerHTML = sized(GRIP_ICON);
|
|
12744
13317
|
b.addEventListener("pointerdown", (e) => {
|
|
12745
13318
|
const el = this.el;
|
|
@@ -12755,6 +13328,10 @@ var DrawingSettingsPopup = class {
|
|
|
12755
13328
|
const origLeft = parseFloat(el.style.left) || 0;
|
|
12756
13329
|
const origTop = parseFloat(el.style.top) || 0;
|
|
12757
13330
|
b.style.cursor = "grabbing";
|
|
13331
|
+
try {
|
|
13332
|
+
b.setPointerCapture(e.pointerId);
|
|
13333
|
+
} catch {
|
|
13334
|
+
}
|
|
12758
13335
|
const move = (ev) => {
|
|
12759
13336
|
const w = el.offsetWidth;
|
|
12760
13337
|
const h = el.offsetHeight;
|
|
@@ -12767,9 +13344,11 @@ var DrawingSettingsPopup = class {
|
|
|
12767
13344
|
b.style.cursor = "grab";
|
|
12768
13345
|
window.removeEventListener("pointermove", move);
|
|
12769
13346
|
window.removeEventListener("pointerup", up);
|
|
13347
|
+
window.removeEventListener("pointercancel", up);
|
|
12770
13348
|
};
|
|
12771
13349
|
window.addEventListener("pointermove", move);
|
|
12772
13350
|
window.addEventListener("pointerup", up);
|
|
13351
|
+
window.addEventListener("pointercancel", up);
|
|
12773
13352
|
});
|
|
12774
13353
|
return b;
|
|
12775
13354
|
}
|
|
@@ -12885,6 +13464,39 @@ var DrawingSettingsPopup = class {
|
|
|
12885
13464
|
});
|
|
12886
13465
|
return b;
|
|
12887
13466
|
}
|
|
13467
|
+
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
13468
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. */
|
|
13469
|
+
widthInput(tip, value, min, max, step, onChange) {
|
|
13470
|
+
const input = document.createElement("input");
|
|
13471
|
+
input.type = "number";
|
|
13472
|
+
input.dataset.tip = tip;
|
|
13473
|
+
input.min = String(min);
|
|
13474
|
+
input.max = String(max);
|
|
13475
|
+
input.step = String(step);
|
|
13476
|
+
input.value = String(value);
|
|
13477
|
+
input.style.cssText = `width:52px;height:${BTN}px;box-sizing:border-box;flex:none;background:transparent;color:inherit;border:1px solid var(--vela-border);border-radius:5px;padding:0 4px;font:12px ${this.theme.fontFamily};font-variant-numeric:tabular-nums;outline:none;`;
|
|
13478
|
+
const commit = () => {
|
|
13479
|
+
const n = parseFloat(input.value);
|
|
13480
|
+
if (!Number.isFinite(n)) {
|
|
13481
|
+
input.value = String(value);
|
|
13482
|
+
return;
|
|
13483
|
+
}
|
|
13484
|
+
const v = Math.min(max, Math.max(min, n));
|
|
13485
|
+
input.value = String(v);
|
|
13486
|
+
value = v;
|
|
13487
|
+
onChange(v);
|
|
13488
|
+
};
|
|
13489
|
+
input.addEventListener("change", commit);
|
|
13490
|
+
input.addEventListener("keydown", (e) => {
|
|
13491
|
+
e.stopPropagation();
|
|
13492
|
+
if (e.key === "Enter") {
|
|
13493
|
+
e.preventDefault();
|
|
13494
|
+
commit();
|
|
13495
|
+
input.blur();
|
|
13496
|
+
}
|
|
13497
|
+
});
|
|
13498
|
+
return input;
|
|
13499
|
+
}
|
|
12888
13500
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
12889
13501
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
12890
13502
|
* opens a floating list of every option so a value is one click away (no cycling through). */
|
|
@@ -13338,6 +13950,9 @@ var UserDrawingController = class {
|
|
|
13338
13950
|
// mirrors showToolbar (drives the gutter reservation)
|
|
13339
13951
|
this.toolbarCollapsed = false;
|
|
13340
13952
|
// the bar is a slim expand-strip (narrower gutter)
|
|
13953
|
+
/** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
|
|
13954
|
+
* picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
|
|
13955
|
+
this.mobileLayout = false;
|
|
13341
13956
|
/** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
|
|
13342
13957
|
* history (drawings + app ops in one timeline) turns this off so the chords
|
|
13343
13958
|
* bubble to its keymap instead — see the renderer's `historyChords` feature. */
|
|
@@ -13396,12 +14011,21 @@ var UserDrawingController = class {
|
|
|
13396
14011
|
}
|
|
13397
14012
|
showToolbar(visible) {
|
|
13398
14013
|
this.toolbarVisible = visible;
|
|
13399
|
-
this.toolbar.setVisible(visible);
|
|
14014
|
+
this.toolbar.setVisible(visible && !this.mobileLayout);
|
|
14015
|
+
this.syncToolbarGutter();
|
|
14016
|
+
}
|
|
14017
|
+
/** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
|
|
14018
|
+
* shell's own drawer picks tools instead) and releases its gutter; flipping back
|
|
14019
|
+
* to desktop restores whatever `showToolbar` last asked for. */
|
|
14020
|
+
setLayoutMode(mode) {
|
|
14021
|
+
this.mobileLayout = mode === "mobile";
|
|
14022
|
+
this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
|
|
13400
14023
|
this.syncToolbarGutter();
|
|
13401
14024
|
}
|
|
13402
14025
|
/** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
|
|
13403
14026
|
syncToolbarGutter() {
|
|
13404
|
-
this.
|
|
14027
|
+
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
14028
|
+
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
13405
14029
|
}
|
|
13406
14030
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
13407
14031
|
setExternalGhost(doc) {
|
|
@@ -14270,7 +14894,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
|
|
|
14270
14894
|
|
|
14271
14895
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
14272
14896
|
var ATTRIBUTION_URL = "https://luxalgo.com/vela";
|
|
14273
|
-
var
|
|
14897
|
+
var STYLE_ID6 = "vela-attribution-styles";
|
|
14274
14898
|
var CSS = `
|
|
14275
14899
|
.vela-attribution {
|
|
14276
14900
|
text-decoration: none;
|
|
@@ -14316,12 +14940,28 @@ var CSS = `
|
|
|
14316
14940
|
max-width: 240px;
|
|
14317
14941
|
opacity: 1;
|
|
14318
14942
|
transform: translateX(0);
|
|
14319
|
-
}
|
|
14320
|
-
|
|
14321
|
-
|
|
14943
|
+
}
|
|
14944
|
+
/* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
|
|
14945
|
+
renderer's own container attribute (data-vela-layout) AND the shell root's
|
|
14946
|
+
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
14947
|
+
container, so only the shell attribute reaches it. */
|
|
14948
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
14949
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
|
|
14950
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
14951
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
14952
|
+
top: 2px;
|
|
14953
|
+
transform: translateX(-6px);
|
|
14954
|
+
}
|
|
14955
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
14956
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
|
|
14957
|
+
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
14958
|
+
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
14959
|
+
`;
|
|
14960
|
+
function ensureStyles6(doc) {
|
|
14961
|
+
let s = doc.getElementById(STYLE_ID6);
|
|
14322
14962
|
if (!s) {
|
|
14323
14963
|
s = doc.createElement("style");
|
|
14324
|
-
s.id =
|
|
14964
|
+
s.id = STYLE_ID6;
|
|
14325
14965
|
doc.head.appendChild(s);
|
|
14326
14966
|
}
|
|
14327
14967
|
s.textContent = CSS;
|
|
@@ -14350,7 +14990,7 @@ function createCustomMark(doc, html, background) {
|
|
|
14350
14990
|
return el;
|
|
14351
14991
|
}
|
|
14352
14992
|
function createAttributionMark(doc, background) {
|
|
14353
|
-
|
|
14993
|
+
ensureStyles6(doc);
|
|
14354
14994
|
const a = doc.createElement("a");
|
|
14355
14995
|
a.className = "vela-attribution";
|
|
14356
14996
|
a.href = ATTRIBUTION_URL;
|
|
@@ -14376,6 +15016,174 @@ function createAttributionMark(doc, background) {
|
|
|
14376
15016
|
return a;
|
|
14377
15017
|
}
|
|
14378
15018
|
|
|
15019
|
+
// src/renderers/shared/dom-raster.ts
|
|
15020
|
+
function hasInk(color) {
|
|
15021
|
+
if (!color || color === "transparent") return false;
|
|
15022
|
+
const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
|
|
15023
|
+
return m ? parseFloat(m[1]) > 1e-3 : true;
|
|
15024
|
+
}
|
|
15025
|
+
function cornerRadius(cs, w, h) {
|
|
15026
|
+
const raw = cs.borderTopLeftRadius;
|
|
15027
|
+
const v = parseFloat(raw) || 0;
|
|
15028
|
+
const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
|
|
15029
|
+
return Math.max(0, Math.min(px, Math.min(w, h) / 2));
|
|
15030
|
+
}
|
|
15031
|
+
function tracePath(ctx, x, y, w, h, r) {
|
|
15032
|
+
ctx.beginPath();
|
|
15033
|
+
if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
|
|
15034
|
+
else ctx.rect(x, y, w, h);
|
|
15035
|
+
}
|
|
15036
|
+
function canDrawUntainted(img) {
|
|
15037
|
+
try {
|
|
15038
|
+
const probe2 = img.ownerDocument.createElement("canvas");
|
|
15039
|
+
probe2.width = 1;
|
|
15040
|
+
probe2.height = 1;
|
|
15041
|
+
const ctx = probe2.getContext("2d");
|
|
15042
|
+
if (!ctx) return false;
|
|
15043
|
+
ctx.drawImage(img, 0, 0, 1, 1);
|
|
15044
|
+
probe2.toDataURL();
|
|
15045
|
+
return true;
|
|
15046
|
+
} catch {
|
|
15047
|
+
return false;
|
|
15048
|
+
}
|
|
15049
|
+
}
|
|
15050
|
+
function rasterizeOverlay(ctx, root, frame) {
|
|
15051
|
+
const win = root.ownerDocument.defaultView;
|
|
15052
|
+
if (!win) return;
|
|
15053
|
+
ctx.save();
|
|
15054
|
+
ctx.scale(frame.dpr, frame.dpr);
|
|
15055
|
+
const drawText = (node, cs, alpha) => {
|
|
15056
|
+
const text = node.textContent?.replace(/\s+/g, " ").trim();
|
|
15057
|
+
if (!text) return;
|
|
15058
|
+
const range = node.ownerDocument.createRange();
|
|
15059
|
+
range.selectNodeContents(node);
|
|
15060
|
+
const r = range.getBoundingClientRect();
|
|
15061
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15062
|
+
ctx.globalAlpha = alpha;
|
|
15063
|
+
ctx.fillStyle = cs.color;
|
|
15064
|
+
ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
|
|
15065
|
+
ctx.textBaseline = "middle";
|
|
15066
|
+
ctx.textAlign = "left";
|
|
15067
|
+
ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
|
|
15068
|
+
};
|
|
15069
|
+
const drawImg = (img, cs, alpha) => {
|
|
15070
|
+
if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
|
|
15071
|
+
const r = img.getBoundingClientRect();
|
|
15072
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15073
|
+
const x = r.left - frame.left;
|
|
15074
|
+
const y = r.top - frame.top;
|
|
15075
|
+
ctx.save();
|
|
15076
|
+
ctx.globalAlpha = alpha;
|
|
15077
|
+
tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
|
|
15078
|
+
ctx.clip();
|
|
15079
|
+
ctx.drawImage(img, x, y, r.width, r.height);
|
|
15080
|
+
ctx.restore();
|
|
15081
|
+
};
|
|
15082
|
+
const drawSvgShapes = (parent, alpha) => {
|
|
15083
|
+
for (const el of parent.children) {
|
|
15084
|
+
if (el.hasAttribute("transform")) continue;
|
|
15085
|
+
const tag = el.tagName.toLowerCase();
|
|
15086
|
+
if (tag === "g") {
|
|
15087
|
+
drawSvgShapes(el, alpha);
|
|
15088
|
+
continue;
|
|
15089
|
+
}
|
|
15090
|
+
const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
|
|
15091
|
+
let path = null;
|
|
15092
|
+
if (tag === "path") {
|
|
15093
|
+
const d = el.getAttribute("d");
|
|
15094
|
+
if (d) path = new Path2D(d);
|
|
15095
|
+
} else if (tag === "circle") {
|
|
15096
|
+
path = new Path2D();
|
|
15097
|
+
path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
|
|
15098
|
+
} else if (tag === "ellipse") {
|
|
15099
|
+
path = new Path2D();
|
|
15100
|
+
path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
|
|
15101
|
+
} else if (tag === "rect") {
|
|
15102
|
+
path = new Path2D();
|
|
15103
|
+
path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
|
|
15104
|
+
} else if (tag === "line") {
|
|
15105
|
+
path = new Path2D();
|
|
15106
|
+
path.moveTo(num3("x1"), num3("y1"));
|
|
15107
|
+
path.lineTo(num3("x2"), num3("y2"));
|
|
15108
|
+
} else if (tag === "polyline" || tag === "polygon") {
|
|
15109
|
+
const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
|
|
15110
|
+
if (pts.length < 4 || pts.some(Number.isNaN)) continue;
|
|
15111
|
+
path = new Path2D();
|
|
15112
|
+
path.moveTo(pts[0], pts[1]);
|
|
15113
|
+
for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
|
|
15114
|
+
if (tag === "polygon") path.closePath();
|
|
15115
|
+
}
|
|
15116
|
+
if (!path) continue;
|
|
15117
|
+
const cs = win.getComputedStyle(el);
|
|
15118
|
+
if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
|
|
15119
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
|
|
15120
|
+
ctx.fillStyle = cs.fill;
|
|
15121
|
+
ctx.fill(path);
|
|
15122
|
+
}
|
|
15123
|
+
const sw = parseFloat(cs.strokeWidth) || 0;
|
|
15124
|
+
if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
|
|
15125
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
|
|
15126
|
+
ctx.strokeStyle = cs.stroke;
|
|
15127
|
+
ctx.lineWidth = sw;
|
|
15128
|
+
if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
|
|
15129
|
+
if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
|
|
15130
|
+
ctx.stroke(path);
|
|
15131
|
+
}
|
|
15132
|
+
}
|
|
15133
|
+
};
|
|
15134
|
+
const drawSvg = (svg, parentAlpha) => {
|
|
15135
|
+
const r = svg.getBoundingClientRect();
|
|
15136
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
15137
|
+
const cs = win.getComputedStyle(svg);
|
|
15138
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
15139
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
15140
|
+
if (alpha <= 1e-3) return;
|
|
15141
|
+
const vb = svg.viewBox.baseVal;
|
|
15142
|
+
const vw = vb && vb.width > 0 ? vb.width : r.width;
|
|
15143
|
+
const vh = vb && vb.height > 0 ? vb.height : r.height;
|
|
15144
|
+
ctx.save();
|
|
15145
|
+
ctx.translate(r.left - frame.left, r.top - frame.top);
|
|
15146
|
+
ctx.scale(r.width / vw, r.height / vh);
|
|
15147
|
+
if (vb) ctx.translate(-vb.x, -vb.y);
|
|
15148
|
+
drawSvgShapes(svg, alpha);
|
|
15149
|
+
ctx.restore();
|
|
15150
|
+
};
|
|
15151
|
+
const drawElement = (el, parentAlpha) => {
|
|
15152
|
+
const cs = win.getComputedStyle(el);
|
|
15153
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
15154
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
15155
|
+
if (alpha <= 1e-3) return;
|
|
15156
|
+
const r = el.getBoundingClientRect();
|
|
15157
|
+
if (r.width > 0 && r.height > 0) {
|
|
15158
|
+
const x = r.left - frame.left;
|
|
15159
|
+
const y = r.top - frame.top;
|
|
15160
|
+
const radius = cornerRadius(cs, r.width, r.height);
|
|
15161
|
+
if (hasInk(cs.backgroundColor)) {
|
|
15162
|
+
ctx.globalAlpha = alpha;
|
|
15163
|
+
ctx.fillStyle = cs.backgroundColor;
|
|
15164
|
+
tracePath(ctx, x, y, r.width, r.height, radius);
|
|
15165
|
+
ctx.fill();
|
|
15166
|
+
}
|
|
15167
|
+
const bw = parseFloat(cs.borderTopWidth) || 0;
|
|
15168
|
+
if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
|
|
15169
|
+
ctx.globalAlpha = alpha;
|
|
15170
|
+
ctx.strokeStyle = cs.borderTopColor;
|
|
15171
|
+
ctx.lineWidth = bw;
|
|
15172
|
+
tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
|
|
15173
|
+
ctx.stroke();
|
|
15174
|
+
}
|
|
15175
|
+
}
|
|
15176
|
+
for (const child of el.childNodes) {
|
|
15177
|
+
if (child.nodeType === 3) drawText(child, cs, alpha);
|
|
15178
|
+
else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
|
|
15179
|
+
else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
|
|
15180
|
+
else if (child instanceof win.HTMLElement) drawElement(child, alpha);
|
|
15181
|
+
}
|
|
15182
|
+
};
|
|
15183
|
+
if (root instanceof win.HTMLElement) drawElement(root, 1);
|
|
15184
|
+
ctx.restore();
|
|
15185
|
+
}
|
|
15186
|
+
|
|
14379
15187
|
// src/renderers/native/vpvr/profile.ts
|
|
14380
15188
|
function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
|
|
14381
15189
|
const from = Math.max(0, i0);
|
|
@@ -14544,12 +15352,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
|
|
|
14544
15352
|
var COLLAPSED_PANE_H = 26;
|
|
14545
15353
|
var TIME_AXIS_H = 22;
|
|
14546
15354
|
var SCROLL_BTN_SIZE = 26;
|
|
15355
|
+
var SCROLL_BTN_SIZE_TOUCH = 24;
|
|
14547
15356
|
var SCROLL_BTN_RADIUS = 5;
|
|
14548
15357
|
var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
|
|
14549
15358
|
var SCROLL_BTN_ICON_PX = 12;
|
|
14550
|
-
var SCROLL_BTN_RIGHT_INSET =
|
|
15359
|
+
var SCROLL_BTN_RIGHT_INSET = 12;
|
|
14551
15360
|
var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
|
|
14552
|
-
var SCROLL_BTN_BOTTOM = TIME_AXIS_H +
|
|
15361
|
+
var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
14553
15362
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
14554
15363
|
var MIN_VISIBLE_BARS = 2;
|
|
14555
15364
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
@@ -14601,8 +15410,12 @@ var NativeRenderer = class {
|
|
|
14601
15410
|
this.symbolPicker = null;
|
|
14602
15411
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
14603
15412
|
this.indicatorTitlesOn = true;
|
|
15413
|
+
/** Plot values beside the legend titles shown — held here so a remount re-applies it. */
|
|
15414
|
+
this.indicatorValuesOn = true;
|
|
14604
15415
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
14605
15416
|
this.legendActionsProvider = null;
|
|
15417
|
+
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
15418
|
+
this.legendOverviewAction = null;
|
|
14606
15419
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
14607
15420
|
this.keyboard = null;
|
|
14608
15421
|
this.keyboardEnabled = true;
|
|
@@ -14712,6 +15525,8 @@ var NativeRenderer = class {
|
|
|
14712
15525
|
// Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
|
|
14713
15526
|
// pane carries merged (own-scale) columns; the constant only clears a single-column scale.
|
|
14714
15527
|
this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
|
|
15528
|
+
// ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
|
|
15529
|
+
this.layoutMode = "desktop";
|
|
14715
15530
|
this.viewportCbs = /* @__PURE__ */ new Set();
|
|
14716
15531
|
this.crosshairCbs = /* @__PURE__ */ new Set();
|
|
14717
15532
|
this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
|
|
@@ -14722,6 +15537,7 @@ var NativeRenderer = class {
|
|
|
14722
15537
|
this.factoryConfig = null;
|
|
14723
15538
|
this.hostSettingsSections = [];
|
|
14724
15539
|
this.clickCbs = /* @__PURE__ */ new Set();
|
|
15540
|
+
this.axisLongPressCbs = /* @__PURE__ */ new Set();
|
|
14725
15541
|
this.inputChangeCbs = /* @__PURE__ */ new Set();
|
|
14726
15542
|
this.removeIndicatorCbs = /* @__PURE__ */ new Set();
|
|
14727
15543
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
@@ -14729,7 +15545,7 @@ var NativeRenderer = class {
|
|
|
14729
15545
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
14730
15546
|
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
14731
15547
|
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"];
|
|
15548
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
14733
15549
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
14734
15550
|
* so moving onto the button doesn't count as leaving). */
|
|
14735
15551
|
this.onScrollProximityMove = (e) => {
|
|
@@ -14902,6 +15718,11 @@ var NativeRenderer = class {
|
|
|
14902
15718
|
this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
|
|
14903
15719
|
return;
|
|
14904
15720
|
// own DOM, no repaint needed
|
|
15721
|
+
case "indicatorValues":
|
|
15722
|
+
this.indicatorValuesOn = value !== false;
|
|
15723
|
+
this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
|
|
15724
|
+
return;
|
|
15725
|
+
// own DOM, no repaint needed
|
|
14905
15726
|
case "attribution":
|
|
14906
15727
|
this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
|
|
14907
15728
|
this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
|
|
@@ -14990,6 +15811,8 @@ var NativeRenderer = class {
|
|
|
14990
15811
|
return this.settingsEnabled;
|
|
14991
15812
|
case "indicatorTitles":
|
|
14992
15813
|
return this.indicatorTitlesOn;
|
|
15814
|
+
case "indicatorValues":
|
|
15815
|
+
return this.indicatorValuesOn;
|
|
14993
15816
|
case "attribution":
|
|
14994
15817
|
return this.attributionHtml ?? this.attributionEnabled;
|
|
14995
15818
|
case "dialogHost":
|
|
@@ -15326,7 +16149,10 @@ var NativeRenderer = class {
|
|
|
15326
16149
|
/** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
|
|
15327
16150
|
* in-chart gear — created on demand, independent of the gear feature being enabled. */
|
|
15328
16151
|
openSettingsDialog(section) {
|
|
15329
|
-
if (!this.settingsDialog && this.plot)
|
|
16152
|
+
if (!this.settingsDialog && this.plot) {
|
|
16153
|
+
this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
|
|
16154
|
+
this.settingsDialog.setLayoutMode(this.layoutMode);
|
|
16155
|
+
}
|
|
15330
16156
|
this.toggleSettingsDialog(section);
|
|
15331
16157
|
}
|
|
15332
16158
|
/** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
|
|
@@ -15415,11 +16241,18 @@ var NativeRenderer = class {
|
|
|
15415
16241
|
this.scrollButton.style.color = this.theme.textColor;
|
|
15416
16242
|
this.scrollButton.style.borderColor = this.theme.borderColor;
|
|
15417
16243
|
}
|
|
15418
|
-
/** Show the button only when the latest bar is scrolled off the right edge
|
|
16244
|
+
/** Show the button only when the latest bar is scrolled off the right edge. On desktop the
|
|
16245
|
+
* cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
|
|
16246
|
+
* appears whenever the latest bars are off-screen and hides again once the view is back
|
|
16247
|
+
* at the right edge. */
|
|
15419
16248
|
updateScrollToRealtimeButton() {
|
|
15420
16249
|
const btn = this.scrollButton;
|
|
15421
16250
|
if (!btn) return;
|
|
15422
16251
|
const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
|
|
16252
|
+
if (this.layoutMode === "mobile") {
|
|
16253
|
+
btn.style.display = latestOffScreen ? "flex" : "none";
|
|
16254
|
+
return;
|
|
16255
|
+
}
|
|
15423
16256
|
btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
|
|
15424
16257
|
}
|
|
15425
16258
|
/** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
|
|
@@ -15441,17 +16274,20 @@ var NativeRenderer = class {
|
|
|
15441
16274
|
this.animator.start();
|
|
15442
16275
|
}
|
|
15443
16276
|
/**
|
|
15444
|
-
* Export the current chart as a PNG data URL by compositing
|
|
15445
|
-
*
|
|
15446
|
-
*
|
|
15447
|
-
*
|
|
15448
|
-
*
|
|
15449
|
-
*
|
|
15450
|
-
*
|
|
15451
|
-
*
|
|
15452
|
-
*
|
|
15453
|
-
*
|
|
15454
|
-
*
|
|
16277
|
+
* Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
|
|
16278
|
+
* an offscreen canvas, in the same stacking order the DOM shows: plugin layers
|
|
16279
|
+
* below the data, geometry (L0), volume columns (L0.25), the visible-range volume
|
|
16280
|
+
* profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
|
|
16281
|
+
* (L1.5). The background is filled first (the canvas2d data layer is transparent —
|
|
16282
|
+
* its bg lives on the wrapper), and a fresh synchronous paint runs first so the
|
|
16283
|
+
* WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
|
|
16284
|
+
* back this tick — the same paint also repaints the drawings layers, so they're
|
|
16285
|
+
* current. DOM chrome joins as a best-effort text/chip raster (see
|
|
16286
|
+
* `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
|
|
16287
|
+
* opts in with a `data-vela-screenshot` attribute on the mount container's
|
|
16288
|
+
* subtree (the widget marks its status line, and its symbol watermark with
|
|
16289
|
+
* `"under"` — drawn beneath the canvases, where it sits on screen). Only the
|
|
16290
|
+
* crosshair (L2) is intentionally excluded.
|
|
15455
16291
|
*/
|
|
15456
16292
|
screenshot() {
|
|
15457
16293
|
if (!this.dataCanvas) return null;
|
|
@@ -15464,9 +16300,25 @@ var NativeRenderer = class {
|
|
|
15464
16300
|
if (!ctx) return null;
|
|
15465
16301
|
ctx.fillStyle = this.theme.background;
|
|
15466
16302
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
16303
|
+
const plotRect = this.plot?.getBoundingClientRect();
|
|
16304
|
+
const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
|
|
16305
|
+
const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
|
|
16306
|
+
if (frame) {
|
|
16307
|
+
for (const el of hostMarked) {
|
|
16308
|
+
if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
16311
|
+
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
16312
|
+
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
16313
|
+
for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
|
|
16314
|
+
if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
|
|
16315
|
+
}
|
|
16316
|
+
if (frame) {
|
|
16317
|
+
for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
|
|
16318
|
+
for (const el of hostMarked) {
|
|
16319
|
+
if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
|
|
16320
|
+
}
|
|
16321
|
+
}
|
|
15470
16322
|
return out.toDataURL("image/png");
|
|
15471
16323
|
}
|
|
15472
16324
|
/**
|
|
@@ -15617,6 +16469,9 @@ var NativeRenderer = class {
|
|
|
15617
16469
|
fling: (v) => this.fling(v),
|
|
15618
16470
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
15619
16471
|
onClick: (x) => this.handleClick(x),
|
|
16472
|
+
onAxisLongPress: (axis, x, y) => {
|
|
16473
|
+
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
16474
|
+
},
|
|
15620
16475
|
beginPriceScale: (x, y) => this.beginPriceScale(x, y),
|
|
15621
16476
|
priceScaleBy: (dy) => this.priceScaleBy(dy),
|
|
15622
16477
|
beginPricePan: (y) => this.beginPricePan(y),
|
|
@@ -15663,6 +16518,14 @@ var NativeRenderer = class {
|
|
|
15663
16518
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
15664
16519
|
});
|
|
15665
16520
|
this.userDrawings.historyChords = this.historyChordsEnabled;
|
|
16521
|
+
if (this.layoutMode === "mobile") {
|
|
16522
|
+
this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
|
|
16523
|
+
this.userDrawings.setLayoutMode(this.layoutMode);
|
|
16524
|
+
if (this.scrollButton) {
|
|
16525
|
+
this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
16526
|
+
this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
16527
|
+
}
|
|
16528
|
+
}
|
|
15666
16529
|
this.keyboard = new KeyboardController({
|
|
15667
16530
|
panByBars: (bars) => this.panByBars(bars),
|
|
15668
16531
|
zoomByStep: (dir) => this.zoomByStep(dir),
|
|
@@ -15675,10 +16538,13 @@ var NativeRenderer = class {
|
|
|
15675
16538
|
});
|
|
15676
16539
|
this.setKeyboardEnabled(this.keyboardEnabled);
|
|
15677
16540
|
this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
|
|
16541
|
+
this.inputsUI.setLayoutMode(this.layoutMode);
|
|
15678
16542
|
this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
|
|
16543
|
+
this.inputsUI.setValuesVisible(this.indicatorValuesOn);
|
|
15679
16544
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
15680
16545
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
15681
16546
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
16547
|
+
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
15682
16548
|
this.inputsUI.setOnChange((c) => {
|
|
15683
16549
|
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
15684
16550
|
});
|
|
@@ -16093,7 +16959,10 @@ var NativeRenderer = class {
|
|
|
16093
16959
|
this.scene.indicators.set(model.id, model);
|
|
16094
16960
|
this.refreshAnchorOffset(model);
|
|
16095
16961
|
this.scene.assignIndicatorZ(model.id);
|
|
16096
|
-
this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, {
|
|
16962
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
16963
|
+
native: !!model.native,
|
|
16964
|
+
...model.shorttitle ? { settingsTitle: model.title } : {}
|
|
16965
|
+
});
|
|
16097
16966
|
this.syncTables(model);
|
|
16098
16967
|
if (model.native?.type === "volume") {
|
|
16099
16968
|
this.volumeActive = true;
|
|
@@ -16154,6 +17023,13 @@ var NativeRenderer = class {
|
|
|
16154
17023
|
this.legendActionsProvider = provider;
|
|
16155
17024
|
this.inputsUI?.setLegendActions(provider);
|
|
16156
17025
|
}
|
|
17026
|
+
setLegendOverviewAction(action) {
|
|
17027
|
+
this.legendOverviewAction = action;
|
|
17028
|
+
this.inputsUI?.setLegendOverviewAction(action);
|
|
17029
|
+
}
|
|
17030
|
+
openIndicatorSettings(indicatorId) {
|
|
17031
|
+
this.inputsUI?.openSettingsFor(indicatorId);
|
|
17032
|
+
}
|
|
16157
17033
|
/**
|
|
16158
17034
|
* Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
|
|
16159
17035
|
* series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
|
|
@@ -16207,6 +17083,24 @@ var NativeRenderer = class {
|
|
|
16207
17083
|
this.removeIndicatorCbs.add(cb);
|
|
16208
17084
|
return () => this.removeIndicatorCbs.delete(cb);
|
|
16209
17085
|
}
|
|
17086
|
+
/** Port surface: the host shell's chrome size class. Mobile switches the renderer
|
|
17087
|
+
* chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
|
|
17088
|
+
* toolbar (the shell provides its own picker), the scroll-to-latest button shown
|
|
17089
|
+
* whenever the latest bars are off-screen (touch has no cursor proximity). */
|
|
17090
|
+
setLayoutMode(mode) {
|
|
17091
|
+
if (mode === this.layoutMode) return;
|
|
17092
|
+
this.layoutMode = mode;
|
|
17093
|
+
this.mountContainer?.setAttribute("data-vela-layout", mode);
|
|
17094
|
+
this.userDrawings?.setLayoutMode(mode);
|
|
17095
|
+
this.settingsDialog?.setLayoutMode(mode);
|
|
17096
|
+
this.inputsUI?.setLayoutMode(mode);
|
|
17097
|
+
if (this.scrollButton) {
|
|
17098
|
+
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
17099
|
+
this.scrollButton.style.width = `${px}px`;
|
|
17100
|
+
this.scrollButton.style.height = `${px}px`;
|
|
17101
|
+
}
|
|
17102
|
+
this.updateScrollToRealtimeButton();
|
|
17103
|
+
}
|
|
16210
17104
|
setSettingsSections(sections) {
|
|
16211
17105
|
this.hostSettingsSections = sections;
|
|
16212
17106
|
this.settingsDialog?.setHostSections(sections);
|
|
@@ -16231,6 +17125,10 @@ var NativeRenderer = class {
|
|
|
16231
17125
|
this.clickCbs.add(cb);
|
|
16232
17126
|
return () => this.clickCbs.delete(cb);
|
|
16233
17127
|
}
|
|
17128
|
+
onAxisLongPress(cb) {
|
|
17129
|
+
this.axisLongPressCbs.add(cb);
|
|
17130
|
+
return () => this.axisLongPressCbs.delete(cb);
|
|
17131
|
+
}
|
|
16234
17132
|
onViewportChange(cb) {
|
|
16235
17133
|
this.viewportCbs.add(cb);
|
|
16236
17134
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -16372,6 +17270,7 @@ var NativeRenderer = class {
|
|
|
16372
17270
|
this.skeletonClockMs += dtMs;
|
|
16373
17271
|
this.paintData();
|
|
16374
17272
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
17273
|
+
this.updateLegendValues();
|
|
16375
17274
|
this.emitViewportChange();
|
|
16376
17275
|
return active;
|
|
16377
17276
|
}
|
|
@@ -16436,7 +17335,7 @@ var NativeRenderer = class {
|
|
|
16436
17335
|
this.lastPointer = null;
|
|
16437
17336
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
16438
17337
|
this.hoverLogical = null;
|
|
16439
|
-
const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
17338
|
+
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
16440
17339
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
16441
17340
|
return;
|
|
16442
17341
|
}
|
|
@@ -16448,7 +17347,7 @@ var NativeRenderer = class {
|
|
|
16448
17347
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
16449
17348
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
16450
17349
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
16451
|
-
const pane = this.
|
|
17350
|
+
const pane = this.paneNodeAtY(y);
|
|
16452
17351
|
const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
|
|
16453
17352
|
const values = /* @__PURE__ */ new Map();
|
|
16454
17353
|
if (onBar) {
|
|
@@ -16465,7 +17364,7 @@ var NativeRenderer = class {
|
|
|
16465
17364
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
16466
17365
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
16467
17366
|
this.hoverLogical = onBar ? logical : null;
|
|
16468
|
-
const event = { time, price, values, ohlc };
|
|
17367
|
+
const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
|
|
16469
17368
|
for (const cb of this.crosshairCbs) cb(event);
|
|
16470
17369
|
}
|
|
16471
17370
|
handleClick(x) {
|
|
@@ -16484,6 +17383,20 @@ var NativeRenderer = class {
|
|
|
16484
17383
|
}
|
|
16485
17384
|
return null;
|
|
16486
17385
|
}
|
|
17386
|
+
/** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
|
|
17387
|
+
* fall BETWEEN bounds (separator gaps, the strip under the last pane). */
|
|
17388
|
+
nearestPaneToY(y) {
|
|
17389
|
+
let best = null;
|
|
17390
|
+
let bestDist = Infinity;
|
|
17391
|
+
for (const pane of this.scene.panes.values()) {
|
|
17392
|
+
const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
|
|
17393
|
+
if (d < bestDist) {
|
|
17394
|
+
bestDist = d;
|
|
17395
|
+
best = pane;
|
|
17396
|
+
}
|
|
17397
|
+
}
|
|
17398
|
+
return best;
|
|
17399
|
+
}
|
|
16487
17400
|
// ── manual vertical scaling (price-axis drag + vertical price pan) ──
|
|
16488
17401
|
/** The scale window a gesture targets: a pane's master scale, or a merged indicator's
|
|
16489
17402
|
* own scale column (resolved from the grabbed x). Both share the four scale fields. */
|
|
@@ -16546,7 +17459,7 @@ var NativeRenderer = class {
|
|
|
16546
17459
|
* still fits the view.)
|
|
16547
17460
|
*/
|
|
16548
17461
|
dataDblClick(_x, y) {
|
|
16549
|
-
const pane = this.paneNodeAtY(y);
|
|
17462
|
+
const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
|
|
16550
17463
|
if (!pane) return;
|
|
16551
17464
|
const maximized = this.maximizedPaneId !== pane.id;
|
|
16552
17465
|
this.maximizedPaneId = maximized ? pane.id : null;
|
|
@@ -16787,29 +17700,63 @@ var NativeRenderer = class {
|
|
|
16787
17700
|
dataWindowGroups(idx, pricePane) {
|
|
16788
17701
|
const groups = [];
|
|
16789
17702
|
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
|
-
}
|
|
17703
|
+
const rows = this.dataWindowRowsFor(model, idx, pricePane);
|
|
16809
17704
|
if (rows.length) groups.push({ name: model.title, rows });
|
|
16810
17705
|
}
|
|
16811
17706
|
return groups;
|
|
16812
17707
|
}
|
|
17708
|
+
/** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
|
|
17709
|
+
dataWindowRowsFor(model, idx, pricePane) {
|
|
17710
|
+
if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
|
|
17711
|
+
const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
|
|
17712
|
+
const off = this.scene.offsetOf(model.id);
|
|
17713
|
+
const rows = [];
|
|
17714
|
+
for (const s of model.series) {
|
|
17715
|
+
let value;
|
|
17716
|
+
let color;
|
|
17717
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
17718
|
+
value = s.bars[idx - off]?.close;
|
|
17719
|
+
color = s.style?.up ?? this.theme.upColor;
|
|
17720
|
+
} else if (isLineLikeSeries(s)) {
|
|
17721
|
+
if (s.visible === false) continue;
|
|
17722
|
+
value = s.points[idx - off]?.value;
|
|
17723
|
+
color = s.points[idx - off]?.color ?? s.style.color;
|
|
17724
|
+
} else {
|
|
17725
|
+
continue;
|
|
17726
|
+
}
|
|
17727
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
17728
|
+
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
17729
|
+
}
|
|
17730
|
+
return rows;
|
|
17731
|
+
}
|
|
17732
|
+
/** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
|
|
17733
|
+
* colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
|
|
17734
|
+
* `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
|
|
17735
|
+
volumeReadoutRows(model, idx) {
|
|
17736
|
+
if (this.volumeHidden) return [];
|
|
17737
|
+
const bar = this.bars[idx];
|
|
17738
|
+
const vol = bar?.volume;
|
|
17739
|
+
if (bar == null || vol == null || !Number.isFinite(vol)) return [];
|
|
17740
|
+
const cfg = this.scene.volumeLayer;
|
|
17741
|
+
const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
|
|
17742
|
+
return [{ label: model.title, value: formatVolume(vol), color }];
|
|
17743
|
+
}
|
|
17744
|
+
/** Refresh the plot values beside every legend title — the same readout the data window
|
|
17745
|
+
* shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
|
|
17746
|
+
* no scene model, so its row is absent from the map and its readout clears. */
|
|
17747
|
+
updateLegendValues() {
|
|
17748
|
+
if (!this.inputsUI) return;
|
|
17749
|
+
const n = this.bars.length;
|
|
17750
|
+
const values = /* @__PURE__ */ new Map();
|
|
17751
|
+
if (n > 0) {
|
|
17752
|
+
const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
|
|
17753
|
+
const pricePane = this.dataWindowPricePane();
|
|
17754
|
+
for (const model of this.scene.indicators.values()) {
|
|
17755
|
+
values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
|
|
17756
|
+
}
|
|
17757
|
+
}
|
|
17758
|
+
this.inputsUI.setPlotValues(values);
|
|
17759
|
+
}
|
|
16813
17760
|
renderFrame(level) {
|
|
16814
17761
|
if (this.animator.active) return;
|
|
16815
17762
|
if (repaintsData(level)) {
|
|
@@ -16821,6 +17768,7 @@ var NativeRenderer = class {
|
|
|
16821
17768
|
}
|
|
16822
17769
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
16823
17770
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
17771
|
+
this.updateLegendValues();
|
|
16824
17772
|
}
|
|
16825
17773
|
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
16826
17774
|
repaintCursorLayers() {
|
|
@@ -16969,7 +17917,7 @@ var NativeRenderer = class {
|
|
|
16969
17917
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
16970
17918
|
}
|
|
16971
17919
|
}
|
|
16972
|
-
return { x, y, time: this.coords.logicalToTime(logical) };
|
|
17920
|
+
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
16973
17921
|
}
|
|
16974
17922
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
16975
17923
|
setSnapMode(mode) {
|
|
@@ -17739,6 +18687,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17739
18687
|
this.barTransform = null;
|
|
17740
18688
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
17741
18689
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
18690
|
+
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
18691
|
+
* replayed into a data engine whenever one is (re)created. */
|
|
18692
|
+
this.typeSettings = /* @__PURE__ */ new Map();
|
|
17742
18693
|
/** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
|
|
17743
18694
|
this.activeEngineStyle = null;
|
|
17744
18695
|
/** The chart's current price style (tracked from the renderer's change events + initial read). */
|
|
@@ -17801,7 +18752,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17801
18752
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17802
18753
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17803
18754
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
17804
|
-
this.renderer.onChartTypeSettingsChange?.((typeId, values) =>
|
|
18755
|
+
this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
|
|
18756
|
+
this.typeSettings.set(typeId, values);
|
|
18757
|
+
this.typeEngines.get(typeId)?.onSettings?.(values);
|
|
18758
|
+
});
|
|
17805
18759
|
this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
|
|
17806
18760
|
this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
|
|
17807
18761
|
this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
|
|
@@ -18289,6 +19243,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18289
19243
|
if (!def?.dataEngine) return;
|
|
18290
19244
|
const engine = def.dataEngine();
|
|
18291
19245
|
this.typeEngines.set(id, engine);
|
|
19246
|
+
const stored = this.typeSettings.get(id);
|
|
19247
|
+
if (stored) engine.onSettings?.(stored);
|
|
18292
19248
|
engine.start({
|
|
18293
19249
|
symbol: this.config.market.symbol ?? "TEST",
|
|
18294
19250
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -18760,6 +19716,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18760
19716
|
return {
|
|
18761
19717
|
id: record.id,
|
|
18762
19718
|
title: record.title,
|
|
19719
|
+
...d.shortTitle ? { shorttitle: d.shortTitle } : {},
|
|
18763
19720
|
overlay: d.overlay,
|
|
18764
19721
|
paneHint: d.paneHint,
|
|
18765
19722
|
native: { type: record.native.type },
|
|
@@ -19367,7 +20324,8 @@ var VpvrIndicator = class {
|
|
|
19367
20324
|
};
|
|
19368
20325
|
var vpvrDescriptor = {
|
|
19369
20326
|
type: "vpvr",
|
|
19370
|
-
title: "
|
|
20327
|
+
title: "Visible Range Volume Profile",
|
|
20328
|
+
shortTitle: "VRVP",
|
|
19371
20329
|
paneHint: "price",
|
|
19372
20330
|
overlay: true,
|
|
19373
20331
|
inputsSchema: () => [
|
|
@@ -19724,4 +20682,4 @@ function resolveElement(container) {
|
|
|
19724
20682
|
return element;
|
|
19725
20683
|
}
|
|
19726
20684
|
|
|
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 };
|
|
20685
|
+
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|