@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
package/dist/index.cjs
CHANGED
|
@@ -481,14 +481,15 @@ registerIcon(
|
|
|
481
481
|
svg24('<path d="m10 20-1.25-2.5L6 16"/><path d="M10 4 8.75 6.5 6 8"/><path d="m14 20 1.25-2.5L18 16"/><path d="m14 4 1.25 2.5L18 8"/><path d="m17 10-5 5-5-5"/><path d="M4 12h16"/>')
|
|
482
482
|
);
|
|
483
483
|
registerIcon("chevron-right", S('<path d="m6 3.5 4.5 4.5L6 12.5"/>'));
|
|
484
|
+
registerIcon("chevron-left", S('<path d="M10 3.5 5.5 8l4.5 4.5"/>'));
|
|
484
485
|
registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
|
|
485
486
|
registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
|
|
486
487
|
registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
|
|
487
488
|
registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
|
|
488
489
|
registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
|
|
489
490
|
registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
|
|
490
|
-
registerIcon("eye", S('<path d="M1.5 8s2.5-4
|
|
491
|
-
registerIcon("eye-off", S('<path d="M1.5 8s2.5-4
|
|
491
|
+
registerIcon("eye", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z"/><circle cx="8" cy="8" r="1.8"/>'));
|
|
492
|
+
registerIcon("eye-off", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z" opacity="0.45"/><path d="m3 13 10-10"/>'));
|
|
492
493
|
registerIcon("lock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.8 0V7"/>'));
|
|
493
494
|
registerIcon("unlock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.7-.7"/>'));
|
|
494
495
|
registerIcon("trash", S('<path d="M2.5 4.3h11M6.2 4.3V3.1a1 1 0 0 1 1-1h1.6a1 1 0 0 1 1 1v1.2M4.2 4.3l.5 9.1a1.15 1.15 0 0 0 1.15 1.1h4.3a1.15 1.15 0 0 0 1.15-1.1l.5-9.1M6.5 7v4.5M9.5 7v4.5"/>'));
|
|
@@ -511,6 +512,7 @@ registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l
|
|
|
511
512
|
registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
|
|
512
513
|
registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
|
|
513
514
|
registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
|
|
515
|
+
registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
|
|
514
516
|
registerIcon("reset", S('<rect x="6" y="6" width="4" height="4" rx="0.8"/><path d="M2.2 8a5.8 5.8 0 1 0 1.9-4.3L2.2 5.3"/><path d="M2.2 2.2v3.1h3.1"/>'));
|
|
515
517
|
registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
|
|
516
518
|
registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
|
|
@@ -1889,10 +1891,10 @@ function polylineHit(px, py, pts, tol) {
|
|
|
1889
1891
|
}
|
|
1890
1892
|
|
|
1891
1893
|
// src/core/drawings/types/FixedRangeVolumeProfile.ts
|
|
1892
|
-
var OUTSIDE_UP = `${BULLISH}
|
|
1893
|
-
var OUTSIDE_DOWN = `${BEARISH}
|
|
1894
|
-
var VA_UP = `${BULLISH}
|
|
1895
|
-
var VA_DOWN = `${BEARISH}
|
|
1894
|
+
var OUTSIDE_UP = `${BULLISH}66`;
|
|
1895
|
+
var OUTSIDE_DOWN = `${BEARISH}66`;
|
|
1896
|
+
var VA_UP = `${BULLISH}BF`;
|
|
1897
|
+
var VA_DOWN = `${BEARISH}BF`;
|
|
1896
1898
|
function defaultFrvpStyle() {
|
|
1897
1899
|
return {
|
|
1898
1900
|
rows: 24,
|
|
@@ -1910,7 +1912,8 @@ function defaultFrvpStyle() {
|
|
|
1910
1912
|
valColor: NEUTRAL,
|
|
1911
1913
|
valStyle: "solid",
|
|
1912
1914
|
showPoc: true,
|
|
1913
|
-
pocColor:
|
|
1915
|
+
pocColor: void 0,
|
|
1916
|
+
// theme contrast ink until the user picks a color
|
|
1914
1917
|
pocStyle: "solid",
|
|
1915
1918
|
showDevelopingPoc: false,
|
|
1916
1919
|
developingPocColor: ACCENT,
|
|
@@ -7052,6 +7055,47 @@ var ProviderRegistry = class {
|
|
|
7052
7055
|
};
|
|
7053
7056
|
|
|
7054
7057
|
// src/chart-types/registry.ts
|
|
7058
|
+
function normalizeSettingsRow(r) {
|
|
7059
|
+
switch (r.kind) {
|
|
7060
|
+
case "row":
|
|
7061
|
+
return { label: r.label, toggle: r.toggle, controls: r.controls, when: r.when };
|
|
7062
|
+
case "toggle": {
|
|
7063
|
+
const controls = [];
|
|
7064
|
+
if (r.number) controls.push({ kind: "number", ...r.number });
|
|
7065
|
+
for (const c of r.colors ?? []) controls.push({ kind: "color", ...c });
|
|
7066
|
+
if (r.width) controls.push({ kind: "width", ...r.width });
|
|
7067
|
+
return { label: r.label, toggle: { key: r.key, defval: r.defval }, controls, when: r.when };
|
|
7068
|
+
}
|
|
7069
|
+
case "number":
|
|
7070
|
+
return { label: r.label, controls: [{ kind: "number", key: r.key, label: r.label, defval: r.defval, min: r.min, max: r.max, step: r.step }], when: r.when };
|
|
7071
|
+
case "color":
|
|
7072
|
+
return { label: r.label, controls: [{ kind: "color", key: r.key, label: r.label, defval: r.defval }], when: r.when };
|
|
7073
|
+
case "select":
|
|
7074
|
+
return { label: r.label, controls: [{ kind: "select", key: r.key, label: r.label, options: r.options, defval: r.defval }], when: r.when };
|
|
7075
|
+
case "range":
|
|
7076
|
+
return {
|
|
7077
|
+
label: r.label,
|
|
7078
|
+
controls: [
|
|
7079
|
+
{ kind: "number", key: r.minKey, label: `${r.label} \u2014 min`, defval: r.defval, min: r.min, max: r.max, step: r.step, placeholder: r.placeholder },
|
|
7080
|
+
{ kind: "hint", text: "\u2013" },
|
|
7081
|
+
{ kind: "number", key: r.maxKey, label: `${r.label} \u2014 max`, defval: r.defval, min: r.min, max: r.max, step: r.step, placeholder: r.placeholder }
|
|
7082
|
+
],
|
|
7083
|
+
when: r.when
|
|
7084
|
+
};
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
function settingsRowValueKeys(r) {
|
|
7088
|
+
if (r.kind === "heading" || r.kind === "header") return [];
|
|
7089
|
+
const n = normalizeSettingsRow(r);
|
|
7090
|
+
const keys = [];
|
|
7091
|
+
if (n.toggle) keys.push({ key: n.toggle.key, type: "boolean", defval: n.toggle.defval });
|
|
7092
|
+
for (const c of n.controls) {
|
|
7093
|
+
if (c.kind === "hint") continue;
|
|
7094
|
+
if (c.kind === "color" || c.kind === "select") keys.push({ key: c.key, type: "string", defval: c.defval });
|
|
7095
|
+
else keys.push({ key: c.key, type: "number", defval: c.defval });
|
|
7096
|
+
}
|
|
7097
|
+
return keys;
|
|
7098
|
+
}
|
|
7055
7099
|
function settingsRowVisible(when, bag) {
|
|
7056
7100
|
if (!when) return true;
|
|
7057
7101
|
const conds = Array.isArray(when) ? when : [when];
|
|
@@ -7156,6 +7200,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7156
7200
|
this.barTransform = null;
|
|
7157
7201
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
7158
7202
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
7203
|
+
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
7204
|
+
* replayed into a data engine whenever one is (re)created. */
|
|
7205
|
+
this.typeSettings = /* @__PURE__ */ new Map();
|
|
7159
7206
|
/** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
|
|
7160
7207
|
this.activeEngineStyle = null;
|
|
7161
7208
|
/** The chart's current price style (tracked from the renderer's change events + initial read). */
|
|
@@ -7218,7 +7265,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7218
7265
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
7219
7266
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
7220
7267
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
7221
|
-
this.renderer.onChartTypeSettingsChange?.((typeId, values) =>
|
|
7268
|
+
this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
|
|
7269
|
+
this.typeSettings.set(typeId, values);
|
|
7270
|
+
this.typeEngines.get(typeId)?.onSettings?.(values);
|
|
7271
|
+
});
|
|
7222
7272
|
this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
|
|
7223
7273
|
this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
|
|
7224
7274
|
this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
|
|
@@ -7706,6 +7756,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7706
7756
|
if (!def?.dataEngine) return;
|
|
7707
7757
|
const engine = def.dataEngine();
|
|
7708
7758
|
this.typeEngines.set(id, engine);
|
|
7759
|
+
const stored = this.typeSettings.get(id);
|
|
7760
|
+
if (stored) engine.onSettings?.(stored);
|
|
7709
7761
|
engine.start({
|
|
7710
7762
|
symbol: this.config.market.symbol ?? "TEST",
|
|
7711
7763
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -8177,6 +8229,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8177
8229
|
return {
|
|
8178
8230
|
id: record.id,
|
|
8179
8231
|
title: record.title,
|
|
8232
|
+
...d.shortTitle ? { shorttitle: d.shortTitle } : {},
|
|
8180
8233
|
overlay: d.overlay,
|
|
8181
8234
|
paneHint: d.paneHint,
|
|
8182
8235
|
native: { type: record.native.type },
|
|
@@ -8637,6 +8690,28 @@ var RendererControl = class {
|
|
|
8637
8690
|
setLegendActions(provider) {
|
|
8638
8691
|
this.renderer.setLegendActions?.(provider);
|
|
8639
8692
|
}
|
|
8693
|
+
/**
|
|
8694
|
+
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
8695
|
+
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
8696
|
+
* unfolding rows in place. Silent no-op on a renderer without a foldable legend.
|
|
8697
|
+
*/
|
|
8698
|
+
setLegendOverviewAction(action) {
|
|
8699
|
+
this.renderer.setLegendOverviewAction?.(action);
|
|
8700
|
+
return this;
|
|
8701
|
+
}
|
|
8702
|
+
/** Whether the active renderer can open a per-indicator settings dialog. */
|
|
8703
|
+
get supportsIndicatorSettings() {
|
|
8704
|
+
return typeof this.renderer.openIndicatorSettings === "function";
|
|
8705
|
+
}
|
|
8706
|
+
/**
|
|
8707
|
+
* Open one indicator's settings dialog — the programmatic twin of the legend gear
|
|
8708
|
+
* (see {@link supportsIndicatorSettings}). Silent no-op without the seam or for an
|
|
8709
|
+
* unknown id.
|
|
8710
|
+
*/
|
|
8711
|
+
openIndicatorSettings(indicatorId) {
|
|
8712
|
+
this.renderer.openIndicatorSettings?.(indicatorId);
|
|
8713
|
+
return this;
|
|
8714
|
+
}
|
|
8640
8715
|
/**
|
|
8641
8716
|
* Export the current chart as a PNG data URL, or null if the active renderer
|
|
8642
8717
|
* doesn't support it (warns). DOM overlays (tables, legend) are not included.
|
|
@@ -8683,6 +8758,10 @@ var RendererControl = class {
|
|
|
8683
8758
|
onCrosshairMove(cb) {
|
|
8684
8759
|
return this.renderer.onCrosshairMove(cb);
|
|
8685
8760
|
}
|
|
8761
|
+
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
8762
|
+
onAxisLongPress(cb) {
|
|
8763
|
+
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
8764
|
+
}
|
|
8686
8765
|
/**
|
|
8687
8766
|
* The current data-window readout — the hovered bar's date/time and OHLCV plus every
|
|
8688
8767
|
* indicator's value there, each already formatted on its pane's scale (the latest bar when
|
|
@@ -8748,6 +8827,13 @@ var RendererControl = class {
|
|
|
8748
8827
|
this.renderer.setSettingsSections?.(sections);
|
|
8749
8828
|
return this;
|
|
8750
8829
|
}
|
|
8830
|
+
/** Tell the renderer's own chrome which size class the host shell is in —
|
|
8831
|
+
* `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
|
|
8832
|
+
* Silent no-op on a renderer without adaptive chrome. */
|
|
8833
|
+
setLayoutMode(mode) {
|
|
8834
|
+
this.renderer.setLayoutMode?.(mode);
|
|
8835
|
+
return this;
|
|
8836
|
+
}
|
|
8751
8837
|
};
|
|
8752
8838
|
|
|
8753
8839
|
// src/core/renderer-defaults.ts
|
|
@@ -9699,7 +9785,7 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
9699
9785
|
const doc = anchor.ownerDocument;
|
|
9700
9786
|
tip = doc.createElement("div");
|
|
9701
9787
|
tip.textContent = text;
|
|
9702
|
-
tip.style.cssText = "position:absolute;z-index:
|
|
9788
|
+
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;");
|
|
9703
9789
|
applyChromeTokens(tip, opts.theme());
|
|
9704
9790
|
opts.host.appendChild(tip);
|
|
9705
9791
|
const a = anchor.getBoundingClientRect();
|
|
@@ -9713,16 +9799,17 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
9713
9799
|
tip.style.top = `${a.bottom - h.top + 6}px`;
|
|
9714
9800
|
}
|
|
9715
9801
|
};
|
|
9716
|
-
const arm = () => {
|
|
9802
|
+
const arm = (e) => {
|
|
9803
|
+
if (e.pointerType !== "mouse") return;
|
|
9717
9804
|
clear();
|
|
9718
9805
|
timer = window.setTimeout(show, opts.delayMs ?? 700);
|
|
9719
9806
|
};
|
|
9720
|
-
anchor.addEventListener("
|
|
9721
|
-
anchor.addEventListener("
|
|
9807
|
+
anchor.addEventListener("pointerenter", arm);
|
|
9808
|
+
anchor.addEventListener("pointerleave", clear);
|
|
9722
9809
|
anchor.addEventListener("pointerdown", clear);
|
|
9723
9810
|
return () => {
|
|
9724
|
-
anchor.removeEventListener("
|
|
9725
|
-
anchor.removeEventListener("
|
|
9811
|
+
anchor.removeEventListener("pointerenter", arm);
|
|
9812
|
+
anchor.removeEventListener("pointerleave", clear);
|
|
9726
9813
|
anchor.removeEventListener("pointerdown", clear);
|
|
9727
9814
|
clear();
|
|
9728
9815
|
};
|
|
@@ -9789,14 +9876,22 @@ function makeDialogDraggable(box, header, options) {
|
|
|
9789
9876
|
|
|
9790
9877
|
// src/renderers/shared/InputsUI.ts
|
|
9791
9878
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
9792
|
-
var STATUS_KEYFRAMES = "@keyframes vela-ind-
|
|
9793
|
-
var LEGEND_ICON_PX =
|
|
9879
|
+
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
9880
|
+
var LEGEND_ICON_PX = 16;
|
|
9881
|
+
var LEGEND_CTL_PX = 18;
|
|
9882
|
+
var LEGEND_ROW_PAD_Y = 2;
|
|
9883
|
+
var LEGEND_ROW_PAD_X = 6;
|
|
9884
|
+
var LEGEND_TITLE_VALUES_GAP_PX = 6;
|
|
9885
|
+
var LEGEND_ACTION_GAP_PX = 6;
|
|
9886
|
+
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;`;
|
|
9887
|
+
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
9794
9888
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
|
|
9795
9889
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
|
|
9796
9890
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
|
|
9797
|
-
var CLOSE_SVG = iconAt("close",
|
|
9891
|
+
var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
|
|
9798
9892
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
|
|
9799
9893
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
|
|
9894
|
+
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
|
|
9800
9895
|
var InputsUI = class {
|
|
9801
9896
|
constructor(container, theme, paneBoundsOf) {
|
|
9802
9897
|
this.container = container;
|
|
@@ -9817,6 +9912,8 @@ var InputsUI = class {
|
|
|
9817
9912
|
this.onToggleVisible = null;
|
|
9818
9913
|
/** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
|
|
9819
9914
|
this.symbolPicker = null;
|
|
9915
|
+
/** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
|
|
9916
|
+
this.mobileLayout = false;
|
|
9820
9917
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
9821
9918
|
this.moveApi = null;
|
|
9822
9919
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
@@ -9838,9 +9935,18 @@ var InputsUI = class {
|
|
|
9838
9935
|
/** Titles switched off entirely (a settings toggle): every pane's legend container
|
|
9839
9936
|
* hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
|
|
9840
9937
|
this.titlesVisible = true;
|
|
9938
|
+
/** Plot values shown beside the titles chart-wide (a settings toggle); a row's own
|
|
9939
|
+
* context-menu choice ({@link LegendRow.showValues}) overrides it per indicator. */
|
|
9940
|
+
this.valuesVisible = true;
|
|
9941
|
+
/** Open legend-row context menu (kept so it can be torn down). */
|
|
9942
|
+
this.rowMenu = null;
|
|
9841
9943
|
/** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
|
|
9842
9944
|
* rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
|
|
9843
9945
|
this.foldToggle = null;
|
|
9946
|
+
/** Host-provided replacement for the fold toggle (multi-chart shells route it to
|
|
9947
|
+
* their indicator overview, e.g. an object tree). While set, the legend stays
|
|
9948
|
+
* folded and the chip runs this instead of unfolding inline. */
|
|
9949
|
+
this.overviewAction = null;
|
|
9844
9950
|
/** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
|
|
9845
9951
|
this.onDialogKey = (e) => {
|
|
9846
9952
|
if (e.key === "Escape") {
|
|
@@ -9864,6 +9970,9 @@ var InputsUI = class {
|
|
|
9864
9970
|
this.onMoveMenuOutside = (e) => {
|
|
9865
9971
|
if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
|
|
9866
9972
|
};
|
|
9973
|
+
this.onRowMenuOutside = (e) => {
|
|
9974
|
+
if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
|
|
9975
|
+
};
|
|
9867
9976
|
if (!container.style.position) container.style.position = "relative";
|
|
9868
9977
|
if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
|
|
9869
9978
|
}
|
|
@@ -9871,6 +9980,36 @@ var InputsUI = class {
|
|
|
9871
9980
|
this.dialogHost = host;
|
|
9872
9981
|
if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
|
|
9873
9982
|
}
|
|
9983
|
+
/** The host shell's chrome size class — mobile makes the inputs dialog fullscreen
|
|
9984
|
+
* (an open dialog re-presents on the flip) and folds the legend behind its chip by
|
|
9985
|
+
* default (a phone-width plot has no room for the rows). */
|
|
9986
|
+
setLayoutMode(mode) {
|
|
9987
|
+
const mobile = mode === "mobile";
|
|
9988
|
+
if (mobile === this.mobileLayout) return;
|
|
9989
|
+
this.mobileLayout = mobile;
|
|
9990
|
+
if (this.overviewAction === null) this.legendFolded = mobile;
|
|
9991
|
+
this.syncFoldToggle();
|
|
9992
|
+
if (this.openId !== null) {
|
|
9993
|
+
const id = this.openId;
|
|
9994
|
+
this.closeOpenDialog();
|
|
9995
|
+
this.openDialog(id);
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
/** Replace the fold toggle's behavior with a HOST action (or restore it with null):
|
|
9999
|
+
* the chip stays — objects icon + indicator count — but a press runs the action
|
|
10000
|
+
* (a multi-chart shell opens its indicator overview) instead of unfolding the rows,
|
|
10001
|
+
* which stay hidden while the override is in force. */
|
|
10002
|
+
setLegendOverviewAction(action) {
|
|
10003
|
+
if (action === this.overviewAction) return;
|
|
10004
|
+
this.overviewAction = action;
|
|
10005
|
+
this.legendFolded = action !== null ? true : this.mobileLayout;
|
|
10006
|
+
this.syncFoldToggle();
|
|
10007
|
+
}
|
|
10008
|
+
/** Open the settings dialog of one indicator (the legend gear's programmatic twin) —
|
|
10009
|
+
* no-op for an unknown id. */
|
|
10010
|
+
openSettingsFor(id) {
|
|
10011
|
+
if (this.rows.has(id)) this.openDialog(id);
|
|
10012
|
+
}
|
|
9874
10013
|
/** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
|
|
9875
10014
|
tip(store, id, anchor, text, wrap = false) {
|
|
9876
10015
|
const list = store.get(id) ?? [];
|
|
@@ -9898,12 +10037,12 @@ var InputsUI = class {
|
|
|
9898
10037
|
}
|
|
9899
10038
|
setTheme(theme) {
|
|
9900
10039
|
this.theme = theme;
|
|
10040
|
+
for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
|
|
9901
10041
|
for (const row of this.rows.values()) {
|
|
9902
|
-
|
|
9903
|
-
row.el.style.background = open ? theme.background : this.idleRowFill();
|
|
10042
|
+
row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
|
|
9904
10043
|
row.el.style.color = theme.textColor;
|
|
9905
10044
|
}
|
|
9906
|
-
if (this.foldToggle) this.
|
|
10045
|
+
if (this.foldToggle) this.syncFoldToggle();
|
|
9907
10046
|
}
|
|
9908
10047
|
/** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
|
|
9909
10048
|
setSymbolPicker(picker) {
|
|
@@ -9934,7 +10073,7 @@ var InputsUI = class {
|
|
|
9934
10073
|
btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
|
|
9935
10074
|
btn.className = "vela-ind-ctl";
|
|
9936
10075
|
btn.dataset.legendAction = action.id;
|
|
9937
|
-
btn.style.cssText =
|
|
10076
|
+
btn.style.cssText = LEGEND_CTL_CSS;
|
|
9938
10077
|
btn.addEventListener("click", (e) => {
|
|
9939
10078
|
e.stopPropagation();
|
|
9940
10079
|
action.run();
|
|
@@ -9952,6 +10091,54 @@ var InputsUI = class {
|
|
|
9952
10091
|
this.titlesVisible = visible;
|
|
9953
10092
|
this.reposition();
|
|
9954
10093
|
}
|
|
10094
|
+
/**
|
|
10095
|
+
* Show/hide the plot values chart-wide (the settings dialog's Indicators → Values
|
|
10096
|
+
* toggle). "All" is meant literally: per-row context-menu overrides are cleared, so
|
|
10097
|
+
* every legend follows the new state.
|
|
10098
|
+
*/
|
|
10099
|
+
setValuesVisible(visible) {
|
|
10100
|
+
this.valuesVisible = visible;
|
|
10101
|
+
for (const row of this.rows.values()) {
|
|
10102
|
+
row.showValues = null;
|
|
10103
|
+
this.renderRowValues(row);
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
/**
|
|
10107
|
+
* Push the current plot values of every indicator at once (the renderer calls this per
|
|
10108
|
+
* paint). Rows absent from the map (e.g. a hidden indicator) show no values. Cheap on
|
|
10109
|
+
* unchanged frames: each row diffs against its last rendered values before touching DOM.
|
|
10110
|
+
*/
|
|
10111
|
+
setPlotValues(values) {
|
|
10112
|
+
for (const row of this.rows.values()) {
|
|
10113
|
+
const next = values.get(row.id) ?? [];
|
|
10114
|
+
const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
|
|
10115
|
+
if (key === row.plotValuesKey) continue;
|
|
10116
|
+
row.plotValues = next;
|
|
10117
|
+
row.plotValuesKey = key;
|
|
10118
|
+
this.renderRowValues(row);
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
10121
|
+
/** Whether a row currently shows its values: its own override, else the chart-wide flag. */
|
|
10122
|
+
rowValuesShown(row) {
|
|
10123
|
+
return row.showValues ?? this.valuesVisible;
|
|
10124
|
+
}
|
|
10125
|
+
/** (Re)build one row's values readout from its stored plot values + visibility. A
|
|
10126
|
+
* highlighted (hovered/selected) row shows only the title + controls — no values. */
|
|
10127
|
+
renderRowValues(row) {
|
|
10128
|
+
if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
|
|
10129
|
+
row.valuesEl.style.display = "none";
|
|
10130
|
+
row.valuesEl.replaceChildren();
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
row.valuesEl.style.display = "inline-flex";
|
|
10134
|
+
row.valuesEl.replaceChildren();
|
|
10135
|
+
for (const v of row.plotValues) {
|
|
10136
|
+
const span = document.createElement("span");
|
|
10137
|
+
span.textContent = v.value;
|
|
10138
|
+
span.style.color = v.color;
|
|
10139
|
+
row.valuesEl.appendChild(span);
|
|
10140
|
+
}
|
|
10141
|
+
}
|
|
9955
10142
|
// ── legend move/merge (menu + drag) ─────────────────────────────────────
|
|
9956
10143
|
/** Open the "Move to" menu for a row, anchored under its move button. */
|
|
9957
10144
|
openMoveMenu(id, anchor) {
|
|
@@ -10003,6 +10190,48 @@ var InputsUI = class {
|
|
|
10003
10190
|
this.moveMenu?.remove();
|
|
10004
10191
|
this.moveMenu = null;
|
|
10005
10192
|
}
|
|
10193
|
+
// ── legend row context menu (right-click) ───────────────────────────────
|
|
10194
|
+
/** Open the row's action menu at the pointer. One entry today: the values toggle. */
|
|
10195
|
+
openRowMenu(id, x, y) {
|
|
10196
|
+
this.closeRowMenu();
|
|
10197
|
+
this.closeMoveMenu();
|
|
10198
|
+
const row = this.rows.get(id);
|
|
10199
|
+
if (!row) return;
|
|
10200
|
+
const menu = document.createElement("div");
|
|
10201
|
+
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);`;
|
|
10202
|
+
applyChromeTokens(menu, this.theme);
|
|
10203
|
+
const item = document.createElement("button");
|
|
10204
|
+
item.type = "button";
|
|
10205
|
+
item.className = "vela-ind-menuitem";
|
|
10206
|
+
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;";
|
|
10207
|
+
const check = document.createElement("span");
|
|
10208
|
+
check.style.cssText = "display:inline-flex;align-items:center;justify-content:center;width:14px;flex:none;line-height:0;";
|
|
10209
|
+
check.innerHTML = this.rowValuesShown(row) ? CHECK_SVG : "";
|
|
10210
|
+
const label = document.createElement("span");
|
|
10211
|
+
label.textContent = "Indicator values";
|
|
10212
|
+
item.append(check, label);
|
|
10213
|
+
item.addEventListener("click", (e) => {
|
|
10214
|
+
e.stopPropagation();
|
|
10215
|
+
this.closeRowMenu();
|
|
10216
|
+
row.showValues = !this.rowValuesShown(row);
|
|
10217
|
+
this.renderRowValues(row);
|
|
10218
|
+
});
|
|
10219
|
+
menu.appendChild(item);
|
|
10220
|
+
document.body.appendChild(menu);
|
|
10221
|
+
const mw = menu.offsetWidth || 180;
|
|
10222
|
+
const mh = menu.offsetHeight || 40;
|
|
10223
|
+
menu.style.left = `${Math.min(x, window.innerWidth - mw - 6)}px`;
|
|
10224
|
+
menu.style.top = `${Math.min(y, window.innerHeight - mh - 6)}px`;
|
|
10225
|
+
this.rowMenu = menu;
|
|
10226
|
+
setTimeout(() => {
|
|
10227
|
+
if (typeof document !== "undefined") document.addEventListener("pointerdown", this.onRowMenuOutside, true);
|
|
10228
|
+
}, 0);
|
|
10229
|
+
}
|
|
10230
|
+
closeRowMenu() {
|
|
10231
|
+
if (typeof document !== "undefined") document.removeEventListener("pointerdown", this.onRowMenuOutside, true);
|
|
10232
|
+
this.rowMenu?.remove();
|
|
10233
|
+
this.rowMenu = null;
|
|
10234
|
+
}
|
|
10006
10235
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
10007
10236
|
setPane(id, paneId) {
|
|
10008
10237
|
const row = this.rows.get(id);
|
|
@@ -10024,7 +10253,8 @@ var InputsUI = class {
|
|
|
10024
10253
|
if (!lg) {
|
|
10025
10254
|
lg = document.createElement("div");
|
|
10026
10255
|
lg.dataset.velaPane = paneId;
|
|
10027
|
-
lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:
|
|
10256
|
+
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;";
|
|
10257
|
+
applyChromeTokens(lg, this.theme);
|
|
10028
10258
|
this.positionLegend(lg, paneId);
|
|
10029
10259
|
this.container.appendChild(lg);
|
|
10030
10260
|
this.legends.set(paneId, lg);
|
|
@@ -10060,15 +10290,16 @@ var InputsUI = class {
|
|
|
10060
10290
|
*/
|
|
10061
10291
|
syncFoldToggle() {
|
|
10062
10292
|
const count = this.rows.size;
|
|
10063
|
-
|
|
10293
|
+
const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
|
|
10294
|
+
if (count < minRows) {
|
|
10064
10295
|
const wasFolded = this.legendFolded;
|
|
10065
|
-
this.legendFolded = false;
|
|
10296
|
+
if (count > 0) this.legendFolded = false;
|
|
10066
10297
|
if (this.foldToggle) {
|
|
10067
10298
|
this.foldToggle.remove();
|
|
10068
10299
|
this.foldToggle = null;
|
|
10069
10300
|
this.disposeTips(this.rowTips, "fold");
|
|
10070
10301
|
}
|
|
10071
|
-
if (wasFolded) this.reposition();
|
|
10302
|
+
if (wasFolded && count > 0) this.reposition();
|
|
10072
10303
|
return;
|
|
10073
10304
|
}
|
|
10074
10305
|
let btn = this.foldToggle;
|
|
@@ -10076,25 +10307,30 @@ var InputsUI = class {
|
|
|
10076
10307
|
btn = document.createElement("button");
|
|
10077
10308
|
btn.type = "button";
|
|
10078
10309
|
btn.className = "vela-ind-fold";
|
|
10079
|
-
this.tip(this.rowTips, "fold", btn, () => this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
|
|
10310
|
+
this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
|
|
10080
10311
|
btn.addEventListener("click", () => {
|
|
10312
|
+
if (this.overviewAction !== null) {
|
|
10313
|
+
this.overviewAction();
|
|
10314
|
+
return;
|
|
10315
|
+
}
|
|
10081
10316
|
this.legendFolded = !this.legendFolded;
|
|
10082
10317
|
this.syncFoldToggle();
|
|
10083
10318
|
});
|
|
10084
10319
|
this.foldToggle = btn;
|
|
10085
10320
|
}
|
|
10086
10321
|
const folded = this.legendFolded;
|
|
10087
|
-
btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
|
|
10088
|
-
|
|
10322
|
+
btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
|
|
10323
|
+
const ink = this.theme.textColor;
|
|
10324
|
+
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;`;
|
|
10089
10325
|
btn.replaceChildren();
|
|
10090
10326
|
const icon2 = document.createElement("span");
|
|
10091
|
-
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
|
|
10092
|
-
icon2.innerHTML = folded ? UNFOLD_SVG : FOLD_SVG;
|
|
10327
|
+
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
|
|
10328
|
+
icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
|
|
10093
10329
|
btn.appendChild(icon2);
|
|
10094
10330
|
if (folded) {
|
|
10095
10331
|
const label = document.createElement("span");
|
|
10096
10332
|
label.textContent = String(count);
|
|
10097
|
-
label.style.cssText =
|
|
10333
|
+
label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
|
|
10098
10334
|
btn.appendChild(label);
|
|
10099
10335
|
}
|
|
10100
10336
|
this.legendFor("price").appendChild(btn);
|
|
@@ -10102,9 +10338,11 @@ var InputsUI = class {
|
|
|
10102
10338
|
}
|
|
10103
10339
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
10104
10340
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
10341
|
+
const settingsTitle = opts.settingsTitle ?? title;
|
|
10105
10342
|
const existing = this.rows.get(id);
|
|
10106
10343
|
if (existing) {
|
|
10107
10344
|
existing.title = title;
|
|
10345
|
+
existing.settingsTitle = settingsTitle;
|
|
10108
10346
|
existing.inputs = inputs;
|
|
10109
10347
|
existing.values = { ...values };
|
|
10110
10348
|
existing.titleEl.textContent = title;
|
|
@@ -10117,8 +10355,7 @@ var InputsUI = class {
|
|
|
10117
10355
|
}
|
|
10118
10356
|
ensureDialogStyles();
|
|
10119
10357
|
const el = document.createElement("div");
|
|
10120
|
-
el.style.cssText = `pointer-events:auto;display:flex;align-items:center;
|
|
10121
|
-
el.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
|
|
10358
|
+
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;`;
|
|
10122
10359
|
el.addEventListener("mouseleave", () => {
|
|
10123
10360
|
if (this.selectedId !== id) this.setRowHighlighted(id, false);
|
|
10124
10361
|
});
|
|
@@ -10138,11 +10375,16 @@ var InputsUI = class {
|
|
|
10138
10375
|
e.preventDefault();
|
|
10139
10376
|
this.onRemove?.(id);
|
|
10140
10377
|
});
|
|
10378
|
+
el.addEventListener("contextmenu", (e) => {
|
|
10379
|
+
e.preventDefault();
|
|
10380
|
+
e.stopPropagation();
|
|
10381
|
+
this.openRowMenu(id, e.clientX, e.clientY);
|
|
10382
|
+
});
|
|
10141
10383
|
const statusEl = document.createElement("span");
|
|
10142
10384
|
statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
10143
|
-
el.appendChild(statusEl);
|
|
10144
10385
|
const titleWrap = document.createElement("span");
|
|
10145
10386
|
titleWrap.style.cssText = "white-space:nowrap;";
|
|
10387
|
+
titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
|
|
10146
10388
|
const titleEl = document.createElement("span");
|
|
10147
10389
|
titleEl.textContent = title;
|
|
10148
10390
|
titleEl.style.cssText = "font-weight:600;";
|
|
@@ -10154,6 +10396,11 @@ var InputsUI = class {
|
|
|
10154
10396
|
titleWrap.appendChild(beta);
|
|
10155
10397
|
}
|
|
10156
10398
|
el.appendChild(titleWrap);
|
|
10399
|
+
const valuesEl = document.createElement("span");
|
|
10400
|
+
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;`;
|
|
10401
|
+
el.appendChild(valuesEl);
|
|
10402
|
+
const controlsEl = document.createElement("span");
|
|
10403
|
+
controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
|
|
10157
10404
|
let eyeEl = null;
|
|
10158
10405
|
if (this.onToggleVisible) {
|
|
10159
10406
|
const eye = document.createElement("button");
|
|
@@ -10162,16 +10409,14 @@ var InputsUI = class {
|
|
|
10162
10409
|
this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
|
|
10163
10410
|
eye.innerHTML = EYE_SVG;
|
|
10164
10411
|
eye.className = "vela-ind-ctl";
|
|
10165
|
-
eye.style.cssText =
|
|
10412
|
+
eye.style.cssText = LEGEND_CTL_CSS;
|
|
10166
10413
|
eye.addEventListener("click", () => {
|
|
10167
10414
|
const row = this.rows.get(id);
|
|
10168
10415
|
this.onToggleVisible?.(id, Boolean(row?.hidden));
|
|
10169
10416
|
});
|
|
10170
|
-
|
|
10417
|
+
controlsEl.appendChild(eye);
|
|
10171
10418
|
eyeEl = eye;
|
|
10172
10419
|
}
|
|
10173
|
-
const controlsEl = document.createElement("span");
|
|
10174
|
-
controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
|
|
10175
10420
|
if (inputs.length > 0) {
|
|
10176
10421
|
const gear = document.createElement("button");
|
|
10177
10422
|
gear.type = "button";
|
|
@@ -10179,7 +10424,7 @@ var InputsUI = class {
|
|
|
10179
10424
|
this.tip(this.rowTips, id, gear, "Settings");
|
|
10180
10425
|
gear.innerHTML = GEAR_SVG;
|
|
10181
10426
|
gear.className = "vela-ind-ctl";
|
|
10182
|
-
gear.style.cssText =
|
|
10427
|
+
gear.style.cssText = LEGEND_CTL_CSS;
|
|
10183
10428
|
gear.addEventListener("click", () => this.openDialog(id));
|
|
10184
10429
|
controlsEl.appendChild(gear);
|
|
10185
10430
|
}
|
|
@@ -10190,7 +10435,7 @@ var InputsUI = class {
|
|
|
10190
10435
|
this.tip(this.rowTips, id, mv, "Move to pane");
|
|
10191
10436
|
mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
|
|
10192
10437
|
mv.className = "vela-ind-ctl";
|
|
10193
|
-
mv.style.cssText =
|
|
10438
|
+
mv.style.cssText = LEGEND_CTL_CSS;
|
|
10194
10439
|
mv.addEventListener("click", (e) => {
|
|
10195
10440
|
e.stopPropagation();
|
|
10196
10441
|
this.openMoveMenu(id, mv);
|
|
@@ -10207,12 +10452,13 @@ var InputsUI = class {
|
|
|
10207
10452
|
this.tip(this.rowTips, id, close, "Remove indicator");
|
|
10208
10453
|
close.innerHTML = CLOSE_SVG;
|
|
10209
10454
|
close.className = "vela-ind-close";
|
|
10210
|
-
close.style.cssText =
|
|
10455
|
+
close.style.cssText = LEGEND_CTL_CSS;
|
|
10211
10456
|
close.addEventListener("click", () => this.onRemove?.(id));
|
|
10212
10457
|
controlsEl.appendChild(close);
|
|
10213
10458
|
el.appendChild(controlsEl);
|
|
10459
|
+
el.appendChild(statusEl);
|
|
10214
10460
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
10215
|
-
this.rows.set(id, { id, title, inputs, values: { ...values }, el, titleEl, statusEl, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
|
|
10461
|
+
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 });
|
|
10216
10462
|
this.syncFoldToggle();
|
|
10217
10463
|
}
|
|
10218
10464
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -10226,24 +10472,31 @@ var InputsUI = class {
|
|
|
10226
10472
|
if (row) row.values = { ...row.values, ...values };
|
|
10227
10473
|
}
|
|
10228
10474
|
/**
|
|
10229
|
-
* Reflect an indicator's live status in its legend row: `'loading'` shows
|
|
10230
|
-
* in flight
|
|
10475
|
+
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
10476
|
+
* dots (a fetch is in flight — the same load affordance as the chart's own bar-load dots,
|
|
10477
|
+
* at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
|
|
10231
10478
|
*/
|
|
10232
10479
|
setStatus(id, status) {
|
|
10233
10480
|
const row = this.rows.get(id);
|
|
10234
10481
|
if (!row) return;
|
|
10235
10482
|
const el = row.statusEl;
|
|
10483
|
+
el.replaceChildren();
|
|
10236
10484
|
if (status === "idle") {
|
|
10237
|
-
el.style.
|
|
10238
|
-
el.style.animation = "none";
|
|
10485
|
+
el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
10239
10486
|
return;
|
|
10240
10487
|
}
|
|
10241
|
-
this.ensureStatusKeyframes();
|
|
10242
10488
|
if (status === "loading") {
|
|
10243
|
-
el.style.cssText =
|
|
10244
|
-
|
|
10245
|
-
|
|
10489
|
+
el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
|
|
10490
|
+
for (let i = 0; i < 3; i += 1) {
|
|
10491
|
+
const dot = document.createElement("span");
|
|
10492
|
+
dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
|
|
10493
|
+
dot.animate?.([{ opacity: 0.12 }, { opacity: 0.55 }], { duration: 800, iterations: Infinity, direction: "alternate", easing: "ease-in-out", delay: -i * 260 });
|
|
10494
|
+
el.appendChild(dot);
|
|
10495
|
+
}
|
|
10496
|
+
return;
|
|
10246
10497
|
}
|
|
10498
|
+
this.ensureStatusKeyframes();
|
|
10499
|
+
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;`;
|
|
10247
10500
|
}
|
|
10248
10501
|
/** Inject the status keyframes once (idempotent). */
|
|
10249
10502
|
ensureStatusKeyframes() {
|
|
@@ -10262,8 +10515,8 @@ var InputsUI = class {
|
|
|
10262
10515
|
if (row.eyeEl) {
|
|
10263
10516
|
row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
|
|
10264
10517
|
row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
|
|
10265
|
-
row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
|
|
10266
10518
|
}
|
|
10519
|
+
this.syncRowActions(row);
|
|
10267
10520
|
}
|
|
10268
10521
|
/**
|
|
10269
10522
|
* Select a legend row (outlining it) and clear any previous selection, so at most one
|
|
@@ -10276,17 +10529,36 @@ var InputsUI = class {
|
|
|
10276
10529
|
this.setRowHighlighted(id, true);
|
|
10277
10530
|
}
|
|
10278
10531
|
/**
|
|
10279
|
-
* Show or hide a row's outline and eye/gear/✕ controls together — driven by
|
|
10280
|
-
*
|
|
10281
|
-
*
|
|
10532
|
+
* Show or hide a row's outline and eye/gear/✕ controls together — driven by title
|
|
10533
|
+
* hover and selection (plot values do not open the chip). An idle unselected row
|
|
10534
|
+
* shows just its title (+ values). Exception: a hidden indicator keeps its eye
|
|
10535
|
+
* visible even when idle, so it can be un-hidden without hovering.
|
|
10282
10536
|
*/
|
|
10283
10537
|
setRowHighlighted(id, highlighted) {
|
|
10284
10538
|
const row = this.rows.get(id);
|
|
10285
10539
|
if (!row) return;
|
|
10540
|
+
row.highlighted = highlighted;
|
|
10541
|
+
this.renderRowValues(row);
|
|
10286
10542
|
row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
|
|
10287
10543
|
row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
|
|
10288
|
-
row
|
|
10289
|
-
|
|
10544
|
+
this.syncRowActions(row);
|
|
10545
|
+
}
|
|
10546
|
+
/**
|
|
10547
|
+
* Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
|
|
10548
|
+
* buttons only appear while highlighted; the eye keeps its inline-flex from
|
|
10549
|
+
* construction and is shown/hidden with the cluster container.
|
|
10550
|
+
*/
|
|
10551
|
+
syncRowActions(row) {
|
|
10552
|
+
const open = row.highlighted;
|
|
10553
|
+
row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
|
|
10554
|
+
for (const child of Array.from(row.controlsEl.children)) {
|
|
10555
|
+
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
10556
|
+
if (child === row.extrasEl) {
|
|
10557
|
+
child.style.display = open ? "contents" : "none";
|
|
10558
|
+
continue;
|
|
10559
|
+
}
|
|
10560
|
+
child.style.display = open ? "inline-flex" : "none";
|
|
10561
|
+
}
|
|
10290
10562
|
}
|
|
10291
10563
|
/** Drop the current selection outline, if any. */
|
|
10292
10564
|
clearSelection() {
|
|
@@ -10314,6 +10586,7 @@ var InputsUI = class {
|
|
|
10314
10586
|
destroy() {
|
|
10315
10587
|
this.closeDialog();
|
|
10316
10588
|
this.closeMoveMenu();
|
|
10589
|
+
this.closeRowMenu();
|
|
10317
10590
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
10318
10591
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
10319
10592
|
for (const lg of this.legends.values()) lg.remove();
|
|
@@ -10369,6 +10642,7 @@ var InputsUI = class {
|
|
|
10369
10642
|
const card = document.createElement("div");
|
|
10370
10643
|
card.className = "vela-ind-dialog";
|
|
10371
10644
|
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;`;
|
|
10645
|
+
if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
|
|
10372
10646
|
applyChromeTokens(card, t);
|
|
10373
10647
|
card.addEventListener("keydown", (e) => {
|
|
10374
10648
|
if (e.key !== "Escape") e.stopPropagation();
|
|
@@ -10376,7 +10650,7 @@ var InputsUI = class {
|
|
|
10376
10650
|
const header = document.createElement("div");
|
|
10377
10651
|
header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
|
|
10378
10652
|
const hTitle = document.createElement("span");
|
|
10379
|
-
hTitle.textContent = row.
|
|
10653
|
+
hTitle.textContent = row.settingsTitle;
|
|
10380
10654
|
hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
|
|
10381
10655
|
const closeBtn = document.createElement("button");
|
|
10382
10656
|
closeBtn.type = "button";
|
|
@@ -10388,7 +10662,7 @@ var InputsUI = class {
|
|
|
10388
10662
|
closeBtn.addEventListener("click", () => this.closeDialog());
|
|
10389
10663
|
header.append(hTitle, closeBtn);
|
|
10390
10664
|
card.appendChild(header);
|
|
10391
|
-
makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
10665
|
+
if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
10392
10666
|
const tabs = document.createElement("div");
|
|
10393
10667
|
tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
10394
10668
|
const tab = document.createElement("div");
|
|
@@ -10408,7 +10682,7 @@ var InputsUI = class {
|
|
|
10408
10682
|
section.appendChild(gh);
|
|
10409
10683
|
}
|
|
10410
10684
|
const grid = document.createElement("div");
|
|
10411
|
-
grid.style.cssText = "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
|
|
10685
|
+
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;";
|
|
10412
10686
|
for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
|
|
10413
10687
|
section.appendChild(grid);
|
|
10414
10688
|
body.appendChild(section);
|
|
@@ -10857,10 +11131,14 @@ function groupInputs(inputs) {
|
|
|
10857
11131
|
var CHECK_SVG = iconAt("check", 12);
|
|
10858
11132
|
var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
|
|
10859
11133
|
var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
|
|
11134
|
+
var DIALOG_STYLE_REV = "10";
|
|
10860
11135
|
function ensureDialogStyles() {
|
|
10861
|
-
if (typeof document === "undefined"
|
|
10862
|
-
const
|
|
11136
|
+
if (typeof document === "undefined") return;
|
|
11137
|
+
const existing = document.getElementById(DIALOG_STYLE_ID);
|
|
11138
|
+
if (existing?.dataset.rev === DIALOG_STYLE_REV) return;
|
|
11139
|
+
const s = existing ?? document.createElement("style");
|
|
10863
11140
|
s.id = DIALOG_STYLE_ID;
|
|
11141
|
+
s.dataset.rev = DIALOG_STYLE_REV;
|
|
10864
11142
|
s.textContent = `
|
|
10865
11143
|
.vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
|
|
10866
11144
|
.vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -10874,19 +11152,19 @@ function ensureDialogStyles() {
|
|
|
10874
11152
|
.vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
|
|
10875
11153
|
.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;}
|
|
10876
11154
|
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
10877
|
-
.vela-ind-ctl{color:var(--vela-
|
|
10878
|
-
.vela-ind-ctl
|
|
10879
|
-
.vela-ind-
|
|
10880
|
-
.vela-ind-close:hover{color:var(--vela-danger) !important;}
|
|
10881
|
-
.vela-ind-fold{
|
|
10882
|
-
.vela-ind-fold:hover{
|
|
11155
|
+
.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;}
|
|
11156
|
+
.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;}
|
|
11157
|
+
.vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
|
|
11158
|
+
.vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
|
|
11159
|
+
.vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
|
|
11160
|
+
.vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
|
|
10883
11161
|
.vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
10884
11162
|
.vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
|
|
10885
11163
|
.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;}
|
|
10886
11164
|
.vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
10887
11165
|
.vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
10888
11166
|
.vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
|
|
10889
|
-
document.head.appendChild(s);
|
|
11167
|
+
if (!existing) document.head.appendChild(s);
|
|
10890
11168
|
}
|
|
10891
11169
|
var TIMEFRAME_OPTIONS = [
|
|
10892
11170
|
{ value: "", label: "Chart" },
|
|
@@ -11579,14 +11857,7 @@ function factoryResetConfig(factory) {
|
|
|
11579
11857
|
const defaults2 = {};
|
|
11580
11858
|
const addRows = (rows) => {
|
|
11581
11859
|
for (const r of rows ?? []) {
|
|
11582
|
-
|
|
11583
|
-
if (r.kind === "range") {
|
|
11584
|
-
defaults2[r.minKey] = r.defval;
|
|
11585
|
-
defaults2[r.maxKey] = r.defval;
|
|
11586
|
-
continue;
|
|
11587
|
-
}
|
|
11588
|
-
defaults2[r.key] = r.defval;
|
|
11589
|
-
if (r.kind === "toggle") for (const c of r.colors ?? []) defaults2[c.key] = c.defval;
|
|
11860
|
+
for (const k of settingsRowValueKeys(r)) defaults2[k.key] = k.defval;
|
|
11590
11861
|
}
|
|
11591
11862
|
};
|
|
11592
11863
|
for (const inst of section.instances ?? []) {
|
|
@@ -13730,6 +14001,11 @@ function easeToward(current, target, dtMs, tauMs) {
|
|
|
13730
14001
|
var FLING_MIN_SPEED = 0.04;
|
|
13731
14002
|
var FLING_STALE_MS = 60;
|
|
13732
14003
|
var DRAG_SLOP = 2;
|
|
14004
|
+
var LONG_PRESS_MS = 350;
|
|
14005
|
+
var TOUCH_SLOP = 8;
|
|
14006
|
+
var TOUCH_TAP_SLOP = 14;
|
|
14007
|
+
var DOUBLE_TAP_MS = 350;
|
|
14008
|
+
var DOUBLE_TAP_SLOP = 30;
|
|
13733
14009
|
var TIME_SCALE_K = 4e-3;
|
|
13734
14010
|
var WHEEL_ZOOM_K = 25e-4;
|
|
13735
14011
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
@@ -13750,6 +14026,12 @@ function wheelPanDelta(deltaX, deltaY, shift) {
|
|
|
13750
14026
|
function effectiveSnapMode(momentaryOverride, sticky) {
|
|
13751
14027
|
return momentaryOverride ? "strong" : sticky;
|
|
13752
14028
|
}
|
|
14029
|
+
function pinchBarSpacing(startBarSpacing, startDist, dist) {
|
|
14030
|
+
return clampBarSpacing(startBarSpacing * (dist / Math.max(1, startDist)));
|
|
14031
|
+
}
|
|
14032
|
+
function pinchPinnedRightOffset(anchorLogical, barCount, width, x, pxPerBar) {
|
|
14033
|
+
return anchorLogical - (barCount - 1) + (width - x) / pxPerBar;
|
|
14034
|
+
}
|
|
13753
14035
|
var InputController = class {
|
|
13754
14036
|
constructor(deps) {
|
|
13755
14037
|
this.deps = deps;
|
|
@@ -13783,6 +14065,23 @@ var InputController = class {
|
|
|
13783
14065
|
// press/release re-shape the drawings preview with the pointer stationary.
|
|
13784
14066
|
this.cursorX = NaN;
|
|
13785
14067
|
this.cursorY = NaN;
|
|
14068
|
+
// ── touch state ──
|
|
14069
|
+
/** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
|
|
14070
|
+
this.touches = /* @__PURE__ */ new Map();
|
|
14071
|
+
this.lpTimer = null;
|
|
14072
|
+
this.pinchStartDist = 1;
|
|
14073
|
+
this.pinchStartBarSpacing = 0;
|
|
14074
|
+
this.pinchAnchorLogical = 0;
|
|
14075
|
+
// Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
|
|
14076
|
+
this.lastTapT = 0;
|
|
14077
|
+
this.lastTapX = 0;
|
|
14078
|
+
this.lastTapY = 0;
|
|
14079
|
+
this.onTouchContextMenu = (e) => {
|
|
14080
|
+
if (this.touches.size === 0) return;
|
|
14081
|
+
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
14082
|
+
e.preventDefault();
|
|
14083
|
+
e.stopImmediatePropagation();
|
|
14084
|
+
};
|
|
13786
14085
|
this.onMiddleGuard = (e) => {
|
|
13787
14086
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
13788
14087
|
};
|
|
@@ -13804,6 +14103,15 @@ var InputController = class {
|
|
|
13804
14103
|
}
|
|
13805
14104
|
if (e.button !== 0) return;
|
|
13806
14105
|
const { x, y } = this.local(e);
|
|
14106
|
+
if (e.pointerType === "touch") {
|
|
14107
|
+
if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
|
|
14108
|
+
this.touches.set(e.pointerId, { x, y });
|
|
14109
|
+
this.beginPinch(e);
|
|
14110
|
+
return;
|
|
14111
|
+
}
|
|
14112
|
+
if (this.touches.size >= 1) return;
|
|
14113
|
+
this.touches.set(e.pointerId, { x, y });
|
|
14114
|
+
}
|
|
13807
14115
|
this.deps.drawingsClearTransient?.();
|
|
13808
14116
|
this.dragging = true;
|
|
13809
14117
|
this.moved = false;
|
|
@@ -13829,6 +14137,9 @@ var InputController = class {
|
|
|
13829
14137
|
if (this.region === "price") this.deps.beginPriceScale(x, y);
|
|
13830
14138
|
else if (this.region === "separator") this.deps.beginPaneResize(y);
|
|
13831
14139
|
else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
|
|
14140
|
+
if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
|
|
14141
|
+
this.startLongPress(x, y, this.region);
|
|
14142
|
+
}
|
|
13832
14143
|
this.lastX = x;
|
|
13833
14144
|
this.lastT = e.timeStamp;
|
|
13834
14145
|
this.vx = 0;
|
|
@@ -13838,28 +14149,42 @@ var InputController = class {
|
|
|
13838
14149
|
const { x, y } = this.local(e);
|
|
13839
14150
|
this.cursorX = x;
|
|
13840
14151
|
this.cursorY = y;
|
|
14152
|
+
if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
|
|
14153
|
+
if (this.dragging && this.region === "pinch") {
|
|
14154
|
+
this.applyPinch();
|
|
14155
|
+
return;
|
|
14156
|
+
}
|
|
14157
|
+
if (this.dragging && this.region === "crosshair") {
|
|
14158
|
+
this.deps.onPointerMove(x, y);
|
|
14159
|
+
return;
|
|
14160
|
+
}
|
|
14161
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
14162
|
+
return;
|
|
14163
|
+
}
|
|
14164
|
+
if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
|
|
14165
|
+
const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
|
|
13841
14166
|
if (this.dragging) {
|
|
13842
14167
|
if (this.region === "price") {
|
|
13843
|
-
if (Math.abs(y - this.startY) >
|
|
14168
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13844
14169
|
this.deps.priceScaleBy(y - this.startY);
|
|
13845
14170
|
} else if (this.region === "separator") {
|
|
13846
|
-
if (Math.abs(y - this.startY) >
|
|
14171
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13847
14172
|
this.deps.paneResizeBy(y - this.startY);
|
|
13848
14173
|
} else if (this.region === "time") {
|
|
13849
|
-
if (Math.abs(x - this.startX) >
|
|
14174
|
+
if (Math.abs(x - this.startX) > slop) this.moved = true;
|
|
13850
14175
|
const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
|
|
13851
14176
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
13852
14177
|
} else if (this.region === "drawing") {
|
|
13853
|
-
if (Math.abs(x - this.startX) >
|
|
14178
|
+
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13854
14179
|
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
13855
14180
|
} else {
|
|
13856
14181
|
const coords = this.deps.getCoords();
|
|
13857
14182
|
const vp = coords.getViewport();
|
|
13858
14183
|
const dx = x - this.startX;
|
|
13859
|
-
if (Math.abs(dx) >
|
|
14184
|
+
if (Math.abs(dx) > slop) this.moved = true;
|
|
13860
14185
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
|
|
13861
14186
|
if (this.verticalPan) {
|
|
13862
|
-
if (Math.abs(y - this.startY) >
|
|
14187
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13863
14188
|
this.deps.pricePanBy(y - this.startY);
|
|
13864
14189
|
}
|
|
13865
14190
|
const dt = e.timeStamp - this.lastT;
|
|
@@ -13876,14 +14201,44 @@ var InputController = class {
|
|
|
13876
14201
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
13877
14202
|
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
13878
14203
|
}
|
|
13879
|
-
this.deps.onPointerMove(x, y);
|
|
14204
|
+
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
13880
14205
|
};
|
|
13881
14206
|
this.onUp = (e) => {
|
|
14207
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
14208
|
+
this.cancelLongPress();
|
|
14209
|
+
if (this.dragging && this.region === "pinch") {
|
|
14210
|
+
if (this.touches.size === 1) {
|
|
14211
|
+
const rest = [...this.touches.values()][0];
|
|
14212
|
+
const vp = this.deps.getCoords().getViewport();
|
|
14213
|
+
this.region = "data";
|
|
14214
|
+
this.verticalPan = false;
|
|
14215
|
+
this.startX = rest.x;
|
|
14216
|
+
this.startY = rest.y;
|
|
14217
|
+
this.startRightOffset = vp.rightOffset;
|
|
14218
|
+
this.startBarSpacing = vp.barSpacing;
|
|
14219
|
+
this.lastX = rest.x;
|
|
14220
|
+
this.lastT = e.timeStamp;
|
|
14221
|
+
this.vx = 0;
|
|
14222
|
+
return;
|
|
14223
|
+
}
|
|
14224
|
+
this.endGesture(e);
|
|
14225
|
+
return;
|
|
14226
|
+
}
|
|
14227
|
+
if (this.dragging && this.region === "crosshair") {
|
|
14228
|
+
this.deps.onPointerMove(null, null);
|
|
14229
|
+
this.endGesture(e);
|
|
14230
|
+
return;
|
|
14231
|
+
}
|
|
14232
|
+
if (this.dragging && this.region === "axis-sheet") {
|
|
14233
|
+
this.endGesture(e);
|
|
14234
|
+
return;
|
|
14235
|
+
}
|
|
14236
|
+
const { x, y } = this.local(e);
|
|
14237
|
+
const wasTouch = e.pointerType === "touch";
|
|
14238
|
+
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
13882
14239
|
if (this.dragging && this.region === "drawing") {
|
|
13883
|
-
const { x, y } = this.local(e);
|
|
13884
14240
|
this.deps.drawingsPointerUp?.(x, y);
|
|
13885
|
-
} else if (
|
|
13886
|
-
const { x, y } = this.local(e);
|
|
14241
|
+
} else if (tapRelease && this.region === "data") {
|
|
13887
14242
|
this.deps.onClick(x, y);
|
|
13888
14243
|
} else if (this.dragging && this.region === "data") {
|
|
13889
14244
|
const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
|
|
@@ -13892,11 +14247,20 @@ var InputController = class {
|
|
|
13892
14247
|
this.deps.fling(-this.vx / pitch);
|
|
13893
14248
|
}
|
|
13894
14249
|
}
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
14250
|
+
const wasTap = wasTouch && tapRelease;
|
|
14251
|
+
this.endGesture(e);
|
|
14252
|
+
if (wasTouch) this.deps.onPointerMove(null, null);
|
|
14253
|
+
if (wasTap) this.registerTap(e.timeStamp, x, y);
|
|
14254
|
+
};
|
|
14255
|
+
/** The browser took the pointer back (system gesture, palm rejection, tab switch):
|
|
14256
|
+
* drop the gesture where it is — no click, no fling, no stuck `dragging`. */
|
|
14257
|
+
this.onCancel = (e) => {
|
|
14258
|
+
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
14259
|
+
this.cancelLongPress();
|
|
14260
|
+
if (!this.dragging) return;
|
|
14261
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
14262
|
+
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
14263
|
+
this.endGesture(e);
|
|
13900
14264
|
};
|
|
13901
14265
|
this.onLeave = () => {
|
|
13902
14266
|
this.cursorX = NaN;
|
|
@@ -13905,12 +14269,7 @@ var InputController = class {
|
|
|
13905
14269
|
};
|
|
13906
14270
|
this.onDblClick = (e) => {
|
|
13907
14271
|
const { x, y } = this.local(e);
|
|
13908
|
-
|
|
13909
|
-
const region = this.regionAt(x, y);
|
|
13910
|
-
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
13911
|
-
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
13912
|
-
else if (region === "time") this.deps.resetView();
|
|
13913
|
-
else this.deps.dataDblClick(x, y);
|
|
14272
|
+
this.doubleActivate(x, y);
|
|
13914
14273
|
};
|
|
13915
14274
|
this.onWheel = (e) => {
|
|
13916
14275
|
e.preventDefault();
|
|
@@ -13931,10 +14290,17 @@ var InputController = class {
|
|
|
13931
14290
|
}
|
|
13932
14291
|
attach(el) {
|
|
13933
14292
|
this.el = el;
|
|
14293
|
+
if (el.style !== void 0) {
|
|
14294
|
+
el.style.touchAction = "none";
|
|
14295
|
+
el.style.userSelect = "none";
|
|
14296
|
+
el.style.setProperty("-webkit-user-select", "none");
|
|
14297
|
+
el.style.setProperty("-webkit-touch-callout", "none");
|
|
14298
|
+
}
|
|
13934
14299
|
el.addEventListener("pointerdown", this.onDown);
|
|
13935
14300
|
el.addEventListener("pointermove", this.onMove);
|
|
13936
14301
|
el.addEventListener("pointerup", this.onUp);
|
|
13937
14302
|
el.addEventListener("pointerleave", this.onLeave);
|
|
14303
|
+
el.addEventListener("pointercancel", this.onCancel);
|
|
13938
14304
|
el.addEventListener("dblclick", this.onDblClick);
|
|
13939
14305
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
13940
14306
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
@@ -13942,6 +14308,7 @@ var InputController = class {
|
|
|
13942
14308
|
const win = el.ownerDocument?.defaultView;
|
|
13943
14309
|
win?.addEventListener("keydown", this.onModifier);
|
|
13944
14310
|
win?.addEventListener("keyup", this.onModifier);
|
|
14311
|
+
win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
13945
14312
|
}
|
|
13946
14313
|
detach() {
|
|
13947
14314
|
const el = this.el;
|
|
@@ -13949,14 +14316,18 @@ var InputController = class {
|
|
|
13949
14316
|
const win = el.ownerDocument?.defaultView;
|
|
13950
14317
|
win?.removeEventListener("keydown", this.onModifier);
|
|
13951
14318
|
win?.removeEventListener("keyup", this.onModifier);
|
|
14319
|
+
win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
13952
14320
|
el.removeEventListener("pointerdown", this.onDown);
|
|
13953
14321
|
el.removeEventListener("pointermove", this.onMove);
|
|
13954
14322
|
el.removeEventListener("pointerup", this.onUp);
|
|
13955
14323
|
el.removeEventListener("pointerleave", this.onLeave);
|
|
14324
|
+
el.removeEventListener("pointercancel", this.onCancel);
|
|
13956
14325
|
el.removeEventListener("dblclick", this.onDblClick);
|
|
13957
14326
|
el.removeEventListener("wheel", this.onWheel);
|
|
13958
14327
|
el.removeEventListener("mousedown", this.onMiddleGuard);
|
|
13959
14328
|
el.removeEventListener("auxclick", this.onMiddleGuard);
|
|
14329
|
+
this.cancelLongPress();
|
|
14330
|
+
this.touches.clear();
|
|
13960
14331
|
this.el = null;
|
|
13961
14332
|
}
|
|
13962
14333
|
local(e) {
|
|
@@ -13980,6 +14351,59 @@ var InputController = class {
|
|
|
13980
14351
|
snapMode(e) {
|
|
13981
14352
|
return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
|
|
13982
14353
|
}
|
|
14354
|
+
/** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
|
|
14355
|
+
* movement past TOUCH_SLOP or a second finger cancels it. */
|
|
14356
|
+
startLongPress(x, y, region) {
|
|
14357
|
+
this.cancelLongPress();
|
|
14358
|
+
this.lpTimer = setTimeout(() => {
|
|
14359
|
+
this.lpTimer = null;
|
|
14360
|
+
if (!this.dragging || this.region !== region) return;
|
|
14361
|
+
if (region === "data") {
|
|
14362
|
+
this.region = "crosshair";
|
|
14363
|
+
this.deps.onPointerMove(x, y);
|
|
14364
|
+
return;
|
|
14365
|
+
}
|
|
14366
|
+
this.region = "axis-sheet";
|
|
14367
|
+
this.moved = true;
|
|
14368
|
+
this.deps.onAxisLongPress?.(region, x, y);
|
|
14369
|
+
}, LONG_PRESS_MS);
|
|
14370
|
+
}
|
|
14371
|
+
cancelLongPress() {
|
|
14372
|
+
if (this.lpTimer !== null) clearTimeout(this.lpTimer);
|
|
14373
|
+
this.lpTimer = null;
|
|
14374
|
+
}
|
|
14375
|
+
/** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
|
|
14376
|
+
beginPinch(e) {
|
|
14377
|
+
this.cancelLongPress();
|
|
14378
|
+
if (this.region === "crosshair") this.deps.onPointerMove(null, null);
|
|
14379
|
+
const coords = this.deps.getCoords();
|
|
14380
|
+
const vp = coords.getViewport();
|
|
14381
|
+
this.deps.apply(vp);
|
|
14382
|
+
const [a, b] = [...this.touches.values()];
|
|
14383
|
+
this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
14384
|
+
this.pinchStartBarSpacing = vp.barSpacing;
|
|
14385
|
+
this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
|
|
14386
|
+
this.region = "pinch";
|
|
14387
|
+
this.dragging = true;
|
|
14388
|
+
this.moved = true;
|
|
14389
|
+
this.vx = 0;
|
|
14390
|
+
this.capture(e.pointerId);
|
|
14391
|
+
}
|
|
14392
|
+
/** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
|
|
14393
|
+
applyPinch() {
|
|
14394
|
+
const coords = this.deps.getCoords();
|
|
14395
|
+
const pts = [...this.touches.values()];
|
|
14396
|
+
if (pts.length < 2) return;
|
|
14397
|
+
const [a, b] = pts;
|
|
14398
|
+
const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
|
|
14399
|
+
const vp = coords.getViewport();
|
|
14400
|
+
const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
|
|
14401
|
+
const midX = (a.x + b.x) / 2;
|
|
14402
|
+
this.deps.apply({
|
|
14403
|
+
barSpacing,
|
|
14404
|
+
rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
|
|
14405
|
+
});
|
|
14406
|
+
}
|
|
13983
14407
|
/** Capture defensively: a synthetic/already-released pointer can't be captured, and
|
|
13984
14408
|
* the move/up pair still routes through the element listeners without it. */
|
|
13985
14409
|
capture(pointerId) {
|
|
@@ -13988,6 +14412,35 @@ var InputController = class {
|
|
|
13988
14412
|
} catch {
|
|
13989
14413
|
}
|
|
13990
14414
|
}
|
|
14415
|
+
/** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
|
|
14416
|
+
* routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
|
|
14417
|
+
registerTap(t, x, y) {
|
|
14418
|
+
const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
|
|
14419
|
+
if (paired) {
|
|
14420
|
+
this.lastTapT = 0;
|
|
14421
|
+
this.doubleActivate(this.lastTapX, this.lastTapY);
|
|
14422
|
+
return;
|
|
14423
|
+
}
|
|
14424
|
+
this.lastTapT = t;
|
|
14425
|
+
this.lastTapX = x;
|
|
14426
|
+
this.lastTapY = y;
|
|
14427
|
+
}
|
|
14428
|
+
endGesture(e) {
|
|
14429
|
+
this.dragging = false;
|
|
14430
|
+
try {
|
|
14431
|
+
this.el?.releasePointerCapture(e.pointerId);
|
|
14432
|
+
} catch {
|
|
14433
|
+
}
|
|
14434
|
+
}
|
|
14435
|
+
/** Shared double-click / double-tap routing, by the region under the point. */
|
|
14436
|
+
doubleActivate(x, y) {
|
|
14437
|
+
if (this.deps.drawingsDblClick?.(x, y)) return;
|
|
14438
|
+
const region = this.regionAt(x, y);
|
|
14439
|
+
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
14440
|
+
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
14441
|
+
else if (region === "time") this.deps.resetView();
|
|
14442
|
+
else this.deps.dataDblClick(x, y);
|
|
14443
|
+
}
|
|
13991
14444
|
};
|
|
13992
14445
|
|
|
13993
14446
|
// src/renderers/native/core/KeyboardController.ts
|
|
@@ -15801,11 +16254,19 @@ var ChromeRenderer = class {
|
|
|
15801
16254
|
const y = dataH + (fullH - dataH) / 2;
|
|
15802
16255
|
const tr = coords.visibleTimeRange();
|
|
15803
16256
|
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
15804
|
-
|
|
15805
|
-
|
|
15806
|
-
|
|
15807
|
-
|
|
15808
|
-
|
|
16257
|
+
const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
|
|
16258
|
+
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);
|
|
16259
|
+
const GAP = 12;
|
|
16260
|
+
const placed = [];
|
|
16261
|
+
const put = (tick) => {
|
|
16262
|
+
const l = tick.x - tick.half;
|
|
16263
|
+
const r = tick.x + tick.half;
|
|
16264
|
+
if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
|
|
16265
|
+
placed.push({ l, r });
|
|
16266
|
+
ctx.fillText(tick.label, tick.x, y);
|
|
16267
|
+
};
|
|
16268
|
+
for (const tick of ticks) if (tick.major) put(tick);
|
|
16269
|
+
for (const tick of ticks) if (!tick.major) put(tick);
|
|
15809
16270
|
ctx.textAlign = "start";
|
|
15810
16271
|
}
|
|
15811
16272
|
};
|
|
@@ -15907,10 +16368,10 @@ var CrosshairRenderer = class {
|
|
|
15907
16368
|
}
|
|
15908
16369
|
/** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
|
|
15909
16370
|
* CONTAINING the foreign time (+ horizontal line when a comparable price came
|
|
15910
|
-
* along), with that bar's time chip in this chart's own timezone
|
|
15911
|
-
*
|
|
15912
|
-
*
|
|
15913
|
-
*
|
|
16371
|
+
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
16372
|
+
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
16373
|
+
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
16374
|
+
* render slightly dimmed so the ghost still reads as foreign. */
|
|
15914
16375
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
15915
16376
|
const cs = scene.style.crosshair;
|
|
15916
16377
|
const dataW = coords.width;
|
|
@@ -15934,7 +16395,20 @@ var CrosshairRenderer = class {
|
|
|
15934
16395
|
ctx.setLineDash([]);
|
|
15935
16396
|
ctx.lineWidth = 1;
|
|
15936
16397
|
ctx.globalAlpha = 0.8;
|
|
15937
|
-
|
|
16398
|
+
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
16399
|
+
if (ext.y != null && ext.price != null) {
|
|
16400
|
+
let pane;
|
|
16401
|
+
for (const p of scene.panes.values()) {
|
|
16402
|
+
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
16403
|
+
pane = p;
|
|
16404
|
+
break;
|
|
16405
|
+
}
|
|
16406
|
+
}
|
|
16407
|
+
if (pane) {
|
|
16408
|
+
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);
|
|
16409
|
+
}
|
|
16410
|
+
}
|
|
16411
|
+
this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
|
|
15938
16412
|
ctx.globalAlpha = 1;
|
|
15939
16413
|
}
|
|
15940
16414
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -16295,6 +16769,7 @@ function colorField(theme, getVal, onVal) {
|
|
|
16295
16769
|
swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
|
|
16296
16770
|
};
|
|
16297
16771
|
paint();
|
|
16772
|
+
trigger.addEventListener("vela-sync", paint);
|
|
16298
16773
|
trigger.addEventListener("click", (e) => {
|
|
16299
16774
|
e.stopPropagation();
|
|
16300
16775
|
if (openPopover && openPopover.trigger === trigger) {
|
|
@@ -16330,6 +16805,101 @@ function closeColorPopover() {
|
|
|
16330
16805
|
closeOpen();
|
|
16331
16806
|
}
|
|
16332
16807
|
|
|
16808
|
+
// src/renderers/native/chrome/WidthField.ts
|
|
16809
|
+
var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
|
|
16810
|
+
var STYLE_ID3 = "vela-width-field";
|
|
16811
|
+
function ensureStyles3() {
|
|
16812
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
|
|
16813
|
+
const st = document.createElement("style");
|
|
16814
|
+
st.id = STYLE_ID3;
|
|
16815
|
+
st.textContent = `
|
|
16816
|
+
.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;}
|
|
16817
|
+
.vela-width-field:hover{border-color:var(--vela-fg-muted);}
|
|
16818
|
+
.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);}
|
|
16819
|
+
.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;}
|
|
16820
|
+
.vela-width-field-item:hover{background:var(--vela-hover-strong);}
|
|
16821
|
+
.vela-width-field-item[data-active='1']{background:var(--vela-active);}
|
|
16822
|
+
`;
|
|
16823
|
+
document.head.appendChild(st);
|
|
16824
|
+
}
|
|
16825
|
+
function lineGlyph(width) {
|
|
16826
|
+
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>`;
|
|
16827
|
+
}
|
|
16828
|
+
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>`;
|
|
16829
|
+
var openPopover2 = null;
|
|
16830
|
+
function closeOpen2() {
|
|
16831
|
+
if (!openPopover2) return;
|
|
16832
|
+
document.removeEventListener("pointerdown", openPopover2.onOutside, true);
|
|
16833
|
+
window.removeEventListener("resize", openPopover2.reflow, true);
|
|
16834
|
+
openPopover2.el.remove();
|
|
16835
|
+
openPopover2 = null;
|
|
16836
|
+
}
|
|
16837
|
+
function place2(pop, trigger) {
|
|
16838
|
+
const r = trigger.getBoundingClientRect();
|
|
16839
|
+
const w = pop.offsetWidth;
|
|
16840
|
+
const h = pop.offsetHeight;
|
|
16841
|
+
let left = r.right - w;
|
|
16842
|
+
let top = r.bottom + 6;
|
|
16843
|
+
if (left < 6) left = 6;
|
|
16844
|
+
if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
|
|
16845
|
+
if (top + h > window.innerHeight - 6) top = r.top - h - 6;
|
|
16846
|
+
if (top < 6) top = 6;
|
|
16847
|
+
pop.style.left = `${Math.round(left)}px`;
|
|
16848
|
+
pop.style.top = `${Math.round(top)}px`;
|
|
16849
|
+
}
|
|
16850
|
+
function widthField(theme, getVal, onVal) {
|
|
16851
|
+
ensureStyles3();
|
|
16852
|
+
const trigger = document.createElement("button");
|
|
16853
|
+
trigger.type = "button";
|
|
16854
|
+
trigger.className = "vela-width-field";
|
|
16855
|
+
const paint = () => {
|
|
16856
|
+
trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
|
|
16857
|
+
};
|
|
16858
|
+
paint();
|
|
16859
|
+
trigger.addEventListener("click", (e) => {
|
|
16860
|
+
e.stopPropagation();
|
|
16861
|
+
if (openPopover2 && openPopover2.trigger === trigger) {
|
|
16862
|
+
closeOpen2();
|
|
16863
|
+
return;
|
|
16864
|
+
}
|
|
16865
|
+
closeOpen2();
|
|
16866
|
+
const pop = document.createElement("div");
|
|
16867
|
+
pop.className = "vela-width-field-pop";
|
|
16868
|
+
applyChromeTokens(pop, theme);
|
|
16869
|
+
pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
|
|
16870
|
+
pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
|
|
16871
|
+
for (const w of WIDTH_FIELD_OPTIONS) {
|
|
16872
|
+
const item = document.createElement("button");
|
|
16873
|
+
item.type = "button";
|
|
16874
|
+
item.className = "vela-width-field-item";
|
|
16875
|
+
item.dataset.active = w === getVal() ? "1" : "0";
|
|
16876
|
+
item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
|
|
16877
|
+
item.addEventListener("click", (ev) => {
|
|
16878
|
+
ev.stopPropagation();
|
|
16879
|
+
closeOpen2();
|
|
16880
|
+
onVal(w);
|
|
16881
|
+
paint();
|
|
16882
|
+
});
|
|
16883
|
+
pop.appendChild(item);
|
|
16884
|
+
}
|
|
16885
|
+
document.body.appendChild(pop);
|
|
16886
|
+
place2(pop, trigger);
|
|
16887
|
+
const onOutside = (ev) => {
|
|
16888
|
+
const t = ev.target;
|
|
16889
|
+
if (pop.contains(t) || trigger.contains(t)) return;
|
|
16890
|
+
closeOpen2();
|
|
16891
|
+
};
|
|
16892
|
+
const reflow = () => place2(pop, trigger);
|
|
16893
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
|
|
16894
|
+
window.addEventListener("resize", reflow, true);
|
|
16895
|
+
openPopover2 = { el: pop, trigger, onOutside, reflow };
|
|
16896
|
+
});
|
|
16897
|
+
return trigger;
|
|
16898
|
+
}
|
|
16899
|
+
function closeWidthPopover() {
|
|
16900
|
+
closeOpen2();
|
|
16901
|
+
}
|
|
16902
|
+
|
|
16333
16903
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
16334
16904
|
var BUILTIN_STYLE_LABELS = {
|
|
16335
16905
|
candles: "Candles",
|
|
@@ -16412,6 +16982,30 @@ function ensureControlStyles() {
|
|
|
16412
16982
|
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
16413
16983
|
!important beats the inline opacity on labels. */
|
|
16414
16984
|
.vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
|
|
16985
|
+
/* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
|
|
16986
|
+
The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
|
|
16987
|
+
row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
|
|
16988
|
+
controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
|
|
16989
|
+
.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;}
|
|
16990
|
+
.vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
|
|
16991
|
+
.vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
|
|
16992
|
+
.vela-sd-mobile .vela-sd-railscrim.open{display:block;}
|
|
16993
|
+
.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);}
|
|
16994
|
+
.vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
|
|
16995
|
+
.vela-sd-mobile .vela-sd-tab{padding:12px;}
|
|
16996
|
+
.vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
|
|
16997
|
+
.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;}
|
|
16998
|
+
.vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
|
|
16999
|
+
.vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
|
|
17000
|
+
.vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
|
|
17001
|
+
.vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
|
|
17002
|
+
.vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
|
|
17003
|
+
.vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
|
|
17004
|
+
.vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
|
|
17005
|
+
.vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
|
|
17006
|
+
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
17007
|
+
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
17008
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
|
|
16415
17009
|
`;
|
|
16416
17010
|
document.head.appendChild(st);
|
|
16417
17011
|
}
|
|
@@ -16438,6 +17032,8 @@ var SettingsDialog = class {
|
|
|
16438
17032
|
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
16439
17033
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
16440
17034
|
this.activeSection = null;
|
|
17035
|
+
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
17036
|
+
this.mobileLayout = false;
|
|
16441
17037
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
16442
17038
|
}
|
|
16443
17039
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
@@ -16456,15 +17052,27 @@ var SettingsDialog = class {
|
|
|
16456
17052
|
}
|
|
16457
17053
|
setTheme(theme) {
|
|
16458
17054
|
this.theme = theme;
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
17055
|
+
this.reopenIfLive();
|
|
17056
|
+
}
|
|
17057
|
+
/** The host shell's chrome size class — mobile presents the dialog fullscreen with
|
|
17058
|
+
* the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
|
|
17059
|
+
* re-presents in place, on the same tab. */
|
|
17060
|
+
setLayoutMode(mode) {
|
|
17061
|
+
const mobile = mode === "mobile";
|
|
17062
|
+
if (mobile === this.mobileLayout) return;
|
|
17063
|
+
this.mobileLayout = mobile;
|
|
17064
|
+
this.reopenIfLive();
|
|
17065
|
+
}
|
|
17066
|
+
/** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
|
|
17067
|
+
reopenIfLive() {
|
|
17068
|
+
if (!this.root) return;
|
|
17069
|
+
const cfg = this.config;
|
|
17070
|
+
const oc = this.onChange;
|
|
17071
|
+
const oi = this.onImport;
|
|
17072
|
+
const orst = this.onReset;
|
|
17073
|
+
const section = this.activeSection;
|
|
17074
|
+
this.close();
|
|
17075
|
+
if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
|
|
16468
17076
|
}
|
|
16469
17077
|
isOpen() {
|
|
16470
17078
|
return this.root !== null;
|
|
@@ -16486,18 +17094,37 @@ var SettingsDialog = class {
|
|
|
16486
17094
|
this.onImport = onImport ?? null;
|
|
16487
17095
|
this.onReset = onReset ?? null;
|
|
16488
17096
|
ensureControlStyles();
|
|
17097
|
+
const mobile = this.mobileLayout;
|
|
16489
17098
|
const scrim = document.createElement("div");
|
|
16490
|
-
scrim.style.cssText = "position:absolute;inset:0;z-index:
|
|
17099
|
+
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;";
|
|
17100
|
+
if (mobile) {
|
|
17101
|
+
scrim.classList.add("vela-sd-mobile");
|
|
17102
|
+
scrim.style.paddingTop = "0";
|
|
17103
|
+
scrim.style.alignItems = "stretch";
|
|
17104
|
+
}
|
|
16491
17105
|
scrim.addEventListener("mousedown", (e) => {
|
|
16492
17106
|
if (e.target === scrim) this.close();
|
|
16493
17107
|
});
|
|
16494
17108
|
const dlg = document.createElement("div");
|
|
16495
17109
|
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;`;
|
|
17110
|
+
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;";
|
|
16496
17111
|
const header = document.createElement("div");
|
|
16497
17112
|
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;`;
|
|
17113
|
+
if (mobile) header.style.paddingLeft = "8px";
|
|
16498
17114
|
const hTitle = document.createElement("span");
|
|
16499
17115
|
hTitle.textContent = "Chart settings";
|
|
16500
17116
|
hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
|
|
17117
|
+
if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
17118
|
+
let toggleRail = null;
|
|
17119
|
+
if (mobile) {
|
|
17120
|
+
const burger = document.createElement("button");
|
|
17121
|
+
burger.type = "button";
|
|
17122
|
+
burger.className = "vela-sd-burger";
|
|
17123
|
+
burger.innerHTML = iconAt("burger", 16);
|
|
17124
|
+
burger.title = "Sections";
|
|
17125
|
+
burger.addEventListener("click", () => toggleRail?.());
|
|
17126
|
+
header.append(burger);
|
|
17127
|
+
}
|
|
16501
17128
|
const closeBtn = document.createElement("button");
|
|
16502
17129
|
closeBtn.type = "button";
|
|
16503
17130
|
closeBtn.innerHTML = iconAt("close", 15);
|
|
@@ -16505,8 +17132,8 @@ var SettingsDialog = class {
|
|
|
16505
17132
|
closeBtn.className = "vela-sd-close";
|
|
16506
17133
|
closeBtn.addEventListener("click", () => this.close());
|
|
16507
17134
|
header.append(hTitle, closeBtn);
|
|
16508
|
-
|
|
16509
|
-
|
|
17135
|
+
if (!mobile) {
|
|
17136
|
+
header.style.cursor = "move";
|
|
16510
17137
|
let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
|
|
16511
17138
|
header.addEventListener("pointerdown", (e) => {
|
|
16512
17139
|
if (e.target?.closest(".vela-sd-close")) return;
|
|
@@ -16687,12 +17314,24 @@ var SettingsDialog = class {
|
|
|
16687
17314
|
renderChartTypeSections("end");
|
|
16688
17315
|
renderHostSections("end");
|
|
16689
17316
|
const shell = document.createElement("div");
|
|
16690
|
-
shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
17317
|
+
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;";
|
|
16691
17318
|
const rail = document.createElement("div");
|
|
16692
|
-
rail.
|
|
17319
|
+
rail.className = "vela-sd-rail";
|
|
17320
|
+
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;`;
|
|
17321
|
+
let railScrim = null;
|
|
17322
|
+
if (mobile) {
|
|
17323
|
+
railScrim = document.createElement("div");
|
|
17324
|
+
railScrim.className = "vela-sd-railscrim";
|
|
17325
|
+
railScrim.addEventListener("click", () => toggleRail?.());
|
|
17326
|
+
toggleRail = (open) => {
|
|
17327
|
+
const on = open ?? !rail.classList.contains("open");
|
|
17328
|
+
rail.classList.toggle("open", on);
|
|
17329
|
+
railScrim?.classList.toggle("open", on);
|
|
17330
|
+
};
|
|
17331
|
+
}
|
|
16693
17332
|
const paneHost = document.createElement("div");
|
|
16694
17333
|
paneHost.className = "vela-sd-pane";
|
|
16695
|
-
paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
|
|
17334
|
+
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;";
|
|
16696
17335
|
const panes = [];
|
|
16697
17336
|
let current = null;
|
|
16698
17337
|
for (const child of [...body.children]) {
|
|
@@ -16727,6 +17366,10 @@ var SettingsDialog = class {
|
|
|
16727
17366
|
p.tab.classList.toggle("on", i === idx);
|
|
16728
17367
|
});
|
|
16729
17368
|
this.activeSection = panes[idx]?.title ?? null;
|
|
17369
|
+
if (mobile) {
|
|
17370
|
+
hTitle.textContent = panes[idx]?.title ?? "Chart settings";
|
|
17371
|
+
toggleRail?.(false);
|
|
17372
|
+
}
|
|
16730
17373
|
};
|
|
16731
17374
|
panes.forEach((p, i) => {
|
|
16732
17375
|
p.tab.addEventListener("click", () => activate(i));
|
|
@@ -16734,10 +17377,11 @@ var SettingsDialog = class {
|
|
|
16734
17377
|
paneHost.appendChild(p.el);
|
|
16735
17378
|
});
|
|
16736
17379
|
this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
|
|
16737
|
-
const wanted = section
|
|
17380
|
+
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"));
|
|
16738
17381
|
activate(wanted >= 0 ? wanted : 0);
|
|
16739
17382
|
this.syncTypeTabs?.(config.series.style);
|
|
16740
17383
|
shell.append(rail, paneHost);
|
|
17384
|
+
if (railScrim) shell.append(railScrim);
|
|
16741
17385
|
dlg.appendChild(shell);
|
|
16742
17386
|
dlg.appendChild(this.footer(config));
|
|
16743
17387
|
scrim.appendChild(dlg);
|
|
@@ -16754,6 +17398,7 @@ var SettingsDialog = class {
|
|
|
16754
17398
|
}
|
|
16755
17399
|
close() {
|
|
16756
17400
|
closeColorPopover();
|
|
17401
|
+
closeWidthPopover();
|
|
16757
17402
|
this.root?.remove();
|
|
16758
17403
|
this.root = null;
|
|
16759
17404
|
this.tabs = [];
|
|
@@ -16794,20 +17439,12 @@ var SettingsDialog = class {
|
|
|
16794
17439
|
body.append(marker);
|
|
16795
17440
|
const values = config.chartTypes[typeId] ?? {};
|
|
16796
17441
|
const bag = {};
|
|
16797
|
-
const seedKey = (key, want, defval) => {
|
|
16798
|
-
const v = values[key];
|
|
16799
|
-
bag[key] = typeof v === want ? v : defval;
|
|
16800
|
-
};
|
|
16801
17442
|
const seed = (rows) => {
|
|
16802
17443
|
for (const r of rows) {
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
seedKey(r.maxKey, "number", r.defval);
|
|
16807
|
-
continue;
|
|
17444
|
+
for (const k of settingsRowValueKeys(r)) {
|
|
17445
|
+
const v = values[k.key];
|
|
17446
|
+
bag[k.key] = typeof v === k.type ? v : k.defval;
|
|
16808
17447
|
}
|
|
16809
|
-
seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
|
|
16810
|
-
if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
|
|
16811
17448
|
}
|
|
16812
17449
|
};
|
|
16813
17450
|
if (section.rows) seed(section.rows);
|
|
@@ -16830,7 +17467,8 @@ var SettingsDialog = class {
|
|
|
16830
17467
|
if (section.instances && section.instances.length > 0) {
|
|
16831
17468
|
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
16832
17469
|
} else if (section.rows) {
|
|
16833
|
-
this.
|
|
17470
|
+
if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
|
|
17471
|
+
else this.flatTypeRows(section.rows, bag, put, refreshers, body);
|
|
16834
17472
|
}
|
|
16835
17473
|
for (const sub of section.subsections ?? []) {
|
|
16836
17474
|
const subMarker = this.section(sub.title);
|
|
@@ -16847,7 +17485,7 @@ var SettingsDialog = class {
|
|
|
16847
17485
|
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
16848
17486
|
const entries = [];
|
|
16849
17487
|
for (const r of rows) {
|
|
16850
|
-
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
|
|
17488
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
|
|
16851
17489
|
entries.push({ el, when: r.when });
|
|
16852
17490
|
body.append(el);
|
|
16853
17491
|
}
|
|
@@ -16855,60 +17493,109 @@ var SettingsDialog = class {
|
|
|
16855
17493
|
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
16856
17494
|
});
|
|
16857
17495
|
}
|
|
16858
|
-
/**
|
|
16859
|
-
|
|
16860
|
-
|
|
16861
|
-
|
|
16862
|
-
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
17496
|
+
/**
|
|
17497
|
+
* One value row for a chart-type descriptor, writing through `put`. EVERY kind is
|
|
17498
|
+
* first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
|
|
17499
|
+
* rendered by the ONE path: optional leading toggle, then the ordered inline
|
|
17500
|
+
* controls in the control column. A control carrying its own `when` registers a
|
|
17501
|
+
* visibility refresher — it appears and disappears live as the bag changes,
|
|
17502
|
+
* independent of the row's gate — and is exempt from the toggle-off dim (it may
|
|
17503
|
+
* exist FOR the off state).
|
|
17504
|
+
*/
|
|
17505
|
+
typeRow(r, bag, put, refreshers) {
|
|
17506
|
+
const n = normalizeSettingsRow(r);
|
|
17507
|
+
const controls = [];
|
|
17508
|
+
for (const c of n.controls) {
|
|
17509
|
+
const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
|
|
17510
|
+
if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
17511
|
+
if (c.kind !== "hint" && c.when) {
|
|
17512
|
+
el.dataset.sdSelfGated = "1";
|
|
17513
|
+
const when = c.when;
|
|
17514
|
+
refreshers.push(() => {
|
|
17515
|
+
el.style.display = settingsRowVisible(when, bag) ? "" : "none";
|
|
17516
|
+
});
|
|
17517
|
+
}
|
|
17518
|
+
controls.push(el);
|
|
17519
|
+
}
|
|
17520
|
+
if (n.toggle) {
|
|
17521
|
+
const t = n.toggle;
|
|
17522
|
+
const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
|
|
17523
|
+
refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
|
|
17524
|
+
return el;
|
|
16867
17525
|
}
|
|
16868
|
-
|
|
16869
|
-
if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
|
|
16870
|
-
if (r.kind === "range") return this.rangeRow(r, bag, put);
|
|
16871
|
-
return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
|
|
17526
|
+
return this.rowWith(n.label, controls);
|
|
16872
17527
|
}
|
|
16873
17528
|
/**
|
|
16874
|
-
*
|
|
16875
|
-
* `
|
|
16876
|
-
*
|
|
16877
|
-
* placeholder names the unset state ('Off' for 0-disables bounds).
|
|
17529
|
+
* Build ONE inline control from its descriptor — the factory behind the composite
|
|
17530
|
+
* row path. `compact` narrows number inputs on toggle rows (the historical inline
|
|
17531
|
+
* width) while standalone rows keep the full-width input.
|
|
16878
17532
|
*/
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
16885
|
-
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
17533
|
+
inlineControl(c, bag, put, compact) {
|
|
17534
|
+
if (c.kind === "hint") {
|
|
17535
|
+
const s = document.createElement("span");
|
|
17536
|
+
s.textContent = c.text;
|
|
17537
|
+
s.style.cssText = "color:var(--vela-fg-muted);";
|
|
17538
|
+
return s;
|
|
17539
|
+
}
|
|
17540
|
+
if (c.kind === "color") {
|
|
17541
|
+
const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
|
|
17542
|
+
sw.title = c.label;
|
|
17543
|
+
return sw;
|
|
17544
|
+
}
|
|
17545
|
+
if (c.kind === "width") {
|
|
17546
|
+
let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
17547
|
+
const wf = widthField(this.theme, () => cur, (v) => {
|
|
17548
|
+
cur = v;
|
|
17549
|
+
put(c.key, v);
|
|
17550
|
+
});
|
|
17551
|
+
wf.title = c.label;
|
|
17552
|
+
return wf;
|
|
17553
|
+
}
|
|
17554
|
+
if (c.kind === "select") {
|
|
17555
|
+
const sel = document.createElement("select");
|
|
17556
|
+
sel.className = "vela-sd-select";
|
|
17557
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
17558
|
+
sel.title = c.label;
|
|
17559
|
+
const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
17560
|
+
for (const [val, lbl] of normalizeSelectOptions(c.options)) {
|
|
17561
|
+
const o = document.createElement("option");
|
|
17562
|
+
o.value = val;
|
|
17563
|
+
o.textContent = lbl;
|
|
17564
|
+
if (val === current2) o.selected = true;
|
|
17565
|
+
sel.appendChild(o);
|
|
16895
17566
|
}
|
|
17567
|
+
sel.addEventListener("change", () => put(c.key, sel.value));
|
|
17568
|
+
sel.addEventListener("vela-sync", () => {
|
|
17569
|
+
sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
|
|
17570
|
+
});
|
|
17571
|
+
return sel;
|
|
17572
|
+
}
|
|
17573
|
+
const ni = document.createElement("input");
|
|
17574
|
+
ni.type = "number";
|
|
17575
|
+
ni.className = "vela-sd-number";
|
|
17576
|
+
if (compact) ni.style.width = "56px";
|
|
17577
|
+
if (c.min !== void 0) ni.min = String(c.min);
|
|
17578
|
+
if (c.max !== void 0) ni.max = String(c.max);
|
|
17579
|
+
ni.step = String(c.step ?? 1);
|
|
17580
|
+
ni.title = c.label;
|
|
17581
|
+
const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
|
|
17582
|
+
if (c.placeholder !== void 0) {
|
|
17583
|
+
ni.placeholder = c.placeholder;
|
|
17584
|
+
if (current !== c.defval) ni.value = String(current);
|
|
16896
17585
|
ni.addEventListener("change", () => {
|
|
16897
|
-
const raw = ni.value.trim() === "" ?
|
|
16898
|
-
const v = Number.isFinite(raw) ? Math.min(
|
|
16899
|
-
ni.value =
|
|
16900
|
-
put(key, v);
|
|
17586
|
+
const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
|
|
17587
|
+
const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
|
|
17588
|
+
ni.value = v === c.defval ? "" : String(v);
|
|
17589
|
+
put(c.key, v);
|
|
16901
17590
|
});
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
wrap.appendChild(box);
|
|
16911
|
-
return wrap;
|
|
17591
|
+
} else {
|
|
17592
|
+
ni.value = String(current);
|
|
17593
|
+
ni.addEventListener("input", () => {
|
|
17594
|
+
const v = Number(ni.value);
|
|
17595
|
+
if (Number.isFinite(v)) put(c.key, v);
|
|
17596
|
+
});
|
|
17597
|
+
}
|
|
17598
|
+
return ni;
|
|
16912
17599
|
}
|
|
16913
17600
|
/**
|
|
16914
17601
|
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
@@ -16921,7 +17608,7 @@ var SettingsDialog = class {
|
|
|
16921
17608
|
const wrap = document.createElement("div");
|
|
16922
17609
|
const strip = document.createElement("div");
|
|
16923
17610
|
strip.className = "vela-sd-itabs";
|
|
16924
|
-
wrap.append(strip);
|
|
17611
|
+
if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
|
|
16925
17612
|
const contents = instances.map((inst, i) => {
|
|
16926
17613
|
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
16927
17614
|
wrap.append(content);
|
|
@@ -17016,8 +17703,8 @@ var SettingsDialog = class {
|
|
|
17016
17703
|
rowsHost.append(entries[entries.length - 1].el);
|
|
17017
17704
|
continue;
|
|
17018
17705
|
}
|
|
17019
|
-
const el = this.typeRow(r, bag, put);
|
|
17020
|
-
const key = r.kind === "range" ? void 0 : r.key;
|
|
17706
|
+
const el = this.typeRow(r, bag, put, refreshers);
|
|
17707
|
+
const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
|
|
17021
17708
|
entries.push({ el, when: r.when, group: g, key });
|
|
17022
17709
|
rowsHost.append(el);
|
|
17023
17710
|
}
|
|
@@ -17083,6 +17770,14 @@ var SettingsDialog = class {
|
|
|
17083
17770
|
*/
|
|
17084
17771
|
layoutSettingsGrids(pane, measureHost) {
|
|
17085
17772
|
if (pane.childElementCount === 0) return;
|
|
17773
|
+
if (this.mobileLayout) {
|
|
17774
|
+
const grid2 = document.createElement("div");
|
|
17775
|
+
grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
|
|
17776
|
+
while (pane.firstChild) grid2.appendChild(pane.firstChild);
|
|
17777
|
+
pane.appendChild(grid2);
|
|
17778
|
+
this.prepareGridItems(grid2);
|
|
17779
|
+
return;
|
|
17780
|
+
}
|
|
17086
17781
|
const rows = [...pane.querySelectorAll(".vela-sd-row")];
|
|
17087
17782
|
const probe2 = document.createElement("div");
|
|
17088
17783
|
probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
|
|
@@ -17119,9 +17814,11 @@ var SettingsDialog = class {
|
|
|
17119
17814
|
}
|
|
17120
17815
|
}
|
|
17121
17816
|
/** A bare color swatch input (for toggle-row right groups / swatch pairs). */
|
|
17122
|
-
|
|
17817
|
+
/** With `get`, the field reads its value live (and repaints on 'vela-sync') —
|
|
17818
|
+
* duplicate-keyed rows stay honest when another row edits the shared key. */
|
|
17819
|
+
swatch(value, onChange, get) {
|
|
17123
17820
|
let current = value;
|
|
17124
|
-
return colorField(this.theme, () => current, (v) => {
|
|
17821
|
+
return colorField(this.theme, () => get ? get() : current, (v) => {
|
|
17125
17822
|
current = v;
|
|
17126
17823
|
onChange(v);
|
|
17127
17824
|
});
|
|
@@ -17173,8 +17870,11 @@ var SettingsDialog = class {
|
|
|
17173
17870
|
}
|
|
17174
17871
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
17175
17872
|
* shared control column; the control group dims and ignores input while the toggle
|
|
17176
|
-
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
17177
|
-
|
|
17873
|
+
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
17874
|
+
* With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
|
|
17875
|
+
* keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
|
|
17876
|
+
* only one visible at a time; see `typeRow`). */
|
|
17877
|
+
toggleRow(label, value, onToggle, controls, get) {
|
|
17178
17878
|
const wrap = document.createElement("div");
|
|
17179
17879
|
const cb = document.createElement("button");
|
|
17180
17880
|
cb.type = "button";
|
|
@@ -17194,6 +17894,12 @@ var SettingsDialog = class {
|
|
|
17194
17894
|
wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
|
|
17195
17895
|
wrap.append(cb, lbl);
|
|
17196
17896
|
cb.addEventListener("click", () => onToggle(cbToggle()));
|
|
17897
|
+
if (get) {
|
|
17898
|
+
wrap.addEventListener("vela-sync", () => {
|
|
17899
|
+
checked = get();
|
|
17900
|
+
cb.classList.toggle("on", checked);
|
|
17901
|
+
});
|
|
17902
|
+
}
|
|
17197
17903
|
return wrap;
|
|
17198
17904
|
}
|
|
17199
17905
|
wrap.className = "vela-sd-row";
|
|
@@ -17204,8 +17910,11 @@ var SettingsDialog = class {
|
|
|
17204
17910
|
box.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
17205
17911
|
for (const c of controls) box.appendChild(c);
|
|
17206
17912
|
const syncDim = (on) => {
|
|
17207
|
-
|
|
17208
|
-
|
|
17913
|
+
for (const c of controls) {
|
|
17914
|
+
if (c.dataset.sdSelfGated === "1") continue;
|
|
17915
|
+
c.style.opacity = on ? "1" : "0.4";
|
|
17916
|
+
c.style.pointerEvents = on ? "" : "none";
|
|
17917
|
+
}
|
|
17209
17918
|
};
|
|
17210
17919
|
syncDim(value);
|
|
17211
17920
|
cb.addEventListener("click", () => {
|
|
@@ -17213,6 +17922,13 @@ var SettingsDialog = class {
|
|
|
17213
17922
|
onToggle(v);
|
|
17214
17923
|
syncDim(v);
|
|
17215
17924
|
});
|
|
17925
|
+
if (get) {
|
|
17926
|
+
wrap.addEventListener("vela-sync", () => {
|
|
17927
|
+
checked = get();
|
|
17928
|
+
cb.classList.toggle("on", checked);
|
|
17929
|
+
syncDim(checked);
|
|
17930
|
+
});
|
|
17931
|
+
}
|
|
17216
17932
|
wrap.append(left, box);
|
|
17217
17933
|
return wrap;
|
|
17218
17934
|
}
|
|
@@ -17232,96 +17948,6 @@ var SettingsDialog = class {
|
|
|
17232
17948
|
wrap.appendChild(ni);
|
|
17233
17949
|
return wrap;
|
|
17234
17950
|
}
|
|
17235
|
-
/** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
|
|
17236
|
-
autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
|
|
17237
|
-
const { wrap } = this.row(label);
|
|
17238
|
-
const box = document.createElement("div");
|
|
17239
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
17240
|
-
const sel = document.createElement("select");
|
|
17241
|
-
sel.className = "vela-sd-select";
|
|
17242
|
-
for (const [val, lbl] of AUTO_MANUAL_OPTS) {
|
|
17243
|
-
const o = document.createElement("option");
|
|
17244
|
-
o.value = val;
|
|
17245
|
-
o.textContent = lbl;
|
|
17246
|
-
if (val === "auto" === isAuto) o.selected = true;
|
|
17247
|
-
sel.appendChild(o);
|
|
17248
|
-
}
|
|
17249
|
-
const ni = document.createElement("input");
|
|
17250
|
-
ni.type = "number";
|
|
17251
|
-
ni.min = String(min);
|
|
17252
|
-
ni.max = String(max);
|
|
17253
|
-
ni.step = "1";
|
|
17254
|
-
ni.value = String(value);
|
|
17255
|
-
ni.className = "vela-sd-number";
|
|
17256
|
-
const unitLbl = document.createElement("span");
|
|
17257
|
-
unitLbl.textContent = unit;
|
|
17258
|
-
unitLbl.style.cssText = "opacity:0.6;";
|
|
17259
|
-
const syncDisabled = (auto) => {
|
|
17260
|
-
ni.disabled = auto;
|
|
17261
|
-
ni.style.opacity = auto ? "0.4" : "1";
|
|
17262
|
-
};
|
|
17263
|
-
syncDisabled(isAuto);
|
|
17264
|
-
sel.addEventListener("change", () => {
|
|
17265
|
-
const auto = sel.value === "auto";
|
|
17266
|
-
onMode(auto);
|
|
17267
|
-
syncDisabled(auto);
|
|
17268
|
-
});
|
|
17269
|
-
ni.addEventListener("input", () => {
|
|
17270
|
-
const n = Number(ni.value);
|
|
17271
|
-
if (Number.isFinite(n)) onValue(n);
|
|
17272
|
-
});
|
|
17273
|
-
box.append(sel, ni, unitLbl);
|
|
17274
|
-
wrap.appendChild(box);
|
|
17275
|
-
return wrap;
|
|
17276
|
-
}
|
|
17277
|
-
/** A row whose control area holds several inline controls (e.g. show + color + width). */
|
|
17278
|
-
inlineRow(label, controls) {
|
|
17279
|
-
const { wrap } = this.row(label);
|
|
17280
|
-
const box = document.createElement("div");
|
|
17281
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
17282
|
-
for (const c of controls) box.appendChild(c);
|
|
17283
|
-
wrap.appendChild(box);
|
|
17284
|
-
return wrap;
|
|
17285
|
-
}
|
|
17286
|
-
/** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
|
|
17287
|
-
hint(text) {
|
|
17288
|
-
const s = document.createElement("span");
|
|
17289
|
-
s.textContent = text;
|
|
17290
|
-
s.style.cssText = "opacity:0.5;font-size:12px;";
|
|
17291
|
-
return s;
|
|
17292
|
-
}
|
|
17293
|
-
/** A bare color input (for inline groups). */
|
|
17294
|
-
colorInput(value, onChange) {
|
|
17295
|
-
const ci = document.createElement("input");
|
|
17296
|
-
ci.type = "color";
|
|
17297
|
-
ci.value = toHex6(value);
|
|
17298
|
-
ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
|
|
17299
|
-
ci.addEventListener("input", () => onChange(ci.value));
|
|
17300
|
-
return ci;
|
|
17301
|
-
}
|
|
17302
|
-
/** A bare compact number input (for inline groups). */
|
|
17303
|
-
numberInput(value, min, max, step, onChange) {
|
|
17304
|
-
const ni = document.createElement("input");
|
|
17305
|
-
ni.type = "number";
|
|
17306
|
-
ni.value = String(value);
|
|
17307
|
-
ni.min = String(min);
|
|
17308
|
-
ni.max = String(max);
|
|
17309
|
-
ni.step = String(step);
|
|
17310
|
-
ni.className = "vela-sd-number";
|
|
17311
|
-
ni.style.flex = "0 0 auto";
|
|
17312
|
-
ni.addEventListener("input", () => {
|
|
17313
|
-
const n = Number(ni.value);
|
|
17314
|
-
if (Number.isFinite(n)) onChange(n);
|
|
17315
|
-
});
|
|
17316
|
-
return ni;
|
|
17317
|
-
}
|
|
17318
|
-
/** A small in-group heading (lighter than a top-level section divider). */
|
|
17319
|
-
subheading(text) {
|
|
17320
|
-
const el = document.createElement("div");
|
|
17321
|
-
el.textContent = text;
|
|
17322
|
-
el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
|
|
17323
|
-
return el;
|
|
17324
|
-
}
|
|
17325
17951
|
/** A dropdown whose option values differ from their display labels. */
|
|
17326
17952
|
selectRowLabeled(label, value, options, onChange) {
|
|
17327
17953
|
const { wrap } = this.row(label);
|
|
@@ -17367,19 +17993,7 @@ var SettingsDialog = class {
|
|
|
17367
17993
|
foot.appendChild(resetBtn);
|
|
17368
17994
|
return foot;
|
|
17369
17995
|
}
|
|
17370
|
-
button(label, onClick) {
|
|
17371
|
-
const b = document.createElement("button");
|
|
17372
|
-
b.type = "button";
|
|
17373
|
-
b.textContent = label;
|
|
17374
|
-
b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
|
|
17375
|
-
b.addEventListener("click", onClick);
|
|
17376
|
-
return b;
|
|
17377
|
-
}
|
|
17378
|
-
ctrlStyle() {
|
|
17379
|
-
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;`;
|
|
17380
|
-
}
|
|
17381
17996
|
};
|
|
17382
|
-
var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
|
|
17383
17997
|
function normalizeSelectOptions(options) {
|
|
17384
17998
|
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
17385
17999
|
}
|
|
@@ -17582,7 +18196,7 @@ var DrawingPainter = class {
|
|
|
17582
18196
|
return;
|
|
17583
18197
|
}
|
|
17584
18198
|
if (d instanceof FixedRangeVolumeProfile) {
|
|
17585
|
-
this.paintFixedRangeVp(ctx, d, proj);
|
|
18199
|
+
this.paintFixedRangeVp(ctx, d, proj, theme);
|
|
17586
18200
|
this.paintLabel(ctx, d, proj, theme);
|
|
17587
18201
|
return;
|
|
17588
18202
|
}
|
|
@@ -18075,7 +18689,7 @@ var DrawingPainter = class {
|
|
|
18075
18689
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
18076
18690
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
18077
18691
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
18078
|
-
paintFixedRangeVp(ctx, d, proj) {
|
|
18692
|
+
paintFixedRangeVp(ctx, d, proj, theme) {
|
|
18079
18693
|
const L = d.layout(proj);
|
|
18080
18694
|
if (!L) return;
|
|
18081
18695
|
const s = d.frvp;
|
|
@@ -18114,7 +18728,7 @@ var DrawingPainter = class {
|
|
|
18114
18728
|
};
|
|
18115
18729
|
hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
|
|
18116
18730
|
hLine(s.showVal, s.valColor, s.valStyle, L.valY);
|
|
18117
|
-
hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
|
|
18731
|
+
hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
|
|
18118
18732
|
const devW = Math.max(1, w);
|
|
18119
18733
|
if (s.showDevelopingPoc) {
|
|
18120
18734
|
this.strokePolyline(ctx, L.developingPoc, {
|
|
@@ -18986,7 +19600,11 @@ var DrawingInteraction = class {
|
|
|
18986
19600
|
const last = draft.anchors[draft.anchors.length - 1];
|
|
18987
19601
|
const lx = last ? proj.xOf(last.time) : NaN;
|
|
18988
19602
|
const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
|
|
18989
|
-
if (
|
|
19603
|
+
if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
|
|
19604
|
+
if (draft.anchors.length >= this.state.need) {
|
|
19605
|
+
const lastIdx = draft.anchors.length - 1;
|
|
19606
|
+
draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
|
|
19607
|
+
}
|
|
18990
19608
|
draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
|
|
18991
19609
|
}
|
|
18992
19610
|
this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
|
|
@@ -19059,11 +19677,11 @@ function applyFree(anchor, pt, free) {
|
|
|
19059
19677
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
19060
19678
|
var BTN = 30;
|
|
19061
19679
|
var ICON = 21;
|
|
19062
|
-
var
|
|
19063
|
-
function
|
|
19064
|
-
if (typeof document === "undefined" || document.getElementById(
|
|
19680
|
+
var STYLE_ID4 = "vela-drawing-popup-styles";
|
|
19681
|
+
function ensureStyles4() {
|
|
19682
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
|
|
19065
19683
|
const s = document.createElement("style");
|
|
19066
|
-
s.id =
|
|
19684
|
+
s.id = STYLE_ID4;
|
|
19067
19685
|
s.textContent = `
|
|
19068
19686
|
.vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
|
|
19069
19687
|
.vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -19082,7 +19700,10 @@ function ensureStyles3() {
|
|
|
19082
19700
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
19083
19701
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
19084
19702
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
19085
|
-
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
19703
|
+
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
19704
|
+
/* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
|
|
19705
|
+
devices (the glyph stays centered; only the hit target widens). */
|
|
19706
|
+
@media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
|
|
19086
19707
|
document.head.appendChild(s);
|
|
19087
19708
|
}
|
|
19088
19709
|
var DrawingSettingsPopup = class {
|
|
@@ -19129,7 +19750,7 @@ var DrawingSettingsPopup = class {
|
|
|
19129
19750
|
* `onClose` fires on an outside-click dismissal (not a programmatic close). */
|
|
19130
19751
|
open(drawing, anchor, actions, onClose) {
|
|
19131
19752
|
this.close();
|
|
19132
|
-
|
|
19753
|
+
ensureStyles4();
|
|
19133
19754
|
this.onClose = onClose ?? null;
|
|
19134
19755
|
const t = this.theme;
|
|
19135
19756
|
const schema = drawing.schema();
|
|
@@ -19155,7 +19776,14 @@ var DrawingSettingsPopup = class {
|
|
|
19155
19776
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
19156
19777
|
}
|
|
19157
19778
|
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 })));
|
|
19158
|
-
if (paths.has("style.lineWidth"))
|
|
19779
|
+
if (paths.has("style.lineWidth")) {
|
|
19780
|
+
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
19781
|
+
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
19782
|
+
bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
19783
|
+
} else {
|
|
19784
|
+
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 }));
|
|
19785
|
+
}
|
|
19786
|
+
}
|
|
19159
19787
|
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 }));
|
|
19160
19788
|
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 })));
|
|
19161
19789
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
@@ -19623,14 +20251,15 @@ var DrawingSettingsPopup = class {
|
|
|
19623
20251
|
const chk = document.createElement("input");
|
|
19624
20252
|
chk.type = "checkbox";
|
|
19625
20253
|
chk.checked = Boolean(s[showPath]);
|
|
19626
|
-
|
|
20254
|
+
const current = s[colorPath] ?? contrastColor(t.background);
|
|
20255
|
+
chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
|
|
19627
20256
|
chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
|
|
19628
20257
|
const lbl = document.createElement("span");
|
|
19629
20258
|
lbl.textContent = label;
|
|
19630
20259
|
lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
|
|
19631
20260
|
const col = document.createElement("button");
|
|
19632
20261
|
col.type = "button";
|
|
19633
|
-
let cur =
|
|
20262
|
+
let cur = current;
|
|
19634
20263
|
col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
|
|
19635
20264
|
col.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
19636
20265
|
col.addEventListener("click", (e) => {
|
|
@@ -19810,7 +20439,8 @@ var DrawingSettingsPopup = class {
|
|
|
19810
20439
|
dragHandle() {
|
|
19811
20440
|
const b = document.createElement("button");
|
|
19812
20441
|
b.type = "button";
|
|
19813
|
-
b.
|
|
20442
|
+
b.className = "vela-dpop-grip";
|
|
20443
|
+
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);`;
|
|
19814
20444
|
b.innerHTML = sized(GRIP_ICON);
|
|
19815
20445
|
b.addEventListener("pointerdown", (e) => {
|
|
19816
20446
|
const el = this.el;
|
|
@@ -19826,6 +20456,10 @@ var DrawingSettingsPopup = class {
|
|
|
19826
20456
|
const origLeft = parseFloat(el.style.left) || 0;
|
|
19827
20457
|
const origTop = parseFloat(el.style.top) || 0;
|
|
19828
20458
|
b.style.cursor = "grabbing";
|
|
20459
|
+
try {
|
|
20460
|
+
b.setPointerCapture(e.pointerId);
|
|
20461
|
+
} catch {
|
|
20462
|
+
}
|
|
19829
20463
|
const move = (ev) => {
|
|
19830
20464
|
const w = el.offsetWidth;
|
|
19831
20465
|
const h = el.offsetHeight;
|
|
@@ -19838,9 +20472,11 @@ var DrawingSettingsPopup = class {
|
|
|
19838
20472
|
b.style.cursor = "grab";
|
|
19839
20473
|
window.removeEventListener("pointermove", move);
|
|
19840
20474
|
window.removeEventListener("pointerup", up);
|
|
20475
|
+
window.removeEventListener("pointercancel", up);
|
|
19841
20476
|
};
|
|
19842
20477
|
window.addEventListener("pointermove", move);
|
|
19843
20478
|
window.addEventListener("pointerup", up);
|
|
20479
|
+
window.addEventListener("pointercancel", up);
|
|
19844
20480
|
});
|
|
19845
20481
|
return b;
|
|
19846
20482
|
}
|
|
@@ -19956,6 +20592,39 @@ var DrawingSettingsPopup = class {
|
|
|
19956
20592
|
});
|
|
19957
20593
|
return b;
|
|
19958
20594
|
}
|
|
20595
|
+
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
20596
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. */
|
|
20597
|
+
widthInput(tip, value, min, max, step, onChange) {
|
|
20598
|
+
const input = document.createElement("input");
|
|
20599
|
+
input.type = "number";
|
|
20600
|
+
input.dataset.tip = tip;
|
|
20601
|
+
input.min = String(min);
|
|
20602
|
+
input.max = String(max);
|
|
20603
|
+
input.step = String(step);
|
|
20604
|
+
input.value = String(value);
|
|
20605
|
+
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;`;
|
|
20606
|
+
const commit = () => {
|
|
20607
|
+
const n = parseFloat(input.value);
|
|
20608
|
+
if (!Number.isFinite(n)) {
|
|
20609
|
+
input.value = String(value);
|
|
20610
|
+
return;
|
|
20611
|
+
}
|
|
20612
|
+
const v = Math.min(max, Math.max(min, n));
|
|
20613
|
+
input.value = String(v);
|
|
20614
|
+
value = v;
|
|
20615
|
+
onChange(v);
|
|
20616
|
+
};
|
|
20617
|
+
input.addEventListener("change", commit);
|
|
20618
|
+
input.addEventListener("keydown", (e) => {
|
|
20619
|
+
e.stopPropagation();
|
|
20620
|
+
if (e.key === "Enter") {
|
|
20621
|
+
e.preventDefault();
|
|
20622
|
+
commit();
|
|
20623
|
+
input.blur();
|
|
20624
|
+
}
|
|
20625
|
+
});
|
|
20626
|
+
return input;
|
|
20627
|
+
}
|
|
19959
20628
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
19960
20629
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
19961
20630
|
* opens a floating list of every option so a value is one click away (no cycling through). */
|
|
@@ -20262,7 +20931,7 @@ var DrawingToolbar = class {
|
|
|
20262
20931
|
this.onCollapse = options.onCollapse ?? (() => {
|
|
20263
20932
|
});
|
|
20264
20933
|
this.onDrawingsSync = options.onDrawingsSync ?? null;
|
|
20265
|
-
|
|
20934
|
+
ensureStyles5();
|
|
20266
20935
|
this.root = document.createElement("div");
|
|
20267
20936
|
this.root.className = "vela-dtb";
|
|
20268
20937
|
this.styleRoot();
|
|
@@ -20745,13 +21414,13 @@ var DrawingToolbar = class {
|
|
|
20745
21414
|
}
|
|
20746
21415
|
};
|
|
20747
21416
|
var MAGNET_ID = "__magnet__";
|
|
20748
|
-
var
|
|
20749
|
-
function
|
|
21417
|
+
var STYLE_ID5 = "vela-drawing-toolbar-styles";
|
|
21418
|
+
function ensureStyles5() {
|
|
20750
21419
|
if (typeof document === "undefined") return;
|
|
20751
|
-
let s = document.getElementById(
|
|
21420
|
+
let s = document.getElementById(STYLE_ID5);
|
|
20752
21421
|
if (!s) {
|
|
20753
21422
|
s = document.createElement("style");
|
|
20754
|
-
s.id =
|
|
21423
|
+
s.id = STYLE_ID5;
|
|
20755
21424
|
document.head.appendChild(s);
|
|
20756
21425
|
}
|
|
20757
21426
|
s.textContent = `
|
|
@@ -21033,6 +21702,9 @@ var UserDrawingController = class {
|
|
|
21033
21702
|
// mirrors showToolbar (drives the gutter reservation)
|
|
21034
21703
|
this.toolbarCollapsed = false;
|
|
21035
21704
|
// the bar is a slim expand-strip (narrower gutter)
|
|
21705
|
+
/** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
|
|
21706
|
+
* picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
|
|
21707
|
+
this.mobileLayout = false;
|
|
21036
21708
|
/** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
|
|
21037
21709
|
* history (drawings + app ops in one timeline) turns this off so the chords
|
|
21038
21710
|
* bubble to its keymap instead — see the renderer's `historyChords` feature. */
|
|
@@ -21091,12 +21763,21 @@ var UserDrawingController = class {
|
|
|
21091
21763
|
}
|
|
21092
21764
|
showToolbar(visible) {
|
|
21093
21765
|
this.toolbarVisible = visible;
|
|
21094
|
-
this.toolbar.setVisible(visible);
|
|
21766
|
+
this.toolbar.setVisible(visible && !this.mobileLayout);
|
|
21767
|
+
this.syncToolbarGutter();
|
|
21768
|
+
}
|
|
21769
|
+
/** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
|
|
21770
|
+
* shell's own drawer picks tools instead) and releases its gutter; flipping back
|
|
21771
|
+
* to desktop restores whatever `showToolbar` last asked for. */
|
|
21772
|
+
setLayoutMode(mode) {
|
|
21773
|
+
this.mobileLayout = mode === "mobile";
|
|
21774
|
+
this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
|
|
21095
21775
|
this.syncToolbarGutter();
|
|
21096
21776
|
}
|
|
21097
21777
|
/** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
|
|
21098
21778
|
syncToolbarGutter() {
|
|
21099
|
-
this.
|
|
21779
|
+
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
21780
|
+
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
21100
21781
|
}
|
|
21101
21782
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
21102
21783
|
setExternalGhost(doc) {
|
|
@@ -21977,7 +22658,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
|
|
|
21977
22658
|
|
|
21978
22659
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
21979
22660
|
var ATTRIBUTION_URL = "https://luxalgo.com/vela";
|
|
21980
|
-
var
|
|
22661
|
+
var STYLE_ID6 = "vela-attribution-styles";
|
|
21981
22662
|
var CSS = `
|
|
21982
22663
|
.vela-attribution {
|
|
21983
22664
|
text-decoration: none;
|
|
@@ -22023,12 +22704,28 @@ var CSS = `
|
|
|
22023
22704
|
max-width: 240px;
|
|
22024
22705
|
opacity: 1;
|
|
22025
22706
|
transform: translateX(0);
|
|
22026
|
-
}
|
|
22027
|
-
|
|
22028
|
-
|
|
22707
|
+
}
|
|
22708
|
+
/* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
|
|
22709
|
+
renderer's own container attribute (data-vela-layout) AND the shell root's
|
|
22710
|
+
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
22711
|
+
container, so only the shell attribute reaches it. */
|
|
22712
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
22713
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
|
|
22714
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
22715
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
22716
|
+
top: 2px;
|
|
22717
|
+
transform: translateX(-6px);
|
|
22718
|
+
}
|
|
22719
|
+
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
22720
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
|
|
22721
|
+
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
22722
|
+
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
22723
|
+
`;
|
|
22724
|
+
function ensureStyles6(doc) {
|
|
22725
|
+
let s = doc.getElementById(STYLE_ID6);
|
|
22029
22726
|
if (!s) {
|
|
22030
22727
|
s = doc.createElement("style");
|
|
22031
|
-
s.id =
|
|
22728
|
+
s.id = STYLE_ID6;
|
|
22032
22729
|
doc.head.appendChild(s);
|
|
22033
22730
|
}
|
|
22034
22731
|
s.textContent = CSS;
|
|
@@ -22057,7 +22754,7 @@ function createCustomMark(doc, html, background) {
|
|
|
22057
22754
|
return el;
|
|
22058
22755
|
}
|
|
22059
22756
|
function createAttributionMark(doc, background) {
|
|
22060
|
-
|
|
22757
|
+
ensureStyles6(doc);
|
|
22061
22758
|
const a = doc.createElement("a");
|
|
22062
22759
|
a.className = "vela-attribution";
|
|
22063
22760
|
a.href = ATTRIBUTION_URL;
|
|
@@ -22083,6 +22780,174 @@ function createAttributionMark(doc, background) {
|
|
|
22083
22780
|
return a;
|
|
22084
22781
|
}
|
|
22085
22782
|
|
|
22783
|
+
// src/renderers/shared/dom-raster.ts
|
|
22784
|
+
function hasInk(color) {
|
|
22785
|
+
if (!color || color === "transparent") return false;
|
|
22786
|
+
const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
|
|
22787
|
+
return m ? parseFloat(m[1]) > 1e-3 : true;
|
|
22788
|
+
}
|
|
22789
|
+
function cornerRadius(cs, w, h) {
|
|
22790
|
+
const raw = cs.borderTopLeftRadius;
|
|
22791
|
+
const v = parseFloat(raw) || 0;
|
|
22792
|
+
const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
|
|
22793
|
+
return Math.max(0, Math.min(px, Math.min(w, h) / 2));
|
|
22794
|
+
}
|
|
22795
|
+
function tracePath(ctx, x, y, w, h, r) {
|
|
22796
|
+
ctx.beginPath();
|
|
22797
|
+
if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
|
|
22798
|
+
else ctx.rect(x, y, w, h);
|
|
22799
|
+
}
|
|
22800
|
+
function canDrawUntainted(img) {
|
|
22801
|
+
try {
|
|
22802
|
+
const probe2 = img.ownerDocument.createElement("canvas");
|
|
22803
|
+
probe2.width = 1;
|
|
22804
|
+
probe2.height = 1;
|
|
22805
|
+
const ctx = probe2.getContext("2d");
|
|
22806
|
+
if (!ctx) return false;
|
|
22807
|
+
ctx.drawImage(img, 0, 0, 1, 1);
|
|
22808
|
+
probe2.toDataURL();
|
|
22809
|
+
return true;
|
|
22810
|
+
} catch {
|
|
22811
|
+
return false;
|
|
22812
|
+
}
|
|
22813
|
+
}
|
|
22814
|
+
function rasterizeOverlay(ctx, root, frame) {
|
|
22815
|
+
const win = root.ownerDocument.defaultView;
|
|
22816
|
+
if (!win) return;
|
|
22817
|
+
ctx.save();
|
|
22818
|
+
ctx.scale(frame.dpr, frame.dpr);
|
|
22819
|
+
const drawText = (node, cs, alpha) => {
|
|
22820
|
+
const text = node.textContent?.replace(/\s+/g, " ").trim();
|
|
22821
|
+
if (!text) return;
|
|
22822
|
+
const range = node.ownerDocument.createRange();
|
|
22823
|
+
range.selectNodeContents(node);
|
|
22824
|
+
const r = range.getBoundingClientRect();
|
|
22825
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
22826
|
+
ctx.globalAlpha = alpha;
|
|
22827
|
+
ctx.fillStyle = cs.color;
|
|
22828
|
+
ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
|
|
22829
|
+
ctx.textBaseline = "middle";
|
|
22830
|
+
ctx.textAlign = "left";
|
|
22831
|
+
ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
|
|
22832
|
+
};
|
|
22833
|
+
const drawImg = (img, cs, alpha) => {
|
|
22834
|
+
if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
|
|
22835
|
+
const r = img.getBoundingClientRect();
|
|
22836
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
22837
|
+
const x = r.left - frame.left;
|
|
22838
|
+
const y = r.top - frame.top;
|
|
22839
|
+
ctx.save();
|
|
22840
|
+
ctx.globalAlpha = alpha;
|
|
22841
|
+
tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
|
|
22842
|
+
ctx.clip();
|
|
22843
|
+
ctx.drawImage(img, x, y, r.width, r.height);
|
|
22844
|
+
ctx.restore();
|
|
22845
|
+
};
|
|
22846
|
+
const drawSvgShapes = (parent, alpha) => {
|
|
22847
|
+
for (const el of parent.children) {
|
|
22848
|
+
if (el.hasAttribute("transform")) continue;
|
|
22849
|
+
const tag = el.tagName.toLowerCase();
|
|
22850
|
+
if (tag === "g") {
|
|
22851
|
+
drawSvgShapes(el, alpha);
|
|
22852
|
+
continue;
|
|
22853
|
+
}
|
|
22854
|
+
const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
|
|
22855
|
+
let path = null;
|
|
22856
|
+
if (tag === "path") {
|
|
22857
|
+
const d = el.getAttribute("d");
|
|
22858
|
+
if (d) path = new Path2D(d);
|
|
22859
|
+
} else if (tag === "circle") {
|
|
22860
|
+
path = new Path2D();
|
|
22861
|
+
path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
|
|
22862
|
+
} else if (tag === "ellipse") {
|
|
22863
|
+
path = new Path2D();
|
|
22864
|
+
path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
|
|
22865
|
+
} else if (tag === "rect") {
|
|
22866
|
+
path = new Path2D();
|
|
22867
|
+
path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
|
|
22868
|
+
} else if (tag === "line") {
|
|
22869
|
+
path = new Path2D();
|
|
22870
|
+
path.moveTo(num3("x1"), num3("y1"));
|
|
22871
|
+
path.lineTo(num3("x2"), num3("y2"));
|
|
22872
|
+
} else if (tag === "polyline" || tag === "polygon") {
|
|
22873
|
+
const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
|
|
22874
|
+
if (pts.length < 4 || pts.some(Number.isNaN)) continue;
|
|
22875
|
+
path = new Path2D();
|
|
22876
|
+
path.moveTo(pts[0], pts[1]);
|
|
22877
|
+
for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
|
|
22878
|
+
if (tag === "polygon") path.closePath();
|
|
22879
|
+
}
|
|
22880
|
+
if (!path) continue;
|
|
22881
|
+
const cs = win.getComputedStyle(el);
|
|
22882
|
+
if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
|
|
22883
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
|
|
22884
|
+
ctx.fillStyle = cs.fill;
|
|
22885
|
+
ctx.fill(path);
|
|
22886
|
+
}
|
|
22887
|
+
const sw = parseFloat(cs.strokeWidth) || 0;
|
|
22888
|
+
if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
|
|
22889
|
+
ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
|
|
22890
|
+
ctx.strokeStyle = cs.stroke;
|
|
22891
|
+
ctx.lineWidth = sw;
|
|
22892
|
+
if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
|
|
22893
|
+
if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
|
|
22894
|
+
ctx.stroke(path);
|
|
22895
|
+
}
|
|
22896
|
+
}
|
|
22897
|
+
};
|
|
22898
|
+
const drawSvg = (svg, parentAlpha) => {
|
|
22899
|
+
const r = svg.getBoundingClientRect();
|
|
22900
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
22901
|
+
const cs = win.getComputedStyle(svg);
|
|
22902
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
22903
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
22904
|
+
if (alpha <= 1e-3) return;
|
|
22905
|
+
const vb = svg.viewBox.baseVal;
|
|
22906
|
+
const vw = vb && vb.width > 0 ? vb.width : r.width;
|
|
22907
|
+
const vh = vb && vb.height > 0 ? vb.height : r.height;
|
|
22908
|
+
ctx.save();
|
|
22909
|
+
ctx.translate(r.left - frame.left, r.top - frame.top);
|
|
22910
|
+
ctx.scale(r.width / vw, r.height / vh);
|
|
22911
|
+
if (vb) ctx.translate(-vb.x, -vb.y);
|
|
22912
|
+
drawSvgShapes(svg, alpha);
|
|
22913
|
+
ctx.restore();
|
|
22914
|
+
};
|
|
22915
|
+
const drawElement = (el, parentAlpha) => {
|
|
22916
|
+
const cs = win.getComputedStyle(el);
|
|
22917
|
+
if (cs.display === "none" || cs.visibility === "hidden") return;
|
|
22918
|
+
const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
|
|
22919
|
+
if (alpha <= 1e-3) return;
|
|
22920
|
+
const r = el.getBoundingClientRect();
|
|
22921
|
+
if (r.width > 0 && r.height > 0) {
|
|
22922
|
+
const x = r.left - frame.left;
|
|
22923
|
+
const y = r.top - frame.top;
|
|
22924
|
+
const radius = cornerRadius(cs, r.width, r.height);
|
|
22925
|
+
if (hasInk(cs.backgroundColor)) {
|
|
22926
|
+
ctx.globalAlpha = alpha;
|
|
22927
|
+
ctx.fillStyle = cs.backgroundColor;
|
|
22928
|
+
tracePath(ctx, x, y, r.width, r.height, radius);
|
|
22929
|
+
ctx.fill();
|
|
22930
|
+
}
|
|
22931
|
+
const bw = parseFloat(cs.borderTopWidth) || 0;
|
|
22932
|
+
if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
|
|
22933
|
+
ctx.globalAlpha = alpha;
|
|
22934
|
+
ctx.strokeStyle = cs.borderTopColor;
|
|
22935
|
+
ctx.lineWidth = bw;
|
|
22936
|
+
tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
|
|
22937
|
+
ctx.stroke();
|
|
22938
|
+
}
|
|
22939
|
+
}
|
|
22940
|
+
for (const child of el.childNodes) {
|
|
22941
|
+
if (child.nodeType === 3) drawText(child, cs, alpha);
|
|
22942
|
+
else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
|
|
22943
|
+
else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
|
|
22944
|
+
else if (child instanceof win.HTMLElement) drawElement(child, alpha);
|
|
22945
|
+
}
|
|
22946
|
+
};
|
|
22947
|
+
if (root instanceof win.HTMLElement) drawElement(root, 1);
|
|
22948
|
+
ctx.restore();
|
|
22949
|
+
}
|
|
22950
|
+
|
|
22086
22951
|
// src/renderers/native/vpvr/profile.ts
|
|
22087
22952
|
function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
|
|
22088
22953
|
const from = Math.max(0, i0);
|
|
@@ -22251,12 +23116,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
|
|
|
22251
23116
|
var COLLAPSED_PANE_H = 26;
|
|
22252
23117
|
var TIME_AXIS_H = 22;
|
|
22253
23118
|
var SCROLL_BTN_SIZE = 26;
|
|
23119
|
+
var SCROLL_BTN_SIZE_TOUCH = 24;
|
|
22254
23120
|
var SCROLL_BTN_RADIUS = 5;
|
|
22255
23121
|
var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
|
|
22256
23122
|
var SCROLL_BTN_ICON_PX = 12;
|
|
22257
|
-
var SCROLL_BTN_RIGHT_INSET =
|
|
23123
|
+
var SCROLL_BTN_RIGHT_INSET = 12;
|
|
22258
23124
|
var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
|
|
22259
|
-
var SCROLL_BTN_BOTTOM = TIME_AXIS_H +
|
|
23125
|
+
var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
22260
23126
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
22261
23127
|
var MIN_VISIBLE_BARS = 2;
|
|
22262
23128
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
@@ -22308,8 +23174,12 @@ var NativeRenderer = class {
|
|
|
22308
23174
|
this.symbolPicker = null;
|
|
22309
23175
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
22310
23176
|
this.indicatorTitlesOn = true;
|
|
23177
|
+
/** Plot values beside the legend titles shown — held here so a remount re-applies it. */
|
|
23178
|
+
this.indicatorValuesOn = true;
|
|
22311
23179
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
22312
23180
|
this.legendActionsProvider = null;
|
|
23181
|
+
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
23182
|
+
this.legendOverviewAction = null;
|
|
22313
23183
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
22314
23184
|
this.keyboard = null;
|
|
22315
23185
|
this.keyboardEnabled = true;
|
|
@@ -22419,6 +23289,8 @@ var NativeRenderer = class {
|
|
|
22419
23289
|
// Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
|
|
22420
23290
|
// pane carries merged (own-scale) columns; the constant only clears a single-column scale.
|
|
22421
23291
|
this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
|
|
23292
|
+
// ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
|
|
23293
|
+
this.layoutMode = "desktop";
|
|
22422
23294
|
this.viewportCbs = /* @__PURE__ */ new Set();
|
|
22423
23295
|
this.crosshairCbs = /* @__PURE__ */ new Set();
|
|
22424
23296
|
this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
|
|
@@ -22429,6 +23301,7 @@ var NativeRenderer = class {
|
|
|
22429
23301
|
this.factoryConfig = null;
|
|
22430
23302
|
this.hostSettingsSections = [];
|
|
22431
23303
|
this.clickCbs = /* @__PURE__ */ new Set();
|
|
23304
|
+
this.axisLongPressCbs = /* @__PURE__ */ new Set();
|
|
22432
23305
|
this.inputChangeCbs = /* @__PURE__ */ new Set();
|
|
22433
23306
|
this.removeIndicatorCbs = /* @__PURE__ */ new Set();
|
|
22434
23307
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
@@ -22436,7 +23309,7 @@ var NativeRenderer = class {
|
|
|
22436
23309
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
22437
23310
|
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
22438
23311
|
this.name = "native";
|
|
22439
|
-
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"];
|
|
23312
|
+
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"];
|
|
22440
23313
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
22441
23314
|
* so moving onto the button doesn't count as leaving). */
|
|
22442
23315
|
this.onScrollProximityMove = (e) => {
|
|
@@ -22609,6 +23482,11 @@ var NativeRenderer = class {
|
|
|
22609
23482
|
this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
|
|
22610
23483
|
return;
|
|
22611
23484
|
// own DOM, no repaint needed
|
|
23485
|
+
case "indicatorValues":
|
|
23486
|
+
this.indicatorValuesOn = value !== false;
|
|
23487
|
+
this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
|
|
23488
|
+
return;
|
|
23489
|
+
// own DOM, no repaint needed
|
|
22612
23490
|
case "attribution":
|
|
22613
23491
|
this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
|
|
22614
23492
|
this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
|
|
@@ -22697,6 +23575,8 @@ var NativeRenderer = class {
|
|
|
22697
23575
|
return this.settingsEnabled;
|
|
22698
23576
|
case "indicatorTitles":
|
|
22699
23577
|
return this.indicatorTitlesOn;
|
|
23578
|
+
case "indicatorValues":
|
|
23579
|
+
return this.indicatorValuesOn;
|
|
22700
23580
|
case "attribution":
|
|
22701
23581
|
return this.attributionHtml ?? this.attributionEnabled;
|
|
22702
23582
|
case "dialogHost":
|
|
@@ -23033,7 +23913,10 @@ var NativeRenderer = class {
|
|
|
23033
23913
|
/** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
|
|
23034
23914
|
* in-chart gear — created on demand, independent of the gear feature being enabled. */
|
|
23035
23915
|
openSettingsDialog(section) {
|
|
23036
|
-
if (!this.settingsDialog && this.plot)
|
|
23916
|
+
if (!this.settingsDialog && this.plot) {
|
|
23917
|
+
this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
|
|
23918
|
+
this.settingsDialog.setLayoutMode(this.layoutMode);
|
|
23919
|
+
}
|
|
23037
23920
|
this.toggleSettingsDialog(section);
|
|
23038
23921
|
}
|
|
23039
23922
|
/** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
|
|
@@ -23122,11 +24005,18 @@ var NativeRenderer = class {
|
|
|
23122
24005
|
this.scrollButton.style.color = this.theme.textColor;
|
|
23123
24006
|
this.scrollButton.style.borderColor = this.theme.borderColor;
|
|
23124
24007
|
}
|
|
23125
|
-
/** Show the button only when the latest bar is scrolled off the right edge
|
|
24008
|
+
/** Show the button only when the latest bar is scrolled off the right edge. On desktop the
|
|
24009
|
+
* cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
|
|
24010
|
+
* appears whenever the latest bars are off-screen and hides again once the view is back
|
|
24011
|
+
* at the right edge. */
|
|
23126
24012
|
updateScrollToRealtimeButton() {
|
|
23127
24013
|
const btn = this.scrollButton;
|
|
23128
24014
|
if (!btn) return;
|
|
23129
24015
|
const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
|
|
24016
|
+
if (this.layoutMode === "mobile") {
|
|
24017
|
+
btn.style.display = latestOffScreen ? "flex" : "none";
|
|
24018
|
+
return;
|
|
24019
|
+
}
|
|
23130
24020
|
btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
|
|
23131
24021
|
}
|
|
23132
24022
|
/** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
|
|
@@ -23148,17 +24038,20 @@ var NativeRenderer = class {
|
|
|
23148
24038
|
this.animator.start();
|
|
23149
24039
|
}
|
|
23150
24040
|
/**
|
|
23151
|
-
* Export the current chart as a PNG data URL by compositing
|
|
23152
|
-
*
|
|
23153
|
-
*
|
|
23154
|
-
*
|
|
23155
|
-
*
|
|
23156
|
-
*
|
|
23157
|
-
*
|
|
23158
|
-
*
|
|
23159
|
-
*
|
|
23160
|
-
*
|
|
23161
|
-
*
|
|
24041
|
+
* Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
|
|
24042
|
+
* an offscreen canvas, in the same stacking order the DOM shows: plugin layers
|
|
24043
|
+
* below the data, geometry (L0), volume columns (L0.25), the visible-range volume
|
|
24044
|
+
* profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
|
|
24045
|
+
* (L1.5). The background is filled first (the canvas2d data layer is transparent —
|
|
24046
|
+
* its bg lives on the wrapper), and a fresh synchronous paint runs first so the
|
|
24047
|
+
* WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
|
|
24048
|
+
* back this tick — the same paint also repaints the drawings layers, so they're
|
|
24049
|
+
* current. DOM chrome joins as a best-effort text/chip raster (see
|
|
24050
|
+
* `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
|
|
24051
|
+
* opts in with a `data-vela-screenshot` attribute on the mount container's
|
|
24052
|
+
* subtree (the widget marks its status line, and its symbol watermark with
|
|
24053
|
+
* `"under"` — drawn beneath the canvases, where it sits on screen). Only the
|
|
24054
|
+
* crosshair (L2) is intentionally excluded.
|
|
23162
24055
|
*/
|
|
23163
24056
|
screenshot() {
|
|
23164
24057
|
if (!this.dataCanvas) return null;
|
|
@@ -23171,9 +24064,25 @@ var NativeRenderer = class {
|
|
|
23171
24064
|
if (!ctx) return null;
|
|
23172
24065
|
ctx.fillStyle = this.theme.background;
|
|
23173
24066
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
23174
|
-
|
|
23175
|
-
|
|
23176
|
-
|
|
24067
|
+
const plotRect = this.plot?.getBoundingClientRect();
|
|
24068
|
+
const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
|
|
24069
|
+
const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
|
|
24070
|
+
if (frame) {
|
|
24071
|
+
for (const el of hostMarked) {
|
|
24072
|
+
if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
|
|
24073
|
+
}
|
|
24074
|
+
}
|
|
24075
|
+
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
24076
|
+
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
24077
|
+
for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
|
|
24078
|
+
if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
|
|
24079
|
+
}
|
|
24080
|
+
if (frame) {
|
|
24081
|
+
for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
|
|
24082
|
+
for (const el of hostMarked) {
|
|
24083
|
+
if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
|
|
24084
|
+
}
|
|
24085
|
+
}
|
|
23177
24086
|
return out.toDataURL("image/png");
|
|
23178
24087
|
}
|
|
23179
24088
|
/**
|
|
@@ -23324,6 +24233,9 @@ var NativeRenderer = class {
|
|
|
23324
24233
|
fling: (v) => this.fling(v),
|
|
23325
24234
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
23326
24235
|
onClick: (x) => this.handleClick(x),
|
|
24236
|
+
onAxisLongPress: (axis, x, y) => {
|
|
24237
|
+
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
24238
|
+
},
|
|
23327
24239
|
beginPriceScale: (x, y) => this.beginPriceScale(x, y),
|
|
23328
24240
|
priceScaleBy: (dy) => this.priceScaleBy(dy),
|
|
23329
24241
|
beginPricePan: (y) => this.beginPricePan(y),
|
|
@@ -23370,6 +24282,14 @@ var NativeRenderer = class {
|
|
|
23370
24282
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
23371
24283
|
});
|
|
23372
24284
|
this.userDrawings.historyChords = this.historyChordsEnabled;
|
|
24285
|
+
if (this.layoutMode === "mobile") {
|
|
24286
|
+
this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
|
|
24287
|
+
this.userDrawings.setLayoutMode(this.layoutMode);
|
|
24288
|
+
if (this.scrollButton) {
|
|
24289
|
+
this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
24290
|
+
this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
|
|
24291
|
+
}
|
|
24292
|
+
}
|
|
23373
24293
|
this.keyboard = new KeyboardController({
|
|
23374
24294
|
panByBars: (bars) => this.panByBars(bars),
|
|
23375
24295
|
zoomByStep: (dir) => this.zoomByStep(dir),
|
|
@@ -23382,10 +24302,13 @@ var NativeRenderer = class {
|
|
|
23382
24302
|
});
|
|
23383
24303
|
this.setKeyboardEnabled(this.keyboardEnabled);
|
|
23384
24304
|
this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
|
|
24305
|
+
this.inputsUI.setLayoutMode(this.layoutMode);
|
|
23385
24306
|
this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
|
|
24307
|
+
this.inputsUI.setValuesVisible(this.indicatorValuesOn);
|
|
23386
24308
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
23387
24309
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
23388
24310
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
24311
|
+
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
23389
24312
|
this.inputsUI.setOnChange((c) => {
|
|
23390
24313
|
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
23391
24314
|
});
|
|
@@ -23800,7 +24723,10 @@ var NativeRenderer = class {
|
|
|
23800
24723
|
this.scene.indicators.set(model.id, model);
|
|
23801
24724
|
this.refreshAnchorOffset(model);
|
|
23802
24725
|
this.scene.assignIndicatorZ(model.id);
|
|
23803
|
-
this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, {
|
|
24726
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
24727
|
+
native: !!model.native,
|
|
24728
|
+
...model.shorttitle ? { settingsTitle: model.title } : {}
|
|
24729
|
+
});
|
|
23804
24730
|
this.syncTables(model);
|
|
23805
24731
|
if (model.native?.type === "volume") {
|
|
23806
24732
|
this.volumeActive = true;
|
|
@@ -23861,6 +24787,13 @@ var NativeRenderer = class {
|
|
|
23861
24787
|
this.legendActionsProvider = provider;
|
|
23862
24788
|
this.inputsUI?.setLegendActions(provider);
|
|
23863
24789
|
}
|
|
24790
|
+
setLegendOverviewAction(action) {
|
|
24791
|
+
this.legendOverviewAction = action;
|
|
24792
|
+
this.inputsUI?.setLegendOverviewAction(action);
|
|
24793
|
+
}
|
|
24794
|
+
openIndicatorSettings(indicatorId) {
|
|
24795
|
+
this.inputsUI?.openSettingsFor(indicatorId);
|
|
24796
|
+
}
|
|
23864
24797
|
/**
|
|
23865
24798
|
* Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
|
|
23866
24799
|
* series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
|
|
@@ -23914,6 +24847,24 @@ var NativeRenderer = class {
|
|
|
23914
24847
|
this.removeIndicatorCbs.add(cb);
|
|
23915
24848
|
return () => this.removeIndicatorCbs.delete(cb);
|
|
23916
24849
|
}
|
|
24850
|
+
/** Port surface: the host shell's chrome size class. Mobile switches the renderer
|
|
24851
|
+
* chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
|
|
24852
|
+
* toolbar (the shell provides its own picker), the scroll-to-latest button shown
|
|
24853
|
+
* whenever the latest bars are off-screen (touch has no cursor proximity). */
|
|
24854
|
+
setLayoutMode(mode) {
|
|
24855
|
+
if (mode === this.layoutMode) return;
|
|
24856
|
+
this.layoutMode = mode;
|
|
24857
|
+
this.mountContainer?.setAttribute("data-vela-layout", mode);
|
|
24858
|
+
this.userDrawings?.setLayoutMode(mode);
|
|
24859
|
+
this.settingsDialog?.setLayoutMode(mode);
|
|
24860
|
+
this.inputsUI?.setLayoutMode(mode);
|
|
24861
|
+
if (this.scrollButton) {
|
|
24862
|
+
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
24863
|
+
this.scrollButton.style.width = `${px}px`;
|
|
24864
|
+
this.scrollButton.style.height = `${px}px`;
|
|
24865
|
+
}
|
|
24866
|
+
this.updateScrollToRealtimeButton();
|
|
24867
|
+
}
|
|
23917
24868
|
setSettingsSections(sections) {
|
|
23918
24869
|
this.hostSettingsSections = sections;
|
|
23919
24870
|
this.settingsDialog?.setHostSections(sections);
|
|
@@ -23938,6 +24889,10 @@ var NativeRenderer = class {
|
|
|
23938
24889
|
this.clickCbs.add(cb);
|
|
23939
24890
|
return () => this.clickCbs.delete(cb);
|
|
23940
24891
|
}
|
|
24892
|
+
onAxisLongPress(cb) {
|
|
24893
|
+
this.axisLongPressCbs.add(cb);
|
|
24894
|
+
return () => this.axisLongPressCbs.delete(cb);
|
|
24895
|
+
}
|
|
23941
24896
|
onViewportChange(cb) {
|
|
23942
24897
|
this.viewportCbs.add(cb);
|
|
23943
24898
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -24079,6 +25034,7 @@ var NativeRenderer = class {
|
|
|
24079
25034
|
this.skeletonClockMs += dtMs;
|
|
24080
25035
|
this.paintData();
|
|
24081
25036
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
25037
|
+
this.updateLegendValues();
|
|
24082
25038
|
this.emitViewportChange();
|
|
24083
25039
|
return active;
|
|
24084
25040
|
}
|
|
@@ -24143,7 +25099,7 @@ var NativeRenderer = class {
|
|
|
24143
25099
|
this.lastPointer = null;
|
|
24144
25100
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
24145
25101
|
this.hoverLogical = null;
|
|
24146
|
-
const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
25102
|
+
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
24147
25103
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
24148
25104
|
return;
|
|
24149
25105
|
}
|
|
@@ -24155,7 +25111,7 @@ var NativeRenderer = class {
|
|
|
24155
25111
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
24156
25112
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
24157
25113
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
24158
|
-
const pane = this.
|
|
25114
|
+
const pane = this.paneNodeAtY(y);
|
|
24159
25115
|
const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
|
|
24160
25116
|
const values = /* @__PURE__ */ new Map();
|
|
24161
25117
|
if (onBar) {
|
|
@@ -24172,7 +25128,7 @@ var NativeRenderer = class {
|
|
|
24172
25128
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
24173
25129
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
24174
25130
|
this.hoverLogical = onBar ? logical : null;
|
|
24175
|
-
const event = { time, price, values, ohlc };
|
|
25131
|
+
const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
|
|
24176
25132
|
for (const cb of this.crosshairCbs) cb(event);
|
|
24177
25133
|
}
|
|
24178
25134
|
handleClick(x) {
|
|
@@ -24191,6 +25147,20 @@ var NativeRenderer = class {
|
|
|
24191
25147
|
}
|
|
24192
25148
|
return null;
|
|
24193
25149
|
}
|
|
25150
|
+
/** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
|
|
25151
|
+
* fall BETWEEN bounds (separator gaps, the strip under the last pane). */
|
|
25152
|
+
nearestPaneToY(y) {
|
|
25153
|
+
let best = null;
|
|
25154
|
+
let bestDist = Infinity;
|
|
25155
|
+
for (const pane of this.scene.panes.values()) {
|
|
25156
|
+
const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
|
|
25157
|
+
if (d < bestDist) {
|
|
25158
|
+
bestDist = d;
|
|
25159
|
+
best = pane;
|
|
25160
|
+
}
|
|
25161
|
+
}
|
|
25162
|
+
return best;
|
|
25163
|
+
}
|
|
24194
25164
|
// ── manual vertical scaling (price-axis drag + vertical price pan) ──
|
|
24195
25165
|
/** The scale window a gesture targets: a pane's master scale, or a merged indicator's
|
|
24196
25166
|
* own scale column (resolved from the grabbed x). Both share the four scale fields. */
|
|
@@ -24253,7 +25223,7 @@ var NativeRenderer = class {
|
|
|
24253
25223
|
* still fits the view.)
|
|
24254
25224
|
*/
|
|
24255
25225
|
dataDblClick(_x, y) {
|
|
24256
|
-
const pane = this.paneNodeAtY(y);
|
|
25226
|
+
const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
|
|
24257
25227
|
if (!pane) return;
|
|
24258
25228
|
const maximized = this.maximizedPaneId !== pane.id;
|
|
24259
25229
|
this.maximizedPaneId = maximized ? pane.id : null;
|
|
@@ -24494,29 +25464,63 @@ var NativeRenderer = class {
|
|
|
24494
25464
|
dataWindowGroups(idx, pricePane) {
|
|
24495
25465
|
const groups = [];
|
|
24496
25466
|
for (const model of this.scene.indicators.values()) {
|
|
24497
|
-
const
|
|
24498
|
-
const off = this.scene.offsetOf(model.id);
|
|
24499
|
-
const rows = [];
|
|
24500
|
-
for (const s of model.series) {
|
|
24501
|
-
let value;
|
|
24502
|
-
let color;
|
|
24503
|
-
if (s.kind === "candle" || s.kind === "bar") {
|
|
24504
|
-
value = s.bars[idx - off]?.close;
|
|
24505
|
-
color = s.style?.up ?? this.theme.upColor;
|
|
24506
|
-
} else if (isLineLikeSeries(s)) {
|
|
24507
|
-
if (s.visible === false) continue;
|
|
24508
|
-
value = s.points[idx - off]?.value;
|
|
24509
|
-
color = s.points[idx - off]?.color ?? s.style.color;
|
|
24510
|
-
} else {
|
|
24511
|
-
continue;
|
|
24512
|
-
}
|
|
24513
|
-
if (value == null || !Number.isFinite(value)) continue;
|
|
24514
|
-
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
24515
|
-
}
|
|
25467
|
+
const rows = this.dataWindowRowsFor(model, idx, pricePane);
|
|
24516
25468
|
if (rows.length) groups.push({ name: model.title, rows });
|
|
24517
25469
|
}
|
|
24518
25470
|
return groups;
|
|
24519
25471
|
}
|
|
25472
|
+
/** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
|
|
25473
|
+
dataWindowRowsFor(model, idx, pricePane) {
|
|
25474
|
+
if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
|
|
25475
|
+
const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
|
|
25476
|
+
const off = this.scene.offsetOf(model.id);
|
|
25477
|
+
const rows = [];
|
|
25478
|
+
for (const s of model.series) {
|
|
25479
|
+
let value;
|
|
25480
|
+
let color;
|
|
25481
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
25482
|
+
value = s.bars[idx - off]?.close;
|
|
25483
|
+
color = s.style?.up ?? this.theme.upColor;
|
|
25484
|
+
} else if (isLineLikeSeries(s)) {
|
|
25485
|
+
if (s.visible === false) continue;
|
|
25486
|
+
value = s.points[idx - off]?.value;
|
|
25487
|
+
color = s.points[idx - off]?.color ?? s.style.color;
|
|
25488
|
+
} else {
|
|
25489
|
+
continue;
|
|
25490
|
+
}
|
|
25491
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
25492
|
+
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
25493
|
+
}
|
|
25494
|
+
return rows;
|
|
25495
|
+
}
|
|
25496
|
+
/** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
|
|
25497
|
+
* colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
|
|
25498
|
+
* `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
|
|
25499
|
+
volumeReadoutRows(model, idx) {
|
|
25500
|
+
if (this.volumeHidden) return [];
|
|
25501
|
+
const bar = this.bars[idx];
|
|
25502
|
+
const vol = bar?.volume;
|
|
25503
|
+
if (bar == null || vol == null || !Number.isFinite(vol)) return [];
|
|
25504
|
+
const cfg = this.scene.volumeLayer;
|
|
25505
|
+
const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
|
|
25506
|
+
return [{ label: model.title, value: formatVolume(vol), color }];
|
|
25507
|
+
}
|
|
25508
|
+
/** Refresh the plot values beside every legend title — the same readout the data window
|
|
25509
|
+
* shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
|
|
25510
|
+
* no scene model, so its row is absent from the map and its readout clears. */
|
|
25511
|
+
updateLegendValues() {
|
|
25512
|
+
if (!this.inputsUI) return;
|
|
25513
|
+
const n = this.bars.length;
|
|
25514
|
+
const values = /* @__PURE__ */ new Map();
|
|
25515
|
+
if (n > 0) {
|
|
25516
|
+
const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
|
|
25517
|
+
const pricePane = this.dataWindowPricePane();
|
|
25518
|
+
for (const model of this.scene.indicators.values()) {
|
|
25519
|
+
values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
|
|
25520
|
+
}
|
|
25521
|
+
}
|
|
25522
|
+
this.inputsUI.setPlotValues(values);
|
|
25523
|
+
}
|
|
24520
25524
|
renderFrame(level) {
|
|
24521
25525
|
if (this.animator.active) return;
|
|
24522
25526
|
if (repaintsData(level)) {
|
|
@@ -24528,6 +25532,7 @@ var NativeRenderer = class {
|
|
|
24528
25532
|
}
|
|
24529
25533
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
24530
25534
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
25535
|
+
this.updateLegendValues();
|
|
24531
25536
|
}
|
|
24532
25537
|
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
24533
25538
|
repaintCursorLayers() {
|
|
@@ -24676,7 +25681,7 @@ var NativeRenderer = class {
|
|
|
24676
25681
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
24677
25682
|
}
|
|
24678
25683
|
}
|
|
24679
|
-
return { x, y, time: this.coords.logicalToTime(logical) };
|
|
25684
|
+
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
24680
25685
|
}
|
|
24681
25686
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
24682
25687
|
setSnapMode(mode) {
|
|
@@ -25368,7 +26373,8 @@ var VpvrIndicator = class {
|
|
|
25368
26373
|
};
|
|
25369
26374
|
var vpvrDescriptor = {
|
|
25370
26375
|
type: "vpvr",
|
|
25371
|
-
title: "
|
|
26376
|
+
title: "Visible Range Volume Profile",
|
|
26377
|
+
shortTitle: "VRVP",
|
|
25372
26378
|
paneHint: "price",
|
|
25373
26379
|
overlay: true,
|
|
25374
26380
|
inputsSchema: () => [
|
|
@@ -25866,6 +26872,7 @@ exports.iconMarkup = iconMarkup;
|
|
|
25866
26872
|
exports.legendActions = legendActions;
|
|
25867
26873
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
25868
26874
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
26875
|
+
exports.normalizeSettingsRow = normalizeSettingsRow;
|
|
25869
26876
|
exports.registerChartType = registerChartType;
|
|
25870
26877
|
exports.registerDefaultEngine = registerDefaultEngine;
|
|
25871
26878
|
exports.registerDrawingType = registerDrawingType;
|
|
@@ -25881,6 +26888,7 @@ exports.rendererDefaults = rendererDefaults;
|
|
|
25881
26888
|
exports.rendererLayers = rendererLayers;
|
|
25882
26889
|
exports.resolveEngines = resolveEngines;
|
|
25883
26890
|
exports.resolveTheme = resolveTheme;
|
|
26891
|
+
exports.settingsRowValueKeys = settingsRowValueKeys;
|
|
25884
26892
|
exports.sharedBarStore = sharedBarStore;
|
|
25885
26893
|
exports.sidePanels = sidePanels;
|
|
25886
26894
|
exports.stableSeriesId = stableSeriesId;
|