@luxalgo/vela 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
- package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
- package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
- package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
- package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
- package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
- package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
- package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
- package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
- package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
- package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
- package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
- package/dist/index.cjs +2010 -397
- package/dist/index.d.cts +83 -25
- package/dist/index.d.ts +83 -25
- package/dist/index.js +4 -4
- package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
- package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
- package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
- package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
- package/dist/plugin.cjs +58 -7
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/ui.cjs +342 -9
- package/dist/ui.d.cts +64 -2
- package/dist/ui.d.ts +64 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +2010 -397
- package/dist/vela.global.min.js +115 -34
- package/dist/widget.cjs +5987 -2242
- package/dist/widget.d.cts +89 -17
- package/dist/widget.d.ts +89 -17
- package/dist/widget.js +169 -18
- package/dist/workspace.cjs +5627 -1587
- package/dist/workspace.d.cts +106 -12
- package/dist/workspace.d.ts +106 -12
- package/dist/workspace.js +494 -52
- package/package.json +1 -1
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"/>'));
|
|
@@ -531,6 +533,11 @@ registerIcon(
|
|
|
531
533
|
"pen-lock",
|
|
532
534
|
svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="13" y="14.5" width="8" height="6" rx="1.2"/><path d="M15 14.5v-1.6a2 2 0 0 1 4 0v1.6"/>')
|
|
533
535
|
);
|
|
536
|
+
registerIcon(
|
|
537
|
+
"pen-sync",
|
|
538
|
+
// Pen + two stacked panes — drawing onto every linked chart at once.
|
|
539
|
+
svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="12.5" y="13.5" width="6" height="4.8" rx="1"/><path d="M15.5 18.3v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.8a1 1 0 0 0-1-1h-1"/>')
|
|
540
|
+
);
|
|
534
541
|
registerIcon("brush", svg24('<path d="M9.5 12 17 4.5a2.12 2.12 0 0 1 3 3L12.5 15"/><path d="M7 14a3 3 0 0 0-3 3c0 1.3-1.2 1.5-1.5 2 .8.9 2 1.5 3.5 1.5a3.5 3.5 0 0 0 3.5-3.5 3 3 0 0 0-2.5-3Z"/>'));
|
|
535
542
|
registerIcon(
|
|
536
543
|
"bucket",
|
|
@@ -1884,10 +1891,10 @@ function polylineHit(px, py, pts, tol) {
|
|
|
1884
1891
|
}
|
|
1885
1892
|
|
|
1886
1893
|
// src/core/drawings/types/FixedRangeVolumeProfile.ts
|
|
1887
|
-
var OUTSIDE_UP = `${BULLISH}
|
|
1888
|
-
var OUTSIDE_DOWN = `${BEARISH}
|
|
1889
|
-
var VA_UP = `${BULLISH}
|
|
1890
|
-
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`;
|
|
1891
1898
|
function defaultFrvpStyle() {
|
|
1892
1899
|
return {
|
|
1893
1900
|
rows: 24,
|
|
@@ -1905,7 +1912,8 @@ function defaultFrvpStyle() {
|
|
|
1905
1912
|
valColor: NEUTRAL,
|
|
1906
1913
|
valStyle: "solid",
|
|
1907
1914
|
showPoc: true,
|
|
1908
|
-
pocColor:
|
|
1915
|
+
pocColor: void 0,
|
|
1916
|
+
// theme contrast ink until the user picks a color
|
|
1909
1917
|
pocStyle: "solid",
|
|
1910
1918
|
showDevelopingPoc: false,
|
|
1911
1919
|
developingPocColor: ACCENT,
|
|
@@ -6534,6 +6542,11 @@ var DrawingController = class {
|
|
|
6534
6542
|
setToolbar(option) {
|
|
6535
6543
|
this.port?.setToolbar(buildToolbar(option).definition);
|
|
6536
6544
|
}
|
|
6545
|
+
/** Display (or clear) another chart's in-progress placement as a ghost — silently
|
|
6546
|
+
* inert on a renderer without the optional `setExternalGhost` seam. */
|
|
6547
|
+
setExternalGhost(doc) {
|
|
6548
|
+
this.port?.setExternalGhost?.(doc);
|
|
6549
|
+
}
|
|
6537
6550
|
/** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
|
|
6538
6551
|
setToolShortcuts(map) {
|
|
6539
6552
|
this.port?.setToolShortcuts?.(map);
|
|
@@ -6764,6 +6777,9 @@ var DrawingController = class {
|
|
|
6764
6777
|
case "arm":
|
|
6765
6778
|
this.setTool(i.type);
|
|
6766
6779
|
break;
|
|
6780
|
+
case "draft":
|
|
6781
|
+
this.events.emit("drawing:draft", { doc: i.doc });
|
|
6782
|
+
break;
|
|
6767
6783
|
case "create": {
|
|
6768
6784
|
const before = this.store.serialize();
|
|
6769
6785
|
const last = this.lastStyle.get(i.doc.type);
|
|
@@ -7039,6 +7055,52 @@ var ProviderRegistry = class {
|
|
|
7039
7055
|
};
|
|
7040
7056
|
|
|
7041
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
|
+
}
|
|
7099
|
+
function settingsRowVisible(when, bag) {
|
|
7100
|
+
if (!when) return true;
|
|
7101
|
+
const conds = Array.isArray(when) ? when : [when];
|
|
7102
|
+
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === bag[c.key]) : bag[c.key] === c.equals);
|
|
7103
|
+
}
|
|
7042
7104
|
var registry2 = /* @__PURE__ */ new Map();
|
|
7043
7105
|
function registerChartType(def) {
|
|
7044
7106
|
registry2.set(def.id, def);
|
|
@@ -7138,6 +7200,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7138
7200
|
this.barTransform = null;
|
|
7139
7201
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
7140
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();
|
|
7141
7206
|
/** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
|
|
7142
7207
|
this.activeEngineStyle = null;
|
|
7143
7208
|
/** The chart's current price style (tracked from the renderer's change events + initial read). */
|
|
@@ -7200,7 +7265,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7200
7265
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
7201
7266
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
7202
7267
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
7203
|
-
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
|
+
});
|
|
7204
7272
|
this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
|
|
7205
7273
|
this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
|
|
7206
7274
|
this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
|
|
@@ -7688,6 +7756,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7688
7756
|
if (!def?.dataEngine) return;
|
|
7689
7757
|
const engine = def.dataEngine();
|
|
7690
7758
|
this.typeEngines.set(id, engine);
|
|
7759
|
+
const stored = this.typeSettings.get(id);
|
|
7760
|
+
if (stored) engine.onSettings?.(stored);
|
|
7691
7761
|
engine.start({
|
|
7692
7762
|
symbol: this.config.market.symbol ?? "TEST",
|
|
7693
7763
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -8159,6 +8229,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8159
8229
|
return {
|
|
8160
8230
|
id: record.id,
|
|
8161
8231
|
title: record.title,
|
|
8232
|
+
...d.shortTitle ? { shorttitle: d.shortTitle } : {},
|
|
8162
8233
|
overlay: d.overlay,
|
|
8163
8234
|
paneHint: d.paneHint,
|
|
8164
8235
|
native: { type: record.native.type },
|
|
@@ -8619,6 +8690,28 @@ var RendererControl = class {
|
|
|
8619
8690
|
setLegendActions(provider) {
|
|
8620
8691
|
this.renderer.setLegendActions?.(provider);
|
|
8621
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
|
+
}
|
|
8622
8715
|
/**
|
|
8623
8716
|
* Export the current chart as a PNG data URL, or null if the active renderer
|
|
8624
8717
|
* doesn't support it (warns). DOM overlays (tables, legend) are not included.
|
|
@@ -8665,6 +8758,10 @@ var RendererControl = class {
|
|
|
8665
8758
|
onCrosshairMove(cb) {
|
|
8666
8759
|
return this.renderer.onCrosshairMove(cb);
|
|
8667
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
|
+
}
|
|
8668
8765
|
/**
|
|
8669
8766
|
* The current data-window readout — the hovered bar's date/time and OHLCV plus every
|
|
8670
8767
|
* indicator's value there, each already formatted on its pane's scale (the latest bar when
|
|
@@ -8730,6 +8827,13 @@ var RendererControl = class {
|
|
|
8730
8827
|
this.renderer.setSettingsSections?.(sections);
|
|
8731
8828
|
return this;
|
|
8732
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
|
+
}
|
|
8733
8837
|
};
|
|
8734
8838
|
|
|
8735
8839
|
// src/core/renderer-defaults.ts
|
|
@@ -9383,6 +9487,14 @@ var DrawingsControl = class {
|
|
|
9383
9487
|
if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
|
|
9384
9488
|
return this;
|
|
9385
9489
|
}
|
|
9490
|
+
/** Display another chart's in-progress placement as a GHOST at reduced opacity
|
|
9491
|
+
* (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
|
|
9492
|
+
* charts. Never a real drawing: no store entry, no selection, no persistence.
|
|
9493
|
+
* Silently inert on a renderer without the optional seam. */
|
|
9494
|
+
setExternalGhost(doc) {
|
|
9495
|
+
this.ctrl.setExternalGhost(doc);
|
|
9496
|
+
return this;
|
|
9497
|
+
}
|
|
9386
9498
|
/** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
|
|
9387
9499
|
add(type, init = {}) {
|
|
9388
9500
|
if (!this.ok("add")) return null;
|
|
@@ -9621,6 +9733,11 @@ function themeTokens(t) {
|
|
|
9621
9733
|
"--vela-hover-strong": wash(0.16),
|
|
9622
9734
|
"--vela-focus": withAlpha(t.textColor, 0.5),
|
|
9623
9735
|
"--vela-focus-soft": withAlpha(t.textColor, 0.12),
|
|
9736
|
+
// Separator hover — the SAME wash the chart's pane separators paint on hover
|
|
9737
|
+
// (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
|
|
9738
|
+
// (the workspace grid) and canvas-drawn ones read as one family.
|
|
9739
|
+
"--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
|
|
9740
|
+
"--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
|
|
9624
9741
|
"--vela-scroll": withAlpha(t.textColor, 0.3),
|
|
9625
9742
|
"--vela-accent": ACCENT,
|
|
9626
9743
|
"--vela-accent-bright": ACCENT_BRIGHT,
|
|
@@ -9668,7 +9785,7 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
9668
9785
|
const doc = anchor.ownerDocument;
|
|
9669
9786
|
tip = doc.createElement("div");
|
|
9670
9787
|
tip.textContent = text;
|
|
9671
|
-
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;");
|
|
9672
9789
|
applyChromeTokens(tip, opts.theme());
|
|
9673
9790
|
opts.host.appendChild(tip);
|
|
9674
9791
|
const a = anchor.getBoundingClientRect();
|
|
@@ -9682,16 +9799,17 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
9682
9799
|
tip.style.top = `${a.bottom - h.top + 6}px`;
|
|
9683
9800
|
}
|
|
9684
9801
|
};
|
|
9685
|
-
const arm = () => {
|
|
9802
|
+
const arm = (e) => {
|
|
9803
|
+
if (e.pointerType !== "mouse") return;
|
|
9686
9804
|
clear();
|
|
9687
9805
|
timer = window.setTimeout(show, opts.delayMs ?? 700);
|
|
9688
9806
|
};
|
|
9689
|
-
anchor.addEventListener("
|
|
9690
|
-
anchor.addEventListener("
|
|
9807
|
+
anchor.addEventListener("pointerenter", arm);
|
|
9808
|
+
anchor.addEventListener("pointerleave", clear);
|
|
9691
9809
|
anchor.addEventListener("pointerdown", clear);
|
|
9692
9810
|
return () => {
|
|
9693
|
-
anchor.removeEventListener("
|
|
9694
|
-
anchor.removeEventListener("
|
|
9811
|
+
anchor.removeEventListener("pointerenter", arm);
|
|
9812
|
+
anchor.removeEventListener("pointerleave", clear);
|
|
9695
9813
|
anchor.removeEventListener("pointerdown", clear);
|
|
9696
9814
|
clear();
|
|
9697
9815
|
};
|
|
@@ -9758,14 +9876,22 @@ function makeDialogDraggable(box, header, options) {
|
|
|
9758
9876
|
|
|
9759
9877
|
// src/renderers/shared/InputsUI.ts
|
|
9760
9878
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
9761
|
-
var STATUS_KEYFRAMES = "@keyframes vela-ind-
|
|
9762
|
-
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;
|
|
9763
9888
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
|
|
9764
9889
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
|
|
9765
9890
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
|
|
9766
|
-
var CLOSE_SVG = iconAt("close",
|
|
9891
|
+
var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
|
|
9767
9892
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
|
|
9768
9893
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
|
|
9894
|
+
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
|
|
9769
9895
|
var InputsUI = class {
|
|
9770
9896
|
constructor(container, theme, paneBoundsOf) {
|
|
9771
9897
|
this.container = container;
|
|
@@ -9786,6 +9912,8 @@ var InputsUI = class {
|
|
|
9786
9912
|
this.onToggleVisible = null;
|
|
9787
9913
|
/** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
|
|
9788
9914
|
this.symbolPicker = null;
|
|
9915
|
+
/** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
|
|
9916
|
+
this.mobileLayout = false;
|
|
9789
9917
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
9790
9918
|
this.moveApi = null;
|
|
9791
9919
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
@@ -9807,9 +9935,18 @@ var InputsUI = class {
|
|
|
9807
9935
|
/** Titles switched off entirely (a settings toggle): every pane's legend container
|
|
9808
9936
|
* hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
|
|
9809
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;
|
|
9810
9943
|
/** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
|
|
9811
9944
|
* rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
|
|
9812
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;
|
|
9813
9950
|
/** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
|
|
9814
9951
|
this.onDialogKey = (e) => {
|
|
9815
9952
|
if (e.key === "Escape") {
|
|
@@ -9833,6 +9970,9 @@ var InputsUI = class {
|
|
|
9833
9970
|
this.onMoveMenuOutside = (e) => {
|
|
9834
9971
|
if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
|
|
9835
9972
|
};
|
|
9973
|
+
this.onRowMenuOutside = (e) => {
|
|
9974
|
+
if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
|
|
9975
|
+
};
|
|
9836
9976
|
if (!container.style.position) container.style.position = "relative";
|
|
9837
9977
|
if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
|
|
9838
9978
|
}
|
|
@@ -9840,6 +9980,36 @@ var InputsUI = class {
|
|
|
9840
9980
|
this.dialogHost = host;
|
|
9841
9981
|
if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
|
|
9842
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
|
+
}
|
|
9843
10013
|
/** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
|
|
9844
10014
|
tip(store, id, anchor, text, wrap = false) {
|
|
9845
10015
|
const list = store.get(id) ?? [];
|
|
@@ -9867,12 +10037,12 @@ var InputsUI = class {
|
|
|
9867
10037
|
}
|
|
9868
10038
|
setTheme(theme) {
|
|
9869
10039
|
this.theme = theme;
|
|
10040
|
+
for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
|
|
9870
10041
|
for (const row of this.rows.values()) {
|
|
9871
|
-
|
|
9872
|
-
row.el.style.background = open ? theme.background : this.idleRowFill();
|
|
10042
|
+
row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
|
|
9873
10043
|
row.el.style.color = theme.textColor;
|
|
9874
10044
|
}
|
|
9875
|
-
if (this.foldToggle) this.
|
|
10045
|
+
if (this.foldToggle) this.syncFoldToggle();
|
|
9876
10046
|
}
|
|
9877
10047
|
/** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
|
|
9878
10048
|
setSymbolPicker(picker) {
|
|
@@ -9903,7 +10073,7 @@ var InputsUI = class {
|
|
|
9903
10073
|
btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
|
|
9904
10074
|
btn.className = "vela-ind-ctl";
|
|
9905
10075
|
btn.dataset.legendAction = action.id;
|
|
9906
|
-
btn.style.cssText =
|
|
10076
|
+
btn.style.cssText = LEGEND_CTL_CSS;
|
|
9907
10077
|
btn.addEventListener("click", (e) => {
|
|
9908
10078
|
e.stopPropagation();
|
|
9909
10079
|
action.run();
|
|
@@ -9921,6 +10091,54 @@ var InputsUI = class {
|
|
|
9921
10091
|
this.titlesVisible = visible;
|
|
9922
10092
|
this.reposition();
|
|
9923
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
|
+
}
|
|
9924
10142
|
// ── legend move/merge (menu + drag) ─────────────────────────────────────
|
|
9925
10143
|
/** Open the "Move to" menu for a row, anchored under its move button. */
|
|
9926
10144
|
openMoveMenu(id, anchor) {
|
|
@@ -9972,6 +10190,48 @@ var InputsUI = class {
|
|
|
9972
10190
|
this.moveMenu?.remove();
|
|
9973
10191
|
this.moveMenu = null;
|
|
9974
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
|
+
}
|
|
9975
10235
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
9976
10236
|
setPane(id, paneId) {
|
|
9977
10237
|
const row = this.rows.get(id);
|
|
@@ -9993,7 +10253,8 @@ var InputsUI = class {
|
|
|
9993
10253
|
if (!lg) {
|
|
9994
10254
|
lg = document.createElement("div");
|
|
9995
10255
|
lg.dataset.velaPane = paneId;
|
|
9996
|
-
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);
|
|
9997
10258
|
this.positionLegend(lg, paneId);
|
|
9998
10259
|
this.container.appendChild(lg);
|
|
9999
10260
|
this.legends.set(paneId, lg);
|
|
@@ -10029,15 +10290,16 @@ var InputsUI = class {
|
|
|
10029
10290
|
*/
|
|
10030
10291
|
syncFoldToggle() {
|
|
10031
10292
|
const count = this.rows.size;
|
|
10032
|
-
|
|
10293
|
+
const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
|
|
10294
|
+
if (count < minRows) {
|
|
10033
10295
|
const wasFolded = this.legendFolded;
|
|
10034
|
-
this.legendFolded = false;
|
|
10296
|
+
if (count > 0) this.legendFolded = false;
|
|
10035
10297
|
if (this.foldToggle) {
|
|
10036
10298
|
this.foldToggle.remove();
|
|
10037
10299
|
this.foldToggle = null;
|
|
10038
10300
|
this.disposeTips(this.rowTips, "fold");
|
|
10039
10301
|
}
|
|
10040
|
-
if (wasFolded) this.reposition();
|
|
10302
|
+
if (wasFolded && count > 0) this.reposition();
|
|
10041
10303
|
return;
|
|
10042
10304
|
}
|
|
10043
10305
|
let btn = this.foldToggle;
|
|
@@ -10045,25 +10307,30 @@ var InputsUI = class {
|
|
|
10045
10307
|
btn = document.createElement("button");
|
|
10046
10308
|
btn.type = "button";
|
|
10047
10309
|
btn.className = "vela-ind-fold";
|
|
10048
|
-
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");
|
|
10049
10311
|
btn.addEventListener("click", () => {
|
|
10312
|
+
if (this.overviewAction !== null) {
|
|
10313
|
+
this.overviewAction();
|
|
10314
|
+
return;
|
|
10315
|
+
}
|
|
10050
10316
|
this.legendFolded = !this.legendFolded;
|
|
10051
10317
|
this.syncFoldToggle();
|
|
10052
10318
|
});
|
|
10053
10319
|
this.foldToggle = btn;
|
|
10054
10320
|
}
|
|
10055
10321
|
const folded = this.legendFolded;
|
|
10056
|
-
btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
|
|
10057
|
-
|
|
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;`;
|
|
10058
10325
|
btn.replaceChildren();
|
|
10059
10326
|
const icon2 = document.createElement("span");
|
|
10060
|
-
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
|
|
10061
|
-
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;
|
|
10062
10329
|
btn.appendChild(icon2);
|
|
10063
10330
|
if (folded) {
|
|
10064
10331
|
const label = document.createElement("span");
|
|
10065
10332
|
label.textContent = String(count);
|
|
10066
|
-
label.style.cssText =
|
|
10333
|
+
label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
|
|
10067
10334
|
btn.appendChild(label);
|
|
10068
10335
|
}
|
|
10069
10336
|
this.legendFor("price").appendChild(btn);
|
|
@@ -10071,9 +10338,11 @@ var InputsUI = class {
|
|
|
10071
10338
|
}
|
|
10072
10339
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
10073
10340
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
10341
|
+
const settingsTitle = opts.settingsTitle ?? title;
|
|
10074
10342
|
const existing = this.rows.get(id);
|
|
10075
10343
|
if (existing) {
|
|
10076
10344
|
existing.title = title;
|
|
10345
|
+
existing.settingsTitle = settingsTitle;
|
|
10077
10346
|
existing.inputs = inputs;
|
|
10078
10347
|
existing.values = { ...values };
|
|
10079
10348
|
existing.titleEl.textContent = title;
|
|
@@ -10086,8 +10355,7 @@ var InputsUI = class {
|
|
|
10086
10355
|
}
|
|
10087
10356
|
ensureDialogStyles();
|
|
10088
10357
|
const el = document.createElement("div");
|
|
10089
|
-
el.style.cssText = `pointer-events:auto;display:flex;align-items:center;
|
|
10090
|
-
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;`;
|
|
10091
10359
|
el.addEventListener("mouseleave", () => {
|
|
10092
10360
|
if (this.selectedId !== id) this.setRowHighlighted(id, false);
|
|
10093
10361
|
});
|
|
@@ -10107,11 +10375,16 @@ var InputsUI = class {
|
|
|
10107
10375
|
e.preventDefault();
|
|
10108
10376
|
this.onRemove?.(id);
|
|
10109
10377
|
});
|
|
10378
|
+
el.addEventListener("contextmenu", (e) => {
|
|
10379
|
+
e.preventDefault();
|
|
10380
|
+
e.stopPropagation();
|
|
10381
|
+
this.openRowMenu(id, e.clientX, e.clientY);
|
|
10382
|
+
});
|
|
10110
10383
|
const statusEl = document.createElement("span");
|
|
10111
10384
|
statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
10112
|
-
el.appendChild(statusEl);
|
|
10113
10385
|
const titleWrap = document.createElement("span");
|
|
10114
10386
|
titleWrap.style.cssText = "white-space:nowrap;";
|
|
10387
|
+
titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
|
|
10115
10388
|
const titleEl = document.createElement("span");
|
|
10116
10389
|
titleEl.textContent = title;
|
|
10117
10390
|
titleEl.style.cssText = "font-weight:600;";
|
|
@@ -10123,6 +10396,11 @@ var InputsUI = class {
|
|
|
10123
10396
|
titleWrap.appendChild(beta);
|
|
10124
10397
|
}
|
|
10125
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;`;
|
|
10126
10404
|
let eyeEl = null;
|
|
10127
10405
|
if (this.onToggleVisible) {
|
|
10128
10406
|
const eye = document.createElement("button");
|
|
@@ -10131,16 +10409,14 @@ var InputsUI = class {
|
|
|
10131
10409
|
this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
|
|
10132
10410
|
eye.innerHTML = EYE_SVG;
|
|
10133
10411
|
eye.className = "vela-ind-ctl";
|
|
10134
|
-
eye.style.cssText =
|
|
10412
|
+
eye.style.cssText = LEGEND_CTL_CSS;
|
|
10135
10413
|
eye.addEventListener("click", () => {
|
|
10136
10414
|
const row = this.rows.get(id);
|
|
10137
10415
|
this.onToggleVisible?.(id, Boolean(row?.hidden));
|
|
10138
10416
|
});
|
|
10139
|
-
|
|
10417
|
+
controlsEl.appendChild(eye);
|
|
10140
10418
|
eyeEl = eye;
|
|
10141
10419
|
}
|
|
10142
|
-
const controlsEl = document.createElement("span");
|
|
10143
|
-
controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
|
|
10144
10420
|
if (inputs.length > 0) {
|
|
10145
10421
|
const gear = document.createElement("button");
|
|
10146
10422
|
gear.type = "button";
|
|
@@ -10148,7 +10424,7 @@ var InputsUI = class {
|
|
|
10148
10424
|
this.tip(this.rowTips, id, gear, "Settings");
|
|
10149
10425
|
gear.innerHTML = GEAR_SVG;
|
|
10150
10426
|
gear.className = "vela-ind-ctl";
|
|
10151
|
-
gear.style.cssText =
|
|
10427
|
+
gear.style.cssText = LEGEND_CTL_CSS;
|
|
10152
10428
|
gear.addEventListener("click", () => this.openDialog(id));
|
|
10153
10429
|
controlsEl.appendChild(gear);
|
|
10154
10430
|
}
|
|
@@ -10159,7 +10435,7 @@ var InputsUI = class {
|
|
|
10159
10435
|
this.tip(this.rowTips, id, mv, "Move to pane");
|
|
10160
10436
|
mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
|
|
10161
10437
|
mv.className = "vela-ind-ctl";
|
|
10162
|
-
mv.style.cssText =
|
|
10438
|
+
mv.style.cssText = LEGEND_CTL_CSS;
|
|
10163
10439
|
mv.addEventListener("click", (e) => {
|
|
10164
10440
|
e.stopPropagation();
|
|
10165
10441
|
this.openMoveMenu(id, mv);
|
|
@@ -10176,12 +10452,13 @@ var InputsUI = class {
|
|
|
10176
10452
|
this.tip(this.rowTips, id, close, "Remove indicator");
|
|
10177
10453
|
close.innerHTML = CLOSE_SVG;
|
|
10178
10454
|
close.className = "vela-ind-close";
|
|
10179
|
-
close.style.cssText =
|
|
10455
|
+
close.style.cssText = LEGEND_CTL_CSS;
|
|
10180
10456
|
close.addEventListener("click", () => this.onRemove?.(id));
|
|
10181
10457
|
controlsEl.appendChild(close);
|
|
10182
10458
|
el.appendChild(controlsEl);
|
|
10459
|
+
el.appendChild(statusEl);
|
|
10183
10460
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
10184
|
-
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 });
|
|
10185
10462
|
this.syncFoldToggle();
|
|
10186
10463
|
}
|
|
10187
10464
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -10195,24 +10472,31 @@ var InputsUI = class {
|
|
|
10195
10472
|
if (row) row.values = { ...row.values, ...values };
|
|
10196
10473
|
}
|
|
10197
10474
|
/**
|
|
10198
|
-
* Reflect an indicator's live status in its legend row: `'loading'` shows
|
|
10199
|
-
* 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.
|
|
10200
10478
|
*/
|
|
10201
10479
|
setStatus(id, status) {
|
|
10202
10480
|
const row = this.rows.get(id);
|
|
10203
10481
|
if (!row) return;
|
|
10204
10482
|
const el = row.statusEl;
|
|
10483
|
+
el.replaceChildren();
|
|
10205
10484
|
if (status === "idle") {
|
|
10206
|
-
el.style.
|
|
10207
|
-
el.style.animation = "none";
|
|
10485
|
+
el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
10208
10486
|
return;
|
|
10209
10487
|
}
|
|
10210
|
-
this.ensureStatusKeyframes();
|
|
10211
10488
|
if (status === "loading") {
|
|
10212
|
-
el.style.cssText =
|
|
10213
|
-
|
|
10214
|
-
|
|
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;
|
|
10215
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;`;
|
|
10216
10500
|
}
|
|
10217
10501
|
/** Inject the status keyframes once (idempotent). */
|
|
10218
10502
|
ensureStatusKeyframes() {
|
|
@@ -10231,8 +10515,8 @@ var InputsUI = class {
|
|
|
10231
10515
|
if (row.eyeEl) {
|
|
10232
10516
|
row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
|
|
10233
10517
|
row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
|
|
10234
|
-
row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
|
|
10235
10518
|
}
|
|
10519
|
+
this.syncRowActions(row);
|
|
10236
10520
|
}
|
|
10237
10521
|
/**
|
|
10238
10522
|
* Select a legend row (outlining it) and clear any previous selection, so at most one
|
|
@@ -10245,17 +10529,36 @@ var InputsUI = class {
|
|
|
10245
10529
|
this.setRowHighlighted(id, true);
|
|
10246
10530
|
}
|
|
10247
10531
|
/**
|
|
10248
|
-
* Show or hide a row's outline and eye/gear/✕ controls together — driven by
|
|
10249
|
-
*
|
|
10250
|
-
*
|
|
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.
|
|
10251
10536
|
*/
|
|
10252
10537
|
setRowHighlighted(id, highlighted) {
|
|
10253
10538
|
const row = this.rows.get(id);
|
|
10254
10539
|
if (!row) return;
|
|
10540
|
+
row.highlighted = highlighted;
|
|
10541
|
+
this.renderRowValues(row);
|
|
10255
10542
|
row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
|
|
10256
10543
|
row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
|
|
10257
|
-
row
|
|
10258
|
-
|
|
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
|
+
}
|
|
10259
10562
|
}
|
|
10260
10563
|
/** Drop the current selection outline, if any. */
|
|
10261
10564
|
clearSelection() {
|
|
@@ -10283,6 +10586,7 @@ var InputsUI = class {
|
|
|
10283
10586
|
destroy() {
|
|
10284
10587
|
this.closeDialog();
|
|
10285
10588
|
this.closeMoveMenu();
|
|
10589
|
+
this.closeRowMenu();
|
|
10286
10590
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
10287
10591
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
10288
10592
|
for (const lg of this.legends.values()) lg.remove();
|
|
@@ -10338,6 +10642,7 @@ var InputsUI = class {
|
|
|
10338
10642
|
const card = document.createElement("div");
|
|
10339
10643
|
card.className = "vela-ind-dialog";
|
|
10340
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;";
|
|
10341
10646
|
applyChromeTokens(card, t);
|
|
10342
10647
|
card.addEventListener("keydown", (e) => {
|
|
10343
10648
|
if (e.key !== "Escape") e.stopPropagation();
|
|
@@ -10345,7 +10650,7 @@ var InputsUI = class {
|
|
|
10345
10650
|
const header = document.createElement("div");
|
|
10346
10651
|
header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
|
|
10347
10652
|
const hTitle = document.createElement("span");
|
|
10348
|
-
hTitle.textContent = row.
|
|
10653
|
+
hTitle.textContent = row.settingsTitle;
|
|
10349
10654
|
hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
|
|
10350
10655
|
const closeBtn = document.createElement("button");
|
|
10351
10656
|
closeBtn.type = "button";
|
|
@@ -10357,7 +10662,7 @@ var InputsUI = class {
|
|
|
10357
10662
|
closeBtn.addEventListener("click", () => this.closeDialog());
|
|
10358
10663
|
header.append(hTitle, closeBtn);
|
|
10359
10664
|
card.appendChild(header);
|
|
10360
|
-
makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
10665
|
+
if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
|
|
10361
10666
|
const tabs = document.createElement("div");
|
|
10362
10667
|
tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
10363
10668
|
const tab = document.createElement("div");
|
|
@@ -10377,7 +10682,7 @@ var InputsUI = class {
|
|
|
10377
10682
|
section.appendChild(gh);
|
|
10378
10683
|
}
|
|
10379
10684
|
const grid = document.createElement("div");
|
|
10380
|
-
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;";
|
|
10381
10686
|
for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
|
|
10382
10687
|
section.appendChild(grid);
|
|
10383
10688
|
body.appendChild(section);
|
|
@@ -10826,10 +11131,14 @@ function groupInputs(inputs) {
|
|
|
10826
11131
|
var CHECK_SVG = iconAt("check", 12);
|
|
10827
11132
|
var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
|
|
10828
11133
|
var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
|
|
11134
|
+
var DIALOG_STYLE_REV = "10";
|
|
10829
11135
|
function ensureDialogStyles() {
|
|
10830
|
-
if (typeof document === "undefined"
|
|
10831
|
-
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");
|
|
10832
11140
|
s.id = DIALOG_STYLE_ID;
|
|
11141
|
+
s.dataset.rev = DIALOG_STYLE_REV;
|
|
10833
11142
|
s.textContent = `
|
|
10834
11143
|
.vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
|
|
10835
11144
|
.vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -10843,19 +11152,19 @@ function ensureDialogStyles() {
|
|
|
10843
11152
|
.vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
|
|
10844
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;}
|
|
10845
11154
|
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
10846
|
-
.vela-ind-ctl{color:var(--vela-
|
|
10847
|
-
.vela-ind-ctl
|
|
10848
|
-
.vela-ind-
|
|
10849
|
-
.vela-ind-close:hover{color:var(--vela-danger) !important;}
|
|
10850
|
-
.vela-ind-fold{
|
|
10851
|
-
.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;}
|
|
10852
11161
|
.vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
10853
11162
|
.vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
|
|
10854
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;}
|
|
10855
11164
|
.vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
10856
11165
|
.vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
10857
11166
|
.vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
|
|
10858
|
-
document.head.appendChild(s);
|
|
11167
|
+
if (!existing) document.head.appendChild(s);
|
|
10859
11168
|
}
|
|
10860
11169
|
var TIMEFRAME_OPTIONS = [
|
|
10861
11170
|
{ value: "", label: "Chart" },
|
|
@@ -11464,6 +11773,44 @@ function basePaintingOf(style) {
|
|
|
11464
11773
|
for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
|
|
11465
11774
|
return "candles";
|
|
11466
11775
|
}
|
|
11776
|
+
function hasOwnCandlePaint(style) {
|
|
11777
|
+
if (BUILTIN_PRICE_STYLES.includes(style)) return false;
|
|
11778
|
+
for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
|
|
11779
|
+
return false;
|
|
11780
|
+
}
|
|
11781
|
+
function candleOverrideFor(style, bags) {
|
|
11782
|
+
if (!hasOwnCandlePaint(style)) return null;
|
|
11783
|
+
const bag = bags[style] ?? {};
|
|
11784
|
+
const color = (v) => isColor(v) ? v : null;
|
|
11785
|
+
const bool = (v) => isBool(v) ? v : null;
|
|
11786
|
+
return {
|
|
11787
|
+
upColor: color(bag.candleUpColor),
|
|
11788
|
+
downColor: color(bag.candleDownColor),
|
|
11789
|
+
bodyVisible: bool(bag.candleBodyVisible),
|
|
11790
|
+
borderVisible: bool(bag.candleBorderVisible),
|
|
11791
|
+
borderUpColor: color(bag.candleBorderUpColor),
|
|
11792
|
+
borderDownColor: color(bag.candleBorderDownColor),
|
|
11793
|
+
wickVisible: bool(bag.candleWickVisible),
|
|
11794
|
+
wickUpColor: color(bag.candleWickUpColor),
|
|
11795
|
+
wickDownColor: color(bag.candleWickDownColor)
|
|
11796
|
+
};
|
|
11797
|
+
}
|
|
11798
|
+
function effectiveCandlePaint(base, override, themeUp, themeDown) {
|
|
11799
|
+
if (!override) return { up: themeUp, down: themeDown, candle: base };
|
|
11800
|
+
return {
|
|
11801
|
+
up: override.upColor ?? themeUp,
|
|
11802
|
+
down: override.downColor ?? themeDown,
|
|
11803
|
+
candle: {
|
|
11804
|
+
bodyVisible: override.bodyVisible ?? base.bodyVisible,
|
|
11805
|
+
borderVisible: override.borderVisible ?? base.borderVisible,
|
|
11806
|
+
borderUpColor: override.borderUpColor ?? base.borderUpColor,
|
|
11807
|
+
borderDownColor: override.borderDownColor ?? base.borderDownColor,
|
|
11808
|
+
wickVisible: override.wickVisible ?? base.wickVisible,
|
|
11809
|
+
wickUpColor: override.wickUpColor ?? base.wickUpColor,
|
|
11810
|
+
wickDownColor: override.wickDownColor ?? base.wickDownColor
|
|
11811
|
+
}
|
|
11812
|
+
};
|
|
11813
|
+
}
|
|
11467
11814
|
function priceStyleIds() {
|
|
11468
11815
|
const out = [...BUILTIN_PRICE_STYLES];
|
|
11469
11816
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
@@ -11502,6 +11849,33 @@ function clampLevel(v) {
|
|
|
11502
11849
|
function clampSpacing(v) {
|
|
11503
11850
|
return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
|
|
11504
11851
|
}
|
|
11852
|
+
function factoryResetConfig(factory) {
|
|
11853
|
+
const bag = {};
|
|
11854
|
+
for (const t of chartTypes()) {
|
|
11855
|
+
const section = t.settings;
|
|
11856
|
+
if (!section) continue;
|
|
11857
|
+
const defaults2 = {};
|
|
11858
|
+
const addRows = (rows) => {
|
|
11859
|
+
for (const r of rows ?? []) {
|
|
11860
|
+
for (const k of settingsRowValueKeys(r)) defaults2[k.key] = k.defval;
|
|
11861
|
+
}
|
|
11862
|
+
};
|
|
11863
|
+
for (const inst of section.instances ?? []) {
|
|
11864
|
+
if (inst.enableKey) defaults2[inst.enableKey] = false;
|
|
11865
|
+
addRows(inst.rows);
|
|
11866
|
+
}
|
|
11867
|
+
for (const sub of section.subsections ?? []) {
|
|
11868
|
+
if (sub.enableKey) defaults2[sub.enableKey] = false;
|
|
11869
|
+
addRows(sub.rows);
|
|
11870
|
+
}
|
|
11871
|
+
if (!section.instances) addRows(section.rows);
|
|
11872
|
+
bag[t.id] = defaults2;
|
|
11873
|
+
}
|
|
11874
|
+
for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
|
|
11875
|
+
bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
|
|
11876
|
+
}
|
|
11877
|
+
return { ...factory, chartTypes: bag };
|
|
11878
|
+
}
|
|
11505
11879
|
function mergeConfig(base, patch) {
|
|
11506
11880
|
const p = asObject(patch);
|
|
11507
11881
|
const ctPatch = asObject(p.chartTypes);
|
|
@@ -11816,6 +12190,9 @@ var SceneGraph = class {
|
|
|
11816
12190
|
this.priceStyle = "candles";
|
|
11817
12191
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
11818
12192
|
this.basePainting = "candles";
|
|
12193
|
+
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
12194
|
+
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
12195
|
+
this.candleOverride = null;
|
|
11819
12196
|
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
11820
12197
|
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
11821
12198
|
* range (resolved per frame via `baselinePriceFor`). */
|
|
@@ -13045,14 +13422,15 @@ var WebGL2Backend = class {
|
|
|
13045
13422
|
emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
13046
13423
|
const spacing = coords.bodySpacing();
|
|
13047
13424
|
const tier = candleTier(spacing);
|
|
13425
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
13048
13426
|
if (tier === "aggregate") {
|
|
13049
|
-
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane,
|
|
13427
|
+
this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
13050
13428
|
return;
|
|
13051
13429
|
}
|
|
13052
13430
|
const drawBody = tier === "full";
|
|
13053
|
-
const up =
|
|
13054
|
-
const down =
|
|
13055
|
-
const cs =
|
|
13431
|
+
const up = paint.up;
|
|
13432
|
+
const down = paint.down;
|
|
13433
|
+
const cs = paint.candle;
|
|
13056
13434
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
13057
13435
|
for (let i = i0; i <= i1; i += 1) {
|
|
13058
13436
|
const bar = bars[i];
|
|
@@ -13623,6 +14001,11 @@ function easeToward(current, target, dtMs, tauMs) {
|
|
|
13623
14001
|
var FLING_MIN_SPEED = 0.04;
|
|
13624
14002
|
var FLING_STALE_MS = 60;
|
|
13625
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;
|
|
13626
14009
|
var TIME_SCALE_K = 4e-3;
|
|
13627
14010
|
var WHEEL_ZOOM_K = 25e-4;
|
|
13628
14011
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
@@ -13643,8 +14026,13 @@ function wheelPanDelta(deltaX, deltaY, shift) {
|
|
|
13643
14026
|
function effectiveSnapMode(momentaryOverride, sticky) {
|
|
13644
14027
|
return momentaryOverride ? "strong" : sticky;
|
|
13645
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
|
+
}
|
|
13646
14035
|
var InputController = class {
|
|
13647
|
-
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
13648
14036
|
constructor(deps) {
|
|
13649
14037
|
this.deps = deps;
|
|
13650
14038
|
/** When true (the default), wheel-zoom pins the right edge instead of the bar
|
|
@@ -13672,9 +14060,40 @@ var InputController = class {
|
|
|
13672
14060
|
this.vx = 0;
|
|
13673
14061
|
// smoothed pointer velocity, px/ms
|
|
13674
14062
|
this.middleDeleted = false;
|
|
14063
|
+
// the last middle press deleted a drawing (suppress autoscroll/paste)
|
|
14064
|
+
// Last cursor position over the element (NaN once it leaves) — lets a modifier
|
|
14065
|
+
// press/release re-shape the drawings preview with the pointer stationary.
|
|
14066
|
+
this.cursorX = NaN;
|
|
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
|
+
};
|
|
13675
14085
|
this.onMiddleGuard = (e) => {
|
|
13676
14086
|
if (e.button === 1 && this.middleDeleted) e.preventDefault();
|
|
13677
14087
|
};
|
|
14088
|
+
/** A modifier press/release with the pointer stationary: re-issue the last cursor
|
|
14089
|
+
* position so the placing ghost / drag preview reflects the new state immediately
|
|
14090
|
+
* (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
|
|
14091
|
+
this.onModifier = (e) => {
|
|
14092
|
+
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
14093
|
+
if (Number.isNaN(this.cursorX)) return;
|
|
14094
|
+
if (this.dragging && this.region !== "drawing") return;
|
|
14095
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
14096
|
+
};
|
|
13678
14097
|
this.onDown = (e) => {
|
|
13679
14098
|
if (e.button === 1) {
|
|
13680
14099
|
const { x: x2, y: y2 } = this.local(e);
|
|
@@ -13684,6 +14103,15 @@ var InputController = class {
|
|
|
13684
14103
|
}
|
|
13685
14104
|
if (e.button !== 0) return;
|
|
13686
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
|
+
}
|
|
13687
14115
|
this.deps.drawingsClearTransient?.();
|
|
13688
14116
|
this.dragging = true;
|
|
13689
14117
|
this.moved = false;
|
|
@@ -13709,6 +14137,9 @@ var InputController = class {
|
|
|
13709
14137
|
if (this.region === "price") this.deps.beginPriceScale(x, y);
|
|
13710
14138
|
else if (this.region === "separator") this.deps.beginPaneResize(y);
|
|
13711
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
|
+
}
|
|
13712
14143
|
this.lastX = x;
|
|
13713
14144
|
this.lastT = e.timeStamp;
|
|
13714
14145
|
this.vx = 0;
|
|
@@ -13716,28 +14147,44 @@ var InputController = class {
|
|
|
13716
14147
|
};
|
|
13717
14148
|
this.onMove = (e) => {
|
|
13718
14149
|
const { x, y } = this.local(e);
|
|
14150
|
+
this.cursorX = x;
|
|
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;
|
|
13719
14166
|
if (this.dragging) {
|
|
13720
14167
|
if (this.region === "price") {
|
|
13721
|
-
if (Math.abs(y - this.startY) >
|
|
14168
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13722
14169
|
this.deps.priceScaleBy(y - this.startY);
|
|
13723
14170
|
} else if (this.region === "separator") {
|
|
13724
|
-
if (Math.abs(y - this.startY) >
|
|
14171
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13725
14172
|
this.deps.paneResizeBy(y - this.startY);
|
|
13726
14173
|
} else if (this.region === "time") {
|
|
13727
|
-
if (Math.abs(x - this.startX) >
|
|
14174
|
+
if (Math.abs(x - this.startX) > slop) this.moved = true;
|
|
13728
14175
|
const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
|
|
13729
14176
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
13730
14177
|
} else if (this.region === "drawing") {
|
|
13731
|
-
if (Math.abs(x - this.startX) >
|
|
13732
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
14178
|
+
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
14179
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
13733
14180
|
} else {
|
|
13734
14181
|
const coords = this.deps.getCoords();
|
|
13735
14182
|
const vp = coords.getViewport();
|
|
13736
14183
|
const dx = x - this.startX;
|
|
13737
|
-
if (Math.abs(dx) >
|
|
14184
|
+
if (Math.abs(dx) > slop) this.moved = true;
|
|
13738
14185
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
|
|
13739
14186
|
if (this.verticalPan) {
|
|
13740
|
-
if (Math.abs(y - this.startY) >
|
|
14187
|
+
if (Math.abs(y - this.startY) > slop) this.moved = true;
|
|
13741
14188
|
this.deps.pricePanBy(y - this.startY);
|
|
13742
14189
|
}
|
|
13743
14190
|
const dt = e.timeStamp - this.lastT;
|
|
@@ -13752,16 +14199,46 @@ var InputController = class {
|
|
|
13752
14199
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
13753
14200
|
const r = this.regionAt(x, y);
|
|
13754
14201
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
13755
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
|
|
14202
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
13756
14203
|
}
|
|
13757
|
-
this.deps.onPointerMove(x, y);
|
|
14204
|
+
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
13758
14205
|
};
|
|
13759
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);
|
|
13760
14239
|
if (this.dragging && this.region === "drawing") {
|
|
13761
|
-
const { x, y } = this.local(e);
|
|
13762
14240
|
this.deps.drawingsPointerUp?.(x, y);
|
|
13763
|
-
} else if (
|
|
13764
|
-
const { x, y } = this.local(e);
|
|
14241
|
+
} else if (tapRelease && this.region === "data") {
|
|
13765
14242
|
this.deps.onClick(x, y);
|
|
13766
14243
|
} else if (this.dragging && this.region === "data") {
|
|
13767
14244
|
const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
|
|
@@ -13770,23 +14247,29 @@ var InputController = class {
|
|
|
13770
14247
|
this.deps.fling(-this.vx / pitch);
|
|
13771
14248
|
}
|
|
13772
14249
|
}
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
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);
|
|
13778
14264
|
};
|
|
13779
14265
|
this.onLeave = () => {
|
|
14266
|
+
this.cursorX = NaN;
|
|
14267
|
+
this.cursorY = NaN;
|
|
13780
14268
|
this.deps.onPointerMove(null, null);
|
|
13781
14269
|
};
|
|
13782
14270
|
this.onDblClick = (e) => {
|
|
13783
14271
|
const { x, y } = this.local(e);
|
|
13784
|
-
|
|
13785
|
-
const region = this.regionAt(x, y);
|
|
13786
|
-
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
13787
|
-
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
13788
|
-
else if (region === "time") this.deps.resetView();
|
|
13789
|
-
else this.deps.dataDblClick(x, y);
|
|
14272
|
+
this.doubleActivate(x, y);
|
|
13790
14273
|
};
|
|
13791
14274
|
this.onWheel = (e) => {
|
|
13792
14275
|
e.preventDefault();
|
|
@@ -13807,26 +14290,44 @@ var InputController = class {
|
|
|
13807
14290
|
}
|
|
13808
14291
|
attach(el) {
|
|
13809
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
|
+
}
|
|
13810
14299
|
el.addEventListener("pointerdown", this.onDown);
|
|
13811
14300
|
el.addEventListener("pointermove", this.onMove);
|
|
13812
14301
|
el.addEventListener("pointerup", this.onUp);
|
|
13813
14302
|
el.addEventListener("pointerleave", this.onLeave);
|
|
14303
|
+
el.addEventListener("pointercancel", this.onCancel);
|
|
13814
14304
|
el.addEventListener("dblclick", this.onDblClick);
|
|
13815
14305
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
13816
14306
|
el.addEventListener("mousedown", this.onMiddleGuard);
|
|
13817
14307
|
el.addEventListener("auxclick", this.onMiddleGuard);
|
|
14308
|
+
const win = el.ownerDocument?.defaultView;
|
|
14309
|
+
win?.addEventListener("keydown", this.onModifier);
|
|
14310
|
+
win?.addEventListener("keyup", this.onModifier);
|
|
14311
|
+
win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
13818
14312
|
}
|
|
13819
14313
|
detach() {
|
|
13820
14314
|
const el = this.el;
|
|
13821
14315
|
if (!el) return;
|
|
14316
|
+
const win = el.ownerDocument?.defaultView;
|
|
14317
|
+
win?.removeEventListener("keydown", this.onModifier);
|
|
14318
|
+
win?.removeEventListener("keyup", this.onModifier);
|
|
14319
|
+
win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
|
|
13822
14320
|
el.removeEventListener("pointerdown", this.onDown);
|
|
13823
14321
|
el.removeEventListener("pointermove", this.onMove);
|
|
13824
14322
|
el.removeEventListener("pointerup", this.onUp);
|
|
13825
14323
|
el.removeEventListener("pointerleave", this.onLeave);
|
|
14324
|
+
el.removeEventListener("pointercancel", this.onCancel);
|
|
13826
14325
|
el.removeEventListener("dblclick", this.onDblClick);
|
|
13827
14326
|
el.removeEventListener("wheel", this.onWheel);
|
|
13828
14327
|
el.removeEventListener("mousedown", this.onMiddleGuard);
|
|
13829
14328
|
el.removeEventListener("auxclick", this.onMiddleGuard);
|
|
14329
|
+
this.cancelLongPress();
|
|
14330
|
+
this.touches.clear();
|
|
13830
14331
|
this.el = null;
|
|
13831
14332
|
}
|
|
13832
14333
|
local(e) {
|
|
@@ -13850,6 +14351,59 @@ var InputController = class {
|
|
|
13850
14351
|
snapMode(e) {
|
|
13851
14352
|
return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
|
|
13852
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
|
+
}
|
|
13853
14407
|
/** Capture defensively: a synthetic/already-released pointer can't be captured, and
|
|
13854
14408
|
* the move/up pair still routes through the element listeners without it. */
|
|
13855
14409
|
capture(pointerId) {
|
|
@@ -13858,6 +14412,35 @@ var InputController = class {
|
|
|
13858
14412
|
} catch {
|
|
13859
14413
|
}
|
|
13860
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
|
+
}
|
|
13861
14444
|
};
|
|
13862
14445
|
|
|
13863
14446
|
// src/renderers/native/core/KeyboardController.ts
|
|
@@ -14236,12 +14819,13 @@ var Canvas2dBackend = class {
|
|
|
14236
14819
|
drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
|
|
14237
14820
|
const spacing = coords.bodySpacing();
|
|
14238
14821
|
const tier = candleTier(spacing);
|
|
14822
|
+
const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
|
|
14239
14823
|
if (tier === "aggregate") {
|
|
14240
|
-
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane,
|
|
14824
|
+
this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
|
|
14241
14825
|
return;
|
|
14242
14826
|
}
|
|
14243
14827
|
const drawBody = tier === "full";
|
|
14244
|
-
const cs =
|
|
14828
|
+
const cs = paint.candle;
|
|
14245
14829
|
const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
|
|
14246
14830
|
for (let i = i0; i <= i1; i += 1) {
|
|
14247
14831
|
const b = bars[i];
|
|
@@ -14249,7 +14833,7 @@ var Canvas2dBackend = class {
|
|
|
14249
14833
|
const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
|
|
14250
14834
|
const x = g.center;
|
|
14251
14835
|
const up = b.close >= b.open;
|
|
14252
|
-
const dir = up ?
|
|
14836
|
+
const dir = up ? paint.up : paint.down;
|
|
14253
14837
|
const bc = barColors.get(b.time);
|
|
14254
14838
|
const color = bc ?? dir;
|
|
14255
14839
|
let top = 0;
|
|
@@ -15360,7 +15944,7 @@ var ChromeRenderer = class {
|
|
|
15360
15944
|
constructor() {
|
|
15361
15945
|
this.canvas = null;
|
|
15362
15946
|
this.ctx = null;
|
|
15363
|
-
// The color for axis tick labels — the
|
|
15947
|
+
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
15364
15948
|
this.axisTextColor = DARK_THEME.textColor;
|
|
15365
15949
|
// Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
|
|
15366
15950
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
@@ -15392,8 +15976,8 @@ var ChromeRenderer = class {
|
|
|
15392
15976
|
return dr;
|
|
15393
15977
|
}
|
|
15394
15978
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
15395
|
-
* `surface` (background + text) paints the axis-scale gutters
|
|
15396
|
-
*
|
|
15979
|
+
* `surface` (background + text) paints the axis-scale gutters — the host passes the live
|
|
15980
|
+
* chart background so the scales read as part of the plot, with contrast-corrected text.
|
|
15397
15981
|
* Falls back to the theme's own colors when no surface is supplied. */
|
|
15398
15982
|
render(scene, coords, theme, surface) {
|
|
15399
15983
|
const ctx = this.ctx;
|
|
@@ -15670,11 +16254,19 @@ var ChromeRenderer = class {
|
|
|
15670
16254
|
const y = dataH + (fullH - dataH) / 2;
|
|
15671
16255
|
const tr = coords.visibleTimeRange();
|
|
15672
16256
|
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
15673
|
-
|
|
15674
|
-
|
|
15675
|
-
|
|
15676
|
-
|
|
15677
|
-
|
|
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);
|
|
15678
16270
|
ctx.textAlign = "start";
|
|
15679
16271
|
}
|
|
15680
16272
|
};
|
|
@@ -15776,10 +16368,10 @@ var CrosshairRenderer = class {
|
|
|
15776
16368
|
}
|
|
15777
16369
|
/** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
|
|
15778
16370
|
* CONTAINING the foreign time (+ horizontal line when a comparable price came
|
|
15779
|
-
* along), with that bar's time chip in this chart's own timezone
|
|
15780
|
-
*
|
|
15781
|
-
*
|
|
15782
|
-
*
|
|
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. */
|
|
15783
16375
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
15784
16376
|
const cs = scene.style.crosshair;
|
|
15785
16377
|
const dataW = coords.width;
|
|
@@ -15803,7 +16395,20 @@ var CrosshairRenderer = class {
|
|
|
15803
16395
|
ctx.setLineDash([]);
|
|
15804
16396
|
ctx.lineWidth = 1;
|
|
15805
16397
|
ctx.globalAlpha = 0.8;
|
|
15806
|
-
|
|
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);
|
|
15807
16412
|
ctx.globalAlpha = 1;
|
|
15808
16413
|
}
|
|
15809
16414
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -16164,6 +16769,7 @@ function colorField(theme, getVal, onVal) {
|
|
|
16164
16769
|
swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
|
|
16165
16770
|
};
|
|
16166
16771
|
paint();
|
|
16772
|
+
trigger.addEventListener("vela-sync", paint);
|
|
16167
16773
|
trigger.addEventListener("click", (e) => {
|
|
16168
16774
|
e.stopPropagation();
|
|
16169
16775
|
if (openPopover && openPopover.trigger === trigger) {
|
|
@@ -16199,6 +16805,101 @@ function closeColorPopover() {
|
|
|
16199
16805
|
closeOpen();
|
|
16200
16806
|
}
|
|
16201
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
|
+
|
|
16202
16903
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
16203
16904
|
var BUILTIN_STYLE_LABELS = {
|
|
16204
16905
|
candles: "Candles",
|
|
@@ -16250,6 +16951,61 @@ function ensureControlStyles() {
|
|
|
16250
16951
|
.vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
|
|
16251
16952
|
.vela-sd-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
16252
16953
|
.vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16954
|
+
/* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
|
|
16955
|
+
!important: the pane grid rewrites inline display ('contents') AFTER the initial
|
|
16956
|
+
visibility pass, so a class must win. */
|
|
16957
|
+
.vela-sd-hide{display:none !important;}
|
|
16958
|
+
/* Indented rail sub-entry (a chart-type section's subsection tab). */
|
|
16959
|
+
.vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
|
|
16960
|
+
/* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
|
|
16961
|
+
and a dashed + that turns on the next absent instance. The rule under it separates
|
|
16962
|
+
the strip from the instance's TOC + rows area. */
|
|
16963
|
+
.vela-sd-itabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:14px 0 0;padding-bottom:12px;border-bottom:1px solid var(--vela-border);}
|
|
16964
|
+
.vela-sd-itab{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;background:transparent;border:1px solid var(--vela-border);border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-family:inherit;font-size:var(--vela-font-size-md);font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
16965
|
+
.vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16966
|
+
.vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
|
|
16967
|
+
.vela-sd-ix{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:-4px;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-size:10px;line-height:1;}
|
|
16968
|
+
.vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16969
|
+
.vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
|
|
16970
|
+
/* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
|
|
16971
|
+
lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
|
|
16972
|
+
vertical rule sits on the rows column so it spans the full content height. When every
|
|
16973
|
+
group gates out the TOC hides and .no-toc drops the rule with it. */
|
|
16974
|
+
.vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
|
|
16975
|
+
.vela-sd-toc{position:sticky;top:0;flex:0 0 auto;min-width:104px;display:flex;flex-direction:column;gap:2px;padding:2px 14px 0 0;}
|
|
16976
|
+
.vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
|
|
16977
|
+
.vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
|
|
16978
|
+
.vela-sd-toc-btn{text-align:left;padding:6px 10px;background:transparent;border:none;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
16979
|
+
.vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
16980
|
+
.vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
16981
|
+
/* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
|
|
16982
|
+
muted and non-interactive. Applied to each row's children so it survives display:contents;
|
|
16983
|
+
!important beats the inline opacity on labels. */
|
|
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;}
|
|
16253
17009
|
`;
|
|
16254
17010
|
document.head.appendChild(st);
|
|
16255
17011
|
}
|
|
@@ -16270,8 +17026,14 @@ var SettingsDialog = class {
|
|
|
16270
17026
|
this.themeControl = null;
|
|
16271
17027
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
16272
17028
|
this.tabs = [];
|
|
17029
|
+
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
17030
|
+
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
17031
|
+
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
17032
|
+
this.typeActiveGroup = /* @__PURE__ */ new Map();
|
|
16273
17033
|
/** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
|
|
16274
17034
|
this.activeSection = null;
|
|
17035
|
+
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
17036
|
+
this.mobileLayout = false;
|
|
16275
17037
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
16276
17038
|
}
|
|
16277
17039
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
@@ -16290,15 +17052,27 @@ var SettingsDialog = class {
|
|
|
16290
17052
|
}
|
|
16291
17053
|
setTheme(theme) {
|
|
16292
17054
|
this.theme = theme;
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
16296
|
-
|
|
16297
|
-
|
|
16298
|
-
|
|
16299
|
-
|
|
16300
|
-
|
|
16301
|
-
|
|
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);
|
|
16302
17076
|
}
|
|
16303
17077
|
isOpen() {
|
|
16304
17078
|
return this.root !== null;
|
|
@@ -16320,18 +17094,37 @@ var SettingsDialog = class {
|
|
|
16320
17094
|
this.onImport = onImport ?? null;
|
|
16321
17095
|
this.onReset = onReset ?? null;
|
|
16322
17096
|
ensureControlStyles();
|
|
17097
|
+
const mobile = this.mobileLayout;
|
|
16323
17098
|
const scrim = document.createElement("div");
|
|
16324
|
-
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
|
+
}
|
|
16325
17105
|
scrim.addEventListener("mousedown", (e) => {
|
|
16326
17106
|
if (e.target === scrim) this.close();
|
|
16327
17107
|
});
|
|
16328
17108
|
const dlg = document.createElement("div");
|
|
16329
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;";
|
|
16330
17111
|
const header = document.createElement("div");
|
|
16331
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";
|
|
16332
17114
|
const hTitle = document.createElement("span");
|
|
16333
17115
|
hTitle.textContent = "Chart settings";
|
|
16334
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
|
+
}
|
|
16335
17128
|
const closeBtn = document.createElement("button");
|
|
16336
17129
|
closeBtn.type = "button";
|
|
16337
17130
|
closeBtn.innerHTML = iconAt("close", 15);
|
|
@@ -16339,8 +17132,8 @@ var SettingsDialog = class {
|
|
|
16339
17132
|
closeBtn.className = "vela-sd-close";
|
|
16340
17133
|
closeBtn.addEventListener("click", () => this.close());
|
|
16341
17134
|
header.append(hTitle, closeBtn);
|
|
16342
|
-
|
|
16343
|
-
|
|
17135
|
+
if (!mobile) {
|
|
17136
|
+
header.style.cursor = "move";
|
|
16344
17137
|
let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
|
|
16345
17138
|
header.addEventListener("pointerdown", (e) => {
|
|
16346
17139
|
if (e.target?.closest(".vela-sd-close")) return;
|
|
@@ -16431,6 +17224,29 @@ var SettingsDialog = class {
|
|
|
16431
17224
|
baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
|
|
16432
17225
|
groups.baseline = baseline;
|
|
16433
17226
|
body.append(baseline);
|
|
17227
|
+
for (const def of chartTypes()) {
|
|
17228
|
+
if (!hasOwnCandlePaint(def.id)) continue;
|
|
17229
|
+
const bag = config.chartTypes[def.id] ?? {};
|
|
17230
|
+
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
17231
|
+
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
17232
|
+
const g = this.group();
|
|
17233
|
+
g.append(this.sectionTitle("Candles"));
|
|
17234
|
+
g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
17235
|
+
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
17236
|
+
this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
|
|
17237
|
+
]));
|
|
17238
|
+
g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
17239
|
+
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
17240
|
+
this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
|
|
17241
|
+
]));
|
|
17242
|
+
g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
17243
|
+
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
17244
|
+
this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
|
|
17245
|
+
]));
|
|
17246
|
+
g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
17247
|
+
groups[def.id] = g;
|
|
17248
|
+
body.append(g);
|
|
17249
|
+
}
|
|
16434
17250
|
showActive(config.series.style);
|
|
16435
17251
|
body.append(this.sectionTitle("Time zone"));
|
|
16436
17252
|
body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
|
|
@@ -16445,7 +17261,16 @@ var SettingsDialog = class {
|
|
|
16445
17261
|
}
|
|
16446
17262
|
}
|
|
16447
17263
|
};
|
|
17264
|
+
const renderChartTypeSections = (placement) => {
|
|
17265
|
+
for (const def of chartTypes()) {
|
|
17266
|
+
const typeSettings = def.settings;
|
|
17267
|
+
if (!typeSettings) continue;
|
|
17268
|
+
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
17269
|
+
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
17270
|
+
}
|
|
17271
|
+
};
|
|
16448
17272
|
renderHostSections("symbol");
|
|
17273
|
+
renderChartTypeSections("after-symbol");
|
|
16449
17274
|
renderHostSections("after-symbol");
|
|
16450
17275
|
body.append(this.section("Scales and lines"));
|
|
16451
17276
|
body.append(this.sectionTitle("Price scale"));
|
|
@@ -16486,35 +17311,27 @@ var SettingsDialog = class {
|
|
|
16486
17311
|
body.append(this.sectionTitle("Theme"));
|
|
16487
17312
|
body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
|
|
16488
17313
|
}
|
|
16489
|
-
|
|
16490
|
-
const typeSettings = def.settings;
|
|
16491
|
-
if (!typeSettings) continue;
|
|
16492
|
-
const marker = this.section(typeSettings.title);
|
|
16493
|
-
marker.dataset.sdStyle = def.id;
|
|
16494
|
-
marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
|
|
16495
|
-
body.append(marker);
|
|
16496
|
-
const values = config.chartTypes[def.id] ?? {};
|
|
16497
|
-
for (const r of typeSettings.rows) {
|
|
16498
|
-
const current2 = values[r.key];
|
|
16499
|
-
if (r.kind === "toggle") {
|
|
16500
|
-
body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
|
|
16501
|
-
} else if (r.kind === "number") {
|
|
16502
|
-
body.append(this.numberRow(r.label, typeof current2 === "number" ? current2 : r.defval, r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => this.emitType(def.id, r.key, v)));
|
|
16503
|
-
} else if (r.kind === "color") {
|
|
16504
|
-
body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
|
|
16505
|
-
} else {
|
|
16506
|
-
body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
|
|
16507
|
-
}
|
|
16508
|
-
}
|
|
16509
|
-
}
|
|
17314
|
+
renderChartTypeSections("end");
|
|
16510
17315
|
renderHostSections("end");
|
|
16511
17316
|
const shell = document.createElement("div");
|
|
16512
|
-
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;";
|
|
16513
17318
|
const rail = document.createElement("div");
|
|
16514
|
-
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
|
+
}
|
|
16515
17332
|
const paneHost = document.createElement("div");
|
|
16516
17333
|
paneHost.className = "vela-sd-pane";
|
|
16517
|
-
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;";
|
|
16518
17335
|
const panes = [];
|
|
16519
17336
|
let current = null;
|
|
16520
17337
|
for (const child of [...body.children]) {
|
|
@@ -16525,7 +17342,7 @@ var SettingsDialog = class {
|
|
|
16525
17342
|
const tab = document.createElement("button");
|
|
16526
17343
|
tab.type = "button";
|
|
16527
17344
|
tab.textContent = title;
|
|
16528
|
-
tab.className = "vela-sd-tab";
|
|
17345
|
+
tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
|
|
16529
17346
|
panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
|
|
16530
17347
|
current = el;
|
|
16531
17348
|
child.remove();
|
|
@@ -16549,6 +17366,10 @@ var SettingsDialog = class {
|
|
|
16549
17366
|
p.tab.classList.toggle("on", i === idx);
|
|
16550
17367
|
});
|
|
16551
17368
|
this.activeSection = panes[idx]?.title ?? null;
|
|
17369
|
+
if (mobile) {
|
|
17370
|
+
hTitle.textContent = panes[idx]?.title ?? "Chart settings";
|
|
17371
|
+
toggleRail?.(false);
|
|
17372
|
+
}
|
|
16552
17373
|
};
|
|
16553
17374
|
panes.forEach((p, i) => {
|
|
16554
17375
|
p.tab.addEventListener("click", () => activate(i));
|
|
@@ -16556,19 +17377,28 @@ var SettingsDialog = class {
|
|
|
16556
17377
|
paneHost.appendChild(p.el);
|
|
16557
17378
|
});
|
|
16558
17379
|
this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
|
|
16559
|
-
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"));
|
|
16560
17381
|
activate(wanted >= 0 ? wanted : 0);
|
|
16561
17382
|
this.syncTypeTabs?.(config.series.style);
|
|
16562
17383
|
shell.append(rail, paneHost);
|
|
17384
|
+
if (railScrim) shell.append(railScrim);
|
|
16563
17385
|
dlg.appendChild(shell);
|
|
16564
17386
|
dlg.appendChild(this.footer(config));
|
|
16565
17387
|
scrim.appendChild(dlg);
|
|
16566
17388
|
this.container.appendChild(scrim);
|
|
16567
17389
|
this.root = scrim;
|
|
16568
|
-
for (const p of panes)
|
|
17390
|
+
for (const p of panes) {
|
|
17391
|
+
const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
|
|
17392
|
+
if (hosts.length === 0) {
|
|
17393
|
+
this.layoutSettingsGrids(p.el, dlg);
|
|
17394
|
+
continue;
|
|
17395
|
+
}
|
|
17396
|
+
for (const h of hosts) this.layoutSettingsGrids(h, dlg);
|
|
17397
|
+
}
|
|
16569
17398
|
}
|
|
16570
17399
|
close() {
|
|
16571
17400
|
closeColorPopover();
|
|
17401
|
+
closeWidthPopover();
|
|
16572
17402
|
this.root?.remove();
|
|
16573
17403
|
this.root = null;
|
|
16574
17404
|
this.tabs = [];
|
|
@@ -16595,35 +17425,370 @@ var SettingsDialog = class {
|
|
|
16595
17425
|
el.textContent = text;
|
|
16596
17426
|
return el;
|
|
16597
17427
|
}
|
|
16598
|
-
/** Vertical breathing space between row clusters — grouping reads from whitespace,
|
|
16599
|
-
* so no drawn rule. */
|
|
16600
|
-
separator() {
|
|
16601
|
-
const el = document.createElement("div");
|
|
16602
|
-
el.className = "vela-sd-sep";
|
|
16603
|
-
el.style.cssText = "height:14px;";
|
|
16604
|
-
return el;
|
|
16605
|
-
}
|
|
16606
17428
|
/**
|
|
16607
|
-
* One
|
|
16608
|
-
*
|
|
16609
|
-
*
|
|
16610
|
-
*
|
|
16611
|
-
*
|
|
16612
|
-
* `display:none` until selected, which would zero layout measurements.
|
|
17429
|
+
* One chart type's settings tab (and its subsection tabs), appended to the linear
|
|
17430
|
+
* `body` for the pane splitter to file. One live values BAG serves the whole section
|
|
17431
|
+
* — instances and subsections share the per-type store, so a `when` gate anywhere
|
|
17432
|
+
* can reference a key edited anywhere else; every edit runs every registered
|
|
17433
|
+
* refresher.
|
|
16613
17434
|
*/
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
const
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
|
|
17435
|
+
chartTypeSection(typeId, section, config, body) {
|
|
17436
|
+
const marker = this.section(section.title);
|
|
17437
|
+
marker.dataset.sdStyle = typeId;
|
|
17438
|
+
marker.dataset.sdVisibility = section.visibility ?? "active";
|
|
17439
|
+
body.append(marker);
|
|
17440
|
+
const values = config.chartTypes[typeId] ?? {};
|
|
17441
|
+
const bag = {};
|
|
17442
|
+
const seed = (rows) => {
|
|
17443
|
+
for (const r of rows) {
|
|
17444
|
+
for (const k of settingsRowValueKeys(r)) {
|
|
17445
|
+
const v = values[k.key];
|
|
17446
|
+
bag[k.key] = typeof v === k.type ? v : k.defval;
|
|
17447
|
+
}
|
|
17448
|
+
}
|
|
17449
|
+
};
|
|
17450
|
+
if (section.rows) seed(section.rows);
|
|
17451
|
+
for (const inst of section.instances ?? []) seed(inst.rows);
|
|
17452
|
+
for (const sub of section.subsections ?? []) seed(sub.rows);
|
|
17453
|
+
for (const inst of section.instances ?? []) {
|
|
17454
|
+
if (!inst.enableKey || inst.enableKey in bag) continue;
|
|
17455
|
+
bag[inst.enableKey] = values[inst.enableKey] === true;
|
|
17456
|
+
}
|
|
17457
|
+
for (const sub of section.subsections ?? []) {
|
|
17458
|
+
if (!sub.enableKey || sub.enableKey in bag) continue;
|
|
17459
|
+
bag[sub.enableKey] = values[sub.enableKey] === true;
|
|
17460
|
+
}
|
|
17461
|
+
const refreshers = [];
|
|
17462
|
+
const put = (key, v) => {
|
|
17463
|
+
bag[key] = v;
|
|
17464
|
+
this.emitType(typeId, key, v);
|
|
17465
|
+
for (const r of refreshers) r();
|
|
17466
|
+
};
|
|
17467
|
+
if (section.instances && section.instances.length > 0) {
|
|
17468
|
+
body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
|
|
17469
|
+
} else if (section.rows) {
|
|
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);
|
|
17472
|
+
}
|
|
17473
|
+
for (const sub of section.subsections ?? []) {
|
|
17474
|
+
const subMarker = this.section(sub.title);
|
|
17475
|
+
subMarker.dataset.sdStyle = typeId;
|
|
17476
|
+
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
17477
|
+
subMarker.dataset.sdSub = "1";
|
|
17478
|
+
body.append(subMarker);
|
|
17479
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
|
|
17480
|
+
}
|
|
17481
|
+
for (const r of refreshers) r();
|
|
17482
|
+
}
|
|
17483
|
+
/** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
|
|
17484
|
+
* them), headings/headers as inline group titles, `when` gates refreshed live. */
|
|
17485
|
+
flatTypeRows(rows, bag, put, refreshers, body) {
|
|
17486
|
+
const entries = [];
|
|
17487
|
+
for (const r of rows) {
|
|
17488
|
+
const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
|
|
17489
|
+
entries.push({ el, when: r.when });
|
|
17490
|
+
body.append(el);
|
|
17491
|
+
}
|
|
17492
|
+
refreshers.push(() => {
|
|
17493
|
+
for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
|
|
17494
|
+
});
|
|
17495
|
+
}
|
|
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;
|
|
17525
|
+
}
|
|
17526
|
+
return this.rowWith(n.label, controls);
|
|
17527
|
+
}
|
|
17528
|
+
/**
|
|
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.
|
|
17532
|
+
*/
|
|
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);
|
|
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);
|
|
17585
|
+
ni.addEventListener("change", () => {
|
|
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);
|
|
17590
|
+
});
|
|
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;
|
|
17599
|
+
}
|
|
17600
|
+
/**
|
|
17601
|
+
* The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
|
|
17602
|
+
* removable one), a dashed `+` while an instance is still absent, and one
|
|
17603
|
+
* grouped-rows content per instance below — only the active tab's content shows.
|
|
17604
|
+
* Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
|
|
17605
|
+
* every section edit, so gates elsewhere stay coherent.
|
|
17606
|
+
*/
|
|
17607
|
+
instancesBlock(typeId, instances, bag, put, refreshers) {
|
|
17608
|
+
const wrap = document.createElement("div");
|
|
17609
|
+
const strip = document.createElement("div");
|
|
17610
|
+
strip.className = "vela-sd-itabs";
|
|
17611
|
+
if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
|
|
17612
|
+
const contents = instances.map((inst, i) => {
|
|
17613
|
+
const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
|
|
17614
|
+
wrap.append(content);
|
|
17615
|
+
return content;
|
|
17616
|
+
});
|
|
17617
|
+
const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
|
|
17618
|
+
const refresh = () => {
|
|
17619
|
+
let active = this.typeActiveInstance.get(typeId) ?? 0;
|
|
17620
|
+
if (!present(instances[active] ?? instances[0])) active = 0;
|
|
17621
|
+
this.typeActiveInstance.set(typeId, active);
|
|
17622
|
+
strip.replaceChildren();
|
|
17623
|
+
instances.forEach((inst, i) => {
|
|
17624
|
+
if (!present(inst)) return;
|
|
17625
|
+
const tab = document.createElement("button");
|
|
17626
|
+
tab.type = "button";
|
|
17627
|
+
tab.className = "vela-sd-itab" + (i === active ? " on" : "");
|
|
17628
|
+
const lbl = document.createElement("span");
|
|
17629
|
+
lbl.textContent = inst.label;
|
|
17630
|
+
tab.append(lbl);
|
|
17631
|
+
if (i === active && inst.enableKey) {
|
|
17632
|
+
const x = document.createElement("span");
|
|
17633
|
+
x.className = "vela-sd-ix";
|
|
17634
|
+
x.textContent = "\u2715";
|
|
17635
|
+
x.title = `Remove ${inst.label}`;
|
|
17636
|
+
x.addEventListener("click", (e) => {
|
|
17637
|
+
e.stopPropagation();
|
|
17638
|
+
this.typeActiveInstance.set(typeId, 0);
|
|
17639
|
+
put(inst.enableKey, false);
|
|
17640
|
+
});
|
|
17641
|
+
tab.append(x);
|
|
17642
|
+
}
|
|
17643
|
+
tab.addEventListener("click", () => {
|
|
17644
|
+
this.typeActiveInstance.set(typeId, i);
|
|
17645
|
+
refresh();
|
|
17646
|
+
});
|
|
17647
|
+
strip.append(tab);
|
|
17648
|
+
});
|
|
17649
|
+
const absent = instances.findIndex((inst) => !present(inst));
|
|
17650
|
+
if (absent >= 0) {
|
|
17651
|
+
const add = document.createElement("button");
|
|
17652
|
+
add.type = "button";
|
|
17653
|
+
add.className = "vela-sd-itab vela-sd-itab-add";
|
|
17654
|
+
add.textContent = "+";
|
|
17655
|
+
add.title = `Add ${instances[absent].label}`;
|
|
17656
|
+
add.addEventListener("click", () => {
|
|
17657
|
+
this.typeActiveInstance.set(typeId, absent);
|
|
17658
|
+
put(instances[absent].enableKey, true);
|
|
17659
|
+
});
|
|
17660
|
+
strip.append(add);
|
|
17661
|
+
}
|
|
17662
|
+
contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
|
|
17663
|
+
};
|
|
17664
|
+
refreshers.push(refresh);
|
|
17665
|
+
return wrap;
|
|
17666
|
+
}
|
|
17667
|
+
/**
|
|
17668
|
+
* A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
|
|
17669
|
+
* ONE rows host holding every row — the active group's rows show, the rest hide.
|
|
17670
|
+
* Rows BEFORE the first heading are the always block, visible above every group.
|
|
17671
|
+
* `header` rows stay in the rows column as in-group subgroup titles (not TOC
|
|
17672
|
+
* entries). A group whose value rows are all gated out (or whose heading's own
|
|
17673
|
+
* `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
|
|
17674
|
+
* rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
|
|
17675
|
+
* across all groups.
|
|
17676
|
+
*
|
|
17677
|
+
* `enableKey` (optional): while that bag boolean is false, every row except the one
|
|
17678
|
+
* whose key matches is soft-disabled (visible but grayed / non-interactive) so the
|
|
17679
|
+
* pane stays browseable with the feature off.
|
|
17680
|
+
*/
|
|
17681
|
+
groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
|
|
17682
|
+
const wrap = document.createElement("div");
|
|
17683
|
+
wrap.className = "vela-sd-struct";
|
|
17684
|
+
const toc = document.createElement("div");
|
|
17685
|
+
toc.className = "vela-sd-toc";
|
|
17686
|
+
const rowsHost = document.createElement("div");
|
|
17687
|
+
rowsHost.dataset.sdRowsHost = "1";
|
|
17688
|
+
rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
|
|
17689
|
+
wrap.append(toc, rowsHost);
|
|
17690
|
+
const entries = [];
|
|
17691
|
+
const groups = [];
|
|
17692
|
+
const groupWhens = [];
|
|
17693
|
+
let g = -1;
|
|
17694
|
+
for (const r of rows) {
|
|
17695
|
+
if (r.kind === "heading") {
|
|
17696
|
+
g = groups.length;
|
|
17697
|
+
groups.push(r.label);
|
|
17698
|
+
groupWhens.push(r.when);
|
|
17699
|
+
continue;
|
|
17700
|
+
}
|
|
17701
|
+
if (r.kind === "header") {
|
|
17702
|
+
entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
|
|
17703
|
+
rowsHost.append(entries[entries.length - 1].el);
|
|
17704
|
+
continue;
|
|
17705
|
+
}
|
|
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;
|
|
17708
|
+
entries.push({ el, when: r.when, group: g, key });
|
|
17709
|
+
rowsHost.append(el);
|
|
17710
|
+
}
|
|
17711
|
+
const refresh = () => {
|
|
17712
|
+
const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
|
|
17713
|
+
let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
|
|
17714
|
+
if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
|
|
17715
|
+
if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
|
|
17716
|
+
toc.replaceChildren();
|
|
17717
|
+
groups.forEach((label, gi) => {
|
|
17718
|
+
if (!live[gi]) return;
|
|
17719
|
+
const b = document.createElement("button");
|
|
17720
|
+
b.type = "button";
|
|
17721
|
+
b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
|
|
17722
|
+
b.textContent = label;
|
|
17723
|
+
b.addEventListener("click", () => {
|
|
17724
|
+
this.typeActiveGroup.set(paneKey, label);
|
|
17725
|
+
refresh();
|
|
17726
|
+
});
|
|
17727
|
+
toc.append(b);
|
|
17728
|
+
});
|
|
17729
|
+
const anyGroup = live.some(Boolean);
|
|
17730
|
+
toc.classList.toggle("vela-sd-hide", !anyGroup);
|
|
17731
|
+
wrap.classList.toggle("no-toc", !anyGroup);
|
|
17732
|
+
const enabled = !enableKey || bag[enableKey] === true;
|
|
17733
|
+
for (let i = 0; i < entries.length; i++) {
|
|
17734
|
+
const e = entries[i];
|
|
17735
|
+
let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
|
|
17736
|
+
if (visible && e.header) {
|
|
17737
|
+
let hasContent = false;
|
|
17738
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
17739
|
+
const n = entries[j];
|
|
17740
|
+
if (n.group !== e.group || n.header) break;
|
|
17741
|
+
if (settingsRowVisible(n.when, bag)) {
|
|
17742
|
+
hasContent = true;
|
|
17743
|
+
break;
|
|
17744
|
+
}
|
|
17745
|
+
}
|
|
17746
|
+
visible = hasContent;
|
|
17747
|
+
}
|
|
17748
|
+
e.el.classList.toggle("vela-sd-hide", !visible);
|
|
17749
|
+
e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
|
|
17750
|
+
}
|
|
17751
|
+
};
|
|
17752
|
+
refreshers.push(refresh);
|
|
17753
|
+
return wrap;
|
|
17754
|
+
}
|
|
17755
|
+
/** Vertical breathing space between row clusters — grouping reads from whitespace,
|
|
17756
|
+
* so no drawn rule. */
|
|
17757
|
+
separator() {
|
|
17758
|
+
const el = document.createElement("div");
|
|
17759
|
+
el.className = "vela-sd-sep";
|
|
17760
|
+
el.style.cssText = "height:14px;";
|
|
17761
|
+
return el;
|
|
17762
|
+
}
|
|
17763
|
+
/**
|
|
17764
|
+
* One pane-wide control column (section-agnostic), sized to the longest setting
|
|
17765
|
+
* title — same idea as the indicator settings dialog's `minmax(..., auto)` label
|
|
17766
|
+
* track, but shared across every section in the tab.
|
|
17767
|
+
*
|
|
17768
|
+
* `measureHost` must be a visible, in-document ancestor (the dialog): panes are
|
|
17769
|
+
* `display:none` until selected, which would zero layout measurements.
|
|
17770
|
+
*/
|
|
17771
|
+
layoutSettingsGrids(pane, measureHost) {
|
|
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
|
+
}
|
|
17781
|
+
const rows = [...pane.querySelectorAll(".vela-sd-row")];
|
|
17782
|
+
const probe2 = document.createElement("div");
|
|
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;";
|
|
17784
|
+
measureHost.appendChild(probe2);
|
|
17785
|
+
let widest = 0;
|
|
17786
|
+
for (const row of rows) {
|
|
17787
|
+
const cell = row.children[0];
|
|
17788
|
+
if (!cell) continue;
|
|
17789
|
+
const clone = cell.cloneNode(true);
|
|
17790
|
+
probe2.appendChild(clone);
|
|
17791
|
+
widest = Math.max(widest, clone.getBoundingClientRect().width);
|
|
16627
17792
|
}
|
|
16628
17793
|
probe2.remove();
|
|
16629
17794
|
const labelTrack = widest > 0 ? `${Math.ceil(widest)}px` : "max-content";
|
|
@@ -16649,9 +17814,11 @@ var SettingsDialog = class {
|
|
|
16649
17814
|
}
|
|
16650
17815
|
}
|
|
16651
17816
|
/** A bare color swatch input (for toggle-row right groups / swatch pairs). */
|
|
16652
|
-
|
|
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) {
|
|
16653
17820
|
let current = value;
|
|
16654
|
-
return colorField(this.theme, () => current, (v) => {
|
|
17821
|
+
return colorField(this.theme, () => get ? get() : current, (v) => {
|
|
16655
17822
|
current = v;
|
|
16656
17823
|
onChange(v);
|
|
16657
17824
|
});
|
|
@@ -16703,8 +17870,11 @@ var SettingsDialog = class {
|
|
|
16703
17870
|
}
|
|
16704
17871
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
16705
17872
|
* shared control column; the control group dims and ignores input while the toggle
|
|
16706
|
-
* is off. With no controls it reads like a plain toggle row (full-width in the grid).
|
|
16707
|
-
|
|
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) {
|
|
16708
17878
|
const wrap = document.createElement("div");
|
|
16709
17879
|
const cb = document.createElement("button");
|
|
16710
17880
|
cb.type = "button";
|
|
@@ -16724,6 +17894,12 @@ var SettingsDialog = class {
|
|
|
16724
17894
|
wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
|
|
16725
17895
|
wrap.append(cb, lbl);
|
|
16726
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
|
+
}
|
|
16727
17903
|
return wrap;
|
|
16728
17904
|
}
|
|
16729
17905
|
wrap.className = "vela-sd-row";
|
|
@@ -16734,8 +17910,11 @@ var SettingsDialog = class {
|
|
|
16734
17910
|
box.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
16735
17911
|
for (const c of controls) box.appendChild(c);
|
|
16736
17912
|
const syncDim = (on) => {
|
|
16737
|
-
|
|
16738
|
-
|
|
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
|
+
}
|
|
16739
17918
|
};
|
|
16740
17919
|
syncDim(value);
|
|
16741
17920
|
cb.addEventListener("click", () => {
|
|
@@ -16743,6 +17922,13 @@ var SettingsDialog = class {
|
|
|
16743
17922
|
onToggle(v);
|
|
16744
17923
|
syncDim(v);
|
|
16745
17924
|
});
|
|
17925
|
+
if (get) {
|
|
17926
|
+
wrap.addEventListener("vela-sync", () => {
|
|
17927
|
+
checked = get();
|
|
17928
|
+
cb.classList.toggle("on", checked);
|
|
17929
|
+
syncDim(checked);
|
|
17930
|
+
});
|
|
17931
|
+
}
|
|
16746
17932
|
wrap.append(left, box);
|
|
16747
17933
|
return wrap;
|
|
16748
17934
|
}
|
|
@@ -16762,102 +17948,12 @@ var SettingsDialog = class {
|
|
|
16762
17948
|
wrap.appendChild(ni);
|
|
16763
17949
|
return wrap;
|
|
16764
17950
|
}
|
|
16765
|
-
/** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
|
|
16766
|
-
autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
|
|
16767
|
-
const { wrap } = this.row(label);
|
|
16768
|
-
const box = document.createElement("div");
|
|
16769
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
16770
|
-
const sel = document.createElement("select");
|
|
16771
|
-
sel.className = "vela-sd-select";
|
|
16772
|
-
for (const [val, lbl] of AUTO_MANUAL_OPTS) {
|
|
16773
|
-
const o = document.createElement("option");
|
|
16774
|
-
o.value = val;
|
|
16775
|
-
o.textContent = lbl;
|
|
16776
|
-
if (val === "auto" === isAuto) o.selected = true;
|
|
16777
|
-
sel.appendChild(o);
|
|
16778
|
-
}
|
|
16779
|
-
const ni = document.createElement("input");
|
|
16780
|
-
ni.type = "number";
|
|
16781
|
-
ni.min = String(min);
|
|
16782
|
-
ni.max = String(max);
|
|
16783
|
-
ni.step = "1";
|
|
16784
|
-
ni.value = String(value);
|
|
16785
|
-
ni.className = "vela-sd-number";
|
|
16786
|
-
const unitLbl = document.createElement("span");
|
|
16787
|
-
unitLbl.textContent = unit;
|
|
16788
|
-
unitLbl.style.cssText = "opacity:0.6;";
|
|
16789
|
-
const syncDisabled = (auto) => {
|
|
16790
|
-
ni.disabled = auto;
|
|
16791
|
-
ni.style.opacity = auto ? "0.4" : "1";
|
|
16792
|
-
};
|
|
16793
|
-
syncDisabled(isAuto);
|
|
16794
|
-
sel.addEventListener("change", () => {
|
|
16795
|
-
const auto = sel.value === "auto";
|
|
16796
|
-
onMode(auto);
|
|
16797
|
-
syncDisabled(auto);
|
|
16798
|
-
});
|
|
16799
|
-
ni.addEventListener("input", () => {
|
|
16800
|
-
const n = Number(ni.value);
|
|
16801
|
-
if (Number.isFinite(n)) onValue(n);
|
|
16802
|
-
});
|
|
16803
|
-
box.append(sel, ni, unitLbl);
|
|
16804
|
-
wrap.appendChild(box);
|
|
16805
|
-
return wrap;
|
|
16806
|
-
}
|
|
16807
|
-
/** A row whose control area holds several inline controls (e.g. show + color + width). */
|
|
16808
|
-
inlineRow(label, controls) {
|
|
16809
|
-
const { wrap } = this.row(label);
|
|
16810
|
-
const box = document.createElement("div");
|
|
16811
|
-
box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
|
|
16812
|
-
for (const c of controls) box.appendChild(c);
|
|
16813
|
-
wrap.appendChild(box);
|
|
16814
|
-
return wrap;
|
|
16815
|
-
}
|
|
16816
|
-
/** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
|
|
16817
|
-
hint(text) {
|
|
16818
|
-
const s = document.createElement("span");
|
|
16819
|
-
s.textContent = text;
|
|
16820
|
-
s.style.cssText = "opacity:0.5;font-size:12px;";
|
|
16821
|
-
return s;
|
|
16822
|
-
}
|
|
16823
|
-
/** A bare color input (for inline groups). */
|
|
16824
|
-
colorInput(value, onChange) {
|
|
16825
|
-
const ci = document.createElement("input");
|
|
16826
|
-
ci.type = "color";
|
|
16827
|
-
ci.value = toHex6(value);
|
|
16828
|
-
ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
|
|
16829
|
-
ci.addEventListener("input", () => onChange(ci.value));
|
|
16830
|
-
return ci;
|
|
16831
|
-
}
|
|
16832
|
-
/** A bare compact number input (for inline groups). */
|
|
16833
|
-
numberInput(value, min, max, step, onChange) {
|
|
16834
|
-
const ni = document.createElement("input");
|
|
16835
|
-
ni.type = "number";
|
|
16836
|
-
ni.value = String(value);
|
|
16837
|
-
ni.min = String(min);
|
|
16838
|
-
ni.max = String(max);
|
|
16839
|
-
ni.step = String(step);
|
|
16840
|
-
ni.className = "vela-sd-number";
|
|
16841
|
-
ni.style.flex = "0 0 auto";
|
|
16842
|
-
ni.addEventListener("input", () => {
|
|
16843
|
-
const n = Number(ni.value);
|
|
16844
|
-
if (Number.isFinite(n)) onChange(n);
|
|
16845
|
-
});
|
|
16846
|
-
return ni;
|
|
16847
|
-
}
|
|
16848
|
-
/** A small in-group heading (lighter than a top-level section divider). */
|
|
16849
|
-
subheading(text) {
|
|
16850
|
-
const el = document.createElement("div");
|
|
16851
|
-
el.textContent = text;
|
|
16852
|
-
el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
|
|
16853
|
-
return el;
|
|
16854
|
-
}
|
|
16855
17951
|
/** A dropdown whose option values differ from their display labels. */
|
|
16856
17952
|
selectRowLabeled(label, value, options, onChange) {
|
|
16857
17953
|
const { wrap } = this.row(label);
|
|
16858
17954
|
const sel = document.createElement("select");
|
|
16859
17955
|
sel.className = "vela-sd-select";
|
|
16860
|
-
sel.style.cssText = "max-width:
|
|
17956
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
16861
17957
|
for (const [val, lbl] of options) {
|
|
16862
17958
|
const o = document.createElement("option");
|
|
16863
17959
|
o.value = val;
|
|
@@ -16873,7 +17969,7 @@ var SettingsDialog = class {
|
|
|
16873
17969
|
const { wrap } = this.row(label);
|
|
16874
17970
|
const sel = document.createElement("select");
|
|
16875
17971
|
sel.className = "vela-sd-select";
|
|
16876
|
-
sel.style.cssText = "max-width:
|
|
17972
|
+
sel.style.cssText = "max-width:200px;flex:0 0 auto;";
|
|
16877
17973
|
for (const opt of options) {
|
|
16878
17974
|
const o = document.createElement("option");
|
|
16879
17975
|
o.value = opt;
|
|
@@ -16897,19 +17993,10 @@ var SettingsDialog = class {
|
|
|
16897
17993
|
foot.appendChild(resetBtn);
|
|
16898
17994
|
return foot;
|
|
16899
17995
|
}
|
|
16900
|
-
button(label, onClick) {
|
|
16901
|
-
const b = document.createElement("button");
|
|
16902
|
-
b.type = "button";
|
|
16903
|
-
b.textContent = label;
|
|
16904
|
-
b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
|
|
16905
|
-
b.addEventListener("click", onClick);
|
|
16906
|
-
return b;
|
|
16907
|
-
}
|
|
16908
|
-
ctrlStyle() {
|
|
16909
|
-
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;`;
|
|
16910
|
-
}
|
|
16911
17996
|
};
|
|
16912
|
-
|
|
17997
|
+
function normalizeSelectOptions(options) {
|
|
17998
|
+
return options.map((o) => typeof o === "string" ? [o, o] : o);
|
|
17999
|
+
}
|
|
16913
18000
|
function timezoneOptions(current) {
|
|
16914
18001
|
const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
|
|
16915
18002
|
const normalized = normalizeTimezone(current);
|
|
@@ -17109,7 +18196,7 @@ var DrawingPainter = class {
|
|
|
17109
18196
|
return;
|
|
17110
18197
|
}
|
|
17111
18198
|
if (d instanceof FixedRangeVolumeProfile) {
|
|
17112
|
-
this.paintFixedRangeVp(ctx, d, proj);
|
|
18199
|
+
this.paintFixedRangeVp(ctx, d, proj, theme);
|
|
17113
18200
|
this.paintLabel(ctx, d, proj, theme);
|
|
17114
18201
|
return;
|
|
17115
18202
|
}
|
|
@@ -17602,7 +18689,7 @@ var DrawingPainter = class {
|
|
|
17602
18689
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
17603
18690
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
17604
18691
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
17605
|
-
paintFixedRangeVp(ctx, d, proj) {
|
|
18692
|
+
paintFixedRangeVp(ctx, d, proj, theme) {
|
|
17606
18693
|
const L = d.layout(proj);
|
|
17607
18694
|
if (!L) return;
|
|
17608
18695
|
const s = d.frvp;
|
|
@@ -17641,7 +18728,7 @@ var DrawingPainter = class {
|
|
|
17641
18728
|
};
|
|
17642
18729
|
hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
|
|
17643
18730
|
hLine(s.showVal, s.valColor, s.valStyle, L.valY);
|
|
17644
|
-
hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
|
|
18731
|
+
hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
|
|
17645
18732
|
const devW = Math.max(1, w);
|
|
17646
18733
|
if (s.showDevelopingPoc) {
|
|
17647
18734
|
this.strokePolyline(ctx, L.developingPoc, {
|
|
@@ -18268,6 +19355,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
18268
19355
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
18269
19356
|
var DRAG_SLOP2 = 3;
|
|
18270
19357
|
var FREEHAND_SAMPLE_PX = 4;
|
|
19358
|
+
var ANGLE_SNAP_STEP = Math.PI / 4;
|
|
19359
|
+
var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
|
|
18271
19360
|
var DrawingInteraction = class {
|
|
18272
19361
|
// magnet target for the ring
|
|
18273
19362
|
constructor(deps) {
|
|
@@ -18308,6 +19397,27 @@ var DrawingInteraction = class {
|
|
|
18308
19397
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
18309
19398
|
return snapped;
|
|
18310
19399
|
}
|
|
19400
|
+
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
19401
|
+
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
19402
|
+
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
19403
|
+
* the result to a candle afterwards would break the exact angle just enforced. */
|
|
19404
|
+
resolveAngleSnapped(pivot, paneId, x, y) {
|
|
19405
|
+
const proj = this.deps.projector();
|
|
19406
|
+
const px = proj.xOf(pivot.time);
|
|
19407
|
+
const py = proj.yOf(pivot.price, paneId);
|
|
19408
|
+
this.snapAt = null;
|
|
19409
|
+
if (py == null) return proj.pxToPoint(x, y, paneId);
|
|
19410
|
+
const r = Math.hypot(x - px, y - py);
|
|
19411
|
+
if (r < 1) return proj.pxToPoint(x, y, paneId);
|
|
19412
|
+
const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
|
|
19413
|
+
return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
|
|
19414
|
+
}
|
|
19415
|
+
/** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
|
|
19416
|
+
* the tool is not a two-point segment (or no anchor is committed yet). */
|
|
19417
|
+
placingPivot(draft) {
|
|
19418
|
+
if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
|
|
19419
|
+
return draft.anchors[draft.anchors.length - 1] ?? null;
|
|
19420
|
+
}
|
|
18311
19421
|
/** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
|
|
18312
19422
|
claim(x, y) {
|
|
18313
19423
|
if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
|
|
@@ -18329,12 +19439,15 @@ var DrawingInteraction = class {
|
|
|
18329
19439
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
18330
19440
|
}
|
|
18331
19441
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
18332
|
-
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging
|
|
19442
|
+
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
19443
|
+
* while placing a line tool it locks the segment angle to 45° steps. */
|
|
18333
19444
|
down(x, y, mode = "off", shift = false) {
|
|
18334
19445
|
const proj = this.deps.projector();
|
|
18335
19446
|
if (this.state.kind === "placing") {
|
|
18336
19447
|
if (this.state.draft.placementMode() !== "click") return;
|
|
18337
|
-
this.
|
|
19448
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
19449
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
19450
|
+
this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
|
|
18338
19451
|
if (this.state.draft.anchors.length >= this.state.need) this.finalize();
|
|
18339
19452
|
else this.deps.changed();
|
|
18340
19453
|
return;
|
|
@@ -18359,8 +19472,9 @@ var DrawingInteraction = class {
|
|
|
18359
19472
|
this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
|
|
18360
19473
|
}
|
|
18361
19474
|
}
|
|
18362
|
-
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
18363
|
-
|
|
19475
|
+
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
19476
|
+
* `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
|
|
19477
|
+
move(x, y, mode = "off", shift = false) {
|
|
18364
19478
|
if (this.state.kind === "idle") {
|
|
18365
19479
|
const tool = this.deps.activeTool();
|
|
18366
19480
|
const had = this.snapAt != null;
|
|
@@ -18374,7 +19488,8 @@ var DrawingInteraction = class {
|
|
|
18374
19488
|
this.captureFreehand(x, y);
|
|
18375
19489
|
return;
|
|
18376
19490
|
}
|
|
18377
|
-
|
|
19491
|
+
const pivot = shift ? this.placingPivot(this.state.draft) : null;
|
|
19492
|
+
this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
|
|
18378
19493
|
this.deps.changed();
|
|
18379
19494
|
return;
|
|
18380
19495
|
}
|
|
@@ -18385,7 +19500,11 @@ var DrawingInteraction = class {
|
|
|
18385
19500
|
this.state.moved = true;
|
|
18386
19501
|
if (this.state.handle >= 0) {
|
|
18387
19502
|
const a = d.anchors[this.state.handle];
|
|
18388
|
-
if (a)
|
|
19503
|
+
if (a) {
|
|
19504
|
+
const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
19505
|
+
const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
|
|
19506
|
+
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
19507
|
+
}
|
|
18389
19508
|
d.constrainHandleDrag(this.state.handle);
|
|
18390
19509
|
} else {
|
|
18391
19510
|
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
@@ -18481,7 +19600,11 @@ var DrawingInteraction = class {
|
|
|
18481
19600
|
const last = draft.anchors[draft.anchors.length - 1];
|
|
18482
19601
|
const lx = last ? proj.xOf(last.time) : NaN;
|
|
18483
19602
|
const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
|
|
18484
|
-
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
|
+
}
|
|
18485
19608
|
draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
|
|
18486
19609
|
}
|
|
18487
19610
|
this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
|
|
@@ -18554,11 +19677,11 @@ function applyFree(anchor, pt, free) {
|
|
|
18554
19677
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
18555
19678
|
var BTN = 30;
|
|
18556
19679
|
var ICON = 21;
|
|
18557
|
-
var
|
|
18558
|
-
function
|
|
18559
|
-
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;
|
|
18560
19683
|
const s = document.createElement("style");
|
|
18561
|
-
s.id =
|
|
19684
|
+
s.id = STYLE_ID4;
|
|
18562
19685
|
s.textContent = `
|
|
18563
19686
|
.vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
|
|
18564
19687
|
.vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
|
|
@@ -18577,7 +19700,10 @@ function ensureStyles3() {
|
|
|
18577
19700
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
18578
19701
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
18579
19702
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
18580
|
-
.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;}}`;
|
|
18581
19707
|
document.head.appendChild(s);
|
|
18582
19708
|
}
|
|
18583
19709
|
var DrawingSettingsPopup = class {
|
|
@@ -18624,7 +19750,7 @@ var DrawingSettingsPopup = class {
|
|
|
18624
19750
|
* `onClose` fires on an outside-click dismissal (not a programmatic close). */
|
|
18625
19751
|
open(drawing, anchor, actions, onClose) {
|
|
18626
19752
|
this.close();
|
|
18627
|
-
|
|
19753
|
+
ensureStyles4();
|
|
18628
19754
|
this.onClose = onClose ?? null;
|
|
18629
19755
|
const t = this.theme;
|
|
18630
19756
|
const schema = drawing.schema();
|
|
@@ -18650,7 +19776,14 @@ var DrawingSettingsPopup = class {
|
|
|
18650
19776
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
18651
19777
|
}
|
|
18652
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 })));
|
|
18653
|
-
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
|
+
}
|
|
18654
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 }));
|
|
18655
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 })));
|
|
18656
19789
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
@@ -19118,14 +20251,15 @@ var DrawingSettingsPopup = class {
|
|
|
19118
20251
|
const chk = document.createElement("input");
|
|
19119
20252
|
chk.type = "checkbox";
|
|
19120
20253
|
chk.checked = Boolean(s[showPath]);
|
|
19121
|
-
|
|
20254
|
+
const current = s[colorPath] ?? contrastColor(t.background);
|
|
20255
|
+
chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
|
|
19122
20256
|
chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
|
|
19123
20257
|
const lbl = document.createElement("span");
|
|
19124
20258
|
lbl.textContent = label;
|
|
19125
20259
|
lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
|
|
19126
20260
|
const col = document.createElement("button");
|
|
19127
20261
|
col.type = "button";
|
|
19128
|
-
let cur =
|
|
20262
|
+
let cur = current;
|
|
19129
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;`;
|
|
19130
20264
|
col.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
19131
20265
|
col.addEventListener("click", (e) => {
|
|
@@ -19305,7 +20439,8 @@ var DrawingSettingsPopup = class {
|
|
|
19305
20439
|
dragHandle() {
|
|
19306
20440
|
const b = document.createElement("button");
|
|
19307
20441
|
b.type = "button";
|
|
19308
|
-
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);`;
|
|
19309
20444
|
b.innerHTML = sized(GRIP_ICON);
|
|
19310
20445
|
b.addEventListener("pointerdown", (e) => {
|
|
19311
20446
|
const el = this.el;
|
|
@@ -19321,6 +20456,10 @@ var DrawingSettingsPopup = class {
|
|
|
19321
20456
|
const origLeft = parseFloat(el.style.left) || 0;
|
|
19322
20457
|
const origTop = parseFloat(el.style.top) || 0;
|
|
19323
20458
|
b.style.cursor = "grabbing";
|
|
20459
|
+
try {
|
|
20460
|
+
b.setPointerCapture(e.pointerId);
|
|
20461
|
+
} catch {
|
|
20462
|
+
}
|
|
19324
20463
|
const move = (ev) => {
|
|
19325
20464
|
const w = el.offsetWidth;
|
|
19326
20465
|
const h = el.offsetHeight;
|
|
@@ -19333,9 +20472,11 @@ var DrawingSettingsPopup = class {
|
|
|
19333
20472
|
b.style.cursor = "grab";
|
|
19334
20473
|
window.removeEventListener("pointermove", move);
|
|
19335
20474
|
window.removeEventListener("pointerup", up);
|
|
20475
|
+
window.removeEventListener("pointercancel", up);
|
|
19336
20476
|
};
|
|
19337
20477
|
window.addEventListener("pointermove", move);
|
|
19338
20478
|
window.addEventListener("pointerup", up);
|
|
20479
|
+
window.addEventListener("pointercancel", up);
|
|
19339
20480
|
});
|
|
19340
20481
|
return b;
|
|
19341
20482
|
}
|
|
@@ -19451,6 +20592,39 @@ var DrawingSettingsPopup = class {
|
|
|
19451
20592
|
});
|
|
19452
20593
|
return b;
|
|
19453
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
|
+
}
|
|
19454
20628
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
19455
20629
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
19456
20630
|
* opens a floating list of every option so a value is one click away (no cycling through). */
|
|
@@ -19738,6 +20912,8 @@ var DrawingToolbar = class {
|
|
|
19738
20912
|
this.eraserActive = false;
|
|
19739
20913
|
this.stayBtn = null;
|
|
19740
20914
|
this.stayActive = false;
|
|
20915
|
+
this.syncBtn = null;
|
|
20916
|
+
this.syncActive = false;
|
|
19741
20917
|
this.collapseBtn = null;
|
|
19742
20918
|
this.collapsed = false;
|
|
19743
20919
|
this.visible = false;
|
|
@@ -19754,7 +20930,8 @@ var DrawingToolbar = class {
|
|
|
19754
20930
|
this.dock = options.dock ?? "absolute";
|
|
19755
20931
|
this.onCollapse = options.onCollapse ?? (() => {
|
|
19756
20932
|
});
|
|
19757
|
-
|
|
20933
|
+
this.onDrawingsSync = options.onDrawingsSync ?? null;
|
|
20934
|
+
ensureStyles5();
|
|
19758
20935
|
this.root = document.createElement("div");
|
|
19759
20936
|
this.root.className = "vela-dtb";
|
|
19760
20937
|
this.styleRoot();
|
|
@@ -19852,6 +21029,10 @@ var DrawingToolbar = class {
|
|
|
19852
21029
|
this.root.appendChild(this.makeMagnetCell());
|
|
19853
21030
|
this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
|
|
19854
21031
|
this.root.appendChild(this.stayBtn);
|
|
21032
|
+
if (this.onDrawingsSync) {
|
|
21033
|
+
this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
|
|
21034
|
+
this.root.appendChild(this.syncBtn);
|
|
21035
|
+
}
|
|
19855
21036
|
this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
|
|
19856
21037
|
this.collapseBtn.classList.add("vela-dtb-collapse");
|
|
19857
21038
|
this.root.appendChild(this.collapseBtn);
|
|
@@ -19859,6 +21040,7 @@ var DrawingToolbar = class {
|
|
|
19859
21040
|
this.paintEraser();
|
|
19860
21041
|
this.paintMagnet();
|
|
19861
21042
|
this.paintStay();
|
|
21043
|
+
this.paintDrawingsSync();
|
|
19862
21044
|
this.paintCollapse();
|
|
19863
21045
|
this.highlight();
|
|
19864
21046
|
}
|
|
@@ -19909,7 +21091,7 @@ var DrawingToolbar = class {
|
|
|
19909
21091
|
cell.appendChild(arrow);
|
|
19910
21092
|
}
|
|
19911
21093
|
this.tipDisposers.push(
|
|
19912
|
-
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right"
|
|
21094
|
+
attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
|
|
19913
21095
|
);
|
|
19914
21096
|
return { cell, icon: icon2, arrow };
|
|
19915
21097
|
}
|
|
@@ -20019,6 +21201,23 @@ var DrawingToolbar = class {
|
|
|
20019
21201
|
b.dataset.active = this.stayActive ? "1" : "";
|
|
20020
21202
|
b.style.opacity = this.stayActive ? "1" : "0.5";
|
|
20021
21203
|
}
|
|
21204
|
+
/** Toggle drawings sync on/off and notify the host. */
|
|
21205
|
+
toggleDrawingsSync() {
|
|
21206
|
+
this.syncActive = !this.syncActive;
|
|
21207
|
+
this.onDrawingsSync?.(this.syncActive);
|
|
21208
|
+
this.paintDrawingsSync();
|
|
21209
|
+
}
|
|
21210
|
+
/** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
|
|
21211
|
+
setDrawingsSyncMode(on) {
|
|
21212
|
+
this.syncActive = on;
|
|
21213
|
+
this.paintDrawingsSync();
|
|
21214
|
+
}
|
|
21215
|
+
paintDrawingsSync() {
|
|
21216
|
+
const b = this.syncBtn;
|
|
21217
|
+
if (!b) return;
|
|
21218
|
+
b.dataset.active = this.syncActive ? "1" : "";
|
|
21219
|
+
b.style.opacity = this.syncActive ? "1" : "0.5";
|
|
21220
|
+
}
|
|
20022
21221
|
// ── flyout ──
|
|
20023
21222
|
/** Create the positioned flyout panel anchored to a cell, register it as open, and start the
|
|
20024
21223
|
* outside-dismiss. Callers fill it with items. */
|
|
@@ -20178,6 +21377,8 @@ var DrawingToolbar = class {
|
|
|
20178
21377
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
20179
21378
|
const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
|
|
20180
21379
|
iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
|
|
21380
|
+
const cell = this.groupCells.get(group.id);
|
|
21381
|
+
if (cell) this.tipText.set(cell, tool?.label ?? group.label);
|
|
20181
21382
|
}
|
|
20182
21383
|
/** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
|
|
20183
21384
|
* `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
|
|
@@ -20201,7 +21402,7 @@ var DrawingToolbar = class {
|
|
|
20201
21402
|
this.tipText.set(btn, title);
|
|
20202
21403
|
if (icon2) btn.innerHTML = hitHtml(icon2);
|
|
20203
21404
|
this.tipDisposers.push(
|
|
20204
|
-
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right"
|
|
21405
|
+
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
|
|
20205
21406
|
);
|
|
20206
21407
|
btn.addEventListener("click", onClick);
|
|
20207
21408
|
return btn;
|
|
@@ -20213,13 +21414,13 @@ var DrawingToolbar = class {
|
|
|
20213
21414
|
}
|
|
20214
21415
|
};
|
|
20215
21416
|
var MAGNET_ID = "__magnet__";
|
|
20216
|
-
var
|
|
20217
|
-
function
|
|
21417
|
+
var STYLE_ID5 = "vela-drawing-toolbar-styles";
|
|
21418
|
+
function ensureStyles5() {
|
|
20218
21419
|
if (typeof document === "undefined") return;
|
|
20219
|
-
let s = document.getElementById(
|
|
21420
|
+
let s = document.getElementById(STYLE_ID5);
|
|
20220
21421
|
if (!s) {
|
|
20221
21422
|
s = document.createElement("style");
|
|
20222
|
-
s.id =
|
|
21423
|
+
s.id = STYLE_ID5;
|
|
20223
21424
|
document.head.appendChild(s);
|
|
20224
21425
|
}
|
|
20225
21426
|
s.textContent = `
|
|
@@ -20267,6 +21468,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
|
|
|
20267
21468
|
var EXPAND_ICON = icon("chevrons-right");
|
|
20268
21469
|
var MAGNET_ICON = icon("magnet");
|
|
20269
21470
|
var STAY_ICON = icon("pen-lock");
|
|
21471
|
+
var SYNC_ICON = icon("pen-sync");
|
|
20270
21472
|
var ERASER_ICON = icon("eraser");
|
|
20271
21473
|
var ARROW_ICON2 = icon("chevron-right");
|
|
20272
21474
|
var CHECK_ICON = icon("check");
|
|
@@ -20490,12 +21692,19 @@ var UserDrawingController = class {
|
|
|
20490
21692
|
this.activeTool = null;
|
|
20491
21693
|
// last-used style for the armed tool (seeds the placement ghost)
|
|
20492
21694
|
this.intentCb = null;
|
|
21695
|
+
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
21696
|
+
this.externalGhost = null;
|
|
21697
|
+
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
21698
|
+
this.lastDraftKey = null;
|
|
20493
21699
|
this.measure = new MeasureOverlay();
|
|
20494
21700
|
// transient ruler — not a persistent drawing
|
|
20495
21701
|
this.toolbarVisible = false;
|
|
20496
21702
|
// mirrors showToolbar (drives the gutter reservation)
|
|
20497
21703
|
this.toolbarCollapsed = false;
|
|
20498
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;
|
|
20499
21708
|
/** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
|
|
20500
21709
|
* history (drawings + app ops in one timeline) turns this off so the chords
|
|
20501
21710
|
* bubble to its keymap instead — see the renderer's `historyChords` feature. */
|
|
@@ -20554,12 +21763,26 @@ var UserDrawingController = class {
|
|
|
20554
21763
|
}
|
|
20555
21764
|
showToolbar(visible) {
|
|
20556
21765
|
this.toolbarVisible = visible;
|
|
20557
|
-
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);
|
|
20558
21775
|
this.syncToolbarGutter();
|
|
20559
21776
|
}
|
|
20560
21777
|
/** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
|
|
20561
21778
|
syncToolbarGutter() {
|
|
20562
|
-
this.
|
|
21779
|
+
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
21780
|
+
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
21781
|
+
}
|
|
21782
|
+
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
21783
|
+
setExternalGhost(doc) {
|
|
21784
|
+
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
21785
|
+
this.render();
|
|
20563
21786
|
}
|
|
20564
21787
|
syncDrawings(docs) {
|
|
20565
21788
|
this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
|
|
@@ -20719,7 +21942,7 @@ var UserDrawingController = class {
|
|
|
20719
21942
|
}
|
|
20720
21943
|
this.interaction.down(x, y, snap, shift);
|
|
20721
21944
|
}
|
|
20722
|
-
pointerMove(x, y, snap = "off") {
|
|
21945
|
+
pointerMove(x, y, snap = "off", shift = false) {
|
|
20723
21946
|
if (this.eraserMode) {
|
|
20724
21947
|
if (this.erasing) this.deleteAt(x, y);
|
|
20725
21948
|
return;
|
|
@@ -20729,7 +21952,7 @@ var UserDrawingController = class {
|
|
|
20729
21952
|
this.render();
|
|
20730
21953
|
return;
|
|
20731
21954
|
}
|
|
20732
|
-
this.interaction.move(x, y, snap);
|
|
21955
|
+
this.interaction.move(x, y, snap, shift);
|
|
20733
21956
|
this.updateHover(x, y);
|
|
20734
21957
|
}
|
|
20735
21958
|
/** Track which drawing is hovered so its handles appear on hover (and only then). */
|
|
@@ -21115,6 +22338,8 @@ var UserDrawingController = class {
|
|
|
21115
22338
|
this.layoutTextEditor();
|
|
21116
22339
|
const ghost = this.interaction.ghost();
|
|
21117
22340
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
22341
|
+
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
22342
|
+
this.emitDraft(ghost);
|
|
21118
22343
|
const markers = this.interaction.placingMarkers(proj);
|
|
21119
22344
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
21120
22345
|
const m = this.interaction.snapMarker();
|
|
@@ -21176,6 +22401,15 @@ var UserDrawingController = class {
|
|
|
21176
22401
|
}
|
|
21177
22402
|
}, () => this.clearSelection());
|
|
21178
22403
|
}
|
|
22404
|
+
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
22405
|
+
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
22406
|
+
* shape changes emit, and the end of a placement emits one `null`. */
|
|
22407
|
+
emitDraft(ghost) {
|
|
22408
|
+
const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
|
|
22409
|
+
if (key === this.lastDraftKey) return;
|
|
22410
|
+
this.lastDraftKey = key;
|
|
22411
|
+
this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
|
|
22412
|
+
}
|
|
21179
22413
|
emit(i) {
|
|
21180
22414
|
if (i.kind === "create") {
|
|
21181
22415
|
if (i.doc.text && i.doc.text.color === void 0) {
|
|
@@ -21424,7 +22658,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
|
|
|
21424
22658
|
|
|
21425
22659
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
21426
22660
|
var ATTRIBUTION_URL = "https://luxalgo.com/vela";
|
|
21427
|
-
var
|
|
22661
|
+
var STYLE_ID6 = "vela-attribution-styles";
|
|
21428
22662
|
var CSS = `
|
|
21429
22663
|
.vela-attribution {
|
|
21430
22664
|
text-decoration: none;
|
|
@@ -21470,12 +22704,28 @@ var CSS = `
|
|
|
21470
22704
|
max-width: 240px;
|
|
21471
22705
|
opacity: 1;
|
|
21472
22706
|
transform: translateX(0);
|
|
21473
|
-
}
|
|
21474
|
-
|
|
21475
|
-
|
|
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);
|
|
21476
22726
|
if (!s) {
|
|
21477
22727
|
s = doc.createElement("style");
|
|
21478
|
-
s.id =
|
|
22728
|
+
s.id = STYLE_ID6;
|
|
21479
22729
|
doc.head.appendChild(s);
|
|
21480
22730
|
}
|
|
21481
22731
|
s.textContent = CSS;
|
|
@@ -21504,7 +22754,7 @@ function createCustomMark(doc, html, background) {
|
|
|
21504
22754
|
return el;
|
|
21505
22755
|
}
|
|
21506
22756
|
function createAttributionMark(doc, background) {
|
|
21507
|
-
|
|
22757
|
+
ensureStyles6(doc);
|
|
21508
22758
|
const a = doc.createElement("a");
|
|
21509
22759
|
a.className = "vela-attribution";
|
|
21510
22760
|
a.href = ATTRIBUTION_URL;
|
|
@@ -21530,6 +22780,174 @@ function createAttributionMark(doc, background) {
|
|
|
21530
22780
|
return a;
|
|
21531
22781
|
}
|
|
21532
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
|
+
|
|
21533
22951
|
// src/renderers/native/vpvr/profile.ts
|
|
21534
22952
|
function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
|
|
21535
22953
|
const from = Math.max(0, i0);
|
|
@@ -21698,12 +23116,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
|
|
|
21698
23116
|
var COLLAPSED_PANE_H = 26;
|
|
21699
23117
|
var TIME_AXIS_H = 22;
|
|
21700
23118
|
var SCROLL_BTN_SIZE = 26;
|
|
23119
|
+
var SCROLL_BTN_SIZE_TOUCH = 24;
|
|
21701
23120
|
var SCROLL_BTN_RADIUS = 5;
|
|
21702
23121
|
var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
|
|
21703
23122
|
var SCROLL_BTN_ICON_PX = 12;
|
|
21704
|
-
var SCROLL_BTN_RIGHT_INSET =
|
|
23123
|
+
var SCROLL_BTN_RIGHT_INSET = 12;
|
|
21705
23124
|
var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
|
|
21706
|
-
var SCROLL_BTN_BOTTOM = TIME_AXIS_H +
|
|
23125
|
+
var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
21707
23126
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
21708
23127
|
var MIN_VISIBLE_BARS = 2;
|
|
21709
23128
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
@@ -21719,10 +23138,10 @@ var SEPARATOR_HIT_PX = 4;
|
|
|
21719
23138
|
var NativeRenderer = class {
|
|
21720
23139
|
constructor(opts) {
|
|
21721
23140
|
this.capabilities = NATIVE_CAPABILITIES;
|
|
21722
|
-
// The app "chrome" surface (drawing toolbar +
|
|
23141
|
+
// The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
|
|
21723
23142
|
// Owned by the app theme alone (mount/setTheme) — config-level background/text edits
|
|
21724
|
-
// (settings dialog, persisted configs) recolor
|
|
21725
|
-
//
|
|
23143
|
+
// (settings dialog, persisted configs) recolor the plot AND the axis scales (see
|
|
23144
|
+
// axisSurface), but never the toolbar/dialog chrome.
|
|
21726
23145
|
this.surfaceBackground = DARK_THEME.background;
|
|
21727
23146
|
this.surfaceTextColor = DARK_THEME.textColor;
|
|
21728
23147
|
// the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
|
|
@@ -21755,8 +23174,12 @@ var NativeRenderer = class {
|
|
|
21755
23174
|
this.symbolPicker = null;
|
|
21756
23175
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
21757
23176
|
this.indicatorTitlesOn = true;
|
|
23177
|
+
/** Plot values beside the legend titles shown — held here so a remount re-applies it. */
|
|
23178
|
+
this.indicatorValuesOn = true;
|
|
21758
23179
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
21759
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;
|
|
21760
23183
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
21761
23184
|
this.keyboard = null;
|
|
21762
23185
|
this.keyboardEnabled = true;
|
|
@@ -21866,6 +23289,8 @@ var NativeRenderer = class {
|
|
|
21866
23289
|
// Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
|
|
21867
23290
|
// pane carries merged (own-scale) columns; the constant only clears a single-column scale.
|
|
21868
23291
|
this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
|
|
23292
|
+
// ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
|
|
23293
|
+
this.layoutMode = "desktop";
|
|
21869
23294
|
this.viewportCbs = /* @__PURE__ */ new Set();
|
|
21870
23295
|
this.crosshairCbs = /* @__PURE__ */ new Set();
|
|
21871
23296
|
this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
|
|
@@ -21876,6 +23301,7 @@ var NativeRenderer = class {
|
|
|
21876
23301
|
this.factoryConfig = null;
|
|
21877
23302
|
this.hostSettingsSections = [];
|
|
21878
23303
|
this.clickCbs = /* @__PURE__ */ new Set();
|
|
23304
|
+
this.axisLongPressCbs = /* @__PURE__ */ new Set();
|
|
21879
23305
|
this.inputChangeCbs = /* @__PURE__ */ new Set();
|
|
21880
23306
|
this.removeIndicatorCbs = /* @__PURE__ */ new Set();
|
|
21881
23307
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
@@ -21883,7 +23309,7 @@ var NativeRenderer = class {
|
|
|
21883
23309
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
21884
23310
|
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
21885
23311
|
this.name = "native";
|
|
21886
|
-
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"];
|
|
21887
23313
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
21888
23314
|
* so moving onto the button doesn't count as leaving). */
|
|
21889
23315
|
this.onScrollProximityMove = (e) => {
|
|
@@ -21922,6 +23348,7 @@ var NativeRenderer = class {
|
|
|
21922
23348
|
this.candleDown = opts.downColor;
|
|
21923
23349
|
this.scene.priceStyle = opts.priceStyle;
|
|
21924
23350
|
this.scene.basePainting = basePaintingOf(opts.priceStyle);
|
|
23351
|
+
this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
|
|
21925
23352
|
}
|
|
21926
23353
|
this.theme = this.deriveTheme(DARK_THEME);
|
|
21927
23354
|
}
|
|
@@ -22055,6 +23482,11 @@ var NativeRenderer = class {
|
|
|
22055
23482
|
this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
|
|
22056
23483
|
return;
|
|
22057
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
|
|
22058
23490
|
case "attribution":
|
|
22059
23491
|
this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
|
|
22060
23492
|
this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
|
|
@@ -22143,6 +23575,8 @@ var NativeRenderer = class {
|
|
|
22143
23575
|
return this.settingsEnabled;
|
|
22144
23576
|
case "indicatorTitles":
|
|
22145
23577
|
return this.indicatorTitlesOn;
|
|
23578
|
+
case "indicatorValues":
|
|
23579
|
+
return this.indicatorValuesOn;
|
|
22146
23580
|
case "attribution":
|
|
22147
23581
|
return this.attributionHtml ?? this.attributionEnabled;
|
|
22148
23582
|
case "dialogHost":
|
|
@@ -22399,6 +23833,7 @@ var NativeRenderer = class {
|
|
|
22399
23833
|
baselineLevel: next.baseline.baselineLevel
|
|
22400
23834
|
};
|
|
22401
23835
|
this.setPriceStyle(next.series.style);
|
|
23836
|
+
this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
|
|
22402
23837
|
this.scene.baselineValue = next.series.baseline;
|
|
22403
23838
|
this.coords.setPitchScale(next.series.spacing);
|
|
22404
23839
|
this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
|
|
@@ -22478,7 +23913,10 @@ var NativeRenderer = class {
|
|
|
22478
23913
|
/** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
|
|
22479
23914
|
* in-chart gear — created on demand, independent of the gear feature being enabled. */
|
|
22480
23915
|
openSettingsDialog(section) {
|
|
22481
|
-
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
|
+
}
|
|
22482
23920
|
this.toggleSettingsDialog(section);
|
|
22483
23921
|
}
|
|
22484
23922
|
/** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
|
|
@@ -22504,7 +23942,7 @@ var NativeRenderer = class {
|
|
|
22504
23942
|
(patch) => this.applyConfig(patch),
|
|
22505
23943
|
(json) => this.applyConfig(json),
|
|
22506
23944
|
() => {
|
|
22507
|
-
if (this.factoryConfig) this.applyConfig(this.factoryConfig);
|
|
23945
|
+
if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
|
|
22508
23946
|
this.settingsDialog?.close();
|
|
22509
23947
|
this.openSettingsDialog();
|
|
22510
23948
|
},
|
|
@@ -22567,11 +24005,18 @@ var NativeRenderer = class {
|
|
|
22567
24005
|
this.scrollButton.style.color = this.theme.textColor;
|
|
22568
24006
|
this.scrollButton.style.borderColor = this.theme.borderColor;
|
|
22569
24007
|
}
|
|
22570
|
-
/** 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. */
|
|
22571
24012
|
updateScrollToRealtimeButton() {
|
|
22572
24013
|
const btn = this.scrollButton;
|
|
22573
24014
|
if (!btn) return;
|
|
22574
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
|
+
}
|
|
22575
24020
|
btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
|
|
22576
24021
|
}
|
|
22577
24022
|
/** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
|
|
@@ -22593,17 +24038,20 @@ var NativeRenderer = class {
|
|
|
22593
24038
|
this.animator.start();
|
|
22594
24039
|
}
|
|
22595
24040
|
/**
|
|
22596
|
-
* Export the current chart as a PNG data URL by compositing
|
|
22597
|
-
*
|
|
22598
|
-
*
|
|
22599
|
-
*
|
|
22600
|
-
*
|
|
22601
|
-
*
|
|
22602
|
-
*
|
|
22603
|
-
*
|
|
22604
|
-
*
|
|
22605
|
-
*
|
|
22606
|
-
*
|
|
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.
|
|
22607
24055
|
*/
|
|
22608
24056
|
screenshot() {
|
|
22609
24057
|
if (!this.dataCanvas) return null;
|
|
@@ -22616,9 +24064,25 @@ var NativeRenderer = class {
|
|
|
22616
24064
|
if (!ctx) return null;
|
|
22617
24065
|
ctx.fillStyle = this.theme.background;
|
|
22618
24066
|
ctx.fillRect(0, 0, out.width, out.height);
|
|
22619
|
-
|
|
22620
|
-
|
|
22621
|
-
|
|
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
|
+
}
|
|
22622
24086
|
return out.toDataURL("image/png");
|
|
22623
24087
|
}
|
|
22624
24088
|
/**
|
|
@@ -22705,10 +24169,16 @@ var NativeRenderer = class {
|
|
|
22705
24169
|
borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
|
|
22706
24170
|
};
|
|
22707
24171
|
}
|
|
24172
|
+
/** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
|
|
24173
|
+
* (`layout.background`) and its contrast-corrected text, so the scales read as part of the
|
|
24174
|
+
* plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
|
|
24175
|
+
axisSurface() {
|
|
24176
|
+
return { background: this.theme.background, textColor: this.theme.textColor };
|
|
24177
|
+
}
|
|
22708
24178
|
// ── lifecycle ──
|
|
22709
24179
|
mount(container, theme) {
|
|
22710
24180
|
this.mountContainer = container;
|
|
22711
|
-
this.
|
|
24181
|
+
this.publishGutters();
|
|
22712
24182
|
this.theme = this.deriveTheme(theme);
|
|
22713
24183
|
this.surfaceBackground = theme.background;
|
|
22714
24184
|
this.surfaceTextColor = theme.textColor;
|
|
@@ -22763,6 +24233,9 @@ var NativeRenderer = class {
|
|
|
22763
24233
|
fling: (v) => this.fling(v),
|
|
22764
24234
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
22765
24235
|
onClick: (x) => this.handleClick(x),
|
|
24236
|
+
onAxisLongPress: (axis, x, y) => {
|
|
24237
|
+
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
24238
|
+
},
|
|
22766
24239
|
beginPriceScale: (x, y) => this.beginPriceScale(x, y),
|
|
22767
24240
|
priceScaleBy: (dy) => this.priceScaleBy(dy),
|
|
22768
24241
|
beginPricePan: (y) => this.beginPricePan(y),
|
|
@@ -22780,7 +24253,7 @@ var NativeRenderer = class {
|
|
|
22780
24253
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
22781
24254
|
drawingsSnapMode: () => this.snapMode,
|
|
22782
24255
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
22783
|
-
drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
|
|
24256
|
+
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
22784
24257
|
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
22785
24258
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
22786
24259
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -22809,6 +24282,14 @@ var NativeRenderer = class {
|
|
|
22809
24282
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
22810
24283
|
});
|
|
22811
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
|
+
}
|
|
22812
24293
|
this.keyboard = new KeyboardController({
|
|
22813
24294
|
panByBars: (bars) => this.panByBars(bars),
|
|
22814
24295
|
zoomByStep: (dir) => this.zoomByStep(dir),
|
|
@@ -22821,10 +24302,13 @@ var NativeRenderer = class {
|
|
|
22821
24302
|
});
|
|
22822
24303
|
this.setKeyboardEnabled(this.keyboardEnabled);
|
|
22823
24304
|
this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
|
|
24305
|
+
this.inputsUI.setLayoutMode(this.layoutMode);
|
|
22824
24306
|
this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
|
|
24307
|
+
this.inputsUI.setValuesVisible(this.indicatorValuesOn);
|
|
22825
24308
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
22826
24309
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
22827
24310
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
24311
|
+
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
22828
24312
|
this.inputsUI.setOnChange((c) => {
|
|
22829
24313
|
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
22830
24314
|
});
|
|
@@ -23017,6 +24501,7 @@ var NativeRenderer = class {
|
|
|
23017
24501
|
this.attributionEl?.remove();
|
|
23018
24502
|
this.attributionEl = null;
|
|
23019
24503
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
24504
|
+
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
23020
24505
|
this.mountContainer = null;
|
|
23021
24506
|
this.wrapper?.remove();
|
|
23022
24507
|
}
|
|
@@ -23185,6 +24670,7 @@ var NativeRenderer = class {
|
|
|
23185
24670
|
const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
|
|
23186
24671
|
if (w === this.rightAxisW) return false;
|
|
23187
24672
|
this.rightAxisW = w;
|
|
24673
|
+
this.publishGutters();
|
|
23188
24674
|
if (this.coords.width > 0) this.syncSize();
|
|
23189
24675
|
return true;
|
|
23190
24676
|
}
|
|
@@ -23237,7 +24723,10 @@ var NativeRenderer = class {
|
|
|
23237
24723
|
this.scene.indicators.set(model.id, model);
|
|
23238
24724
|
this.refreshAnchorOffset(model);
|
|
23239
24725
|
this.scene.assignIndicatorZ(model.id);
|
|
23240
|
-
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
|
+
});
|
|
23241
24730
|
this.syncTables(model);
|
|
23242
24731
|
if (model.native?.type === "volume") {
|
|
23243
24732
|
this.volumeActive = true;
|
|
@@ -23298,6 +24787,13 @@ var NativeRenderer = class {
|
|
|
23298
24787
|
this.legendActionsProvider = provider;
|
|
23299
24788
|
this.inputsUI?.setLegendActions(provider);
|
|
23300
24789
|
}
|
|
24790
|
+
setLegendOverviewAction(action) {
|
|
24791
|
+
this.legendOverviewAction = action;
|
|
24792
|
+
this.inputsUI?.setLegendOverviewAction(action);
|
|
24793
|
+
}
|
|
24794
|
+
openIndicatorSettings(indicatorId) {
|
|
24795
|
+
this.inputsUI?.openSettingsFor(indicatorId);
|
|
24796
|
+
}
|
|
23301
24797
|
/**
|
|
23302
24798
|
* Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
|
|
23303
24799
|
* series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
|
|
@@ -23344,12 +24840,31 @@ var NativeRenderer = class {
|
|
|
23344
24840
|
if (style === this.scene.priceStyle) return;
|
|
23345
24841
|
this.scene.priceStyle = style;
|
|
23346
24842
|
this.scene.basePainting = basePaintingOf(style);
|
|
24843
|
+
this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
|
|
23347
24844
|
for (const cb of this.priceStyleCbs) cb(style);
|
|
23348
24845
|
}
|
|
23349
24846
|
onRemoveIndicator(cb) {
|
|
23350
24847
|
this.removeIndicatorCbs.add(cb);
|
|
23351
24848
|
return () => this.removeIndicatorCbs.delete(cb);
|
|
23352
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
|
+
}
|
|
23353
24868
|
setSettingsSections(sections) {
|
|
23354
24869
|
this.hostSettingsSections = sections;
|
|
23355
24870
|
this.settingsDialog?.setHostSections(sections);
|
|
@@ -23374,6 +24889,10 @@ var NativeRenderer = class {
|
|
|
23374
24889
|
this.clickCbs.add(cb);
|
|
23375
24890
|
return () => this.clickCbs.delete(cb);
|
|
23376
24891
|
}
|
|
24892
|
+
onAxisLongPress(cb) {
|
|
24893
|
+
this.axisLongPressCbs.add(cb);
|
|
24894
|
+
return () => this.axisLongPressCbs.delete(cb);
|
|
24895
|
+
}
|
|
23377
24896
|
onViewportChange(cb) {
|
|
23378
24897
|
this.viewportCbs.add(cb);
|
|
23379
24898
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -23505,13 +25024,17 @@ var NativeRenderer = class {
|
|
|
23505
25024
|
active = true;
|
|
23506
25025
|
}
|
|
23507
25026
|
}
|
|
23508
|
-
this.
|
|
25027
|
+
const clamped = this.clampViewport(barSpacing, rightOffset);
|
|
25028
|
+
if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
|
|
25029
|
+
if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
|
|
25030
|
+
this.coords.setViewport(clamped);
|
|
23509
25031
|
this.computeScales();
|
|
23510
25032
|
if (this.easeScales(dtMs)) active = true;
|
|
23511
25033
|
if (this.easeLiveBar(dtMs)) active = true;
|
|
23512
25034
|
this.skeletonClockMs += dtMs;
|
|
23513
25035
|
this.paintData();
|
|
23514
25036
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
25037
|
+
this.updateLegendValues();
|
|
23515
25038
|
this.emitViewportChange();
|
|
23516
25039
|
return active;
|
|
23517
25040
|
}
|
|
@@ -23576,7 +25099,7 @@ var NativeRenderer = class {
|
|
|
23576
25099
|
this.lastPointer = null;
|
|
23577
25100
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
23578
25101
|
this.hoverLogical = null;
|
|
23579
|
-
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 };
|
|
23580
25103
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
23581
25104
|
return;
|
|
23582
25105
|
}
|
|
@@ -23588,7 +25111,7 @@ var NativeRenderer = class {
|
|
|
23588
25111
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
23589
25112
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
23590
25113
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
23591
|
-
const pane = this.
|
|
25114
|
+
const pane = this.paneNodeAtY(y);
|
|
23592
25115
|
const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
|
|
23593
25116
|
const values = /* @__PURE__ */ new Map();
|
|
23594
25117
|
if (onBar) {
|
|
@@ -23605,7 +25128,7 @@ var NativeRenderer = class {
|
|
|
23605
25128
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
23606
25129
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
23607
25130
|
this.hoverLogical = onBar ? logical : null;
|
|
23608
|
-
const event = { time, price, values, ohlc };
|
|
25131
|
+
const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
|
|
23609
25132
|
for (const cb of this.crosshairCbs) cb(event);
|
|
23610
25133
|
}
|
|
23611
25134
|
handleClick(x) {
|
|
@@ -23624,6 +25147,20 @@ var NativeRenderer = class {
|
|
|
23624
25147
|
}
|
|
23625
25148
|
return null;
|
|
23626
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
|
+
}
|
|
23627
25164
|
// ── manual vertical scaling (price-axis drag + vertical price pan) ──
|
|
23628
25165
|
/** The scale window a gesture targets: a pane's master scale, or a merged indicator's
|
|
23629
25166
|
* own scale column (resolved from the grabbed x). Both share the four scale fields. */
|
|
@@ -23686,7 +25223,7 @@ var NativeRenderer = class {
|
|
|
23686
25223
|
* still fits the view.)
|
|
23687
25224
|
*/
|
|
23688
25225
|
dataDblClick(_x, y) {
|
|
23689
|
-
const pane = this.paneNodeAtY(y);
|
|
25226
|
+
const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
|
|
23690
25227
|
if (!pane) return;
|
|
23691
25228
|
const maximized = this.maximizedPaneId !== pane.id;
|
|
23692
25229
|
this.maximizedPaneId = maximized ? pane.id : null;
|
|
@@ -23927,29 +25464,63 @@ var NativeRenderer = class {
|
|
|
23927
25464
|
dataWindowGroups(idx, pricePane) {
|
|
23928
25465
|
const groups = [];
|
|
23929
25466
|
for (const model of this.scene.indicators.values()) {
|
|
23930
|
-
const
|
|
23931
|
-
const off = this.scene.offsetOf(model.id);
|
|
23932
|
-
const rows = [];
|
|
23933
|
-
for (const s of model.series) {
|
|
23934
|
-
let value;
|
|
23935
|
-
let color;
|
|
23936
|
-
if (s.kind === "candle" || s.kind === "bar") {
|
|
23937
|
-
value = s.bars[idx - off]?.close;
|
|
23938
|
-
color = s.style?.up ?? this.theme.upColor;
|
|
23939
|
-
} else if (isLineLikeSeries(s)) {
|
|
23940
|
-
if (s.visible === false) continue;
|
|
23941
|
-
value = s.points[idx - off]?.value;
|
|
23942
|
-
color = s.points[idx - off]?.color ?? s.style.color;
|
|
23943
|
-
} else {
|
|
23944
|
-
continue;
|
|
23945
|
-
}
|
|
23946
|
-
if (value == null || !Number.isFinite(value)) continue;
|
|
23947
|
-
rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
|
|
23948
|
-
}
|
|
25467
|
+
const rows = this.dataWindowRowsFor(model, idx, pricePane);
|
|
23949
25468
|
if (rows.length) groups.push({ name: model.title, rows });
|
|
23950
25469
|
}
|
|
23951
25470
|
return groups;
|
|
23952
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
|
+
}
|
|
23953
25524
|
renderFrame(level) {
|
|
23954
25525
|
if (this.animator.active) return;
|
|
23955
25526
|
if (repaintsData(level)) {
|
|
@@ -23957,9 +25528,38 @@ var NativeRenderer = class {
|
|
|
23957
25528
|
this.paintData();
|
|
23958
25529
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
23959
25530
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
23960
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
25531
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
23961
25532
|
}
|
|
23962
25533
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
25534
|
+
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
25535
|
+
this.updateLegendValues();
|
|
25536
|
+
}
|
|
25537
|
+
/** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
|
|
25538
|
+
repaintCursorLayers() {
|
|
25539
|
+
const pane = this.scene.panes.get(PRICE_PANE_ID);
|
|
25540
|
+
if (!pane) return;
|
|
25541
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
25542
|
+
for (const l of this.extLayers) {
|
|
25543
|
+
if (!l.def.repaintOnCursor) continue;
|
|
25544
|
+
l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
|
|
25545
|
+
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
25546
|
+
}
|
|
25547
|
+
}
|
|
25548
|
+
/** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
|
|
25549
|
+
extLayerArgs(id, scale, bounds, nowMs) {
|
|
25550
|
+
return {
|
|
25551
|
+
bars: this.scene.bars,
|
|
25552
|
+
data: this.scene.nativeData.get(id),
|
|
25553
|
+
settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
|
|
25554
|
+
pending: this.scene.nativePending.get(id) ?? [],
|
|
25555
|
+
coords: this.coords,
|
|
25556
|
+
scale,
|
|
25557
|
+
bounds,
|
|
25558
|
+
theme: this.theme,
|
|
25559
|
+
priceStyle: this.scene.priceStyle,
|
|
25560
|
+
nowMs,
|
|
25561
|
+
cursor: this.lastPointer
|
|
25562
|
+
};
|
|
23963
25563
|
}
|
|
23964
25564
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
23965
25565
|
paintData() {
|
|
@@ -23992,18 +25592,16 @@ var NativeRenderer = class {
|
|
|
23992
25592
|
});
|
|
23993
25593
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
23994
25594
|
for (const l of this.extLayers) {
|
|
23995
|
-
l.
|
|
23996
|
-
|
|
23997
|
-
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
nowMs
|
|
24006
|
-
});
|
|
25595
|
+
const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
|
|
25596
|
+
l.instance.render(args);
|
|
25597
|
+
if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
|
|
25598
|
+
const mod = l.instance.modulateBase(args);
|
|
25599
|
+
if (mod) {
|
|
25600
|
+
if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
|
|
25601
|
+
if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
|
|
25602
|
+
if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
|
|
25603
|
+
}
|
|
25604
|
+
}
|
|
24007
25605
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
24008
25606
|
}
|
|
24009
25607
|
}
|
|
@@ -24013,7 +25611,7 @@ var NativeRenderer = class {
|
|
|
24013
25611
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
24014
25612
|
this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
|
|
24015
25613
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
24016
|
-
this.chrome.render(this.scene, this.coords, this.theme,
|
|
25614
|
+
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
24017
25615
|
this.userDrawings?.render();
|
|
24018
25616
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
24019
25617
|
this.paintedData = true;
|
|
@@ -24083,7 +25681,7 @@ var NativeRenderer = class {
|
|
|
24083
25681
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
24084
25682
|
}
|
|
24085
25683
|
}
|
|
24086
|
-
return { x, y, time: this.coords.logicalToTime(logical) };
|
|
25684
|
+
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
24087
25685
|
}
|
|
24088
25686
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
24089
25687
|
setSnapMode(mode) {
|
|
@@ -24447,15 +26045,18 @@ var NativeRenderer = class {
|
|
|
24447
26045
|
setToolbarGutter(px) {
|
|
24448
26046
|
if (px === this.toolbarGutter) return;
|
|
24449
26047
|
this.toolbarGutter = px;
|
|
24450
|
-
this.
|
|
26048
|
+
this.publishGutters();
|
|
24451
26049
|
this.positionAttribution();
|
|
24452
26050
|
this.syncSize();
|
|
24453
26051
|
}
|
|
24454
|
-
/** Publish the
|
|
24455
|
-
*
|
|
24456
|
-
*
|
|
24457
|
-
|
|
26052
|
+
/** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
|
|
26053
|
+
* drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
|
|
26054
|
+
* incl. merged own-scale columns), so host overlays sharing that container (a
|
|
26055
|
+
* status line, a watermark, a custom legend) can anchor to the plot's edges
|
|
26056
|
+
* without reaching into the renderer's DOM. */
|
|
26057
|
+
publishGutters() {
|
|
24458
26058
|
this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
|
|
26059
|
+
this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
|
|
24459
26060
|
}
|
|
24460
26061
|
/** The built-in mark, or the host's own when one is set. */
|
|
24461
26062
|
buildAttributionEl() {
|
|
@@ -24525,7 +26126,13 @@ var NativeRenderer = class {
|
|
|
24525
26126
|
this.fitContent();
|
|
24526
26127
|
this.didInitialFit = true;
|
|
24527
26128
|
}
|
|
24528
|
-
this.
|
|
26129
|
+
if (this.animator.active) {
|
|
26130
|
+
this.computeScales();
|
|
26131
|
+
this.paintData();
|
|
26132
|
+
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
26133
|
+
} else {
|
|
26134
|
+
this.scheduler.flushNow(4 /* Full */);
|
|
26135
|
+
}
|
|
24529
26136
|
}
|
|
24530
26137
|
applyBackground() {
|
|
24531
26138
|
this.wrapper.style.background = this.theme.background;
|
|
@@ -24594,6 +26201,9 @@ function sanitizeHighlights(value) {
|
|
|
24594
26201
|
}
|
|
24595
26202
|
return out.sort((a, b) => a.from - b.from);
|
|
24596
26203
|
}
|
|
26204
|
+
function clamp012(v) {
|
|
26205
|
+
return Math.max(0, Math.min(1, v));
|
|
26206
|
+
}
|
|
24597
26207
|
function isPriceStyle2(v) {
|
|
24598
26208
|
return typeof v === "string" && priceStyleIds().includes(v);
|
|
24599
26209
|
}
|
|
@@ -24763,7 +26373,8 @@ var VpvrIndicator = class {
|
|
|
24763
26373
|
};
|
|
24764
26374
|
var vpvrDescriptor = {
|
|
24765
26375
|
type: "vpvr",
|
|
24766
|
-
title: "
|
|
26376
|
+
title: "Visible Range Volume Profile",
|
|
26377
|
+
shortTitle: "VRVP",
|
|
24767
26378
|
paneHint: "price",
|
|
24768
26379
|
overlay: true,
|
|
24769
26380
|
inputsSchema: () => [
|
|
@@ -25261,6 +26872,7 @@ exports.iconMarkup = iconMarkup;
|
|
|
25261
26872
|
exports.legendActions = legendActions;
|
|
25262
26873
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
25263
26874
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
26875
|
+
exports.normalizeSettingsRow = normalizeSettingsRow;
|
|
25264
26876
|
exports.registerChartType = registerChartType;
|
|
25265
26877
|
exports.registerDefaultEngine = registerDefaultEngine;
|
|
25266
26878
|
exports.registerDrawingType = registerDrawingType;
|
|
@@ -25276,6 +26888,7 @@ exports.rendererDefaults = rendererDefaults;
|
|
|
25276
26888
|
exports.rendererLayers = rendererLayers;
|
|
25277
26889
|
exports.resolveEngines = resolveEngines;
|
|
25278
26890
|
exports.resolveTheme = resolveTheme;
|
|
26891
|
+
exports.settingsRowValueKeys = settingsRowValueKeys;
|
|
25279
26892
|
exports.sharedBarStore = sharedBarStore;
|
|
25280
26893
|
exports.sidePanels = sidePanels;
|
|
25281
26894
|
exports.stableSeriesId = stableSeriesId;
|