@luxalgo/vela 0.8.3 → 0.8.4
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-wbtJ3-Sa.d.cts → DataProvider-Bw58CjYy.d.cts} +1 -1
- package/dist/{DataProvider-oH78tYXL.d.ts → DataProvider-Byo8pEv0.d.ts} +1 -1
- package/dist/{chunk-KWI3YDBW.js → chunk-ALTGHKIM.js} +584 -25
- package/dist/{chunk-TO3GIK7Z.js → chunk-AUOB7A5Y.js} +2 -2
- package/dist/{chunk-VHGACEHO.js → chunk-I2U4NZD2.js} +7 -1
- package/dist/{chunk-2IPXYM4B.js → chunk-MOJ4T26N.js} +88 -5
- package/dist/{chunk-HMHA733L.js → chunk-UNUVPQFW.js} +1924 -734
- package/dist/{chunk-G7B7ZCBF.js → chunk-WY5H4S4I.js} +1 -1
- package/dist/{contributions-BhOBRenr.d.cts → contributions-BY3zSYiZ.d.ts} +56 -5
- package/dist/{contributions-HGqnNb_P.d.ts → contributions-DOm40g5-.d.cts} +56 -5
- package/dist/index.cjs +2583 -828
- package/dist/index.d.cts +9 -15
- package/dist/index.d.ts +9 -15
- package/dist/index.js +4 -4
- package/dist/{options-lMUwmRwh.d.cts → options-CSXbjMM5.d.cts} +64 -4
- package/dist/{options-lMUwmRwh.d.ts → options-CSXbjMM5.d.ts} +64 -4
- package/dist/{plugin-Cr2Pj8q8.d.ts → plugin-BDxUhGO3.d.ts} +3 -3
- package/dist/{plugin-Do5J5xuF.d.cts → plugin-CiQVyETg.d.cts} +3 -3
- package/dist/plugin.cjs +555 -17
- 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/{statusline-model-B-2xGlB_.d.cts → statusline-model-C9tYXuux.d.ts} +19 -4
- package/dist/{statusline-model-CY1wzxHg.d.ts → statusline-model-DUBX6Fib.d.cts} +19 -4
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +2590 -835
- package/dist/vela.global.min.js +124 -54
- package/dist/widget.cjs +2807 -969
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +2807 -969
- package/dist/workspace.d.cts +19 -5
- package/dist/workspace.d.ts +19 -5
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, normalizeTimezone, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, TIMEZONES, tzMenuLabel, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection,
|
|
2
|
-
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, DatePicker, eventDismissedPopover, toggleSelectList, normalizeDateInput, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-
|
|
3
|
-
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, announceSurface, holdForExit, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-
|
|
1
|
+
import { sanitizeShowOn, deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, normalizeTimezone, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, shownOnTimeframe, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, TIMEZONES, tzMenuLabel, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PriceActionZone, Liquidity, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, TrendLine, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, magnifierTimeframesBelow, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, defaultText, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, FibRetracement, FIB_PRESETS, matchFibPreset, FibLevels, timeframeBandOf, TIMEFRAME_BANDS, stableSeriesId, DIRECTION_OPTIONS } from './chunk-ALTGHKIM.js';
|
|
2
|
+
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, DatePicker, eventDismissedPopover, toggleSelectList, normalizeDateInput, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-WY5H4S4I.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, announceSurface, holdForExit, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, toHex6, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-I2U4NZD2.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -57,6 +57,7 @@ var FIBONACCI_TYPES = [
|
|
|
57
57
|
"fibwedge",
|
|
58
58
|
"fibspiral"
|
|
59
59
|
];
|
|
60
|
+
var PRICE_ACTION_TYPES = ["fairvaluegap", "orderblock", "liquidity"];
|
|
60
61
|
var GANN_TYPES = ["gannfan", "gannbox", "gannsquare"];
|
|
61
62
|
var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldensupersonic"];
|
|
62
63
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
@@ -84,6 +85,7 @@ var TOOLBAR_LAYOUT = [
|
|
|
84
85
|
label: "Fibonacci",
|
|
85
86
|
sections: [
|
|
86
87
|
{ label: "Fibonacci", types: FIBONACCI_TYPES },
|
|
88
|
+
{ label: "Price Action", types: PRICE_ACTION_TYPES },
|
|
87
89
|
{ label: "Gann", types: GANN_TYPES },
|
|
88
90
|
{ label: "Geometry", types: GEOMETRY_TYPES }
|
|
89
91
|
]
|
|
@@ -261,6 +263,7 @@ var DrawingStore = class {
|
|
|
261
263
|
if (patch.visible !== void 0) d.visible = patch.visible;
|
|
262
264
|
if (patch.zIndex !== void 0) d.zIndex = patch.zIndex;
|
|
263
265
|
if (patch.props !== void 0) d.applyProps(patch.props);
|
|
266
|
+
if ("showOn" in patch) d.showOn = sanitizeShowOn(patch.showOn);
|
|
264
267
|
this.emit();
|
|
265
268
|
return d;
|
|
266
269
|
}
|
|
@@ -1151,38 +1154,11 @@ function attachForming(closed, forming) {
|
|
|
1151
1154
|
}
|
|
1152
1155
|
|
|
1153
1156
|
// src/data/MultiProviderFeed.ts
|
|
1154
|
-
function intervalMs(timeframe) {
|
|
1155
|
-
const map2 = {
|
|
1156
|
-
"1m": 6e4,
|
|
1157
|
-
"3m": 18e4,
|
|
1158
|
-
"5m": 3e5,
|
|
1159
|
-
"15m": 9e5,
|
|
1160
|
-
"30m": 18e5,
|
|
1161
|
-
"1h": 36e5,
|
|
1162
|
-
"2h": 72e5,
|
|
1163
|
-
"4h": 144e5,
|
|
1164
|
-
"1d": 864e5,
|
|
1165
|
-
"1w": 6048e5,
|
|
1166
|
-
"1M": 2592e6,
|
|
1167
|
-
"1": 6e4,
|
|
1168
|
-
"5": 3e5,
|
|
1169
|
-
"15": 9e5,
|
|
1170
|
-
"30": 18e5,
|
|
1171
|
-
"60": 36e5,
|
|
1172
|
-
"240": 144e5,
|
|
1173
|
-
D: 864e5,
|
|
1174
|
-
W: 6048e5,
|
|
1175
|
-
M: 2592e6
|
|
1176
|
-
};
|
|
1177
|
-
return map2[timeframe] ?? 36e5;
|
|
1178
|
-
}
|
|
1179
1157
|
var MultiProviderFeed = class {
|
|
1180
1158
|
constructor(store = sharedBarStore) {
|
|
1181
1159
|
this.registry = new ProviderRegistry();
|
|
1182
1160
|
/** The chart symbol's resolved provider — the default for bare secondary symbols. */
|
|
1183
1161
|
this.primaryProvider = null;
|
|
1184
|
-
/** A copy of offline history, used to synthesize ticks for the `data` path. */
|
|
1185
|
-
this.liveBars = [];
|
|
1186
1162
|
/** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
|
|
1187
1163
|
this.symInfoCache = /* @__PURE__ */ new Map();
|
|
1188
1164
|
/** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
|
|
@@ -1292,10 +1268,7 @@ var MultiProviderFeed = class {
|
|
|
1292
1268
|
}
|
|
1293
1269
|
// ── MarketDataFeed ─────────────────────────────────────────────────────
|
|
1294
1270
|
async load(cfg) {
|
|
1295
|
-
if (cfg.data && cfg.data.length > 0)
|
|
1296
|
-
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
1297
|
-
return cfg.data;
|
|
1298
|
-
}
|
|
1271
|
+
if (cfg.data && cfg.data.length > 0) return cfg.data;
|
|
1299
1272
|
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
1300
1273
|
this.primaryProvider = resolved.provider;
|
|
1301
1274
|
this.prefetchSymbolInfo(resolved);
|
|
@@ -1303,10 +1276,7 @@ var MultiProviderFeed = class {
|
|
|
1303
1276
|
}
|
|
1304
1277
|
/** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
|
|
1305
1278
|
async loadProgressive(cfg, onBatch, opts) {
|
|
1306
|
-
if (cfg.data && cfg.data.length > 0)
|
|
1307
|
-
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
1308
|
-
return cfg.data;
|
|
1309
|
-
}
|
|
1279
|
+
if (cfg.data && cfg.data.length > 0) return cfg.data;
|
|
1310
1280
|
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
1311
1281
|
this.primaryProvider = resolved.provider;
|
|
1312
1282
|
this.prefetchSymbolInfo(resolved);
|
|
@@ -1332,39 +1302,16 @@ var MultiProviderFeed = class {
|
|
|
1332
1302
|
if (!resolved) return [];
|
|
1333
1303
|
return this.cache.loadRange(canonical(cfg, resolved), range);
|
|
1334
1304
|
}
|
|
1305
|
+
/** Offline `data` has no source of ticks: the caller's bars are shown exactly as given,
|
|
1306
|
+
* and live updates need a registered provider that implements `subscribe`. */
|
|
1335
1307
|
subscribe(cfg, onBar) {
|
|
1336
|
-
if (cfg.data && cfg.data.length > 0) return
|
|
1308
|
+
if (cfg.data && cfg.data.length > 0) return () => {
|
|
1309
|
+
};
|
|
1337
1310
|
const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
|
|
1338
1311
|
if (!resolved) return () => {
|
|
1339
1312
|
};
|
|
1340
1313
|
return this.cache.subscribe(canonical(cfg, resolved), onBar);
|
|
1341
1314
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Synthesize price-relative ticks for offline `data` runs (no network), so the
|
|
1344
|
-
* forming-candle path is exercised at any price scale. Mirrors the bundled
|
|
1345
|
-
* provider feed's offline behavior.
|
|
1346
|
-
*/
|
|
1347
|
-
synthesizeTicks(cfg, onBar) {
|
|
1348
|
-
if (this.liveBars.length === 0) return () => {
|
|
1349
|
-
};
|
|
1350
|
-
const step = intervalMs(cfg.timeframe ?? "60");
|
|
1351
|
-
const timer = setInterval(() => {
|
|
1352
|
-
const bars = this.liveBars;
|
|
1353
|
-
const forming = bars[bars.length - 1];
|
|
1354
|
-
if (!forming) return;
|
|
1355
|
-
const jitter = forming.close * 15e-4 * (Math.random() - 0.5) * 2;
|
|
1356
|
-
const close = Math.max(0, forming.close + jitter);
|
|
1357
|
-
const updated = { ...forming, close, high: Math.max(forming.high, close), low: Math.min(forming.low, close) };
|
|
1358
|
-
bars[bars.length - 1] = updated;
|
|
1359
|
-
onBar(updated);
|
|
1360
|
-
if (Math.random() < 0.12) {
|
|
1361
|
-
const next = { time: updated.time + step, open: updated.close, high: updated.close, low: updated.close, close: updated.close, volume: 0 };
|
|
1362
|
-
bars.push(next);
|
|
1363
|
-
onBar(next);
|
|
1364
|
-
}
|
|
1365
|
-
}, 1e3);
|
|
1366
|
-
return () => clearInterval(timer);
|
|
1367
|
-
}
|
|
1368
1315
|
};
|
|
1369
1316
|
function rawSymbol(cfg) {
|
|
1370
1317
|
return cfg.symbol ?? "";
|
|
@@ -1716,6 +1663,34 @@ var DrawingsControl = class {
|
|
|
1716
1663
|
if (this.ok("setFavorites")) this.ctrl.setFavorites([...types]);
|
|
1717
1664
|
return this;
|
|
1718
1665
|
}
|
|
1666
|
+
/** Every tool's remembered settings, by type — what the next drawing of each type starts
|
|
1667
|
+
* from (its style, text styling and per-type settings such as fib levels). Plain JSON:
|
|
1668
|
+
* persist it and hand it back to {@link setToolDefaults} on the next load. */
|
|
1669
|
+
toolDefaults() {
|
|
1670
|
+
return this.ctrl.toolDefaults();
|
|
1671
|
+
}
|
|
1672
|
+
/** Replace every tool's remembered settings (e.g. restoring persisted prefs). Malformed
|
|
1673
|
+
* entries and unknown types are dropped. Follow changes on `drawing:defaults`. */
|
|
1674
|
+
setToolDefaults(map2) {
|
|
1675
|
+
this.ctrl.setToolDefaults(map2);
|
|
1676
|
+
return this;
|
|
1677
|
+
}
|
|
1678
|
+
/** Forget one tool's remembered settings (every tool's without an argument). */
|
|
1679
|
+
resetToolDefaults(type) {
|
|
1680
|
+
this.ctrl.resetToolDefaults(type);
|
|
1681
|
+
return this;
|
|
1682
|
+
}
|
|
1683
|
+
/** Every tool's saved looks ("styles" in the settings panel), by type. Plain JSON —
|
|
1684
|
+
* persist it and hand it back to {@link setToolTemplates}. */
|
|
1685
|
+
toolTemplates() {
|
|
1686
|
+
return this.ctrl.toolTemplates();
|
|
1687
|
+
}
|
|
1688
|
+
/** Replace every tool's saved looks (e.g. restoring persisted prefs). Follow changes on
|
|
1689
|
+
* `drawing:templates`. */
|
|
1690
|
+
setToolTemplates(map2) {
|
|
1691
|
+
this.ctrl.setToolTemplates(map2);
|
|
1692
|
+
return this;
|
|
1693
|
+
}
|
|
1719
1694
|
/** Aliases mirroring `renderer.getConfig()/applyConfig()` for symmetry. */
|
|
1720
1695
|
getConfig() {
|
|
1721
1696
|
return this.ctrl.toJSON();
|
|
@@ -1812,8 +1787,8 @@ var ReplayControl = class {
|
|
|
1812
1787
|
}
|
|
1813
1788
|
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
1814
1789
|
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
1815
|
-
play(
|
|
1816
|
-
this.ctrl.replayPlay(
|
|
1790
|
+
play(intervalMs) {
|
|
1791
|
+
this.ctrl.replayPlay(intervalMs);
|
|
1817
1792
|
return this;
|
|
1818
1793
|
}
|
|
1819
1794
|
pause() {
|
|
@@ -1885,6 +1860,74 @@ var SecondClock = class _SecondClock {
|
|
|
1885
1860
|
}
|
|
1886
1861
|
};
|
|
1887
1862
|
|
|
1863
|
+
// src/core/drawings/defaults.ts
|
|
1864
|
+
function captureToolDefaults(d) {
|
|
1865
|
+
const out = { style: { ...d.style } };
|
|
1866
|
+
if (d.text) {
|
|
1867
|
+
const { value: _value, ...styling } = d.text;
|
|
1868
|
+
out.text = styling;
|
|
1869
|
+
}
|
|
1870
|
+
const props = d.serialize().props;
|
|
1871
|
+
if (props !== void 0) out.props = clonePlain(props);
|
|
1872
|
+
return out;
|
|
1873
|
+
}
|
|
1874
|
+
function applyToolDefaults(d, defaults) {
|
|
1875
|
+
if (!defaults) return;
|
|
1876
|
+
if (defaults.style) d.style = { ...d.style, ...defaults.style };
|
|
1877
|
+
if (defaults.text) d.text = { ...d.text ?? defaultText(), ...defaults.text, value: d.text?.value ?? "" };
|
|
1878
|
+
if (defaults.props) d.applyProps(clonePlain(defaults.props));
|
|
1879
|
+
}
|
|
1880
|
+
function sameToolDefaults(a, b) {
|
|
1881
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
1882
|
+
}
|
|
1883
|
+
function sanitizeToolDefaults(raw) {
|
|
1884
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1885
|
+
const r = raw;
|
|
1886
|
+
const obj = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
1887
|
+
const out = {};
|
|
1888
|
+
const style = obj(r.style);
|
|
1889
|
+
if (style) out.style = style;
|
|
1890
|
+
const text = obj(r.text);
|
|
1891
|
+
if (text) {
|
|
1892
|
+
const { value: _value, ...styling } = text;
|
|
1893
|
+
out.text = styling;
|
|
1894
|
+
}
|
|
1895
|
+
const props = obj(r.props);
|
|
1896
|
+
if (props) out.props = props;
|
|
1897
|
+
return out.style || out.text || out.props ? out : null;
|
|
1898
|
+
}
|
|
1899
|
+
var TEMPLATE_NAME_MAX = 60;
|
|
1900
|
+
function sanitizeToolTemplates(raw) {
|
|
1901
|
+
if (!Array.isArray(raw)) return [];
|
|
1902
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1903
|
+
for (const entry of raw) {
|
|
1904
|
+
if (entry == null || typeof entry !== "object") continue;
|
|
1905
|
+
const e = entry;
|
|
1906
|
+
const name = typeof e.name === "string" ? e.name.trim().slice(0, TEMPLATE_NAME_MAX) : "";
|
|
1907
|
+
const settings = sanitizeToolDefaults(e.settings);
|
|
1908
|
+
if (name && settings) byName.set(name, { name, settings });
|
|
1909
|
+
}
|
|
1910
|
+
return [...byName.values()];
|
|
1911
|
+
}
|
|
1912
|
+
var LINE_TOOLS = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "hline", "hray", "vline", "crossline", "infoline", "trendangle"]);
|
|
1913
|
+
var AREA_TOOLS = /* @__PURE__ */ new Set(["box", "rotatedrect", "ellipse", "circle", "triangle", "parallelchannel", "disjointchannel", "flattopbottom"]);
|
|
1914
|
+
function builtinToolTemplates(type) {
|
|
1915
|
+
if (LINE_TOOLS.has(type)) {
|
|
1916
|
+
return [
|
|
1917
|
+
{ name: "Support", settings: { style: { lineColor: BULLISH, lineWidth: 2, lineStyle: "solid" } } },
|
|
1918
|
+
{ name: "Resistance", settings: { style: { lineColor: BEARISH, lineWidth: 2, lineStyle: "solid" } } },
|
|
1919
|
+
{ name: "Projection", settings: { style: { lineColor: NEUTRAL, lineWidth: 1, lineStyle: "dashed" } } }
|
|
1920
|
+
];
|
|
1921
|
+
}
|
|
1922
|
+
if (AREA_TOOLS.has(type)) {
|
|
1923
|
+
return [
|
|
1924
|
+
{ name: "Demand", settings: { style: { lineColor: BULLISH, fillColor: `${BULLISH}26` } } },
|
|
1925
|
+
{ name: "Supply", settings: { style: { lineColor: BEARISH, fillColor: `${BEARISH}26` } } }
|
|
1926
|
+
];
|
|
1927
|
+
}
|
|
1928
|
+
return [];
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1888
1931
|
// src/core/drawings/effectiveColor.ts
|
|
1889
1932
|
var VALID_FILL = VALID;
|
|
1890
1933
|
var INVALID_FILL = INVALID;
|
|
@@ -1982,6 +2025,10 @@ var DrawingHistory = class {
|
|
|
1982
2025
|
};
|
|
1983
2026
|
|
|
1984
2027
|
// src/core/drawings/DrawingController.ts
|
|
2028
|
+
function factoryDefaults(d) {
|
|
2029
|
+
const fresh = createDrawing(d.type, { paneId: d.paneId });
|
|
2030
|
+
return fresh ? captureToolDefaults(fresh) : void 0;
|
|
2031
|
+
}
|
|
1985
2032
|
var DrawingController = class {
|
|
1986
2033
|
constructor(renderer, events, option, seriesGateway) {
|
|
1987
2034
|
this.events = events;
|
|
@@ -2000,8 +2047,10 @@ var DrawingController = class {
|
|
|
2000
2047
|
// ordered; [0] is the primary (settings-popup) selection
|
|
2001
2048
|
this.clipboard = [];
|
|
2002
2049
|
// in-memory copy buffer (per chart)
|
|
2003
|
-
|
|
2004
|
-
|
|
2050
|
+
/** Per-tool remembered settings — the last drawing of each type the user styled seeds the next. */
|
|
2051
|
+
this.defaults = /* @__PURE__ */ new Map();
|
|
2052
|
+
/** Per-tool saved looks ("styles"), in the order they were saved. */
|
|
2053
|
+
this.templates = /* @__PURE__ */ new Map();
|
|
2005
2054
|
this.subs = [];
|
|
2006
2055
|
this.enabled = !!renderer.capabilities.userDrawings && !!renderer.userDrawingsPort;
|
|
2007
2056
|
this.port = this.enabled ? renderer.userDrawingsPort : null;
|
|
@@ -2023,7 +2072,8 @@ var DrawingController = class {
|
|
|
2023
2072
|
if (!this.port) return;
|
|
2024
2073
|
const changed = type !== this.activeTool;
|
|
2025
2074
|
this.activeTool = type;
|
|
2026
|
-
|
|
2075
|
+
const defaults = type ? this.defaults.get(type) : void 0;
|
|
2076
|
+
this.port.setActiveTool(type, defaults?.style, defaults);
|
|
2027
2077
|
if (changed) this.events.emit("drawing:tool", { type });
|
|
2028
2078
|
}
|
|
2029
2079
|
getTool() {
|
|
@@ -2091,6 +2141,84 @@ var DrawingController = class {
|
|
|
2091
2141
|
this.port?.setFavorites?.(list);
|
|
2092
2142
|
this.events.emit("drawing:favorites", { favorites: list });
|
|
2093
2143
|
}
|
|
2144
|
+
/** Every tool's remembered settings, by type (plain JSON — persist it as-is). */
|
|
2145
|
+
toolDefaults() {
|
|
2146
|
+
const out = {};
|
|
2147
|
+
for (const [type, d] of this.defaults) out[type] = clonePlain(d);
|
|
2148
|
+
return out;
|
|
2149
|
+
}
|
|
2150
|
+
/** Replace every tool's remembered settings (restoring persisted prefs). Unknown types and
|
|
2151
|
+
* malformed entries are dropped; an unchanged set is a no-op and emits nothing. */
|
|
2152
|
+
setToolDefaults(map2) {
|
|
2153
|
+
const next = /* @__PURE__ */ new Map();
|
|
2154
|
+
for (const [type, raw] of Object.entries(map2 ?? {})) {
|
|
2155
|
+
if (!getDrawingType(type)) continue;
|
|
2156
|
+
const d = sanitizeToolDefaults(raw);
|
|
2157
|
+
if (d) next.set(type, clonePlain(d));
|
|
2158
|
+
}
|
|
2159
|
+
const changed = /* @__PURE__ */ new Set([...this.defaults.keys(), ...next.keys()]);
|
|
2160
|
+
for (const type of [...changed]) if (sameToolDefaults(this.defaults.get(type), next.get(type))) changed.delete(type);
|
|
2161
|
+
if (changed.size === 0) return;
|
|
2162
|
+
this.defaults.clear();
|
|
2163
|
+
for (const [type, d] of next) this.defaults.set(type, d);
|
|
2164
|
+
if (this.activeTool && changed.has(this.activeTool)) this.setTool(this.activeTool);
|
|
2165
|
+
for (const type of changed) this.events.emit("drawing:defaults", { type });
|
|
2166
|
+
}
|
|
2167
|
+
/** Forget a tool's remembered settings (every tool's when `type` is omitted) — new drawings
|
|
2168
|
+
* start from the factory defaults again. */
|
|
2169
|
+
resetToolDefaults(type) {
|
|
2170
|
+
const types = type ? this.defaults.has(type) ? [type] : [] : [...this.defaults.keys()];
|
|
2171
|
+
for (const t of types) {
|
|
2172
|
+
this.defaults.delete(t);
|
|
2173
|
+
this.events.emit("drawing:defaults", { type: t });
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
/** Every tool's saved looks, by type (plain JSON — persist it as-is). */
|
|
2177
|
+
toolTemplates() {
|
|
2178
|
+
const out = {};
|
|
2179
|
+
for (const [type, list] of this.templates) out[type] = clonePlain(list);
|
|
2180
|
+
return out;
|
|
2181
|
+
}
|
|
2182
|
+
/** Replace every tool's saved looks (restoring persisted prefs). Unknown types and
|
|
2183
|
+
* malformed entries are dropped; an unchanged set is a no-op and emits nothing. */
|
|
2184
|
+
setToolTemplates(map2) {
|
|
2185
|
+
const next = /* @__PURE__ */ new Map();
|
|
2186
|
+
for (const [type, raw] of Object.entries(map2 ?? {})) {
|
|
2187
|
+
if (!getDrawingType(type)) continue;
|
|
2188
|
+
const list = sanitizeToolTemplates(raw);
|
|
2189
|
+
if (list.length > 0) next.set(type, list);
|
|
2190
|
+
}
|
|
2191
|
+
const changed = [.../* @__PURE__ */ new Set([...this.templates.keys(), ...next.keys()])].filter(
|
|
2192
|
+
(t) => JSON.stringify(this.templates.get(t) ?? []) !== JSON.stringify(next.get(t) ?? [])
|
|
2193
|
+
);
|
|
2194
|
+
if (changed.length === 0) return;
|
|
2195
|
+
this.templates = next;
|
|
2196
|
+
this.pushTemplates();
|
|
2197
|
+
for (const type of changed) this.events.emit("drawing:templates", { type });
|
|
2198
|
+
}
|
|
2199
|
+
/** Save the given settings as a named look for a tool (a same-named one is replaced). */
|
|
2200
|
+
saveToolTemplate(type, name, settings) {
|
|
2201
|
+
if (!getDrawingType(type)) return;
|
|
2202
|
+
const [entry] = sanitizeToolTemplates([{ name, settings }]);
|
|
2203
|
+
if (!entry) return;
|
|
2204
|
+
const list = (this.templates.get(type) ?? []).filter((t) => t.name !== entry.name);
|
|
2205
|
+
this.templates.set(type, [...list, clonePlain(entry)]);
|
|
2206
|
+
this.pushTemplates();
|
|
2207
|
+
this.events.emit("drawing:templates", { type });
|
|
2208
|
+
}
|
|
2209
|
+
/** Delete a tool's saved look by name. */
|
|
2210
|
+
removeToolTemplate(type, name) {
|
|
2211
|
+
const list = this.templates.get(type);
|
|
2212
|
+
if (!list?.some((t) => t.name === name)) return;
|
|
2213
|
+
const rest = list.filter((t) => t.name !== name);
|
|
2214
|
+
if (rest.length > 0) this.templates.set(type, rest);
|
|
2215
|
+
else this.templates.delete(type);
|
|
2216
|
+
this.pushTemplates();
|
|
2217
|
+
this.events.emit("drawing:templates", { type });
|
|
2218
|
+
}
|
|
2219
|
+
pushTemplates() {
|
|
2220
|
+
this.port?.setToolTemplates?.(this.toolTemplates());
|
|
2221
|
+
}
|
|
2094
2222
|
setToolbar(option) {
|
|
2095
2223
|
this.port?.setToolbar(buildToolbar(option).definition);
|
|
2096
2224
|
}
|
|
@@ -2106,21 +2234,22 @@ var DrawingController = class {
|
|
|
2106
2234
|
// ── programmatic CRUD (facade-facing) ── each is one undo step
|
|
2107
2235
|
add(type, init = {}) {
|
|
2108
2236
|
if (!this.enabled) return null;
|
|
2109
|
-
const
|
|
2110
|
-
const style = { ...last ?? {}, ...init.style ?? {} };
|
|
2237
|
+
const defaults = this.defaults.get(type);
|
|
2111
2238
|
const d = createDrawing(type, {
|
|
2112
2239
|
id: this.store.nextId(),
|
|
2113
2240
|
paneId: init.paneId ?? "price",
|
|
2114
2241
|
anchors: init.anchors,
|
|
2115
|
-
style,
|
|
2116
2242
|
text: init.text,
|
|
2117
|
-
props: init.props,
|
|
2118
2243
|
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
2119
2244
|
});
|
|
2120
2245
|
if (!d) return null;
|
|
2246
|
+
applyToolDefaults(d, defaults);
|
|
2247
|
+
if (init.style) d.style = { ...d.style, ...init.style };
|
|
2248
|
+
if (init.text) d.text = { ...d.text ?? init.text, ...init.text };
|
|
2249
|
+
if (init.props) d.applyProps(init.props);
|
|
2121
2250
|
this.history.record(this.store.serialize());
|
|
2122
2251
|
this.store.add(d);
|
|
2123
|
-
this.
|
|
2252
|
+
this.captureDefaults(d.id);
|
|
2124
2253
|
this.events.emit("drawing:created", { id: d.id });
|
|
2125
2254
|
return d;
|
|
2126
2255
|
}
|
|
@@ -2132,9 +2261,8 @@ var DrawingController = class {
|
|
|
2132
2261
|
const before = this.store.serialize();
|
|
2133
2262
|
const cur = patch.style ? this.store.get(id) : void 0;
|
|
2134
2263
|
const resolved = patch.style && cur ? { ...patch, style: { ...cur.style, ...patch.style } } : patch;
|
|
2135
|
-
if (this.
|
|
2264
|
+
if (this.updateDrawing(id, resolved)) {
|
|
2136
2265
|
this.history.record(before);
|
|
2137
|
-
this.captureStyle(id);
|
|
2138
2266
|
this.events.emit("drawing:edited", { id });
|
|
2139
2267
|
}
|
|
2140
2268
|
}
|
|
@@ -2142,7 +2270,7 @@ var DrawingController = class {
|
|
|
2142
2270
|
updateMany(patches) {
|
|
2143
2271
|
this.history.begin(this.store.serialize());
|
|
2144
2272
|
for (const { id, patch } of patches) {
|
|
2145
|
-
if (this.
|
|
2273
|
+
if (this.updateDrawing(id, patch)) {
|
|
2146
2274
|
this.history.markDirty();
|
|
2147
2275
|
this.events.emit("drawing:edited", { id });
|
|
2148
2276
|
}
|
|
@@ -2322,10 +2450,27 @@ var DrawingController = class {
|
|
|
2322
2450
|
resolve(ids) {
|
|
2323
2451
|
return ids.map((id) => this.store.get(id)).filter((d) => d != null);
|
|
2324
2452
|
}
|
|
2325
|
-
/**
|
|
2326
|
-
|
|
2453
|
+
/** `store.update` that remembers the drawing's settings only when the patch changed them:
|
|
2454
|
+
* moving an older drawing must not hand its look back to the tool. */
|
|
2455
|
+
updateDrawing(id, patch) {
|
|
2456
|
+
const cur = this.store.get(id);
|
|
2457
|
+
const before = cur ? captureToolDefaults(cur) : void 0;
|
|
2458
|
+
if (!this.store.update(id, patch)) return false;
|
|
2459
|
+
const after = this.store.get(id);
|
|
2460
|
+
if (after && !sameToolDefaults(before, captureToolDefaults(after))) this.captureDefaults(id);
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
/** Remember a drawing's settings as its tool's defaults (they seed the next one), and
|
|
2464
|
+
* announce a real change so a host can save it. */
|
|
2465
|
+
captureDefaults(id) {
|
|
2327
2466
|
const d = this.store.get(id);
|
|
2328
|
-
if (d)
|
|
2467
|
+
if (!d) return;
|
|
2468
|
+
const captured = captureToolDefaults(d);
|
|
2469
|
+
const next = sameToolDefaults(captured, factoryDefaults(d)) ? void 0 : captured;
|
|
2470
|
+
if (sameToolDefaults(this.defaults.get(d.type), next)) return;
|
|
2471
|
+
if (next) this.defaults.set(d.type, next);
|
|
2472
|
+
else this.defaults.delete(d.type);
|
|
2473
|
+
this.events.emit("drawing:defaults", { type: d.type });
|
|
2329
2474
|
}
|
|
2330
2475
|
sync() {
|
|
2331
2476
|
this.port?.syncDrawings(this.store.serialize().drawings);
|
|
@@ -2341,22 +2486,20 @@ var DrawingController = class {
|
|
|
2341
2486
|
break;
|
|
2342
2487
|
case "create": {
|
|
2343
2488
|
const before = this.store.serialize();
|
|
2344
|
-
const
|
|
2345
|
-
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
2346
|
-
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
2489
|
+
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId() });
|
|
2347
2490
|
if (!d) return;
|
|
2491
|
+
applyToolDefaults(d, this.defaults.get(d.type));
|
|
2348
2492
|
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
2349
2493
|
this.history.record(before);
|
|
2350
2494
|
this.store.add(d);
|
|
2351
|
-
this.
|
|
2495
|
+
this.captureDefaults(d.id);
|
|
2352
2496
|
this.events.emit("drawing:created", { id: d.id });
|
|
2353
2497
|
break;
|
|
2354
2498
|
}
|
|
2355
2499
|
case "edit": {
|
|
2356
2500
|
const before = this.store.serialize();
|
|
2357
|
-
if (this.
|
|
2501
|
+
if (this.updateDrawing(i.doc.id, i.doc)) {
|
|
2358
2502
|
this.history.record(before);
|
|
2359
|
-
this.captureStyle(i.doc.id);
|
|
2360
2503
|
this.events.emit("drawing:edited", { id: i.doc.id });
|
|
2361
2504
|
}
|
|
2362
2505
|
break;
|
|
@@ -2364,7 +2507,7 @@ var DrawingController = class {
|
|
|
2364
2507
|
case "edit-many": {
|
|
2365
2508
|
this.history.begin(this.store.serialize());
|
|
2366
2509
|
for (const doc of i.docs) {
|
|
2367
|
-
if (this.
|
|
2510
|
+
if (this.updateDrawing(doc.id, doc)) {
|
|
2368
2511
|
this.history.markDirty();
|
|
2369
2512
|
this.events.emit("drawing:edited", { id: doc.id });
|
|
2370
2513
|
}
|
|
@@ -2388,6 +2531,12 @@ var DrawingController = class {
|
|
|
2388
2531
|
case "favorite":
|
|
2389
2532
|
this.setFavorite(i.type, i.on);
|
|
2390
2533
|
break;
|
|
2534
|
+
case "template-save":
|
|
2535
|
+
this.saveToolTemplate(i.type, i.name, i.settings);
|
|
2536
|
+
break;
|
|
2537
|
+
case "template-remove":
|
|
2538
|
+
this.removeToolTemplate(i.type, i.name);
|
|
2539
|
+
break;
|
|
2391
2540
|
case "snap-mode":
|
|
2392
2541
|
if (i.mode !== this.snapMode) {
|
|
2393
2542
|
this.snapMode = i.mode;
|
|
@@ -3069,17 +3218,17 @@ var DrawingToolbar = class {
|
|
|
3069
3218
|
this.paintStay();
|
|
3070
3219
|
}
|
|
3071
3220
|
makeButton(icon2, title, onClick) {
|
|
3072
|
-
const
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
this.tipText.set(
|
|
3077
|
-
if (icon2)
|
|
3221
|
+
const btn = document.createElement("button");
|
|
3222
|
+
btn.type = "button";
|
|
3223
|
+
btn.className = "vela-dtb-btn";
|
|
3224
|
+
btn.setAttribute("aria-label", title);
|
|
3225
|
+
this.tipText.set(btn, title);
|
|
3226
|
+
if (icon2) btn.innerHTML = hitHtml(icon2);
|
|
3078
3227
|
this.tipDisposers.push(
|
|
3079
|
-
attachChromeTooltip(
|
|
3228
|
+
attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
|
|
3080
3229
|
);
|
|
3081
|
-
|
|
3082
|
-
return
|
|
3230
|
+
btn.addEventListener("click", onClick);
|
|
3231
|
+
return btn;
|
|
3083
3232
|
}
|
|
3084
3233
|
divider() {
|
|
3085
3234
|
const d = document.createElement("div");
|
|
@@ -3216,8 +3365,8 @@ function resolveAnimations(animations) {
|
|
|
3216
3365
|
|
|
3217
3366
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
3218
3367
|
var PROPS_TAB = "Properties";
|
|
3219
|
-
function bagOf(
|
|
3220
|
-
return decl.prop ?
|
|
3368
|
+
function bagOf(row2, decl) {
|
|
3369
|
+
return decl.prop ? row2.propValues : row2.values;
|
|
3221
3370
|
}
|
|
3222
3371
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
3223
3372
|
var IndicatorInputsDialog = class {
|
|
@@ -3250,13 +3399,13 @@ var IndicatorInputsDialog = class {
|
|
|
3250
3399
|
isOpen() {
|
|
3251
3400
|
return this.dialog !== null;
|
|
3252
3401
|
}
|
|
3253
|
-
open(
|
|
3402
|
+
open(row2) {
|
|
3254
3403
|
this.close();
|
|
3255
|
-
if (
|
|
3256
|
-
this.row =
|
|
3257
|
-
this.openId =
|
|
3258
|
-
this.snapshot = { ...
|
|
3259
|
-
this.propSnapshot = { ...
|
|
3404
|
+
if (row2.inputs.length === 0 && row2.props.length === 0) return;
|
|
3405
|
+
this.row = row2;
|
|
3406
|
+
this.openId = row2.id;
|
|
3407
|
+
this.snapshot = { ...row2.values };
|
|
3408
|
+
this.propSnapshot = { ...row2.propValues };
|
|
3260
3409
|
ensureDialogStyles();
|
|
3261
3410
|
const t = this.host.theme();
|
|
3262
3411
|
const border = "var(--vela-border)";
|
|
@@ -3264,7 +3413,7 @@ var IndicatorInputsDialog = class {
|
|
|
3264
3413
|
const host = this.host.dialogHost() ?? this.host.container;
|
|
3265
3414
|
const ui = new Dialog({
|
|
3266
3415
|
host,
|
|
3267
|
-
title:
|
|
3416
|
+
title: row2.title,
|
|
3268
3417
|
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
3269
3418
|
// whole body, killing the chart and the body-portaled popovers.
|
|
3270
3419
|
modal: false,
|
|
@@ -3295,9 +3444,9 @@ var IndicatorInputsDialog = class {
|
|
|
3295
3444
|
theme: () => this.host.theme(),
|
|
3296
3445
|
text: () => "Close"
|
|
3297
3446
|
}));
|
|
3298
|
-
const tabDefs = tabInputs(
|
|
3299
|
-
if (
|
|
3300
|
-
const propDecls =
|
|
3447
|
+
const tabDefs = tabInputs(row2.inputs);
|
|
3448
|
+
if (row2.props.length > 0) {
|
|
3449
|
+
const propDecls = row2.props.map((p) => {
|
|
3301
3450
|
const d = { ...p, prop: true };
|
|
3302
3451
|
delete d.tab;
|
|
3303
3452
|
return d;
|
|
@@ -3331,7 +3480,7 @@ var IndicatorInputsDialog = class {
|
|
|
3331
3480
|
tab.addEventListener("click", () => activate(idx));
|
|
3332
3481
|
tabs.appendChild(tab);
|
|
3333
3482
|
tabEls.push(tab);
|
|
3334
|
-
bodies.push(this.buildTabBody(def.inputs,
|
|
3483
|
+
bodies.push(this.buildTabBody(def.inputs, row2, gates));
|
|
3335
3484
|
if (def.inputs.every((d) => d.when)) {
|
|
3336
3485
|
gates.push({ el: tab, visible: (bag) => def.inputs.some((d) => inputVisible(d.when, bag)) });
|
|
3337
3486
|
}
|
|
@@ -3340,7 +3489,7 @@ var IndicatorInputsDialog = class {
|
|
|
3340
3489
|
for (const b of bodies) ui.body.appendChild(b);
|
|
3341
3490
|
activate(0);
|
|
3342
3491
|
const refresh = () => {
|
|
3343
|
-
const bag = valuesBag(
|
|
3492
|
+
const bag = valuesBag(row2);
|
|
3344
3493
|
for (const g of gates) g.el.style.display = g.visible(bag) ? "" : "none";
|
|
3345
3494
|
if (tabEls[active]?.style.display === "none") {
|
|
3346
3495
|
const next = tabEls.findIndex((el) => el.style.display !== "none");
|
|
@@ -3368,7 +3517,7 @@ var IndicatorInputsDialog = class {
|
|
|
3368
3517
|
}
|
|
3369
3518
|
/** One tab's scrollable body — one shared grid so every group's controls share a
|
|
3370
3519
|
* left edge (a 100px number field lines up with a wider session/time pair). */
|
|
3371
|
-
buildTabBody(inputs,
|
|
3520
|
+
buildTabBody(inputs, row2, gates) {
|
|
3372
3521
|
const body = document.createElement("div");
|
|
3373
3522
|
body.style.cssText = inputDialogBodyStyle(this.host.mobile());
|
|
3374
3523
|
for (const group of groupInputs(inputs)) {
|
|
@@ -3380,7 +3529,7 @@ var IndicatorInputsDialog = class {
|
|
|
3380
3529
|
section.appendChild(header);
|
|
3381
3530
|
}
|
|
3382
3531
|
for (const inputRow of group.rows) {
|
|
3383
|
-
const el = this.buildInputRow(section, inputRow,
|
|
3532
|
+
const el = this.buildInputRow(section, inputRow, row2);
|
|
3384
3533
|
if (inputRow.some((d) => d.when)) {
|
|
3385
3534
|
gates.push({ el, visible: (bag) => rowVisible(inputRow, bag) });
|
|
3386
3535
|
}
|
|
@@ -3413,26 +3562,26 @@ var IndicatorInputsDialog = class {
|
|
|
3413
3562
|
}
|
|
3414
3563
|
/** Restore every input and prop to its open-time value (re-running the indicator), then close. */
|
|
3415
3564
|
revertAndClose() {
|
|
3416
|
-
const
|
|
3565
|
+
const row2 = this.row;
|
|
3417
3566
|
const snap = this.snapshot;
|
|
3418
|
-
if (
|
|
3419
|
-
for (const inp of
|
|
3567
|
+
if (row2 && snap) {
|
|
3568
|
+
for (const inp of row2.inputs) {
|
|
3420
3569
|
const snapped = snap[inp.key];
|
|
3421
3570
|
const before = snapped !== void 0 ? snapped : inp.defval;
|
|
3422
|
-
if (
|
|
3423
|
-
|
|
3424
|
-
this.host.onChange?.({ indicatorId:
|
|
3571
|
+
if (row2.values[inp.key] !== before) {
|
|
3572
|
+
row2.values[inp.key] = before;
|
|
3573
|
+
this.host.onChange?.({ indicatorId: row2.id, key: inp.key, value: before });
|
|
3425
3574
|
}
|
|
3426
3575
|
}
|
|
3427
3576
|
}
|
|
3428
3577
|
const propSnap = this.propSnapshot;
|
|
3429
|
-
if (
|
|
3430
|
-
for (const p of
|
|
3578
|
+
if (row2 && propSnap) {
|
|
3579
|
+
for (const p of row2.props) {
|
|
3431
3580
|
const snapped = propSnap[p.key];
|
|
3432
3581
|
const before = snapped !== void 0 ? snapped : p.defval;
|
|
3433
|
-
if (
|
|
3434
|
-
|
|
3435
|
-
this.host.onChange?.({ indicatorId:
|
|
3582
|
+
if (row2.propValues[p.key] !== before) {
|
|
3583
|
+
row2.propValues[p.key] = before;
|
|
3584
|
+
this.host.onChange?.({ indicatorId: row2.id, key: p.key, value: before, kind: "prop" });
|
|
3436
3585
|
}
|
|
3437
3586
|
}
|
|
3438
3587
|
}
|
|
@@ -3450,35 +3599,35 @@ var IndicatorInputsDialog = class {
|
|
|
3450
3599
|
* rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
|
|
3451
3600
|
* survives the rebuild, so Cancel after a reset still reverts the whole session. */
|
|
3452
3601
|
resetToDefaults() {
|
|
3453
|
-
const
|
|
3454
|
-
if (!
|
|
3602
|
+
const row2 = this.row;
|
|
3603
|
+
if (!row2) return;
|
|
3455
3604
|
const snap = this.snapshot;
|
|
3456
3605
|
const propSnap = this.propSnapshot;
|
|
3457
|
-
for (const inp of
|
|
3458
|
-
if (
|
|
3459
|
-
|
|
3460
|
-
this.host.onChange?.({ indicatorId:
|
|
3606
|
+
for (const inp of row2.inputs) {
|
|
3607
|
+
if (row2.values[inp.key] !== inp.defval) {
|
|
3608
|
+
row2.values[inp.key] = inp.defval;
|
|
3609
|
+
this.host.onChange?.({ indicatorId: row2.id, key: inp.key, value: inp.defval });
|
|
3461
3610
|
}
|
|
3462
3611
|
}
|
|
3463
|
-
for (const p of
|
|
3464
|
-
if (
|
|
3465
|
-
|
|
3466
|
-
this.host.onChange?.({ indicatorId:
|
|
3612
|
+
for (const p of row2.props) {
|
|
3613
|
+
if (row2.propValues[p.key] !== p.defval) {
|
|
3614
|
+
row2.propValues[p.key] = p.defval;
|
|
3615
|
+
this.host.onChange?.({ indicatorId: row2.id, key: p.key, value: p.defval, kind: "prop" });
|
|
3467
3616
|
}
|
|
3468
3617
|
}
|
|
3469
|
-
this.open(
|
|
3618
|
+
this.open(row2);
|
|
3470
3619
|
if (snap) this.snapshot = snap;
|
|
3471
3620
|
if (propSnap) this.propSnapshot = propSnap;
|
|
3472
3621
|
}
|
|
3473
3622
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
3474
|
-
commit(
|
|
3475
|
-
bagOf(
|
|
3476
|
-
this.host.onChange?.({ indicatorId:
|
|
3623
|
+
commit(row2, decl, value) {
|
|
3624
|
+
bagOf(row2, decl)[decl.key] = value;
|
|
3625
|
+
this.host.onChange?.({ indicatorId: row2.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
|
|
3477
3626
|
this.refreshVisibility?.();
|
|
3478
3627
|
}
|
|
3479
3628
|
/** One settings row (or several `inline=` inputs) placed into a section's grid. */
|
|
3480
|
-
buildInputRow(grid, decls,
|
|
3481
|
-
const idOf = (inp) => `vela-inp-${
|
|
3629
|
+
buildInputRow(grid, decls, row2) {
|
|
3630
|
+
const idOf = (inp) => `vela-inp-${row2.id}-${inp.key}`;
|
|
3482
3631
|
const append = (el) => {
|
|
3483
3632
|
grid.appendChild(el);
|
|
3484
3633
|
return el;
|
|
@@ -3489,7 +3638,7 @@ var IndicatorInputsDialog = class {
|
|
|
3489
3638
|
inline: decls.map((d) => ({
|
|
3490
3639
|
label: nameOf(d) || void 0,
|
|
3491
3640
|
id: idOf(d),
|
|
3492
|
-
control: this.buildControl(
|
|
3641
|
+
control: this.buildControl(row2, d, idOf(d)),
|
|
3493
3642
|
toggleFirst: d.type === "bool",
|
|
3494
3643
|
fit: d.type === "color" || d.type === "bool"
|
|
3495
3644
|
})),
|
|
@@ -3500,7 +3649,7 @@ var IndicatorInputsDialog = class {
|
|
|
3500
3649
|
const id = idOf(lead);
|
|
3501
3650
|
const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
|
|
3502
3651
|
if (lead.type === "bool") {
|
|
3503
|
-
const current = Boolean(bagOf(
|
|
3652
|
+
const current = Boolean(bagOf(row2, lead)[lead.key] ?? lead.defval);
|
|
3504
3653
|
return append(fieldRow({
|
|
3505
3654
|
label: nameOf(lead),
|
|
3506
3655
|
id,
|
|
@@ -3509,7 +3658,7 @@ var IndicatorInputsDialog = class {
|
|
|
3509
3658
|
toggle: {
|
|
3510
3659
|
id,
|
|
3511
3660
|
checked: current,
|
|
3512
|
-
onChange: (v) => this.commit(
|
|
3661
|
+
onChange: (v) => this.commit(row2, lead, v)
|
|
3513
3662
|
}
|
|
3514
3663
|
}));
|
|
3515
3664
|
}
|
|
@@ -3519,7 +3668,7 @@ var IndicatorInputsDialog = class {
|
|
|
3519
3668
|
id,
|
|
3520
3669
|
stacked: true,
|
|
3521
3670
|
info,
|
|
3522
|
-
control: this.buildControl(
|
|
3671
|
+
control: this.buildControl(row2, lead, id)
|
|
3523
3672
|
}));
|
|
3524
3673
|
}
|
|
3525
3674
|
const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
|
|
@@ -3529,13 +3678,13 @@ var IndicatorInputsDialog = class {
|
|
|
3529
3678
|
fit,
|
|
3530
3679
|
controlWidth: fit ? void 0 : 100,
|
|
3531
3680
|
info,
|
|
3532
|
-
control: this.buildControl(
|
|
3681
|
+
control: this.buildControl(row2, lead, id)
|
|
3533
3682
|
}));
|
|
3534
3683
|
}
|
|
3535
3684
|
/** Build the typed control for one input, committing edits live via `onChange`. */
|
|
3536
|
-
buildControl(
|
|
3537
|
-
const current = bagOf(
|
|
3538
|
-
const emit = (value) => this.commit(
|
|
3685
|
+
buildControl(row2, inp, id) {
|
|
3686
|
+
const current = bagOf(row2, inp)[inp.key] ?? inp.defval;
|
|
3687
|
+
const emit = (value) => this.commit(row2, inp, value);
|
|
3539
3688
|
if (inp.type === "bool") {
|
|
3540
3689
|
return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
|
|
3541
3690
|
}
|
|
@@ -3546,7 +3695,7 @@ var IndicatorInputsDialog = class {
|
|
|
3546
3695
|
kind: "color",
|
|
3547
3696
|
id,
|
|
3548
3697
|
theme: this.host.theme(),
|
|
3549
|
-
get: () => String(bagOf(
|
|
3698
|
+
get: () => String(bagOf(row2, inp)[inp.key] ?? inp.defval),
|
|
3550
3699
|
onChange: (v) => emit(v),
|
|
3551
3700
|
// An input change re-executes the script over its whole history: commit the
|
|
3552
3701
|
// opacity drag once on release, not once per pointer move.
|
|
@@ -3831,21 +3980,21 @@ var IndicatorInputsDialog = class {
|
|
|
3831
3980
|
const picker = this.host.symbolPicker();
|
|
3832
3981
|
if (!picker) return this.buildTextField(id, current, emit);
|
|
3833
3982
|
let value = current;
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3983
|
+
const btn = document.createElement("button");
|
|
3984
|
+
btn.type = "button";
|
|
3985
|
+
btn.id = id;
|
|
3986
|
+
btn.style.cssText = `${this.ctrlStyle()}display:flex;align-items:center;cursor:pointer;text-align:left;`;
|
|
3838
3987
|
const label = document.createElement("span");
|
|
3839
3988
|
label.textContent = value || "Select symbol\u2026";
|
|
3840
3989
|
label.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
3841
|
-
|
|
3842
|
-
|
|
3990
|
+
btn.append(label);
|
|
3991
|
+
btn.addEventListener("click", () => picker(value, (picked) => {
|
|
3843
3992
|
if (!picked) return;
|
|
3844
3993
|
value = picked;
|
|
3845
3994
|
label.textContent = picked;
|
|
3846
3995
|
emit(picked);
|
|
3847
3996
|
}));
|
|
3848
|
-
return
|
|
3997
|
+
return btn;
|
|
3849
3998
|
}
|
|
3850
3999
|
/** Shared field chrome for text / number / select controls (fills its wrapper's width). */
|
|
3851
4000
|
ctrlStyle() {
|
|
@@ -3891,9 +4040,9 @@ function tabInputs(inputs) {
|
|
|
3891
4040
|
function rowVisible(decls, bag) {
|
|
3892
4041
|
return decls.some((d) => inputVisible(d.when, bag));
|
|
3893
4042
|
}
|
|
3894
|
-
function valuesBag(
|
|
4043
|
+
function valuesBag(row2) {
|
|
3895
4044
|
const bag = {};
|
|
3896
|
-
for (const inp of
|
|
4045
|
+
for (const inp of row2.inputs) bag[inp.key] = row2.values[inp.key] ?? inp.defval;
|
|
3897
4046
|
return bag;
|
|
3898
4047
|
}
|
|
3899
4048
|
function groupInputs(inputs) {
|
|
@@ -4089,7 +4238,7 @@ var InputsUI = class {
|
|
|
4089
4238
|
if (!this.selectedId) return;
|
|
4090
4239
|
const target = e.target;
|
|
4091
4240
|
if (target) {
|
|
4092
|
-
for (const
|
|
4241
|
+
for (const row2 of this.rows.values()) if (row2.el.contains(target)) return;
|
|
4093
4242
|
}
|
|
4094
4243
|
this.clearSelection();
|
|
4095
4244
|
};
|
|
@@ -4172,9 +4321,9 @@ var InputsUI = class {
|
|
|
4172
4321
|
setTheme(theme) {
|
|
4173
4322
|
this.theme = theme;
|
|
4174
4323
|
for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
|
|
4175
|
-
for (const
|
|
4176
|
-
|
|
4177
|
-
|
|
4324
|
+
for (const row2 of this.rows.values()) {
|
|
4325
|
+
row2.el.style.background = row2.highlighted ? theme.background : this.idleRowFill();
|
|
4326
|
+
row2.el.style.color = theme.textColor;
|
|
4178
4327
|
}
|
|
4179
4328
|
if (this.foldToggle) this.syncFoldToggle();
|
|
4180
4329
|
}
|
|
@@ -4193,26 +4342,26 @@ var InputsUI = class {
|
|
|
4193
4342
|
*/
|
|
4194
4343
|
setLegendActions(provider) {
|
|
4195
4344
|
this.legendActions = provider;
|
|
4196
|
-
for (const
|
|
4345
|
+
for (const row2 of this.rows.values()) this.renderExtras(row2.id, row2.extrasEl);
|
|
4197
4346
|
}
|
|
4198
4347
|
/** (Re)build one row's contributed buttons. Same look, reveal and tooltips as the built-ins. */
|
|
4199
4348
|
renderExtras(id, extrasEl) {
|
|
4200
4349
|
this.disposeTips(this.extrasTips, id);
|
|
4201
4350
|
extrasEl.replaceChildren();
|
|
4202
4351
|
for (const action of this.legendActions?.(id) ?? []) {
|
|
4203
|
-
const
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
this.tip(this.extrasTips, id,
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4352
|
+
const btn = document.createElement("button");
|
|
4353
|
+
btn.type = "button";
|
|
4354
|
+
btn.setAttribute("aria-label", action.tooltip);
|
|
4355
|
+
this.tip(this.extrasTips, id, btn, action.tooltip);
|
|
4356
|
+
btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX2);
|
|
4357
|
+
btn.className = "vela-ind-ctl";
|
|
4358
|
+
btn.dataset.legendAction = action.id;
|
|
4359
|
+
btn.style.cssText = LEGEND_CTL_CSS;
|
|
4360
|
+
btn.addEventListener("click", (e) => {
|
|
4212
4361
|
e.stopPropagation();
|
|
4213
4362
|
action.run();
|
|
4214
4363
|
});
|
|
4215
|
-
extrasEl.appendChild(
|
|
4364
|
+
extrasEl.appendChild(btn);
|
|
4216
4365
|
}
|
|
4217
4366
|
}
|
|
4218
4367
|
/**
|
|
@@ -4222,17 +4371,17 @@ var InputsUI = class {
|
|
|
4222
4371
|
*/
|
|
4223
4372
|
setLegendCallouts(provider) {
|
|
4224
4373
|
this.legendCallouts = provider;
|
|
4225
|
-
for (const
|
|
4374
|
+
for (const row2 of this.rows.values()) this.renderCallouts(row2);
|
|
4226
4375
|
}
|
|
4227
4376
|
/** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
|
|
4228
4377
|
* click (when the view carries content) deploys a panel of text and actions. */
|
|
4229
|
-
renderCallouts(
|
|
4230
|
-
this.disposeTips(this.calloutTips,
|
|
4231
|
-
for (const bubble of
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
const views = this.legendCallouts?.(
|
|
4235
|
-
|
|
4378
|
+
renderCallouts(row2) {
|
|
4379
|
+
this.disposeTips(this.calloutTips, row2.id);
|
|
4380
|
+
for (const bubble of row2.callouts) bubble.destroy();
|
|
4381
|
+
row2.callouts = [];
|
|
4382
|
+
row2.calloutsEl.replaceChildren();
|
|
4383
|
+
const views = this.legendCallouts?.(row2.id) ?? [];
|
|
4384
|
+
row2.calloutsEl.style.display = legendCalloutsDisplay(row2.highlighted, views.length > 0);
|
|
4236
4385
|
for (const view of views) {
|
|
4237
4386
|
const bubble = new CalloutBubble({
|
|
4238
4387
|
icon: view.icon,
|
|
@@ -4246,9 +4395,9 @@ var InputsUI = class {
|
|
|
4246
4395
|
theme: () => this.theme
|
|
4247
4396
|
});
|
|
4248
4397
|
bubble.el.dataset.legendCallout = view.id;
|
|
4249
|
-
this.tip(this.calloutTips,
|
|
4250
|
-
|
|
4251
|
-
|
|
4398
|
+
this.tip(this.calloutTips, row2.id, bubble.el, view.tooltip);
|
|
4399
|
+
row2.callouts.push(bubble);
|
|
4400
|
+
row2.calloutsEl.appendChild(bubble.el);
|
|
4252
4401
|
}
|
|
4253
4402
|
}
|
|
4254
4403
|
/** Reposition the per-pane legend containers after a layout change. */
|
|
@@ -4268,9 +4417,9 @@ var InputsUI = class {
|
|
|
4268
4417
|
*/
|
|
4269
4418
|
setValuesVisible(visible) {
|
|
4270
4419
|
this.valuesVisible = visible;
|
|
4271
|
-
for (const
|
|
4272
|
-
|
|
4273
|
-
this.renderRowValues(
|
|
4420
|
+
for (const row2 of this.rows.values()) {
|
|
4421
|
+
row2.showValues = null;
|
|
4422
|
+
this.renderRowValues(row2);
|
|
4274
4423
|
}
|
|
4275
4424
|
}
|
|
4276
4425
|
/**
|
|
@@ -4279,35 +4428,35 @@ var InputsUI = class {
|
|
|
4279
4428
|
* unchanged frames: each row diffs against its last rendered values before touching DOM.
|
|
4280
4429
|
*/
|
|
4281
4430
|
setPlotValues(values) {
|
|
4282
|
-
for (const
|
|
4283
|
-
const next = values.get(
|
|
4431
|
+
for (const row2 of this.rows.values()) {
|
|
4432
|
+
const next = values.get(row2.id) ?? [];
|
|
4284
4433
|
const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
|
|
4285
|
-
if (key ===
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
this.renderRowValues(
|
|
4434
|
+
if (key === row2.plotValuesKey) continue;
|
|
4435
|
+
row2.plotValues = next;
|
|
4436
|
+
row2.plotValuesKey = key;
|
|
4437
|
+
this.renderRowValues(row2);
|
|
4289
4438
|
}
|
|
4290
4439
|
}
|
|
4291
4440
|
/** Whether a row currently shows its values: its own override, else the chart-wide flag. */
|
|
4292
|
-
rowValuesShown(
|
|
4293
|
-
return
|
|
4441
|
+
rowValuesShown(row2) {
|
|
4442
|
+
return row2.showValues ?? this.valuesVisible;
|
|
4294
4443
|
}
|
|
4295
4444
|
/** (Re)build one row's values readout from its stored plot values + visibility. A
|
|
4296
4445
|
* highlighted (hovered/selected) row shows only the title + controls — no values. */
|
|
4297
|
-
renderRowValues(
|
|
4298
|
-
if (!this.rowValuesShown(
|
|
4299
|
-
|
|
4300
|
-
|
|
4446
|
+
renderRowValues(row2) {
|
|
4447
|
+
if (!this.rowValuesShown(row2) || row2.plotValues.length === 0 || row2.highlighted) {
|
|
4448
|
+
row2.valuesEl.style.display = "none";
|
|
4449
|
+
row2.valuesEl.replaceChildren();
|
|
4301
4450
|
return;
|
|
4302
4451
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4452
|
+
row2.valuesEl.style.display = "block";
|
|
4453
|
+
row2.valuesEl.replaceChildren();
|
|
4454
|
+
row2.plotValues.forEach((v, i) => {
|
|
4306
4455
|
const span = document.createElement("span");
|
|
4307
4456
|
span.textContent = v.value;
|
|
4308
4457
|
span.style.color = v.color;
|
|
4309
4458
|
if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
|
|
4310
|
-
|
|
4459
|
+
row2.valuesEl.appendChild(span);
|
|
4311
4460
|
});
|
|
4312
4461
|
}
|
|
4313
4462
|
// ── legend move/merge (menu + drag) ─────────────────────────────────────
|
|
@@ -4316,8 +4465,8 @@ var InputsUI = class {
|
|
|
4316
4465
|
this.closeMoveMenu();
|
|
4317
4466
|
const api = this.moveApi;
|
|
4318
4467
|
if (!api) return;
|
|
4319
|
-
const
|
|
4320
|
-
const currentPane =
|
|
4468
|
+
const row2 = this.rows.get(id);
|
|
4469
|
+
const currentPane = row2?.paneId ?? "price";
|
|
4321
4470
|
const panes = api.panes();
|
|
4322
4471
|
const items = [];
|
|
4323
4472
|
for (const p of panes) {
|
|
@@ -4357,13 +4506,13 @@ var InputsUI = class {
|
|
|
4357
4506
|
openRowMenu(id, x, y) {
|
|
4358
4507
|
this.closeRowMenu();
|
|
4359
4508
|
this.closeMoveMenu();
|
|
4360
|
-
const
|
|
4361
|
-
if (!
|
|
4509
|
+
const row2 = this.rows.get(id);
|
|
4510
|
+
if (!row2) return;
|
|
4362
4511
|
this.rowMenuId = id;
|
|
4363
4512
|
const items = [{
|
|
4364
4513
|
id: "values",
|
|
4365
4514
|
label: "Indicator values",
|
|
4366
|
-
checked: this.rowValuesShown(
|
|
4515
|
+
checked: this.rowValuesShown(row2)
|
|
4367
4516
|
}];
|
|
4368
4517
|
if (!this.rowMenu) {
|
|
4369
4518
|
this.rowMenu = new Menu({
|
|
@@ -4390,11 +4539,11 @@ var InputsUI = class {
|
|
|
4390
4539
|
}
|
|
4391
4540
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
4392
4541
|
setPane(id, paneId) {
|
|
4393
|
-
const
|
|
4394
|
-
if (!
|
|
4395
|
-
const prev2 =
|
|
4396
|
-
|
|
4397
|
-
this.attach(this.legendFor(paneId),
|
|
4542
|
+
const row2 = this.rows.get(id);
|
|
4543
|
+
if (!row2 || row2.paneId === paneId) return;
|
|
4544
|
+
const prev2 = row2.paneId;
|
|
4545
|
+
row2.paneId = paneId;
|
|
4546
|
+
this.attach(this.legendFor(paneId), row2.el, row2.native);
|
|
4398
4547
|
this.syncFoldToggle();
|
|
4399
4548
|
if (prev2 !== "price") {
|
|
4400
4549
|
const lg = this.legends.get(prev2);
|
|
@@ -4424,9 +4573,9 @@ var InputsUI = class {
|
|
|
4424
4573
|
lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
|
|
4425
4574
|
const collapsed = this.paneCollapse.has(paneId);
|
|
4426
4575
|
const masterId = this.paneCollapse.get(paneId) ?? null;
|
|
4427
|
-
for (const
|
|
4428
|
-
if (
|
|
4429
|
-
|
|
4576
|
+
for (const row2 of this.rows.values()) {
|
|
4577
|
+
if (row2.paneId !== paneId) continue;
|
|
4578
|
+
row2.el.style.display = (collapsed ? row2.id !== masterId : this.legendFolded) ? "none" : "flex";
|
|
4430
4579
|
}
|
|
4431
4580
|
let top = bounds.top + 8;
|
|
4432
4581
|
if (collapsed && Number.isFinite(bounds.height)) {
|
|
@@ -4460,13 +4609,13 @@ var InputsUI = class {
|
|
|
4460
4609
|
if (wasFolded && count > 0) this.reposition();
|
|
4461
4610
|
return;
|
|
4462
4611
|
}
|
|
4463
|
-
let
|
|
4464
|
-
if (!
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
this.tip(this.rowTips, "fold",
|
|
4469
|
-
|
|
4612
|
+
let btn = this.foldToggle;
|
|
4613
|
+
if (!btn) {
|
|
4614
|
+
btn = document.createElement("button");
|
|
4615
|
+
btn.type = "button";
|
|
4616
|
+
btn.className = "vela-ind-fold";
|
|
4617
|
+
this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
|
|
4618
|
+
btn.addEventListener("click", () => {
|
|
4470
4619
|
if (this.overviewAction !== null) {
|
|
4471
4620
|
this.overviewAction();
|
|
4472
4621
|
return;
|
|
@@ -4474,24 +4623,24 @@ var InputsUI = class {
|
|
|
4474
4623
|
this.legendFolded = !this.legendFolded;
|
|
4475
4624
|
this.syncFoldToggle();
|
|
4476
4625
|
});
|
|
4477
|
-
this.foldToggle =
|
|
4626
|
+
this.foldToggle = btn;
|
|
4478
4627
|
}
|
|
4479
4628
|
const folded = this.legendFolded;
|
|
4480
|
-
|
|
4629
|
+
btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
|
|
4481
4630
|
const ink = this.theme.textColor;
|
|
4482
|
-
|
|
4483
|
-
|
|
4631
|
+
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;`;
|
|
4632
|
+
btn.replaceChildren();
|
|
4484
4633
|
const icon2 = document.createElement("span");
|
|
4485
4634
|
icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
|
|
4486
4635
|
icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
|
|
4487
|
-
|
|
4636
|
+
btn.appendChild(icon2);
|
|
4488
4637
|
if (folded) {
|
|
4489
4638
|
const label = document.createElement("span");
|
|
4490
4639
|
label.textContent = String(count);
|
|
4491
4640
|
label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
|
|
4492
|
-
|
|
4641
|
+
btn.appendChild(label);
|
|
4493
4642
|
}
|
|
4494
|
-
this.legendFor("price").appendChild(
|
|
4643
|
+
this.legendFor("price").appendChild(btn);
|
|
4495
4644
|
this.reposition();
|
|
4496
4645
|
}
|
|
4497
4646
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
@@ -4573,8 +4722,8 @@ var InputsUI = class {
|
|
|
4573
4722
|
eye.className = "vela-ind-ctl";
|
|
4574
4723
|
eye.style.cssText = LEGEND_CTL_CSS;
|
|
4575
4724
|
eye.addEventListener("click", () => {
|
|
4576
|
-
const
|
|
4577
|
-
this.onToggleVisible?.(id, Boolean(
|
|
4725
|
+
const row3 = this.rows.get(id);
|
|
4726
|
+
this.onToggleVisible?.(id, Boolean(row3?.hidden));
|
|
4578
4727
|
});
|
|
4579
4728
|
controlsEl.appendChild(eye);
|
|
4580
4729
|
eyeEl = eye;
|
|
@@ -4619,9 +4768,9 @@ var InputsUI = class {
|
|
|
4619
4768
|
controlsEl.appendChild(close);
|
|
4620
4769
|
el.appendChild(controlsEl);
|
|
4621
4770
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
4622
|
-
const
|
|
4623
|
-
this.rows.set(id,
|
|
4624
|
-
this.renderCallouts(
|
|
4771
|
+
const row2 = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
|
|
4772
|
+
this.rows.set(id, row2);
|
|
4773
|
+
this.renderCallouts(row2);
|
|
4625
4774
|
this.syncFoldToggle();
|
|
4626
4775
|
}
|
|
4627
4776
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -4631,10 +4780,10 @@ var InputsUI = class {
|
|
|
4631
4780
|
}
|
|
4632
4781
|
/** Reflect programmatic input changes (so a re-opened dialog shows current values). */
|
|
4633
4782
|
setValues(id, values, props) {
|
|
4634
|
-
const
|
|
4635
|
-
if (!
|
|
4636
|
-
|
|
4637
|
-
if (props)
|
|
4783
|
+
const row2 = this.rows.get(id);
|
|
4784
|
+
if (!row2) return;
|
|
4785
|
+
row2.values = { ...row2.values, ...values };
|
|
4786
|
+
if (props) row2.propValues = { ...row2.propValues, ...props };
|
|
4638
4787
|
}
|
|
4639
4788
|
/**
|
|
4640
4789
|
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
@@ -4642,9 +4791,9 @@ var InputsUI = class {
|
|
|
4642
4791
|
* at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
|
|
4643
4792
|
*/
|
|
4644
4793
|
setStatus(id, status) {
|
|
4645
|
-
const
|
|
4646
|
-
if (!
|
|
4647
|
-
const el =
|
|
4794
|
+
const row2 = this.rows.get(id);
|
|
4795
|
+
if (!row2) return;
|
|
4796
|
+
const el = row2.statusEl;
|
|
4648
4797
|
el.replaceChildren();
|
|
4649
4798
|
if (status === "idle") {
|
|
4650
4799
|
el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
|
|
@@ -4673,15 +4822,15 @@ var InputsUI = class {
|
|
|
4673
4822
|
}
|
|
4674
4823
|
/** Mark a row hidden/shown — swaps the eye glyph and dims the row; does NOT remove it. */
|
|
4675
4824
|
setVisible(id, visible) {
|
|
4676
|
-
const
|
|
4677
|
-
if (!
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
if (
|
|
4681
|
-
|
|
4682
|
-
|
|
4825
|
+
const row2 = this.rows.get(id);
|
|
4826
|
+
if (!row2) return;
|
|
4827
|
+
row2.hidden = !visible;
|
|
4828
|
+
row2.el.style.opacity = visible ? "1" : "0.5";
|
|
4829
|
+
if (row2.eyeEl) {
|
|
4830
|
+
row2.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
|
|
4831
|
+
row2.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
|
|
4683
4832
|
}
|
|
4684
|
-
this.syncRowActions(
|
|
4833
|
+
this.syncRowActions(row2);
|
|
4685
4834
|
}
|
|
4686
4835
|
/**
|
|
4687
4836
|
* Select a legend row (outlining it) and clear any previous selection, so at most one
|
|
@@ -4700,32 +4849,32 @@ var InputsUI = class {
|
|
|
4700
4849
|
* visible even when idle, so it can be un-hidden without hovering.
|
|
4701
4850
|
*/
|
|
4702
4851
|
setRowHighlighted(id, highlighted) {
|
|
4703
|
-
const
|
|
4704
|
-
if (!
|
|
4705
|
-
|
|
4706
|
-
this.renderRowValues(
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
this.syncRowActions(
|
|
4852
|
+
const row2 = this.rows.get(id);
|
|
4853
|
+
if (!row2) return;
|
|
4854
|
+
row2.highlighted = highlighted;
|
|
4855
|
+
this.renderRowValues(row2);
|
|
4856
|
+
row2.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
|
|
4857
|
+
row2.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
|
|
4858
|
+
this.syncRowActions(row2);
|
|
4710
4859
|
}
|
|
4711
4860
|
/**
|
|
4712
4861
|
* Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
|
|
4713
4862
|
* buttons only appear while highlighted; the eye keeps its inline-flex from
|
|
4714
4863
|
* construction and is shown/hidden with the cluster container.
|
|
4715
4864
|
*/
|
|
4716
|
-
syncRowActions(
|
|
4717
|
-
const open =
|
|
4718
|
-
|
|
4865
|
+
syncRowActions(row2) {
|
|
4866
|
+
const open = row2.highlighted;
|
|
4867
|
+
row2.controlsEl.style.display = open || row2.hidden ? "inline-flex" : "none";
|
|
4719
4868
|
if (open) {
|
|
4720
|
-
|
|
4721
|
-
for (const bubble of
|
|
4869
|
+
row2.el.appendChild(row2.statusEl);
|
|
4870
|
+
for (const bubble of row2.callouts) bubble.hidePanel();
|
|
4722
4871
|
} else {
|
|
4723
|
-
|
|
4872
|
+
row2.el.insertBefore(row2.statusEl, row2.valuesEl);
|
|
4724
4873
|
}
|
|
4725
|
-
|
|
4726
|
-
for (const child of Array.from(
|
|
4727
|
-
if (!(child instanceof HTMLElement) || child ===
|
|
4728
|
-
if (child ===
|
|
4874
|
+
row2.calloutsEl.style.display = legendCalloutsDisplay(open, row2.callouts.length > 0);
|
|
4875
|
+
for (const child of Array.from(row2.controlsEl.children)) {
|
|
4876
|
+
if (!(child instanceof HTMLElement) || child === row2.eyeEl) continue;
|
|
4877
|
+
if (child === row2.extrasEl) {
|
|
4729
4878
|
child.style.display = open ? "contents" : "none";
|
|
4730
4879
|
continue;
|
|
4731
4880
|
}
|
|
@@ -4739,9 +4888,9 @@ var InputsUI = class {
|
|
|
4739
4888
|
this.selectedId = null;
|
|
4740
4889
|
}
|
|
4741
4890
|
remove(id) {
|
|
4742
|
-
const
|
|
4743
|
-
for (const bubble of
|
|
4744
|
-
|
|
4891
|
+
const row2 = this.rows.get(id);
|
|
4892
|
+
for (const bubble of row2?.callouts ?? []) bubble.destroy();
|
|
4893
|
+
row2?.el.remove();
|
|
4745
4894
|
this.rows.delete(id);
|
|
4746
4895
|
this.disposeTips(this.rowTips, id);
|
|
4747
4896
|
this.disposeTips(this.extrasTips, id);
|
|
@@ -4749,11 +4898,11 @@ var InputsUI = class {
|
|
|
4749
4898
|
if (this.selectedId === id) this.selectedId = null;
|
|
4750
4899
|
if (this.inputsDialog.openId === id) this.inputsDialog.close();
|
|
4751
4900
|
this.syncFoldToggle();
|
|
4752
|
-
if (
|
|
4753
|
-
const lg = this.legends.get(
|
|
4901
|
+
if (row2 && row2.paneId !== "price") {
|
|
4902
|
+
const lg = this.legends.get(row2.paneId);
|
|
4754
4903
|
if (lg && lg.childElementCount === 0) {
|
|
4755
4904
|
lg.remove();
|
|
4756
|
-
this.legends.delete(
|
|
4905
|
+
this.legends.delete(row2.paneId);
|
|
4757
4906
|
}
|
|
4758
4907
|
}
|
|
4759
4908
|
}
|
|
@@ -4766,7 +4915,7 @@ var InputsUI = class {
|
|
|
4766
4915
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
4767
4916
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
4768
4917
|
for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
|
|
4769
|
-
for (const
|
|
4918
|
+
for (const row2 of this.rows.values()) for (const bubble of row2.callouts) bubble.destroy();
|
|
4770
4919
|
for (const lg of this.legends.values()) lg.remove();
|
|
4771
4920
|
this.legends.clear();
|
|
4772
4921
|
this.rows.clear();
|
|
@@ -4787,8 +4936,8 @@ var InputsUI = class {
|
|
|
4787
4936
|
this.inputsDialog.close();
|
|
4788
4937
|
}
|
|
4789
4938
|
openDialog(id) {
|
|
4790
|
-
const
|
|
4791
|
-
if (
|
|
4939
|
+
const row2 = this.rows.get(id);
|
|
4940
|
+
if (row2) this.inputsDialog.open(row2);
|
|
4792
4941
|
}
|
|
4793
4942
|
/** Idle legend-row fill — a translucent wash of the chart background, so the title keeps
|
|
4794
4943
|
* contrast when candles reach it without laying a solid block over the plot. */
|
|
@@ -4960,9 +5109,9 @@ var PaneControls = class {
|
|
|
4960
5109
|
const soloChip = stateChipRole != null && !hovered;
|
|
4961
5110
|
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
4962
5111
|
for (const child of cluster.children) {
|
|
4963
|
-
const
|
|
4964
|
-
|
|
4965
|
-
|
|
5112
|
+
const btn = child;
|
|
5113
|
+
btn.style.display = "inline-flex";
|
|
5114
|
+
btn.style.visibility = soloChip && btn.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
4966
5115
|
}
|
|
4967
5116
|
}
|
|
4968
5117
|
}
|
|
@@ -9976,12 +10125,12 @@ var MARK_DECK_STEP = 3;
|
|
|
9976
10125
|
var MARK_FAN_GAP = 4;
|
|
9977
10126
|
var MARK_HIT_PAD = 3;
|
|
9978
10127
|
var MARK_FAN_HOLD = 8;
|
|
9979
|
-
function snapMarkBar(time, barTimes,
|
|
10128
|
+
function snapMarkBar(time, barTimes, intervalMs) {
|
|
9980
10129
|
const n = barTimes.length;
|
|
9981
|
-
if (n === 0 || !(
|
|
10130
|
+
if (n === 0 || !(intervalMs > 0) || !Number.isFinite(time)) return null;
|
|
9982
10131
|
if (time < barTimes[0]) return null;
|
|
9983
10132
|
const last = barTimes[n - 1];
|
|
9984
|
-
if (time >= last +
|
|
10133
|
+
if (time >= last + intervalMs) return n - 1 + Math.floor((time - last) / intervalMs);
|
|
9985
10134
|
let lo = 0;
|
|
9986
10135
|
let hi = n - 1;
|
|
9987
10136
|
while (lo < hi) {
|
|
@@ -9989,14 +10138,14 @@ function snapMarkBar(time, barTimes, intervalMs2) {
|
|
|
9989
10138
|
if (barTimes[mid] <= time) lo = mid;
|
|
9990
10139
|
else hi = mid - 1;
|
|
9991
10140
|
}
|
|
9992
|
-
if (time < barTimes[lo] +
|
|
10141
|
+
if (time < barTimes[lo] + intervalMs) return lo;
|
|
9993
10142
|
return lo + 1;
|
|
9994
10143
|
}
|
|
9995
|
-
function clusterMarks(marks, barTimes,
|
|
10144
|
+
function clusterMarks(marks, barTimes, intervalMs, hidden) {
|
|
9996
10145
|
const byKey = /* @__PURE__ */ new Map();
|
|
9997
10146
|
marks.forEach((m, seq) => {
|
|
9998
10147
|
if (m.group !== void 0 && hidden(m.group)) return;
|
|
9999
|
-
const bar = snapMarkBar(m.time, barTimes,
|
|
10148
|
+
const bar = snapMarkBar(m.time, barTimes, intervalMs);
|
|
10000
10149
|
if (bar === null) return;
|
|
10001
10150
|
const key = `${bar}|${m.group ?? ""}`;
|
|
10002
10151
|
let c = byKey.get(key);
|
|
@@ -11603,16 +11752,16 @@ var SettingsDialog = class {
|
|
|
11603
11752
|
}
|
|
11604
11753
|
};
|
|
11605
11754
|
for (const { group: g, depth } of rows) {
|
|
11606
|
-
const
|
|
11755
|
+
const row2 = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
|
|
11607
11756
|
this.emit({ marks: { groups: { [g.id]: v } } });
|
|
11608
11757
|
refreshDimming();
|
|
11609
11758
|
});
|
|
11610
11759
|
if (depth > 0) {
|
|
11611
|
-
|
|
11612
|
-
|
|
11760
|
+
row2.classList.add("vela-sd-nested");
|
|
11761
|
+
row2.style.setProperty("--vela-sd-depth", String(depth));
|
|
11613
11762
|
}
|
|
11614
|
-
rowsById.set(g.id,
|
|
11615
|
-
body.append(sid(
|
|
11763
|
+
rowsById.set(g.id, row2);
|
|
11764
|
+
body.append(sid(row2, markGroupSettingsId(g.id)));
|
|
11616
11765
|
}
|
|
11617
11766
|
refreshDimming();
|
|
11618
11767
|
}
|
|
@@ -12434,6 +12583,16 @@ var DrawingPainter = class {
|
|
|
12434
12583
|
this.paintMagnifier(ctx, d, proj, theme);
|
|
12435
12584
|
return;
|
|
12436
12585
|
}
|
|
12586
|
+
if (d instanceof PriceActionZone) {
|
|
12587
|
+
this.paintPriceZone(ctx, d, proj, theme);
|
|
12588
|
+
this.paintLabel(ctx, d, proj, theme);
|
|
12589
|
+
return;
|
|
12590
|
+
}
|
|
12591
|
+
if (d instanceof Liquidity) {
|
|
12592
|
+
this.paintLiquidity(ctx, d, proj);
|
|
12593
|
+
this.paintLabel(ctx, d, proj, theme);
|
|
12594
|
+
return;
|
|
12595
|
+
}
|
|
12437
12596
|
if (d instanceof PatternDrawing) {
|
|
12438
12597
|
this.paintPattern(ctx, d, proj, theme);
|
|
12439
12598
|
return;
|
|
@@ -12476,9 +12635,11 @@ var DrawingPainter = class {
|
|
|
12476
12635
|
case "arrow": {
|
|
12477
12636
|
const pts = d.handlePoints(proj);
|
|
12478
12637
|
if (pts.length < 2) return;
|
|
12638
|
+
const ext = d instanceof TrendLine ? d.extension() : "none";
|
|
12639
|
+
const [ex1, ey1, ex2, ey2] = extendEndpoints(pts[0][0], pts[0][1], pts[1][0], pts[1][1], ext, proj.width, proj.height);
|
|
12479
12640
|
this.stroke(ctx, d.style, () => {
|
|
12480
|
-
ctx.moveTo(
|
|
12481
|
-
ctx.lineTo(
|
|
12641
|
+
ctx.moveTo(ex1, ey1);
|
|
12642
|
+
ctx.lineTo(ex2, ey2);
|
|
12482
12643
|
});
|
|
12483
12644
|
if (d.style.arrowRight) this.paintArrowhead(ctx, pts[0], pts[1], d.style);
|
|
12484
12645
|
if (d.style.arrowLeft) this.paintArrowhead(ctx, pts[1], pts[0], d.style);
|
|
@@ -12620,9 +12781,15 @@ var DrawingPainter = class {
|
|
|
12620
12781
|
ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
|
|
12621
12782
|
ctx.textBaseline = "top";
|
|
12622
12783
|
ctx.textAlign = layout.align;
|
|
12623
|
-
ctx.fillStyle = text.color ?? theme.textColor;
|
|
12624
12784
|
const lh = labelLineHeight(fs);
|
|
12625
12785
|
const lines = text.value.split("\n");
|
|
12786
|
+
if (d.type !== "text" && d.type !== "box") {
|
|
12787
|
+
ctx.lineJoin = "round";
|
|
12788
|
+
ctx.lineWidth = 3;
|
|
12789
|
+
ctx.strokeStyle = theme.background;
|
|
12790
|
+
lines.forEach((line, i) => ctx.strokeText(line, layout.x, layout.top + i * lh));
|
|
12791
|
+
}
|
|
12792
|
+
ctx.fillStyle = text.color ?? theme.textColor;
|
|
12626
12793
|
lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
|
|
12627
12794
|
if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
|
|
12628
12795
|
ctx.restore();
|
|
@@ -12677,6 +12844,78 @@ var DrawingPainter = class {
|
|
|
12677
12844
|
ctx.textAlign = "left";
|
|
12678
12845
|
ctx.textBaseline = "alphabetic";
|
|
12679
12846
|
}
|
|
12847
|
+
/** Paint a price-action zone (fair value gap, order block): the box and its halfway line,
|
|
12848
|
+
* quieter once it has run its course, a dot where it stopped, and — once price flips it —
|
|
12849
|
+
* the dashed box it carries on as. With no zone under the anchor, a faint tick marks it. */
|
|
12850
|
+
paintPriceZone(ctx, d, proj, theme) {
|
|
12851
|
+
const z = d.zone(proj);
|
|
12852
|
+
if (!z) {
|
|
12853
|
+
const a = d.anchors[0];
|
|
12854
|
+
if (!a) return;
|
|
12855
|
+
const x = Math.round(proj.xOf(a.time)) + 0.5;
|
|
12856
|
+
this.stroke(ctx, { lineColor: withAlpha(theme.textColor, 0.35), lineWidth: 1, lineStyle: "dashed" }, () => {
|
|
12857
|
+
ctx.moveTo(x, 0);
|
|
12858
|
+
ctx.lineTo(x, proj.height);
|
|
12859
|
+
});
|
|
12860
|
+
return;
|
|
12861
|
+
}
|
|
12862
|
+
const h = z.yBottom - z.yTop;
|
|
12863
|
+
const stroke = toHex6(z.fill, z.fill);
|
|
12864
|
+
ctx.save();
|
|
12865
|
+
ctx.globalAlpha *= z.faded ? 0.6 : 1;
|
|
12866
|
+
ctx.fillStyle = z.fill;
|
|
12867
|
+
ctx.fillRect(z.x1, z.yTop, z.x2 - z.x1, h);
|
|
12868
|
+
if (z.border) {
|
|
12869
|
+
ctx.lineWidth = 1;
|
|
12870
|
+
ctx.setLineDash([]);
|
|
12871
|
+
ctx.strokeStyle = stroke;
|
|
12872
|
+
ctx.strokeRect(Math.round(z.x1) + 0.5, Math.round(z.yTop) + 0.5, Math.round(z.x2 - z.x1), Math.round(h));
|
|
12873
|
+
}
|
|
12874
|
+
if (z.midY != null) {
|
|
12875
|
+
const y = Math.round(z.midY) + 0.5;
|
|
12876
|
+
this.stroke(ctx, { lineColor: stroke, lineWidth: 1, lineStyle: "dashed" }, () => {
|
|
12877
|
+
ctx.moveTo(z.x1, y);
|
|
12878
|
+
ctx.lineTo(z.x2, y);
|
|
12879
|
+
});
|
|
12880
|
+
}
|
|
12881
|
+
ctx.restore();
|
|
12882
|
+
if (z.flip) {
|
|
12883
|
+
const f = z.flip;
|
|
12884
|
+
ctx.save();
|
|
12885
|
+
ctx.fillStyle = f.fill;
|
|
12886
|
+
ctx.fillRect(f.x1, z.yTop, f.x2 - f.x1, h);
|
|
12887
|
+
ctx.lineWidth = 1;
|
|
12888
|
+
ctx.strokeStyle = toHex6(f.fill, f.fill);
|
|
12889
|
+
ctx.setLineDash([5, 4]);
|
|
12890
|
+
ctx.strokeRect(Math.round(f.x1) + 0.5, Math.round(z.yTop) + 0.5, Math.round(f.x2 - f.x1), Math.round(h));
|
|
12891
|
+
ctx.restore();
|
|
12892
|
+
}
|
|
12893
|
+
if (z.marker) {
|
|
12894
|
+
ctx.beginPath();
|
|
12895
|
+
ctx.arc(z.marker.x, z.marker.y, 3.5, 0, Math.PI * 2);
|
|
12896
|
+
ctx.fillStyle = stroke;
|
|
12897
|
+
ctx.fill();
|
|
12898
|
+
}
|
|
12899
|
+
}
|
|
12900
|
+
/** Paint a liquidity level: the line to where it stopped, and a cross where it was swept. */
|
|
12901
|
+
paintLiquidity(ctx, d, proj) {
|
|
12902
|
+
const l = d.line(proj);
|
|
12903
|
+
if (!l) return;
|
|
12904
|
+
const y = Math.round(l.y) + 0.5;
|
|
12905
|
+
this.stroke(ctx, d.style, () => {
|
|
12906
|
+
ctx.moveTo(l.x1, y);
|
|
12907
|
+
ctx.lineTo(l.x2, y);
|
|
12908
|
+
});
|
|
12909
|
+
if (l.sweep) {
|
|
12910
|
+
const { x, y: sy } = l.sweep;
|
|
12911
|
+
this.stroke(ctx, { lineColor: d.style.lineColor, lineWidth: 1.75, lineStyle: "solid" }, () => {
|
|
12912
|
+
ctx.moveTo(x - 4, sy - 4);
|
|
12913
|
+
ctx.lineTo(x + 4, sy + 4);
|
|
12914
|
+
ctx.moveTo(x + 4, sy - 4);
|
|
12915
|
+
ctx.lineTo(x - 4, sy + 4);
|
|
12916
|
+
});
|
|
12917
|
+
}
|
|
12918
|
+
}
|
|
12680
12919
|
/** Paint a concentric-ring fib tool (circles / arcs / wedge): each enabled level as an arc of
|
|
12681
12920
|
* radius R0·ratio over the tool's angular sweep, plus any bounding lines + a ratio label per ring. */
|
|
12682
12921
|
paintRadialFib(ctx, d, proj, theme) {
|
|
@@ -13146,15 +13385,15 @@ var DrawingPainter = class {
|
|
|
13146
13385
|
const { profile, maxW, anchorX, grow, yEdges } = L;
|
|
13147
13386
|
if (profile.maxTotal <= 0 || maxW <= 0) return;
|
|
13148
13387
|
for (let k = 0; k < profile.rows.length; k += 1) {
|
|
13149
|
-
const
|
|
13150
|
-
const total =
|
|
13388
|
+
const row2 = profile.rows[k];
|
|
13389
|
+
const total = row2.up + row2.down;
|
|
13151
13390
|
if (total <= 0) continue;
|
|
13152
13391
|
const yA = yEdges[k];
|
|
13153
13392
|
const yB = yEdges[k + 1];
|
|
13154
13393
|
const yTop = Math.min(yA, yB);
|
|
13155
13394
|
const h = Math.max(1, Math.abs(yB - yA) - 1);
|
|
13156
13395
|
const w2 = total / profile.maxTotal * maxW;
|
|
13157
|
-
const upW =
|
|
13396
|
+
const upW = row2.up / total * w2;
|
|
13158
13397
|
const inVa = k >= profile.vaFrom && k <= profile.vaTo;
|
|
13159
13398
|
const upColor = inVa ? s.vaUpColor : s.upColor;
|
|
13160
13399
|
const downColor = inVa ? s.vaDownColor : s.downColor;
|
|
@@ -13619,11 +13858,20 @@ var DrawingPainter = class {
|
|
|
13619
13858
|
ctx.arc(x1, y1, arcR, Math.min(0, end), Math.max(0, end));
|
|
13620
13859
|
ctx.stroke();
|
|
13621
13860
|
}
|
|
13622
|
-
|
|
13861
|
+
const label = `${angleDeg.toFixed(1)}\xB0`;
|
|
13862
|
+
const lx = x1 + arcR + 6;
|
|
13863
|
+
const ly = dy < 0 ? y1 + 14 : y1 - 6;
|
|
13623
13864
|
ctx.font = `11px ${theme.fontFamily}`;
|
|
13624
13865
|
ctx.textAlign = "left";
|
|
13625
13866
|
ctx.textBaseline = "alphabetic";
|
|
13626
|
-
ctx.
|
|
13867
|
+
ctx.save();
|
|
13868
|
+
ctx.lineJoin = "round";
|
|
13869
|
+
ctx.lineWidth = 3;
|
|
13870
|
+
ctx.strokeStyle = theme.background;
|
|
13871
|
+
ctx.strokeText(label, lx, ly);
|
|
13872
|
+
ctx.restore();
|
|
13873
|
+
ctx.fillStyle = color;
|
|
13874
|
+
ctx.fillText(label, lx, ly);
|
|
13627
13875
|
}
|
|
13628
13876
|
/** Paint a fixed-size arrow marker as a filled glyph at its anchor. */
|
|
13629
13877
|
paintArrowMark(ctx, d, proj) {
|
|
@@ -13766,6 +14014,12 @@ function labelLayout(d, proj) {
|
|
|
13766
14014
|
const pts = d.handlePoints(proj);
|
|
13767
14015
|
const lines = text.value.split("\n").length;
|
|
13768
14016
|
const lh = labelLineHeight(namedFontSize(text.size));
|
|
14017
|
+
if (d instanceof ArrowMark) {
|
|
14018
|
+
const b = d.bounds(proj);
|
|
14019
|
+
if (!b) return null;
|
|
14020
|
+
const x = b.x + b.w / 2;
|
|
14021
|
+
return d.type === "arrowmarkdown" ? { x, top: b.y - 4 - lh * lines, align: "center" } : { x, top: b.y + b.h + 4, align: "center" };
|
|
14022
|
+
}
|
|
13769
14023
|
switch (d.type) {
|
|
13770
14024
|
case "text":
|
|
13771
14025
|
return pts[0] ? { x: pts[0][0] + 2, top: pts[0][1] + 2, align: "left" } : null;
|
|
@@ -13780,11 +14034,13 @@ function labelLayout(d, proj) {
|
|
|
13780
14034
|
if (pts.length < 2) return null;
|
|
13781
14035
|
const [x1, y1] = pts[0];
|
|
13782
14036
|
const [x2, y2] = pts[1];
|
|
14037
|
+
const t = text.place === "start" ? 0.12 : text.place === "end" ? 0.88 : 0.5;
|
|
14038
|
+
const below = text.side === "below";
|
|
13783
14039
|
return {
|
|
13784
14040
|
x: 0,
|
|
13785
|
-
top: -6 - lh * lines,
|
|
14041
|
+
top: below ? 6 : -6 - lh * lines,
|
|
13786
14042
|
align: "center",
|
|
13787
|
-
rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx:
|
|
14043
|
+
rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: x1 + (x2 - x1) * t, cy: y1 + (y2 - y1) * t }
|
|
13788
14044
|
};
|
|
13789
14045
|
}
|
|
13790
14046
|
case "box": {
|
|
@@ -13965,8 +14221,9 @@ var DrawingInteraction = class {
|
|
|
13965
14221
|
const tool = this.deps.activeTool();
|
|
13966
14222
|
if (tool) {
|
|
13967
14223
|
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
13968
|
-
const draft = createDrawing(tool, { paneId, anchors: [this.resolve(x, y, paneId, mode)]
|
|
14224
|
+
const draft = createDrawing(tool, { paneId, anchors: [this.resolve(x, y, paneId, mode)] });
|
|
13969
14225
|
if (!draft) return;
|
|
14226
|
+
applyToolDefaults(draft, this.deps.toolDefaults());
|
|
13970
14227
|
this.state = { kind: "placing", draft, need: draft.anchorSchema().max, cursor: null };
|
|
13971
14228
|
if (draft.placementMode() === "click" && draft.anchors.length >= this.state.need) this.finalize();
|
|
13972
14229
|
else this.deps.changed();
|
|
@@ -14165,7 +14422,8 @@ var DrawingInteraction = class {
|
|
|
14165
14422
|
if (this.state.kind !== "placing") return null;
|
|
14166
14423
|
const { draft, cursor } = this.state;
|
|
14167
14424
|
if (!cursor) return draft.anchors.length >= 1 && draft.type === "hline" ? draft : null;
|
|
14168
|
-
|
|
14425
|
+
const { paneId, style, text, props } = draft.serialize();
|
|
14426
|
+
return createDrawing(draft.type, { paneId, anchors: [...draft.anchors, this.placementAnchor(draft, cursor)], style, text, props });
|
|
14169
14427
|
}
|
|
14170
14428
|
/** Map a raw placement cursor to the anchor the drawing actually stores.
|
|
14171
14429
|
* Parallel channel: its 3rd anchor is the parallel-line price measured AT THE
|
|
@@ -14269,7 +14527,7 @@ function applyFree(anchor, pt, free) {
|
|
|
14269
14527
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
14270
14528
|
}
|
|
14271
14529
|
|
|
14272
|
-
// src/renderers/native/drawings/
|
|
14530
|
+
// src/renderers/native/drawings/DrawingSettingsSections.ts
|
|
14273
14531
|
var LEVEL_UNITS = [
|
|
14274
14532
|
{ value: "price", label: "Price" },
|
|
14275
14533
|
{ value: "points", label: "Points" }
|
|
@@ -14278,15 +14536,319 @@ var FRVP_ANCHOR = [
|
|
|
14278
14536
|
{ value: "right", label: "Right" },
|
|
14279
14537
|
{ value: "left", label: "Left" }
|
|
14280
14538
|
];
|
|
14281
|
-
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14539
|
+
function buildPositionSection(grid, drawing, actions, theme) {
|
|
14540
|
+
const live = () => {
|
|
14541
|
+
const d = actions.resolve();
|
|
14542
|
+
return d instanceof PositionTool ? d : drawing;
|
|
14543
|
+
};
|
|
14544
|
+
const refreshers = [];
|
|
14545
|
+
const refreshAll = () => refreshers.forEach((f) => f());
|
|
14546
|
+
const fmt = (n) => Math.round(n * 1e8) / 1e8;
|
|
14547
|
+
const numberRow = (label, path, clamp) => {
|
|
14548
|
+
const ctrl = buildFieldControl({
|
|
14549
|
+
kind: "number",
|
|
14550
|
+
value: fmt(live()[path]),
|
|
14551
|
+
min: clamp?.min,
|
|
14552
|
+
max: clamp?.max,
|
|
14553
|
+
step: clamp?.step,
|
|
14554
|
+
integer: clamp?.integer,
|
|
14555
|
+
fill: false,
|
|
14556
|
+
commit: "blur",
|
|
14557
|
+
onChange: (n) => {
|
|
14558
|
+
actions.patch({ [path]: n });
|
|
14559
|
+
refreshAll();
|
|
14560
|
+
}
|
|
14561
|
+
});
|
|
14562
|
+
refreshers.push(() => {
|
|
14563
|
+
if (document.activeElement !== ctrl.el.querySelector("input")) ctrl.setValue?.(fmt(live()[path]));
|
|
14564
|
+
});
|
|
14565
|
+
grid.appendChild(fieldRow({ label, control: ctrl.el }));
|
|
14566
|
+
};
|
|
14567
|
+
const levelRow = (label, path, level) => {
|
|
14568
|
+
let mode = "price";
|
|
14569
|
+
const display = () => fmt(live().levelDisplayValue(level, mode));
|
|
14570
|
+
const ni = buildFieldControl({
|
|
14571
|
+
kind: "number",
|
|
14572
|
+
value: display(),
|
|
14573
|
+
fill: false,
|
|
14574
|
+
commit: "blur",
|
|
14575
|
+
onChange: (n) => {
|
|
14576
|
+
actions.patch({ [path]: live().levelPriceFromDisplay(level, mode, n) });
|
|
14577
|
+
refreshAll();
|
|
14578
|
+
}
|
|
14579
|
+
});
|
|
14580
|
+
const sel = buildFieldControl({
|
|
14581
|
+
kind: "select",
|
|
14582
|
+
options: LEVEL_UNITS,
|
|
14583
|
+
value: mode,
|
|
14584
|
+
fill: false,
|
|
14585
|
+
theme,
|
|
14586
|
+
onChange: (v) => {
|
|
14587
|
+
mode = v;
|
|
14588
|
+
ni.setValue?.(display());
|
|
14589
|
+
}
|
|
14590
|
+
});
|
|
14591
|
+
refreshers.push(() => {
|
|
14592
|
+
if (document.activeElement !== ni.el.querySelector("input")) ni.setValue?.(display());
|
|
14593
|
+
});
|
|
14594
|
+
grid.appendChild(fieldRow({ label, control: [ni.el, sel.el] }));
|
|
14595
|
+
};
|
|
14596
|
+
grid.appendChild(fieldSection("Account", { variant: "inputs", first: true }));
|
|
14597
|
+
numberRow("Risk %", "riskPercent", { min: 0, max: 100, step: 0.1 });
|
|
14598
|
+
numberRow("Account balance", "accountBalance", { min: 0, max: 1e12, step: 1, integer: true });
|
|
14599
|
+
numberRow("Position size", "quantity");
|
|
14600
|
+
grid.appendChild(fieldSection("Levels", { variant: "inputs" }));
|
|
14601
|
+
const dir = buildFieldControl({
|
|
14602
|
+
kind: "select",
|
|
14603
|
+
options: DIRECTION_OPTIONS,
|
|
14604
|
+
value: live().direction,
|
|
14605
|
+
fill: false,
|
|
14606
|
+
theme,
|
|
14607
|
+
onChange: (v) => {
|
|
14608
|
+
actions.patch({ direction: v });
|
|
14609
|
+
refreshAll();
|
|
14610
|
+
}
|
|
14611
|
+
});
|
|
14612
|
+
refreshers.push(() => dir.setValue?.(live().direction));
|
|
14613
|
+
grid.appendChild(fieldRow({ label: "Direction", control: dir.el }));
|
|
14614
|
+
numberRow("Entry price", "entryPrice");
|
|
14615
|
+
levelRow("Stop", "stopPrice", "stop");
|
|
14616
|
+
levelRow("Target", "targetPrice", "target");
|
|
14617
|
+
grid.appendChild(fieldSection("Display", { variant: "inputs" }));
|
|
14618
|
+
const toggles = [
|
|
14619
|
+
["Show text", "showText"],
|
|
14620
|
+
["Show direction & ratio", "showHeader"],
|
|
14621
|
+
["Show loss & size", "showLossSize"],
|
|
14622
|
+
["Show target label", "showTargetLabel"],
|
|
14623
|
+
["Show stop label", "showStopLabel"],
|
|
14624
|
+
["Show level prices", "showPrices"]
|
|
14625
|
+
];
|
|
14626
|
+
for (const [label, path] of toggles) {
|
|
14627
|
+
grid.appendChild(fieldRow({
|
|
14628
|
+
label,
|
|
14629
|
+
bool: true,
|
|
14630
|
+
toggle: {
|
|
14631
|
+
checked: live()[path],
|
|
14632
|
+
onChange: (v) => {
|
|
14633
|
+
actions.patch({ [path]: v });
|
|
14634
|
+
refreshAll();
|
|
14635
|
+
},
|
|
14636
|
+
get: () => live()[path]
|
|
14637
|
+
}
|
|
14638
|
+
}));
|
|
14639
|
+
}
|
|
14640
|
+
const summary = document.createElement("div");
|
|
14641
|
+
summary.className = "vela-field-span";
|
|
14642
|
+
summary.style.cssText = "opacity:0.7;font-size:11px;line-height:1.4;border-top:1px solid var(--vela-border);padding-top:6px;font-variant-numeric:tabular-nums;";
|
|
14643
|
+
refreshers.push(() => {
|
|
14644
|
+
const d = live();
|
|
14645
|
+
summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
|
|
14646
|
+
});
|
|
14647
|
+
grid.appendChild(summary);
|
|
14648
|
+
refreshAll();
|
|
14649
|
+
}
|
|
14650
|
+
function buildProfileSection(grid, drawing, actions, theme) {
|
|
14651
|
+
const styleOf = () => {
|
|
14652
|
+
const d = actions.resolve();
|
|
14653
|
+
return d instanceof FixedRangeVolumeProfile ? d.frvp : drawing.frvp;
|
|
14654
|
+
};
|
|
14655
|
+
const s = styleOf();
|
|
14656
|
+
const numberRow = (label, path, min, max, step, integer = true) => {
|
|
14657
|
+
grid.appendChild(fieldRow({
|
|
14658
|
+
label,
|
|
14659
|
+
control: buildFieldControl({
|
|
14660
|
+
kind: "number",
|
|
14661
|
+
value: s[path],
|
|
14662
|
+
min,
|
|
14663
|
+
max,
|
|
14664
|
+
step,
|
|
14665
|
+
integer,
|
|
14666
|
+
fill: false,
|
|
14667
|
+
commit: "blur",
|
|
14668
|
+
onChange: (n) => actions.patch({ [`frvp.${path}`]: n })
|
|
14669
|
+
}).el
|
|
14670
|
+
}));
|
|
14671
|
+
};
|
|
14672
|
+
numberRow("Rows", "rows", 1, 500, 1);
|
|
14673
|
+
numberRow("Value Area", "valueAreaPct", 0, 100, 1);
|
|
14674
|
+
numberRow("Width %", "widthPct", 0, 100, 1);
|
|
14675
|
+
grid.appendChild(fieldRow({
|
|
14676
|
+
label: "Anchor",
|
|
14677
|
+
control: buildFieldControl({
|
|
14678
|
+
kind: "select",
|
|
14679
|
+
options: FRVP_ANCHOR,
|
|
14680
|
+
value: s.anchor,
|
|
14681
|
+
fill: false,
|
|
14682
|
+
theme,
|
|
14683
|
+
onChange: (v) => actions.patch({ "frvp.anchor": v })
|
|
14684
|
+
}).el
|
|
14685
|
+
}));
|
|
14686
|
+
const colorRow = (label, path) => {
|
|
14687
|
+
grid.appendChild(fieldRow({
|
|
14688
|
+
label,
|
|
14689
|
+
fit: true,
|
|
14690
|
+
control: buildFieldControl({
|
|
14691
|
+
kind: "color",
|
|
14692
|
+
theme,
|
|
14693
|
+
get: () => styleOf()[path],
|
|
14694
|
+
onChange: (v) => actions.patch({ [`frvp.${path}`]: v })
|
|
14695
|
+
}).el
|
|
14696
|
+
}));
|
|
14697
|
+
};
|
|
14698
|
+
colorRow("Up Volume", "upColor");
|
|
14699
|
+
colorRow("Down Volume", "downColor");
|
|
14700
|
+
colorRow("Value Area Up", "vaUpColor");
|
|
14701
|
+
colorRow("Value Area Down", "vaDownColor");
|
|
14702
|
+
const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
|
|
14703
|
+
const levelRow = (label, showPath, colorPath, stylePath) => {
|
|
14704
|
+
const row2 = document.createElement("div");
|
|
14705
|
+
row2.className = "vela-field-span";
|
|
14706
|
+
row2.style.cssText = "display:flex;align-items:center;gap:8px;";
|
|
14707
|
+
const sw = buildFieldControl({
|
|
14708
|
+
kind: "switch",
|
|
14709
|
+
checked: Boolean(s[showPath]),
|
|
14710
|
+
onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
|
|
14711
|
+
});
|
|
14712
|
+
const lbl = document.createElement("span");
|
|
14713
|
+
lbl.className = "vela-field-label";
|
|
14714
|
+
lbl.style.flex = "1";
|
|
14715
|
+
lbl.textContent = label;
|
|
14716
|
+
let cur = s[colorPath] ?? contrastColor(theme.background);
|
|
14717
|
+
const col = buildFieldControl({
|
|
14718
|
+
kind: "color",
|
|
14719
|
+
theme,
|
|
14720
|
+
get: () => cur,
|
|
14721
|
+
onChange: (v) => {
|
|
14722
|
+
cur = v;
|
|
14723
|
+
actions.patch({ [`frvp.${colorPath}`]: v });
|
|
14724
|
+
}
|
|
14725
|
+
});
|
|
14726
|
+
const style = buildFieldControl({
|
|
14727
|
+
kind: "select",
|
|
14728
|
+
options: styles,
|
|
14729
|
+
value: s[stylePath],
|
|
14730
|
+
fill: false,
|
|
14731
|
+
theme,
|
|
14732
|
+
onChange: (v) => actions.patch({ [`frvp.${stylePath}`]: v })
|
|
14733
|
+
});
|
|
14734
|
+
row2.append(sw.el, lbl, col.el, style.el);
|
|
14735
|
+
grid.appendChild(row2);
|
|
14736
|
+
};
|
|
14737
|
+
levelRow("VAH", "showVah", "vahColor", "vahStyle");
|
|
14738
|
+
levelRow("VAL", "showVal", "valColor", "valStyle");
|
|
14739
|
+
levelRow("POC", "showPoc", "pocColor", "pocStyle");
|
|
14740
|
+
levelRow("Developing POC", "showDevelopingPoc", "developingPocColor", "developingPocStyle");
|
|
14741
|
+
levelRow("Developing VA", "showDevelopingVa", "developingVaColor", "developingVaStyle");
|
|
14742
|
+
}
|
|
14743
|
+
|
|
14744
|
+
// src/renderers/native/drawings/DrawingSettingsPanel.ts
|
|
14745
|
+
var LINE_LABEL_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle"]);
|
|
14746
|
+
var NO_POINTS = /* @__PURE__ */ new Set(["freehand", "highlighter", "vline", "position"]);
|
|
14747
|
+
var MAX_POINTS = 8;
|
|
14748
|
+
var POSITION_PATHS = /* @__PURE__ */ new Set(["riskPercent", "accountBalance", "quantity", "direction", "entryPrice", "stopPrice", "targetPrice", "showText", "showHeader", "showLossSize", "showTargetLabel", "showStopLabel", "showPrices"]);
|
|
14749
|
+
var LEVEL_PATHS = /* @__PURE__ */ new Set(["showRatios"]);
|
|
14750
|
+
var INTRADAY = ["s", "1", "5", "15", "60", "240"];
|
|
14751
|
+
var HIGHER = ["60", "240", "D", "W", "M"];
|
|
14752
|
+
var openFolds = /* @__PURE__ */ new Map();
|
|
14753
|
+
var STYLE_ID4 = "vela-dsp-styles";
|
|
14754
|
+
var STYLE_REV = "1";
|
|
14755
|
+
var CHEVRON = '<svg viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 2.5 3.5 3.5-3.5 3.5"/></svg>';
|
|
14756
|
+
var CHEVRON_DOWN = '<svg viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m2.5 4.5 3.5 3.5 3.5-3.5"/></svg>';
|
|
14757
|
+
var CROSS = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="m4.5 4.5 7 7M11.5 4.5l-7 7"/></svg>';
|
|
14758
|
+
var EXTEND_ICONS = {
|
|
14759
|
+
none: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M5 7h8"/><circle cx="5" cy="7" r="1.5" fill="currentColor"/><circle cx="13" cy="7" r="1.5" fill="currentColor"/></svg>',
|
|
14760
|
+
left: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M1 7h12"/><path d="m3.5 4.5-2.5 2.5 2.5 2.5"/><circle cx="13" cy="7" r="1.5" fill="currentColor"/></svg>',
|
|
14761
|
+
right: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M5 7h12"/><path d="m14.5 4.5 2.5 2.5-2.5 2.5"/><circle cx="5" cy="7" r="1.5" fill="currentColor"/></svg>',
|
|
14762
|
+
both: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M1 7h16"/><path d="m3.5 4.5-2.5 2.5 2.5 2.5M14.5 4.5l2.5 2.5-2.5 2.5"/></svg>'
|
|
14285
14763
|
};
|
|
14286
|
-
var
|
|
14287
|
-
|
|
14764
|
+
var widthIcon = (w) => `<svg viewBox="0 0 18 14" width="18" height="14"><rect x="2" y="${7 - w / 2}" width="14" height="${w}" rx="${w / 2}" fill="currentColor"/></svg>`;
|
|
14765
|
+
var dashIcon = (s) => `<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="${s === "dashed" ? "4 3" : s === "dotted" ? "0.1 3.5" : "0"}"><path d="M2 7h14"/></svg>`;
|
|
14766
|
+
function ensureStyles4() {
|
|
14767
|
+
if (typeof document === "undefined") return;
|
|
14768
|
+
const existing = document.getElementById(STYLE_ID4);
|
|
14769
|
+
if (existing?.dataset.rev === STYLE_REV) return;
|
|
14770
|
+
const s = existing ?? document.createElement("style");
|
|
14771
|
+
s.id = STYLE_ID4;
|
|
14772
|
+
s.dataset.rev = STYLE_REV;
|
|
14773
|
+
s.textContent = `
|
|
14774
|
+
.vela-dsp-pass { pointer-events: none !important; }
|
|
14775
|
+
.vela-dialog.vela-dsp { pointer-events: auto; width: 360px; min-width: 0; max-width: calc(100% - 24px); max-height: calc(100% - 24px); font-size: 13px; }
|
|
14776
|
+
.vela-dsp .vela-dialog-header { padding: 8px 8px 8px 16px; gap: 6px; }
|
|
14777
|
+
.vela-dsp .vela-dialog-title { font-size: 15px; line-height: 22px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14778
|
+
.vela-dsp .vela-dialog-footer { padding: 10px 12px; display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--vela-border); }
|
|
14779
|
+
.vela-dsp .vela-dialog-btn { height: 30px; font-size: 13px; }
|
|
14780
|
+
.vela-dsp-scroll { overflow-y: auto; overflow-x: hidden; flex: 1 1 auto; min-height: 0; }
|
|
14781
|
+
.vela-dsp-block { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
|
|
14782
|
+
.vela-dsp-block + .vela-dsp-block, .vela-dsp-fold { border-top: 1px solid var(--vela-border); }
|
|
14783
|
+
.vela-dsp-h { font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--vela-fg-muted); }
|
|
14784
|
+
.vela-dsp-row { display: flex; align-items: center; gap: 8px; min-height: 30px; }
|
|
14785
|
+
.vela-dsp-row[hidden] { display: none; }
|
|
14786
|
+
.vela-dsp-lab { flex: 0 0 96px; color: var(--vela-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
14787
|
+
.vela-dsp-row > .vela-dsp-lab + * { min-width: 0; }
|
|
14788
|
+
.vela-dsp-spacer { flex: 1; }
|
|
14789
|
+
.vela-dsp-fold-h { all: unset; box-sizing: border-box; width: 100%; display: flex; align-items: center; gap: 8px; padding: 11px 16px; cursor: pointer; }
|
|
14790
|
+
.vela-dsp-fold-h:hover { background: var(--vela-hover); }
|
|
14791
|
+
.vela-dsp-fold-h:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: -2px; }
|
|
14792
|
+
.vela-dsp-fold-t { font-weight: 600; color: var(--vela-fg-bright); white-space: nowrap; }
|
|
14793
|
+
.vela-dsp-fold-s { flex: 1; min-width: 0; text-align: right; color: var(--vela-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
|
|
14794
|
+
.vela-dsp-chev { display: flex; color: var(--vela-fg-muted); transition: transform var(--vela-dur-fast) ease; }
|
|
14795
|
+
.vela-dsp-fold[data-open] .vela-dsp-chev { transform: rotate(90deg); }
|
|
14796
|
+
.vela-dsp-fold-b { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
14797
|
+
.vela-dsp-fold:not([data-open]) .vela-dsp-fold-b { display: none; }
|
|
14798
|
+
.vela-dsp-seg { display: inline-flex; flex: none; border: 1px solid var(--vela-border-strong); border-radius: 6px; overflow: hidden; }
|
|
14799
|
+
.vela-dsp-seg[data-full] { display: flex; flex: 1; }
|
|
14800
|
+
.vela-dsp-seg > button { all: unset; box-sizing: border-box; cursor: pointer; height: 28px; min-width: 32px; padding: 0 8px; display: flex; align-items: center; justify-content: center; color: var(--vela-fg-muted); white-space: nowrap; }
|
|
14801
|
+
.vela-dsp-seg[data-full] > button { flex: 1 1 0; min-width: 0; }
|
|
14802
|
+
.vela-dsp-seg > button + button { border-left: 1px solid var(--vela-border); }
|
|
14803
|
+
.vela-dsp-seg > button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
14804
|
+
.vela-dsp-seg > button[aria-pressed='true'] { background: var(--vela-active); color: var(--vela-fg-bright); }
|
|
14805
|
+
.vela-dsp-seg > button:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: -2px; }
|
|
14806
|
+
.vela-dsp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
14807
|
+
.vela-dsp-chip { all: unset; box-sizing: border-box; cursor: pointer; height: 26px; padding: 0 10px; border-radius: 13px; border: 1px solid var(--vela-border-strong); color: var(--vela-fg-muted); display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
14808
|
+
.vela-dsp-chip:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
|
|
14809
|
+
.vela-dsp-chip[aria-pressed='true'] { background: var(--vela-active); border-color: transparent; color: var(--vela-fg-bright); }
|
|
14810
|
+
.vela-dsp-chip:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: 1px; }
|
|
14811
|
+
.vela-dsp-note { font-size: 12px; color: var(--vela-fg-muted); line-height: 1.45; }
|
|
14812
|
+
.vela-dsp-pt { display: grid; grid-template-columns: 34px minmax(0, 120px) minmax(0, 1fr); align-items: center; gap: 8px; min-height: 30px; }
|
|
14813
|
+
.vela-dsp-pt-k { font-weight: 600; color: var(--vela-fg-muted); }
|
|
14814
|
+
.vela-dsp-pt-t { color: var(--vela-fg-muted); font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
14815
|
+
.vela-dsp-tf[hidden], .vela-dsp-note[hidden] { display: none; }
|
|
14816
|
+
.vela-dsp-tf { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 4px; }
|
|
14817
|
+
.vela-dsp-tf > button { all: unset; box-sizing: border-box; cursor: pointer; position: relative; height: 28px; border-radius: 6px; border: 1px solid var(--vela-border-strong); color: var(--vela-fg-muted); display: flex; align-items: center; justify-content: center; font-variant-numeric: tabular-nums; }
|
|
14818
|
+
.vela-dsp-tf > button:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
|
|
14819
|
+
.vela-dsp-tf > button[aria-pressed='true'] { background: var(--vela-active); border-color: transparent; color: var(--vela-fg-bright); }
|
|
14820
|
+
.vela-dsp-tf > button[data-here]::after { content: ''; position: absolute; top: 3px; right: 3px; width: 5px; height: 5px; border-radius: 50%; background: var(--vela-selected-bg); }
|
|
14821
|
+
.vela-dsp-grid { padding: 0; }
|
|
14822
|
+
.vela-dsp-lv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 14px; }
|
|
14823
|
+
.vela-dsp-lv-c { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
14824
|
+
.vela-dsp-lv-c .vela-num:not([data-fill])[data-compact] { width: 76px; }
|
|
14825
|
+
.vela-dsp-lv-c .vela-num input { height: 28px; }
|
|
14826
|
+
.vela-dsp-lv-c[data-off] .vela-num input { color: var(--vela-fg-muted); }
|
|
14827
|
+
.vela-dsp-style { all: unset; box-sizing: border-box; cursor: pointer; flex: none; height: 28px; max-width: 150px; padding: 0 6px 0 10px; border-radius: 6px; border: 1px solid var(--vela-border-strong); display: inline-flex; align-items: center; gap: 4px; color: var(--vela-fg); font-size: 12px; }
|
|
14828
|
+
.vela-dsp-style > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14829
|
+
.vela-dsp-style:hover, .vela-dsp-style[aria-expanded='true'] { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
14830
|
+
.vela-dsp-menu { min-width: 220px; max-height: 320px; overflow-y: auto; padding: 4px; display: flex; flex-direction: column; gap: 1px; }
|
|
14831
|
+
.vela-dsp-mh { padding: 6px 8px 4px; font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--vela-fg-muted); }
|
|
14832
|
+
.vela-dsp-mi { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 8px; padding: 0 4px 0 8px; height: 30px; border-radius: 5px; color: inherit; }
|
|
14833
|
+
.vela-dsp-mi:hover, .vela-dsp-mi:focus-visible { background: var(--vela-hover-strong); }
|
|
14834
|
+
.vela-dsp-mi[data-current] { color: var(--vela-fg-bright); font-weight: 600; }
|
|
14835
|
+
.vela-dsp-mi > .vela-dsp-mi-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14836
|
+
.vela-dsp-mi-dot { flex: none; width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--vela-border-strong); }
|
|
14837
|
+
.vela-dsp-mi-x { all: unset; cursor: pointer; flex: none; width: 24px; height: 24px; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: var(--vela-fg-muted); }
|
|
14838
|
+
.vela-dsp-mi-x:hover { background: var(--vela-hover); color: var(--vela-danger); }
|
|
14839
|
+
.vela-dsp-msep { height: 1px; background: var(--vela-border); margin: 4px 0; }
|
|
14840
|
+
.vela-dsp-mname { display: flex; gap: 6px; padding: 4px; }
|
|
14841
|
+
.vela-dsp-mname > input { flex: 1; min-width: 0; height: 28px; box-sizing: border-box; padding: 0 8px; border-radius: 6px; border: 1px solid var(--vela-border-strong); background: transparent; color: inherit; font: inherit; outline: none; }
|
|
14842
|
+
.vela-dsp-mname > input:focus { border-color: var(--vela-selected-bg); }`;
|
|
14843
|
+
if (!existing) document.head.appendChild(s);
|
|
14844
|
+
}
|
|
14845
|
+
var DrawingSettingsPanel = class {
|
|
14846
|
+
constructor(host, theme, env) {
|
|
14288
14847
|
this.host = host;
|
|
14848
|
+
this.env = env;
|
|
14289
14849
|
this.ui = null;
|
|
14850
|
+
this.menu = null;
|
|
14851
|
+
this.refresh = null;
|
|
14290
14852
|
this.theme = theme;
|
|
14291
14853
|
}
|
|
14292
14854
|
setTheme(theme) {
|
|
@@ -14296,69 +14858,97 @@ var DrawingSettingsDialog = class {
|
|
|
14296
14858
|
return this.ui != null;
|
|
14297
14859
|
}
|
|
14298
14860
|
contains(node) {
|
|
14299
|
-
return this.ui?.contains(node) === true;
|
|
14861
|
+
return node != null && this.ui?.contains(node) === true;
|
|
14862
|
+
}
|
|
14863
|
+
/** Re-read the drawing after a change made outside the panel (a drag, an undo). */
|
|
14864
|
+
sync() {
|
|
14865
|
+
this.refresh?.();
|
|
14300
14866
|
}
|
|
14301
|
-
open(drawing, actions,
|
|
14867
|
+
open(drawing, actions, anchor) {
|
|
14302
14868
|
this.close();
|
|
14303
|
-
|
|
14304
|
-
const
|
|
14305
|
-
const
|
|
14306
|
-
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
14312
|
-
|
|
14313
|
-
|
|
14869
|
+
ensureStyles4();
|
|
14870
|
+
const first = actions.resolve() ?? drawing;
|
|
14871
|
+
const snapshot = clonePlain(first.serialize());
|
|
14872
|
+
const live = () => actions.resolve() ?? first;
|
|
14873
|
+
const meta = getDrawingType(first.type);
|
|
14874
|
+
const scroll = document.createElement("div");
|
|
14875
|
+
scroll.className = "vela-dsp-scroll";
|
|
14876
|
+
let watchers = [];
|
|
14877
|
+
const runWatchers = () => {
|
|
14878
|
+
for (const fn of watchers) fn();
|
|
14879
|
+
};
|
|
14880
|
+
const build = {
|
|
14881
|
+
live,
|
|
14882
|
+
edit: (p) => {
|
|
14883
|
+
actions.patch(p);
|
|
14884
|
+
runWatchers();
|
|
14885
|
+
},
|
|
14886
|
+
watch: (fn) => {
|
|
14887
|
+
watchers.push(fn);
|
|
14888
|
+
fn();
|
|
14889
|
+
},
|
|
14890
|
+
rebuild: () => {
|
|
14891
|
+
const top = scroll.scrollTop;
|
|
14892
|
+
watchers = [];
|
|
14893
|
+
scroll.replaceChildren(...this.sections(build));
|
|
14894
|
+
scroll.scrollTop = top;
|
|
14895
|
+
styleBtn.refresh();
|
|
14896
|
+
},
|
|
14897
|
+
actions
|
|
14898
|
+
};
|
|
14899
|
+
const cancel = () => {
|
|
14900
|
+
actions.preview?.(null);
|
|
14901
|
+
actions.restore?.(snapshot);
|
|
14902
|
+
this.close();
|
|
14903
|
+
};
|
|
14314
14904
|
const ui = new Dialog({
|
|
14315
14905
|
host: this.host,
|
|
14316
|
-
title:
|
|
14317
|
-
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
14318
|
-
// whole body, killing the chart and the body-portaled popovers.
|
|
14906
|
+
title: meta?.label ?? "Settings",
|
|
14319
14907
|
modal: false,
|
|
14320
14908
|
contained: true,
|
|
14321
14909
|
align: "center",
|
|
14322
14910
|
draggable: true,
|
|
14323
14911
|
flush: true,
|
|
14324
|
-
className: "vela-
|
|
14912
|
+
className: "vela-dsp",
|
|
14325
14913
|
closeOnEscape: false,
|
|
14326
|
-
footer: (foot) =>
|
|
14327
|
-
foot.append(
|
|
14328
|
-
btn("Cancel", false, () => {
|
|
14329
|
-
actions.restore?.(snapshot);
|
|
14330
|
-
this.close();
|
|
14331
|
-
}),
|
|
14332
|
-
btn("Ok", true, () => this.close())
|
|
14333
|
-
);
|
|
14334
|
-
},
|
|
14914
|
+
footer: (foot) => foot.append(button("Cancel", false, cancel), button("Ok", true, () => this.close())),
|
|
14335
14915
|
onOpenChange: (open) => {
|
|
14336
14916
|
if (!open) this.close();
|
|
14337
14917
|
}
|
|
14338
14918
|
});
|
|
14339
14919
|
applyChromeTokens(ui.panel, this.theme);
|
|
14340
|
-
ui.
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
ui.backdrop.addEventListener("pointerdown", onBackdropDown);
|
|
14350
|
-
ui.positioner.addEventListener("pointerdown", onBackdropDown);
|
|
14920
|
+
ui.backdrop.classList.add("vela-dsp-pass");
|
|
14921
|
+
ui.positioner.classList.add("vela-dsp-pass");
|
|
14922
|
+
const hostW = this.host.clientWidth;
|
|
14923
|
+
const leftHalf = anchor != null && hostW > 0 && anchor.x + anchor.w / 2 < hostW / 2;
|
|
14924
|
+
ui.positioner.style.justifyContent = leftHalf ? "flex-end" : "flex-start";
|
|
14925
|
+
ui.positioner.style.padding = "0 12px";
|
|
14926
|
+
const styleBtn = this.styleMenuButton(build);
|
|
14927
|
+
ui.titleEl.after(styleBtn.el);
|
|
14928
|
+
ui.body.appendChild(scroll);
|
|
14351
14929
|
ui.panel.addEventListener("keydown", (e) => {
|
|
14352
14930
|
if (e.key === "Escape") {
|
|
14353
14931
|
e.preventDefault();
|
|
14354
|
-
|
|
14355
|
-
|
|
14932
|
+
if (this.menu) this.menu.hide();
|
|
14933
|
+
else cancel();
|
|
14356
14934
|
}
|
|
14357
14935
|
});
|
|
14358
14936
|
this.ui = ui;
|
|
14937
|
+
this.refresh = () => {
|
|
14938
|
+
if (!actions.resolve()) {
|
|
14939
|
+
this.close();
|
|
14940
|
+
return;
|
|
14941
|
+
}
|
|
14942
|
+
runWatchers();
|
|
14943
|
+
styleBtn.refresh();
|
|
14944
|
+
};
|
|
14945
|
+
build.rebuild();
|
|
14359
14946
|
ui.show();
|
|
14360
14947
|
}
|
|
14361
14948
|
close() {
|
|
14949
|
+
this.menu?.hide();
|
|
14950
|
+
this.menu = null;
|
|
14951
|
+
this.refresh = null;
|
|
14362
14952
|
const ui = this.ui;
|
|
14363
14953
|
this.ui = null;
|
|
14364
14954
|
ui?.destroy();
|
|
@@ -14366,292 +14956,771 @@ var DrawingSettingsDialog = class {
|
|
|
14366
14956
|
destroy() {
|
|
14367
14957
|
this.close();
|
|
14368
14958
|
}
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
const
|
|
14375
|
-
const
|
|
14376
|
-
|
|
14377
|
-
const
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
}
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
}
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14959
|
+
// ── sections ──
|
|
14960
|
+
sections(b) {
|
|
14961
|
+
const d = b.live();
|
|
14962
|
+
const schema = d.schema();
|
|
14963
|
+
const fields = schema.fields;
|
|
14964
|
+
const has = (path) => fields.some((f) => f.path === path);
|
|
14965
|
+
const out = [];
|
|
14966
|
+
if (schema.textIsContent && has("text.value")) out.push(this.textContentBlock(b, has));
|
|
14967
|
+
const style = this.styleBlock(b, fields, has);
|
|
14968
|
+
if (style) out.push(style);
|
|
14969
|
+
if (d.editableLevels()) out.push(this.levelsBlock(b));
|
|
14970
|
+
if (d instanceof PositionTool) out.push(this.richBlock("Position", (grid) => buildPositionSection(grid, d, b.actions, this.theme)));
|
|
14971
|
+
if (d instanceof FixedRangeVolumeProfile) out.push(this.richBlock("Profile", (grid) => buildProfileSection(grid, d, b.actions, this.theme)));
|
|
14972
|
+
const own = this.optionsBlock(b, fields, d);
|
|
14973
|
+
if (own) out.push(own);
|
|
14974
|
+
if (!schema.textIsContent && fields.some((f) => f.path.startsWith("text."))) out.push(this.textFold(b, has));
|
|
14975
|
+
const points = this.pointsFold(b);
|
|
14976
|
+
if (points) out.push(points);
|
|
14977
|
+
out.push(this.showOnFold(b));
|
|
14978
|
+
return out;
|
|
14979
|
+
}
|
|
14980
|
+
/** Line, width, dash, ends and fill — the look every stroked tool shares. */
|
|
14981
|
+
styleBlock(b, fields, has) {
|
|
14982
|
+
const rows = [];
|
|
14983
|
+
if (has("style.lineColor") || has("style.lineWidth")) {
|
|
14984
|
+
const ctrls = [];
|
|
14985
|
+
if (has("style.lineColor")) ctrls.push(this.swatch(b, "Line color", () => b.live().style.lineColor || DEFAULT_DRAWING_COLOR, "style.lineColor"));
|
|
14986
|
+
const wf = fields.find((f) => f.path === "style.lineWidth");
|
|
14987
|
+
if (wf) {
|
|
14988
|
+
if ((wf.min ?? 1) > 1) ctrls.push(this.number(b, "Line width", () => b.live().style.lineWidth, "style.lineWidth", wf));
|
|
14989
|
+
else ctrls.push(this.seg(b, "Line width", [1, 2, 3, 4].map((w) => ({ value: w, html: widthIcon(w), title: `${w}px` })), () => b.live().style.lineWidth, (v) => b.edit({ "style.lineWidth": v })));
|
|
14990
|
+
}
|
|
14991
|
+
rows.push(row("Line", ...ctrls));
|
|
14992
|
+
}
|
|
14993
|
+
if (has("style.lineStyle")) {
|
|
14994
|
+
rows.push(row("Style", this.seg(b, "Line style", LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, html: dashIcon(o.value), title: o.label })), () => b.live().style.lineStyle, (v) => b.edit({ "style.lineStyle": v }))));
|
|
14995
|
+
}
|
|
14996
|
+
if (has("style.extendLeft") && has("style.extendRight")) {
|
|
14997
|
+
const opts = [
|
|
14998
|
+
{ value: "none", html: EXTEND_ICONS.none, title: "Don't extend" },
|
|
14999
|
+
{ value: "left", html: EXTEND_ICONS.left, title: "Extend left" },
|
|
15000
|
+
{ value: "right", html: EXTEND_ICONS.right, title: "Extend right" },
|
|
15001
|
+
{ value: "both", html: EXTEND_ICONS.both, title: "Extend both ways" }
|
|
15002
|
+
];
|
|
15003
|
+
const read = () => {
|
|
15004
|
+
const { extendLeft: l, extendRight: r } = b.live().style;
|
|
15005
|
+
return l && r ? "both" : l ? "left" : r ? "right" : "none";
|
|
15006
|
+
};
|
|
15007
|
+
rows.push(row("Extend", this.seg(b, "Extend", opts, read, (v) => b.edit({ "style.extendLeft": v === "left" || v === "both", "style.extendRight": v === "right" || v === "both" }))));
|
|
15008
|
+
}
|
|
15009
|
+
const arrows = [];
|
|
15010
|
+
if (has("style.arrowLeft")) arrows.push(this.chip(b, "Start arrow", () => !!b.live().style.arrowLeft, (on) => b.edit({ "style.arrowLeft": on })));
|
|
15011
|
+
if (has("style.arrowRight")) arrows.push(this.chip(b, has("style.arrowLeft") ? "End arrow" : "Arrow", () => !!b.live().style.arrowRight, (on) => b.edit({ "style.arrowRight": on })));
|
|
15012
|
+
if (arrows.length) rows.push(row("Ends", chips(...arrows)));
|
|
15013
|
+
if (has("style.fillColor")) {
|
|
15014
|
+
rows.push(row("Fill", this.swatch(b, "Fill", () => effectiveFillColor(b.live(), this.theme) ?? b.live().style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor")));
|
|
15015
|
+
}
|
|
15016
|
+
if (!has("text.value") && has("text.color")) rows.push(row("Text", ...this.textStyling(b, has)));
|
|
15017
|
+
return rows.length ? block(null, ...rows) : null;
|
|
15018
|
+
}
|
|
15019
|
+
/** The tool's own options — every schema field the shared look and the rich sections
|
|
15020
|
+
* don't cover, each as the control its kind calls for. */
|
|
15021
|
+
optionsBlock(b, fields, d) {
|
|
15022
|
+
const skip = (f) => f.path.startsWith("style.") || f.path.startsWith("text.") || f.path.startsWith("frvp.") || d instanceof PositionTool && POSITION_PATHS.has(f.path) || d.editableLevels() != null && LEVEL_PATHS.has(f.path);
|
|
15023
|
+
const rows = [];
|
|
15024
|
+
const own = fields.filter((f) => !skip(f));
|
|
15025
|
+
for (let i = 0; i < own.length; i += 1) {
|
|
15026
|
+
const f = own[i];
|
|
15027
|
+
const next = own[i + 1];
|
|
15028
|
+
const base = f.kind === "color" && next?.kind === "lineStyle" ? pairBase(f.label, next.label) : f.kind === "color" && next?.kind === "color" ? upperLowerBase(f.label, next.label) : null;
|
|
15029
|
+
if (base) {
|
|
15030
|
+
const color = this.fieldControl(b, f);
|
|
15031
|
+
const style = this.fieldControl(b, next);
|
|
15032
|
+
if (color && style) rows.push(row(base, color, style));
|
|
15033
|
+
i += 1;
|
|
15034
|
+
continue;
|
|
14440
15035
|
}
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
const
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
label,
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14465
|
-
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
|
|
15036
|
+
const ctrl = this.fieldControl(b, f);
|
|
15037
|
+
if (!ctrl) continue;
|
|
15038
|
+
const r = row(f.label, ctrl);
|
|
15039
|
+
const cond = f.when;
|
|
15040
|
+
if (cond) b.watch(() => r.hidden = !cond.in.includes(readPath(b.live(), cond.path)));
|
|
15041
|
+
rows.push(r);
|
|
15042
|
+
}
|
|
15043
|
+
const props = d.serialize().props ?? {};
|
|
15044
|
+
const sizes = TEXT_SIZE_OPTIONS.map((o) => ({ value: o.value, html: o.label }));
|
|
15045
|
+
if ("numbersSize" in props) rows.push(row("Numbers", this.seg(b, "Numbers size", sizes, () => b.live().serialize().props?.numbersSize ?? "small", (v) => b.edit({ numbersSize: v }), true)));
|
|
15046
|
+
if ("labelsSize" in props) rows.push(row("Labels", this.seg(b, "Labels size", sizes, () => b.live().serialize().props?.labelsSize ?? "normal", (v) => b.edit({ labelsSize: v }), true)));
|
|
15047
|
+
return rows.length ? block(null, ...rows) : null;
|
|
15048
|
+
}
|
|
15049
|
+
fieldControl(b, f) {
|
|
15050
|
+
const read = () => readPath(b.live(), f.path);
|
|
15051
|
+
switch (f.kind) {
|
|
15052
|
+
case "color":
|
|
15053
|
+
return this.swatch(b, f.label, () => read() || DEFAULT_DRAWING_COLOR, f.path);
|
|
15054
|
+
case "lineStyle":
|
|
15055
|
+
return this.seg(b, f.label, LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, html: dashIcon(o.value), title: o.label })), () => read(), (v) => b.edit({ [f.path]: v }));
|
|
15056
|
+
case "boolean":
|
|
15057
|
+
return this.switch(b, () => read() !== false && read() != null, (v) => b.edit({ [f.path]: v }));
|
|
15058
|
+
case "number":
|
|
15059
|
+
case "opacity":
|
|
15060
|
+
return this.number(b, f.label, () => Number(read() ?? 0), f.path, f);
|
|
15061
|
+
case "select": {
|
|
15062
|
+
const options = f.path === "magnifier.timeframe" ? magnifierTimeframesBelow(this.env.chartBarMs()) : f.options ?? [];
|
|
15063
|
+
if (options.length === 0) return null;
|
|
15064
|
+
const sel = buildFieldControl({ kind: "select", options, value: String(read() ?? options[0].value), fill: false, theme: this.theme, onChange: (v) => b.edit({ [f.path]: v }) });
|
|
15065
|
+
b.watch(() => sel.setValue?.(String(read() ?? "")));
|
|
15066
|
+
return sel.el;
|
|
15067
|
+
}
|
|
15068
|
+
case "text": {
|
|
15069
|
+
const tf = buildFieldControl({ kind: "text", value: String(read() ?? ""), fill: true, onChange: (v) => b.edit({ [f.path]: v }) });
|
|
15070
|
+
return tf.el;
|
|
15071
|
+
}
|
|
15072
|
+
default:
|
|
15073
|
+
return null;
|
|
14469
15074
|
}
|
|
14470
|
-
const summary = document.createElement("div");
|
|
14471
|
-
summary.className = "vela-field-span";
|
|
14472
|
-
summary.style.cssText = "opacity:0.7;font-size:11px;line-height:1.4;border-top:1px solid var(--vela-border);padding-top:6px;font-variant-numeric:tabular-nums;";
|
|
14473
|
-
refreshers.push(() => {
|
|
14474
|
-
const d = live();
|
|
14475
|
-
summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
|
|
14476
|
-
});
|
|
14477
|
-
grid.appendChild(summary);
|
|
14478
|
-
refreshAll();
|
|
14479
15075
|
}
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
15076
|
+
/** A levelled tool's levels: every level in a two-column grid (on/off, ratio, color) — the
|
|
15077
|
+
* ones that are off stay listed, ready to switch on. A retracement leads with its ready-made
|
|
15078
|
+
* sets; the horizontal fibs follow with how their level text and fill show. */
|
|
15079
|
+
levelsBlock(b) {
|
|
15080
|
+
const d = b.live();
|
|
15081
|
+
const rows = [];
|
|
15082
|
+
const grid = document.createElement("div");
|
|
15083
|
+
grid.className = "vela-dsp-lv";
|
|
15084
|
+
const key = () => JSON.stringify(b.live().editableLevels() ?? []);
|
|
15085
|
+
let last = "";
|
|
15086
|
+
let editing = false;
|
|
15087
|
+
const mine = (p) => {
|
|
15088
|
+
editing = true;
|
|
15089
|
+
try {
|
|
15090
|
+
b.edit(p);
|
|
15091
|
+
} finally {
|
|
15092
|
+
editing = false;
|
|
15093
|
+
}
|
|
15094
|
+
last = key();
|
|
14484
15095
|
};
|
|
14485
|
-
const
|
|
14486
|
-
|
|
14487
|
-
grid.
|
|
14488
|
-
|
|
14489
|
-
|
|
15096
|
+
const fill = () => {
|
|
15097
|
+
last = key();
|
|
15098
|
+
grid.replaceChildren();
|
|
15099
|
+
const free = b.live() instanceof FibLevels;
|
|
15100
|
+
(b.live().editableLevels() ?? []).forEach((lv, i) => {
|
|
15101
|
+
const cell = document.createElement("div");
|
|
15102
|
+
cell.className = "vela-dsp-lv-c";
|
|
15103
|
+
cell.toggleAttribute("data-off", !lv.enabled);
|
|
15104
|
+
const on = buildFieldControl({
|
|
15105
|
+
kind: "switch",
|
|
15106
|
+
checked: lv.enabled,
|
|
15107
|
+
onChange: (v) => {
|
|
15108
|
+
cell.toggleAttribute("data-off", !v);
|
|
15109
|
+
mine({ [`levels.${i}.enabled`]: v });
|
|
15110
|
+
}
|
|
15111
|
+
});
|
|
15112
|
+
let ratio = lv.ratio;
|
|
15113
|
+
const num4 = buildFieldControl({
|
|
14490
15114
|
kind: "number",
|
|
14491
|
-
value:
|
|
14492
|
-
min,
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
integer,
|
|
15115
|
+
value: lv.ratio,
|
|
15116
|
+
min: free ? void 0 : 0,
|
|
15117
|
+
step: 0.01,
|
|
15118
|
+
compact: true,
|
|
14496
15119
|
fill: false,
|
|
14497
15120
|
commit: "blur",
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
value: s.anchor,
|
|
14511
|
-
fill: false,
|
|
14512
|
-
theme: this.theme,
|
|
14513
|
-
onChange: (v) => actions.patch({ "frvp.anchor": v })
|
|
14514
|
-
}).el
|
|
14515
|
-
}));
|
|
14516
|
-
const colorRow = (label, path) => {
|
|
14517
|
-
grid.appendChild(fieldRow({
|
|
14518
|
-
label,
|
|
14519
|
-
fit: true,
|
|
14520
|
-
control: buildFieldControl({
|
|
15121
|
+
title: `Level ${i + 1}`,
|
|
15122
|
+
onChange: (n) => {
|
|
15123
|
+
if (!Number.isFinite(n) || !free && n <= 0) {
|
|
15124
|
+
num4.setValue?.(ratio);
|
|
15125
|
+
return;
|
|
15126
|
+
}
|
|
15127
|
+
ratio = n;
|
|
15128
|
+
mine({ [`levels.${i}.ratio`]: n });
|
|
15129
|
+
}
|
|
15130
|
+
});
|
|
15131
|
+
let color = lv.color;
|
|
15132
|
+
const swatch = buildFieldControl({
|
|
14521
15133
|
kind: "color",
|
|
14522
15134
|
theme: this.theme,
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
|
|
14533
|
-
const levelRow = (label, showPath, colorPath, stylePath) => {
|
|
14534
|
-
const row = document.createElement("div");
|
|
14535
|
-
row.className = "vela-field-span";
|
|
14536
|
-
row.style.cssText = "display:flex;align-items:center;gap:8px;";
|
|
14537
|
-
const sw = buildFieldControl({
|
|
14538
|
-
kind: "switch",
|
|
14539
|
-
checked: Boolean(s[showPath]),
|
|
14540
|
-
onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
|
|
14541
|
-
});
|
|
14542
|
-
const lbl = document.createElement("span");
|
|
14543
|
-
lbl.className = "vela-field-label";
|
|
14544
|
-
lbl.style.flex = "1";
|
|
14545
|
-
lbl.textContent = label;
|
|
14546
|
-
let cur = s[colorPath] ?? contrastColor(this.theme.background);
|
|
14547
|
-
const col = buildFieldControl({
|
|
14548
|
-
kind: "color",
|
|
14549
|
-
theme: this.theme,
|
|
14550
|
-
get: () => cur,
|
|
14551
|
-
onChange: (v) => {
|
|
14552
|
-
cur = v;
|
|
14553
|
-
actions.patch({ [`frvp.${colorPath}`]: v });
|
|
14554
|
-
}
|
|
15135
|
+
title: `Level ${i + 1} color`,
|
|
15136
|
+
get: () => color,
|
|
15137
|
+
onChange: (v) => {
|
|
15138
|
+
color = v;
|
|
15139
|
+
mine({ [`levels.${i}.color`]: v });
|
|
15140
|
+
}
|
|
15141
|
+
});
|
|
15142
|
+
cell.append(on.el, num4.el, swatch.el);
|
|
15143
|
+
grid.appendChild(cell);
|
|
14555
15144
|
});
|
|
14556
|
-
|
|
15145
|
+
};
|
|
15146
|
+
b.watch(() => {
|
|
15147
|
+
if (!editing && key() !== last && !grid.contains(document.activeElement)) fill();
|
|
15148
|
+
});
|
|
15149
|
+
if (d instanceof FibRetracement) {
|
|
15150
|
+
rows.push(
|
|
15151
|
+
this.seg(
|
|
15152
|
+
b,
|
|
15153
|
+
"Level set",
|
|
15154
|
+
FIB_PRESETS.map((p) => ({ value: p.key, html: p.label })),
|
|
15155
|
+
() => {
|
|
15156
|
+
const f = b.live();
|
|
15157
|
+
return matchFibPreset(f.levels, f.reverse) ?? void 0;
|
|
15158
|
+
},
|
|
15159
|
+
(k) => {
|
|
15160
|
+
const preset = FIB_PRESETS.find((p) => p.key === k);
|
|
15161
|
+
b.edit({ levels: clonePlain(preset.levels), reverse: preset.reverse });
|
|
15162
|
+
},
|
|
15163
|
+
true
|
|
15164
|
+
)
|
|
15165
|
+
);
|
|
15166
|
+
}
|
|
15167
|
+
if (d instanceof MachFigure) rows.push(row("Ratios", this.switch(b, () => b.live().showRatios !== false, (v) => b.edit({ showRatios: v }))));
|
|
15168
|
+
rows.push(grid);
|
|
15169
|
+
if (d instanceof FibLevels) {
|
|
15170
|
+
const fib = () => b.live();
|
|
15171
|
+
const flags = [
|
|
15172
|
+
this.chip(b, "Ratios", () => fib().showRatios, (on) => b.edit({ showRatios: on })),
|
|
15173
|
+
this.chip(b, "Prices", () => fib().showPrices, (on) => b.edit({ showPrices: on })),
|
|
15174
|
+
this.chip(b, "Background", () => fib().background, (on) => b.edit({ background: on }))
|
|
15175
|
+
];
|
|
15176
|
+
if (d instanceof FibRetracement) flags.push(this.chip(b, "Reverse", () => b.live().reverse, (on) => b.edit({ reverse: on })));
|
|
15177
|
+
rows.push(row("Show", chips(...flags)));
|
|
15178
|
+
rows.push(
|
|
15179
|
+
row(
|
|
15180
|
+
"Label side",
|
|
15181
|
+
this.seg(b, "Label side", [
|
|
15182
|
+
{ value: "left", html: "Left" },
|
|
15183
|
+
{ value: "right", html: "Right" }
|
|
15184
|
+
], () => fib().labelSide, (v) => b.edit({ labelSide: v }))
|
|
15185
|
+
)
|
|
15186
|
+
);
|
|
15187
|
+
}
|
|
15188
|
+
return block("Levels", ...rows);
|
|
15189
|
+
}
|
|
15190
|
+
/** A rich per-type section on the shared field grid. */
|
|
15191
|
+
richBlock(title, fill) {
|
|
15192
|
+
const grid = fieldGrid({ variant: "inputs" });
|
|
15193
|
+
grid.classList.add("vela-dsp-grid");
|
|
15194
|
+
fill(grid);
|
|
15195
|
+
return block(title, grid);
|
|
15196
|
+
}
|
|
15197
|
+
/** A note, a callout, a text label: the words lead the panel. */
|
|
15198
|
+
textContentBlock(b, has) {
|
|
15199
|
+
return block(null, this.textArea(b, "Text", 3), row("Style", ...this.textStyling(b, has)));
|
|
15200
|
+
}
|
|
15201
|
+
/** A shape's optional label: folded to one line saying what it reads. */
|
|
15202
|
+
textFold(b, has) {
|
|
15203
|
+
const type = b.live().type;
|
|
15204
|
+
const words = () => b.live().text?.value?.trim() ?? "";
|
|
15205
|
+
const summary = () => {
|
|
15206
|
+
if (!has("text.value")) return sizeLabel(b.live().text?.size);
|
|
15207
|
+
const w = words();
|
|
15208
|
+
return w ? `\u201C${w.split("\n")[0]}\u201D` : "None";
|
|
15209
|
+
};
|
|
15210
|
+
return this.fold(b, "text", has("text.value") ? "Label" : "Text", summary, () => {
|
|
15211
|
+
const rows = [];
|
|
15212
|
+
if (has("text.value")) rows.push(this.textArea(b, "Add a label", 1));
|
|
15213
|
+
rows.push(row("Style", ...this.textStyling(b, has)));
|
|
15214
|
+
if (LINE_LABEL_TYPES.has(type) && has("text.value")) {
|
|
15215
|
+
const place = row(
|
|
15216
|
+
"Place",
|
|
15217
|
+
this.seg(b, "Position along the line", [
|
|
15218
|
+
{ value: "start", html: "Start" },
|
|
15219
|
+
{ value: "middle", html: "Middle" },
|
|
15220
|
+
{ value: "end", html: "End" }
|
|
15221
|
+
], () => b.live().text?.place ?? "middle", (v) => b.edit({ "text.place": v }), true)
|
|
15222
|
+
);
|
|
15223
|
+
const side = row(
|
|
15224
|
+
"Side",
|
|
15225
|
+
this.seg(b, "Side of the line", [
|
|
15226
|
+
{ value: "above", html: "Above" },
|
|
15227
|
+
{ value: "below", html: "Below" }
|
|
15228
|
+
], () => b.live().text?.side ?? "above", (v) => b.edit({ "text.side": v }), true)
|
|
15229
|
+
);
|
|
15230
|
+
b.watch(() => place.hidden = side.hidden = words() === "");
|
|
15231
|
+
rows.push(place, side);
|
|
15232
|
+
}
|
|
15233
|
+
return rows;
|
|
15234
|
+
});
|
|
15235
|
+
}
|
|
15236
|
+
textArea(b, placeholder, rows) {
|
|
15237
|
+
const ta = new TextArea({
|
|
15238
|
+
value: b.live().text?.value ?? "",
|
|
15239
|
+
rows,
|
|
15240
|
+
size: "sm",
|
|
15241
|
+
autoGrow: true,
|
|
15242
|
+
maxLines: 6,
|
|
15243
|
+
placeholder,
|
|
15244
|
+
onChange: (v) => b.edit({ "text.value": v })
|
|
15245
|
+
});
|
|
15246
|
+
ta.input.addEventListener("keydown", (e) => {
|
|
15247
|
+
if (e.key !== "Escape") e.stopPropagation();
|
|
15248
|
+
});
|
|
15249
|
+
b.watch(() => {
|
|
15250
|
+
const v = b.live().text?.value ?? "";
|
|
15251
|
+
if (document.activeElement !== ta.input && ta.input.value !== v) ta.input.value = v;
|
|
15252
|
+
});
|
|
15253
|
+
ta.el.style.width = "100%";
|
|
15254
|
+
return ta.el;
|
|
15255
|
+
}
|
|
15256
|
+
textStyling(b, has) {
|
|
15257
|
+
const out = [];
|
|
15258
|
+
if (has("text.color")) out.push(this.swatch(b, "Text color", () => b.live().text?.color || this.theme.textColor, "text.color"));
|
|
15259
|
+
if (has("text.size")) {
|
|
15260
|
+
const sel = buildFieldControl({
|
|
14557
15261
|
kind: "select",
|
|
14558
|
-
options:
|
|
14559
|
-
value:
|
|
15262
|
+
options: TEXT_SIZE_OPTIONS,
|
|
15263
|
+
value: b.live().text?.size ?? "normal",
|
|
14560
15264
|
fill: false,
|
|
14561
15265
|
theme: this.theme,
|
|
14562
|
-
onChange: (v) =>
|
|
15266
|
+
onChange: (v) => b.edit({ "text.size": v })
|
|
14563
15267
|
});
|
|
14564
|
-
|
|
14565
|
-
|
|
15268
|
+
b.watch(() => sel.setValue?.(b.live().text?.size ?? "normal"));
|
|
15269
|
+
out.push(sel.el);
|
|
15270
|
+
}
|
|
15271
|
+
const flags = [];
|
|
15272
|
+
if (has("text.bold")) flags.push(this.chip(b, "B", () => !!b.live().text?.bold, (on) => b.edit({ "text.bold": on }), "Bold"));
|
|
15273
|
+
if (has("text.italic")) flags.push(this.chip(b, "I", () => !!b.live().text?.italic, (on) => b.edit({ "text.italic": on }), "Italic"));
|
|
15274
|
+
if (flags.length) out.push(chips(...flags));
|
|
15275
|
+
return out;
|
|
15276
|
+
}
|
|
15277
|
+
/** Each anchor's price (editable) and time. */
|
|
15278
|
+
pointsFold(b) {
|
|
15279
|
+
const d = b.live();
|
|
15280
|
+
if (NO_POINTS.has(d.type) || d.anchors.length === 0 || d.anchors.length > MAX_POINTS) return null;
|
|
15281
|
+
const slots = d.anchorSchema().slots;
|
|
15282
|
+
const free = (i) => slots[i]?.free ?? slots[slots.length - 1]?.free ?? "both";
|
|
15283
|
+
const shown = d.anchors.map((_, i) => i).filter((i) => free(i) !== "none");
|
|
15284
|
+
if (shown.length === 0) return null;
|
|
15285
|
+
const priced = shown.filter((i) => free(i) !== "x");
|
|
15286
|
+
const shownPrice = (n) => {
|
|
15287
|
+
const dec = this.env.priceDecimals() ?? Math.max(2, valueDecimals(n));
|
|
15288
|
+
return Number(n.toFixed(Math.min(dec, 10)));
|
|
14566
15289
|
};
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
|
|
14584
|
-
|
|
15290
|
+
const fmt = (n) => String(shownPrice(n));
|
|
15291
|
+
const summary = () => {
|
|
15292
|
+
const a = b.live().anchors;
|
|
15293
|
+
if (priced.length) return priced.map((i) => fmt(a[i]?.price ?? 0)).join(" \u2192 ");
|
|
15294
|
+
const t = a[shown[0]]?.time;
|
|
15295
|
+
return shown.length === 1 && t != null ? formatTimeStamp(t, this.env.timeZone(), this.env.chartBarMs()) : `${shown.length} points`;
|
|
15296
|
+
};
|
|
15297
|
+
return this.fold(
|
|
15298
|
+
b,
|
|
15299
|
+
"points",
|
|
15300
|
+
"Points",
|
|
15301
|
+
summary,
|
|
15302
|
+
() => shown.map((i, n) => {
|
|
15303
|
+
const key = shown.length === 1 ? free(i) === "x" ? "Time" : "Price" : String.fromCharCode(65 + n);
|
|
15304
|
+
const line = document.createElement("div");
|
|
15305
|
+
line.className = "vela-dsp-pt";
|
|
15306
|
+
const k = document.createElement("span");
|
|
15307
|
+
k.className = "vela-dsp-pt-k";
|
|
15308
|
+
k.textContent = key;
|
|
15309
|
+
line.appendChild(k);
|
|
15310
|
+
if (free(i) !== "x") {
|
|
15311
|
+
const ni = buildFieldControl({
|
|
15312
|
+
kind: "number",
|
|
15313
|
+
value: shownPrice(d.anchors[i].price),
|
|
15314
|
+
fill: true,
|
|
15315
|
+
commit: "blur",
|
|
15316
|
+
title: `Point ${key} price`,
|
|
15317
|
+
onChange: (v) => b.edit({ [`anchors.${i}.price`]: v })
|
|
15318
|
+
});
|
|
15319
|
+
const input = ni.el.querySelector("input");
|
|
15320
|
+
b.watch(() => {
|
|
15321
|
+
if (document.activeElement !== input) ni.setValue?.(shownPrice(b.live().anchors[i]?.price ?? 0));
|
|
15322
|
+
});
|
|
15323
|
+
line.appendChild(ni.el);
|
|
15324
|
+
} else {
|
|
15325
|
+
line.appendChild(document.createElement("span"));
|
|
14585
15326
|
}
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
15327
|
+
const t = document.createElement("span");
|
|
15328
|
+
t.className = "vela-dsp-pt-t";
|
|
15329
|
+
b.watch(() => {
|
|
15330
|
+
const time = b.live().anchors[i]?.time;
|
|
15331
|
+
t.textContent = time != null ? formatTimeStamp(time, this.env.timeZone(), this.env.chartBarMs()) : "";
|
|
15332
|
+
});
|
|
15333
|
+
line.appendChild(t);
|
|
15334
|
+
return line;
|
|
15335
|
+
})
|
|
15336
|
+
);
|
|
15337
|
+
}
|
|
15338
|
+
/** The timeframes the drawing shows on: one line while it's every timeframe, the band
|
|
15339
|
+
* picker only once the user goes custom. */
|
|
15340
|
+
showOnFold(b) {
|
|
15341
|
+
const bands = () => b.live().showOn;
|
|
15342
|
+
let pickedCustom = false;
|
|
15343
|
+
const mode = () => pickedCustom && bands() ? "custom" : modeOf(bands());
|
|
15344
|
+
const here = timeframeBandOf(this.env.chartBarMs());
|
|
15345
|
+
const summary = () => {
|
|
15346
|
+
const s = bands();
|
|
15347
|
+
if (!s) return "All timeframes";
|
|
15348
|
+
if (mode() === "intraday") return "Intraday";
|
|
15349
|
+
if (mode() === "higher") return "1H and up";
|
|
15350
|
+
if (s.length === 0) return "Nowhere";
|
|
15351
|
+
return s.map((k) => TIMEFRAME_BANDS.find((t) => t.key === k)?.label ?? k).join(", ");
|
|
15352
|
+
};
|
|
15353
|
+
return this.fold(b, "show", "Show on", summary, () => {
|
|
15354
|
+
const set = (next) => b.edit({ showOn: next });
|
|
15355
|
+
const pick = this.seg(
|
|
15356
|
+
b,
|
|
15357
|
+
"Show on",
|
|
15358
|
+
[
|
|
15359
|
+
{ value: "all", html: "All" },
|
|
15360
|
+
{ value: "intraday", html: "Intraday" },
|
|
15361
|
+
{ value: "higher", html: "1H and up" },
|
|
15362
|
+
{ value: "custom", html: "Custom" }
|
|
15363
|
+
],
|
|
15364
|
+
mode,
|
|
15365
|
+
(v) => {
|
|
15366
|
+
pickedCustom = v === "custom";
|
|
15367
|
+
if (v === "all") set(void 0);
|
|
15368
|
+
else if (v === "intraday") set([...INTRADAY]);
|
|
15369
|
+
else if (v === "higher") set([...HIGHER]);
|
|
15370
|
+
else set([...bands() ?? TIMEFRAME_BANDS.map((t) => t.key)]);
|
|
15371
|
+
},
|
|
15372
|
+
true
|
|
15373
|
+
);
|
|
15374
|
+
const grid = document.createElement("div");
|
|
15375
|
+
grid.className = "vela-dsp-tf";
|
|
15376
|
+
for (const band of TIMEFRAME_BANDS) {
|
|
15377
|
+
const btn = document.createElement("button");
|
|
15378
|
+
btn.type = "button";
|
|
15379
|
+
btn.textContent = band.label;
|
|
15380
|
+
if (band.key === here) {
|
|
15381
|
+
btn.dataset.here = "";
|
|
15382
|
+
btn.title = "The chart's timeframe";
|
|
14596
15383
|
}
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
15384
|
+
btn.addEventListener("click", () => {
|
|
15385
|
+
const cur = new Set(bands() ?? []);
|
|
15386
|
+
if (cur.has(band.key)) cur.delete(band.key);
|
|
15387
|
+
else cur.add(band.key);
|
|
15388
|
+
set(TIMEFRAME_BANDS.map((t) => t.key).filter((k) => cur.has(k)));
|
|
15389
|
+
});
|
|
15390
|
+
b.watch(() => btn.setAttribute("aria-pressed", String(bands()?.includes(band.key) ?? true)));
|
|
15391
|
+
grid.appendChild(btn);
|
|
15392
|
+
}
|
|
15393
|
+
const note = document.createElement("div");
|
|
15394
|
+
note.className = "vela-dsp-note";
|
|
15395
|
+
b.watch(() => {
|
|
15396
|
+
grid.hidden = mode() !== "custom";
|
|
15397
|
+
const s = bands();
|
|
15398
|
+
const hidden = s != null && here != null && !s.includes(here);
|
|
15399
|
+
note.hidden = !hidden;
|
|
15400
|
+
note.textContent = s?.length === 0 ? "Hidden on every timeframe." : "Hidden on this chart's timeframe.";
|
|
14607
15401
|
});
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
15402
|
+
return [pick, grid, note];
|
|
15403
|
+
});
|
|
15404
|
+
}
|
|
15405
|
+
// ── the style menu ──
|
|
15406
|
+
/** The header's style menu: the tool's ready-made and saved looks (hover to preview, click
|
|
15407
|
+
* to apply), saving the current look, and going back to the tool's defaults. */
|
|
15408
|
+
styleMenuButton(b) {
|
|
15409
|
+
const btn = document.createElement("button");
|
|
15410
|
+
btn.type = "button";
|
|
15411
|
+
btn.className = "vela-dsp-style";
|
|
15412
|
+
btn.setAttribute("aria-haspopup", "menu");
|
|
15413
|
+
btn.setAttribute("aria-expanded", "false");
|
|
15414
|
+
const label = document.createElement("span");
|
|
15415
|
+
const chev = document.createElement("span");
|
|
15416
|
+
chev.style.display = "flex";
|
|
15417
|
+
chev.innerHTML = CHEVRON_DOWN;
|
|
15418
|
+
btn.append(label, chev);
|
|
15419
|
+
const all = () => [...builtinToolTemplates(b.live().type), ...b.actions.templates?.() ?? []];
|
|
15420
|
+
const refresh = () => {
|
|
15421
|
+
const match = all().filter((t) => matchesTemplate(b.live(), t.settings)).pop();
|
|
15422
|
+
label.textContent = match?.name ?? "Style";
|
|
15423
|
+
btn.title = match ? `Style: ${match.name}` : "Styles";
|
|
15424
|
+
};
|
|
15425
|
+
btn.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
15426
|
+
btn.addEventListener("click", (e) => {
|
|
15427
|
+
e.stopPropagation();
|
|
15428
|
+
if (this.menu) {
|
|
15429
|
+
this.menu.hide();
|
|
15430
|
+
return;
|
|
15431
|
+
}
|
|
15432
|
+
this.openStyleMenu(btn, b, all, refresh);
|
|
15433
|
+
});
|
|
15434
|
+
refresh();
|
|
15435
|
+
return { el: btn, refresh };
|
|
15436
|
+
}
|
|
15437
|
+
openStyleMenu(anchor, b, all, refresh) {
|
|
15438
|
+
closeOpenPopovers();
|
|
15439
|
+
const t = this.theme;
|
|
15440
|
+
const pop = new Popover({
|
|
15441
|
+
trigger: anchor,
|
|
15442
|
+
host: this.host,
|
|
15443
|
+
theme: t,
|
|
15444
|
+
position: "absolute",
|
|
15445
|
+
boundary: this.host,
|
|
15446
|
+
boundaryInset: 4,
|
|
15447
|
+
viewportInset: 0,
|
|
15448
|
+
gap: 6,
|
|
15449
|
+
align: "end",
|
|
15450
|
+
zIndex: 6e3,
|
|
15451
|
+
onClose: () => {
|
|
15452
|
+
b.actions.preview?.(null);
|
|
15453
|
+
anchor.setAttribute("aria-expanded", "false");
|
|
15454
|
+
if (this.menu === pop) this.menu = null;
|
|
15455
|
+
}
|
|
15456
|
+
});
|
|
15457
|
+
const el = pop.el;
|
|
15458
|
+
el.classList.add("vela-dsp-menu");
|
|
15459
|
+
el.setAttribute("role", "menu");
|
|
15460
|
+
el.style.cssText += `background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:13px ${t.fontFamily};pointer-events:auto;`;
|
|
15461
|
+
applyChromeTokens(el, t);
|
|
15462
|
+
const fill = () => {
|
|
15463
|
+
el.replaceChildren();
|
|
15464
|
+
const builtins = builtinToolTemplates(b.live().type);
|
|
15465
|
+
const saved = b.actions.templates?.() ?? [];
|
|
15466
|
+
const item = (tpl, removable) => {
|
|
15467
|
+
const row2 = document.createElement("button");
|
|
15468
|
+
row2.type = "button";
|
|
15469
|
+
row2.className = "vela-dsp-mi";
|
|
15470
|
+
row2.setAttribute("role", "menuitem");
|
|
15471
|
+
if (matchesTemplate(b.live(), tpl.settings)) row2.dataset.current = "";
|
|
15472
|
+
const dot = document.createElement("span");
|
|
15473
|
+
dot.className = "vela-dsp-mi-dot";
|
|
15474
|
+
dot.style.background = templateColor(tpl.settings) ?? "transparent";
|
|
15475
|
+
const name = document.createElement("span");
|
|
15476
|
+
name.className = "vela-dsp-mi-t";
|
|
15477
|
+
name.textContent = tpl.name;
|
|
15478
|
+
row2.append(dot, name);
|
|
15479
|
+
if (removable) {
|
|
15480
|
+
const x = document.createElement("span");
|
|
15481
|
+
x.className = "vela-dsp-mi-x";
|
|
15482
|
+
x.setAttribute("role", "button");
|
|
15483
|
+
x.setAttribute("aria-label", `Delete ${tpl.name}`);
|
|
15484
|
+
x.innerHTML = CROSS;
|
|
15485
|
+
x.addEventListener("click", (e) => {
|
|
15486
|
+
e.stopPropagation();
|
|
15487
|
+
b.actions.preview?.(null);
|
|
15488
|
+
b.actions.removeTemplate?.(tpl.name);
|
|
15489
|
+
fill();
|
|
15490
|
+
refresh();
|
|
15491
|
+
});
|
|
15492
|
+
row2.appendChild(x);
|
|
14616
15493
|
}
|
|
15494
|
+
row2.addEventListener("pointerenter", () => b.actions.preview?.(tpl.settings));
|
|
15495
|
+
row2.addEventListener("pointerleave", () => b.actions.preview?.(null));
|
|
15496
|
+
row2.addEventListener("click", () => {
|
|
15497
|
+
b.actions.preview?.(null);
|
|
15498
|
+
b.actions.applyToolSettings?.(tpl.settings);
|
|
15499
|
+
pop.hide();
|
|
15500
|
+
b.rebuild();
|
|
15501
|
+
});
|
|
15502
|
+
return row2;
|
|
15503
|
+
};
|
|
15504
|
+
if (builtins.length) {
|
|
15505
|
+
el.appendChild(menuHeading("Presets"));
|
|
15506
|
+
for (const tpl of builtins) el.appendChild(item(tpl, false));
|
|
15507
|
+
}
|
|
15508
|
+
if (saved.length) {
|
|
15509
|
+
el.appendChild(menuHeading("Saved"));
|
|
15510
|
+
for (const tpl of saved) el.appendChild(item(tpl, true));
|
|
15511
|
+
}
|
|
15512
|
+
if (builtins.length || saved.length) el.appendChild(separator());
|
|
15513
|
+
if (b.actions.saveTemplate) el.appendChild(this.saveRow(b, fill, refresh));
|
|
15514
|
+
const reset = document.createElement("button");
|
|
15515
|
+
reset.type = "button";
|
|
15516
|
+
reset.className = "vela-dsp-mi";
|
|
15517
|
+
reset.setAttribute("role", "menuitem");
|
|
15518
|
+
reset.innerHTML = '<span class="vela-dsp-mi-t">Reset to default</span>';
|
|
15519
|
+
reset.addEventListener("click", () => {
|
|
15520
|
+
b.actions.resetSettings();
|
|
15521
|
+
pop.hide();
|
|
15522
|
+
b.rebuild();
|
|
14617
15523
|
});
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
15524
|
+
el.appendChild(reset);
|
|
15525
|
+
};
|
|
15526
|
+
fill();
|
|
15527
|
+
anchor.setAttribute("aria-expanded", "true");
|
|
15528
|
+
this.menu = pop;
|
|
15529
|
+
pop.show();
|
|
15530
|
+
}
|
|
15531
|
+
/** "Save as style…": a menu row that turns into a name field. */
|
|
15532
|
+
saveRow(b, refill, refresh) {
|
|
15533
|
+
const wrap = document.createElement("div");
|
|
15534
|
+
const start = document.createElement("button");
|
|
15535
|
+
start.type = "button";
|
|
15536
|
+
start.className = "vela-dsp-mi";
|
|
15537
|
+
start.setAttribute("role", "menuitem");
|
|
15538
|
+
start.innerHTML = '<span class="vela-dsp-mi-t">Save as style\u2026</span>';
|
|
15539
|
+
start.addEventListener("click", (e) => {
|
|
15540
|
+
e.stopPropagation();
|
|
15541
|
+
const form = document.createElement("div");
|
|
15542
|
+
form.className = "vela-dsp-mname";
|
|
15543
|
+
const input = document.createElement("input");
|
|
15544
|
+
input.type = "text";
|
|
15545
|
+
input.placeholder = "Style name";
|
|
15546
|
+
input.maxLength = 60;
|
|
15547
|
+
input.setAttribute("aria-label", "Style name");
|
|
15548
|
+
const save = button("Save", true, () => commit());
|
|
15549
|
+
save.style.height = "28px";
|
|
15550
|
+
const commit = () => {
|
|
15551
|
+
const name = input.value.trim();
|
|
15552
|
+
if (!name) return;
|
|
15553
|
+
b.actions.saveTemplate?.(name);
|
|
15554
|
+
refill();
|
|
15555
|
+
refresh();
|
|
15556
|
+
};
|
|
15557
|
+
input.addEventListener("keydown", (ev) => {
|
|
15558
|
+
ev.stopPropagation();
|
|
15559
|
+
if (ev.key === "Enter") commit();
|
|
15560
|
+
if (ev.key === "Escape") refill();
|
|
14635
15561
|
});
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
15562
|
+
form.append(input, save);
|
|
15563
|
+
wrap.replaceChildren(form);
|
|
15564
|
+
input.focus();
|
|
15565
|
+
});
|
|
15566
|
+
wrap.appendChild(start);
|
|
15567
|
+
return wrap;
|
|
15568
|
+
}
|
|
15569
|
+
// ── controls ──
|
|
15570
|
+
/** A fold: a one-line header saying what's set, opening to its rows. */
|
|
15571
|
+
fold(b, key, title, summary, body) {
|
|
15572
|
+
const type = b.live().type;
|
|
15573
|
+
const open = openFolds.get(type) ?? /* @__PURE__ */ new Set();
|
|
15574
|
+
openFolds.set(type, open);
|
|
15575
|
+
const wrap = document.createElement("div");
|
|
15576
|
+
wrap.className = "vela-dsp-fold";
|
|
15577
|
+
const head = document.createElement("button");
|
|
15578
|
+
head.type = "button";
|
|
15579
|
+
head.className = "vela-dsp-fold-h";
|
|
15580
|
+
const t = document.createElement("span");
|
|
15581
|
+
t.className = "vela-dsp-fold-t";
|
|
15582
|
+
t.textContent = title;
|
|
15583
|
+
const s = document.createElement("span");
|
|
15584
|
+
s.className = "vela-dsp-fold-s";
|
|
15585
|
+
const chev = document.createElement("span");
|
|
15586
|
+
chev.className = "vela-dsp-chev";
|
|
15587
|
+
chev.innerHTML = CHEVRON;
|
|
15588
|
+
head.append(t, s, chev);
|
|
15589
|
+
const content = document.createElement("div");
|
|
15590
|
+
content.className = "vela-dsp-fold-b";
|
|
15591
|
+
let built = false;
|
|
15592
|
+
const setOpen = (on) => {
|
|
15593
|
+
if (on) open.add(key);
|
|
15594
|
+
else open.delete(key);
|
|
15595
|
+
wrap.toggleAttribute("data-open", on);
|
|
15596
|
+
head.setAttribute("aria-expanded", String(on));
|
|
15597
|
+
if (on && !built) {
|
|
15598
|
+
built = true;
|
|
15599
|
+
content.append(...body());
|
|
14649
15600
|
}
|
|
14650
|
-
|
|
15601
|
+
};
|
|
15602
|
+
head.addEventListener("click", () => setOpen(!open.has(key)));
|
|
15603
|
+
b.watch(() => s.textContent = summary());
|
|
15604
|
+
setOpen(open.has(key));
|
|
15605
|
+
wrap.append(head, content);
|
|
15606
|
+
return wrap;
|
|
15607
|
+
}
|
|
15608
|
+
seg(b, label, options, read, pick, full = false) {
|
|
15609
|
+
const el = document.createElement("div");
|
|
15610
|
+
el.className = "vela-dsp-seg";
|
|
15611
|
+
el.setAttribute("role", "group");
|
|
15612
|
+
el.setAttribute("aria-label", label);
|
|
15613
|
+
if (full) el.dataset.full = "";
|
|
15614
|
+
const buttons = options.map((o) => {
|
|
15615
|
+
const btn = document.createElement("button");
|
|
15616
|
+
btn.type = "button";
|
|
15617
|
+
btn.innerHTML = o.html;
|
|
15618
|
+
if (o.title) {
|
|
15619
|
+
btn.title = o.title;
|
|
15620
|
+
btn.setAttribute("aria-label", o.title);
|
|
15621
|
+
}
|
|
15622
|
+
btn.addEventListener("click", () => {
|
|
15623
|
+
pick(o.value);
|
|
15624
|
+
sync();
|
|
15625
|
+
});
|
|
15626
|
+
el.appendChild(btn);
|
|
15627
|
+
return btn;
|
|
15628
|
+
});
|
|
15629
|
+
const sync = () => {
|
|
15630
|
+
const cur = read();
|
|
15631
|
+
options.forEach((o, i) => buttons[i].setAttribute("aria-pressed", String(o.value === cur)));
|
|
15632
|
+
};
|
|
15633
|
+
b.watch(sync);
|
|
15634
|
+
return el;
|
|
15635
|
+
}
|
|
15636
|
+
chip(b, text, read, set, title) {
|
|
15637
|
+
const btn = document.createElement("button");
|
|
15638
|
+
btn.type = "button";
|
|
15639
|
+
btn.className = "vela-dsp-chip";
|
|
15640
|
+
btn.textContent = text;
|
|
15641
|
+
if (title) {
|
|
15642
|
+
btn.title = title;
|
|
15643
|
+
btn.setAttribute("aria-label", title);
|
|
15644
|
+
}
|
|
15645
|
+
if (text === "B") btn.style.fontWeight = "700";
|
|
15646
|
+
if (text === "I") btn.style.fontStyle = "italic";
|
|
15647
|
+
btn.addEventListener("click", () => set(!read()));
|
|
15648
|
+
b.watch(() => btn.setAttribute("aria-pressed", String(read())));
|
|
15649
|
+
return btn;
|
|
15650
|
+
}
|
|
15651
|
+
swatch(b, title, read, path) {
|
|
15652
|
+
const ctrl = buildFieldControl({ kind: "color", theme: this.theme, title, get: read, onChange: (v) => b.edit({ [path]: v }) });
|
|
15653
|
+
return ctrl.el;
|
|
15654
|
+
}
|
|
15655
|
+
switch(b, read, set) {
|
|
15656
|
+
const sw = buildFieldControl({ kind: "switch", checked: read(), onChange: set });
|
|
15657
|
+
b.watch(() => sw.setValue?.(read()));
|
|
15658
|
+
const wrap = document.createElement("div");
|
|
15659
|
+
wrap.style.cssText = "display:flex;flex:1;justify-content:flex-end;";
|
|
15660
|
+
wrap.appendChild(sw.el);
|
|
15661
|
+
return wrap;
|
|
15662
|
+
}
|
|
15663
|
+
number(b, title, read, path, f) {
|
|
15664
|
+
const step = f.step ?? 1;
|
|
15665
|
+
const ni = buildFieldControl({
|
|
15666
|
+
kind: "number",
|
|
15667
|
+
value: read(),
|
|
15668
|
+
min: f.min,
|
|
15669
|
+
max: f.max,
|
|
15670
|
+
step,
|
|
15671
|
+
integer: step >= 1 && Number.isInteger(step),
|
|
15672
|
+
fill: false,
|
|
15673
|
+
commit: "blur",
|
|
15674
|
+
title,
|
|
15675
|
+
onChange: (v) => b.edit({ [path]: v })
|
|
14651
15676
|
});
|
|
15677
|
+
const input = ni.el.querySelector("input");
|
|
15678
|
+
b.watch(() => {
|
|
15679
|
+
if (document.activeElement !== input) ni.setValue?.(read());
|
|
15680
|
+
});
|
|
15681
|
+
return ni.el;
|
|
14652
15682
|
}
|
|
14653
15683
|
};
|
|
14654
|
-
function
|
|
15684
|
+
function block(title, ...rows) {
|
|
15685
|
+
const el = document.createElement("div");
|
|
15686
|
+
el.className = "vela-dsp-block";
|
|
15687
|
+
if (title) {
|
|
15688
|
+
const h = document.createElement("div");
|
|
15689
|
+
h.className = "vela-dsp-h";
|
|
15690
|
+
h.textContent = title;
|
|
15691
|
+
el.appendChild(h);
|
|
15692
|
+
}
|
|
15693
|
+
el.append(...rows);
|
|
15694
|
+
return el;
|
|
15695
|
+
}
|
|
15696
|
+
function row(label, ...controls) {
|
|
15697
|
+
const el = document.createElement("div");
|
|
15698
|
+
el.className = "vela-dsp-row";
|
|
15699
|
+
const lab = document.createElement("span");
|
|
15700
|
+
lab.className = "vela-dsp-lab";
|
|
15701
|
+
lab.textContent = label;
|
|
15702
|
+
lab.title = label;
|
|
15703
|
+
el.append(lab, ...controls);
|
|
15704
|
+
return el;
|
|
15705
|
+
}
|
|
15706
|
+
function chips(...items) {
|
|
15707
|
+
const el = document.createElement("div");
|
|
15708
|
+
el.className = "vela-dsp-chips";
|
|
15709
|
+
el.append(...items);
|
|
15710
|
+
return el;
|
|
15711
|
+
}
|
|
15712
|
+
function menuHeading(text) {
|
|
15713
|
+
const el = document.createElement("div");
|
|
15714
|
+
el.className = "vela-dsp-mh";
|
|
15715
|
+
el.textContent = text;
|
|
15716
|
+
return el;
|
|
15717
|
+
}
|
|
15718
|
+
function separator() {
|
|
15719
|
+
const el = document.createElement("div");
|
|
15720
|
+
el.className = "vela-dsp-msep";
|
|
15721
|
+
return el;
|
|
15722
|
+
}
|
|
15723
|
+
function button(label, primary, onClick) {
|
|
14655
15724
|
const b = document.createElement("button");
|
|
14656
15725
|
b.type = "button";
|
|
14657
15726
|
b.textContent = label;
|
|
@@ -14659,6 +15728,50 @@ function btn(label, primary, onClick) {
|
|
|
14659
15728
|
b.addEventListener("click", onClick);
|
|
14660
15729
|
return b;
|
|
14661
15730
|
}
|
|
15731
|
+
function sizeLabel(size) {
|
|
15732
|
+
return TEXT_SIZE_OPTIONS.find((o) => o.value === (size ?? "normal"))?.label ?? "Normal";
|
|
15733
|
+
}
|
|
15734
|
+
function readPath(d, path) {
|
|
15735
|
+
let cur = d;
|
|
15736
|
+
for (const k of path.split(".")) {
|
|
15737
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
15738
|
+
cur = cur[k];
|
|
15739
|
+
}
|
|
15740
|
+
return cur;
|
|
15741
|
+
}
|
|
15742
|
+
function pairBase(colorLabel, styleLabel3) {
|
|
15743
|
+
const c = /^(.*) colou?r$/i.exec(colorLabel);
|
|
15744
|
+
const st = /^(.*) style$/i.exec(styleLabel3);
|
|
15745
|
+
return c && st && c[1] === st[1] ? c[1] : null;
|
|
15746
|
+
}
|
|
15747
|
+
function upperLowerBase(upper, lower) {
|
|
15748
|
+
const u = /^Upper (.+)$/i.exec(upper);
|
|
15749
|
+
const l = /^Lower (.+)$/i.exec(lower);
|
|
15750
|
+
if (!u || !l || u[1] !== l[1]) return null;
|
|
15751
|
+
const name = u[1].replace(/ colou?r$/i, "");
|
|
15752
|
+
const word = name.charAt(0).toUpperCase() + name.slice(1);
|
|
15753
|
+
return /fill$/i.test(word) ? word : `${word}s`;
|
|
15754
|
+
}
|
|
15755
|
+
function modeOf(showOn) {
|
|
15756
|
+
if (!showOn) return "all";
|
|
15757
|
+
const same = (a) => a.length === showOn.length && a.every((k) => showOn.includes(k));
|
|
15758
|
+
if (same(INTRADAY)) return "intraday";
|
|
15759
|
+
if (same(HIGHER)) return "higher";
|
|
15760
|
+
return "custom";
|
|
15761
|
+
}
|
|
15762
|
+
function matchesTemplate(d, s) {
|
|
15763
|
+
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
15764
|
+
const style = d.style;
|
|
15765
|
+
for (const [k, v] of Object.entries(s.style ?? {})) if (!same(style[k], v)) return false;
|
|
15766
|
+
const text = d.text ?? {};
|
|
15767
|
+
for (const [k, v] of Object.entries(s.text ?? {})) if (!same(text[k], v)) return false;
|
|
15768
|
+
const props = d.serialize().props ?? {};
|
|
15769
|
+
for (const [k, v] of Object.entries(s.props ?? {})) if (!same(props[k], v)) return false;
|
|
15770
|
+
return Object.keys(s.style ?? {}).length + Object.keys(s.text ?? {}).length + Object.keys(s.props ?? {}).length > 0;
|
|
15771
|
+
}
|
|
15772
|
+
function templateColor(s) {
|
|
15773
|
+
return s.style?.lineColor ?? s.style?.fillColor ?? s.text?.color;
|
|
15774
|
+
}
|
|
14662
15775
|
|
|
14663
15776
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
14664
15777
|
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
@@ -14680,15 +15793,15 @@ function distinctValues(drawings, read) {
|
|
|
14680
15793
|
}
|
|
14681
15794
|
var BTN = 30;
|
|
14682
15795
|
var ICON = 21;
|
|
14683
|
-
var
|
|
14684
|
-
var
|
|
14685
|
-
function
|
|
15796
|
+
var STYLE_ID5 = "vela-drawing-popup-styles";
|
|
15797
|
+
var STYLE_REV2 = "4";
|
|
15798
|
+
function ensureStyles5() {
|
|
14686
15799
|
if (typeof document === "undefined") return;
|
|
14687
|
-
const existing = document.getElementById(
|
|
14688
|
-
if (existing?.dataset.rev ===
|
|
15800
|
+
const existing = document.getElementById(STYLE_ID5);
|
|
15801
|
+
if (existing?.dataset.rev === STYLE_REV2) return;
|
|
14689
15802
|
const s = existing ?? document.createElement("style");
|
|
14690
|
-
s.id =
|
|
14691
|
-
s.dataset.rev =
|
|
15803
|
+
s.id = STYLE_ID5;
|
|
15804
|
+
s.dataset.rev = STYLE_REV2;
|
|
14692
15805
|
s.textContent = `
|
|
14693
15806
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
14694
15807
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
@@ -14704,13 +15817,14 @@ function ensureStyles4() {
|
|
|
14704
15817
|
if (!existing) document.head.appendChild(s);
|
|
14705
15818
|
}
|
|
14706
15819
|
var DrawingSettingsPopup = class {
|
|
14707
|
-
constructor(host, theme,
|
|
15820
|
+
constructor(host, theme, env = {}) {
|
|
14708
15821
|
this.host = host;
|
|
14709
|
-
this.chartBarMs = chartBarMs;
|
|
14710
15822
|
this.el = null;
|
|
14711
15823
|
this.tipEl = null;
|
|
14712
15824
|
// floating hover-label (above/below the toolbar)
|
|
14713
15825
|
this.textPanel = null;
|
|
15826
|
+
/** The single drawing the bar is open on (a multi-selection has no settings panel). */
|
|
15827
|
+
this.current = null;
|
|
14714
15828
|
this.colorPop = null;
|
|
14715
15829
|
this.colorOwner = null;
|
|
14716
15830
|
this.menuPop = null;
|
|
@@ -14718,9 +15832,10 @@ var DrawingSettingsPopup = class {
|
|
|
14718
15832
|
this.onClose = null;
|
|
14719
15833
|
this.onOutside = (e) => {
|
|
14720
15834
|
const node = e.target;
|
|
14721
|
-
if (this.
|
|
14722
|
-
if (this.
|
|
14723
|
-
this.
|
|
15835
|
+
if (this.panel.isOpen()) {
|
|
15836
|
+
if (this.panel.contains(node) || this.isOwnChrome(node)) return;
|
|
15837
|
+
if (this.host.contains(node)) return;
|
|
15838
|
+
this.panel.close();
|
|
14724
15839
|
return;
|
|
14725
15840
|
}
|
|
14726
15841
|
if (this.isOwnChrome(node)) return;
|
|
@@ -14729,19 +15844,16 @@ var DrawingSettingsPopup = class {
|
|
|
14729
15844
|
cb?.(e);
|
|
14730
15845
|
};
|
|
14731
15846
|
this.theme = theme;
|
|
14732
|
-
this.
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
14739
|
-
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
14740
|
-
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
15847
|
+
this.chartBarMs = env.chartBarMs ?? (() => 0);
|
|
15848
|
+
this.panel = new DrawingSettingsPanel(host, theme, {
|
|
15849
|
+
chartBarMs: this.chartBarMs,
|
|
15850
|
+
timeZone: env.timeZone ?? (() => "UTC"),
|
|
15851
|
+
priceDecimals: env.priceDecimals ?? (() => null)
|
|
15852
|
+
});
|
|
14741
15853
|
}
|
|
14742
15854
|
setTheme(theme) {
|
|
14743
15855
|
this.theme = theme;
|
|
14744
|
-
this.
|
|
15856
|
+
this.panel.setTheme(theme);
|
|
14745
15857
|
}
|
|
14746
15858
|
isOpen() {
|
|
14747
15859
|
return this.el != null;
|
|
@@ -14749,7 +15861,22 @@ var DrawingSettingsPopup = class {
|
|
|
14749
15861
|
/** Whether `node` belongs to this popup — the bar, its host-floated color/menu shells,
|
|
14750
15862
|
* or a kit popover (select list / color chip) portaled into the chart host. */
|
|
14751
15863
|
contains(node) {
|
|
14752
|
-
return node != null && (this.isOwnChrome(node) || this.
|
|
15864
|
+
return node != null && (this.isOwnChrome(node) || this.panel.contains(node));
|
|
15865
|
+
}
|
|
15866
|
+
/** Open the full settings panel for the drawing the bar is open on. False when there is
|
|
15867
|
+
* none (no bar, or a multi-selection). */
|
|
15868
|
+
openPanel() {
|
|
15869
|
+
const cur = this.current;
|
|
15870
|
+
if (!cur) return false;
|
|
15871
|
+
this.hideTip();
|
|
15872
|
+
this.closeColorPopover();
|
|
15873
|
+
this.closeMenu();
|
|
15874
|
+
this.panel.open(cur.drawing, cur.actions, cur.anchor);
|
|
15875
|
+
return true;
|
|
15876
|
+
}
|
|
15877
|
+
/** The drawings changed under the panel (a drag, an undo) — let it re-read them. */
|
|
15878
|
+
syncPanel() {
|
|
15879
|
+
this.panel.sync();
|
|
14753
15880
|
}
|
|
14754
15881
|
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
14755
15882
|
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
@@ -14759,10 +15886,12 @@ var DrawingSettingsPopup = class {
|
|
|
14759
15886
|
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
14760
15887
|
* modifier press (multi-select) from a plain one. */
|
|
14761
15888
|
open(drawings, anchor, actions, onClose) {
|
|
14762
|
-
this.close();
|
|
14763
15889
|
const drawing = drawings[0];
|
|
15890
|
+
const keepPanel = this.panel.isOpen() && drawings.length === 1 && drawing != null && this.current?.drawing.id === drawing.id;
|
|
15891
|
+
this.close(keepPanel);
|
|
14764
15892
|
if (!drawing) return;
|
|
14765
|
-
|
|
15893
|
+
this.current = drawings.length === 1 ? { drawing, actions, anchor } : null;
|
|
15894
|
+
ensureStyles5();
|
|
14766
15895
|
this.onClose = onClose ?? null;
|
|
14767
15896
|
const t = this.theme;
|
|
14768
15897
|
const multi = drawings.length > 1;
|
|
@@ -14790,11 +15919,11 @@ var DrawingSettingsPopup = class {
|
|
|
14790
15919
|
}
|
|
14791
15920
|
if (paths.has("size")) {
|
|
14792
15921
|
const sz = common((d) => d.size ?? "normal");
|
|
14793
|
-
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label:
|
|
15922
|
+
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel2 }));
|
|
14794
15923
|
}
|
|
14795
15924
|
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
14796
15925
|
const mag = (d) => d;
|
|
14797
|
-
const options = this.
|
|
15926
|
+
const options = magnifierTimeframesBelow(this.chartBarMs());
|
|
14798
15927
|
if (options.length > 0) {
|
|
14799
15928
|
bar.appendChild(
|
|
14800
15929
|
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
@@ -14823,7 +15952,6 @@ var DrawingSettingsPopup = class {
|
|
|
14823
15952
|
}
|
|
14824
15953
|
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
14825
15954
|
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
14826
|
-
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
14827
15955
|
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
14828
15956
|
if (isPosition) {
|
|
14829
15957
|
const pos = (d) => d;
|
|
@@ -14892,7 +16020,7 @@ var DrawingSettingsPopup = class {
|
|
|
14892
16020
|
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
14893
16021
|
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
14894
16022
|
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
14895
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label:
|
|
16023
|
+
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel2 }));
|
|
14896
16024
|
const textField = !multi && paths.has("text.value");
|
|
14897
16025
|
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
14898
16026
|
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
@@ -14901,13 +16029,11 @@ var DrawingSettingsPopup = class {
|
|
|
14901
16029
|
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
14902
16030
|
const fib = (d) => d;
|
|
14903
16031
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
14904
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label:
|
|
14905
|
-
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label:
|
|
16032
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel2 }));
|
|
16033
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel2 }));
|
|
14906
16034
|
}
|
|
14907
16035
|
bar.appendChild(this.divider());
|
|
14908
|
-
if (!multi
|
|
14909
|
-
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
14910
|
-
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
16036
|
+
if (!multi) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.openPanel()));
|
|
14911
16037
|
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
14912
16038
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
14913
16039
|
del.style.color = "var(--vela-danger)";
|
|
@@ -14925,16 +16051,17 @@ var DrawingSettingsPopup = class {
|
|
|
14925
16051
|
this.position(anchor);
|
|
14926
16052
|
setTimeout(() => document.addEventListener("pointerdown", this.onOutside), 0);
|
|
14927
16053
|
}
|
|
14928
|
-
close() {
|
|
16054
|
+
close(keepPanel = false) {
|
|
14929
16055
|
document.removeEventListener("pointerdown", this.onOutside);
|
|
14930
16056
|
this.closeColorPopover();
|
|
14931
16057
|
this.closeMenu();
|
|
14932
16058
|
this.hideTip();
|
|
14933
|
-
this.
|
|
16059
|
+
if (!keepPanel) this.panel.close();
|
|
14934
16060
|
this.el?.remove();
|
|
14935
16061
|
this.el = null;
|
|
14936
16062
|
this.textPanel = null;
|
|
14937
16063
|
this.onClose = null;
|
|
16064
|
+
this.current = null;
|
|
14938
16065
|
}
|
|
14939
16066
|
destroy() {
|
|
14940
16067
|
this.close();
|
|
@@ -14990,7 +16117,7 @@ var DrawingSettingsPopup = class {
|
|
|
14990
16117
|
if (withColorAndSize) {
|
|
14991
16118
|
tools.append(
|
|
14992
16119
|
this.colorButton("Text color", TYPE_ICON, text?.color || this.theme.textColor, (v) => actions.patch({ "text.color": v }), 14),
|
|
14993
|
-
this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), text?.size ?? "normal", (s) => sizeIcon(s), (v) => actions.patch({ "text.size": v }), { label:
|
|
16120
|
+
this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), text?.size ?? "normal", (s) => sizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel2 })
|
|
14994
16121
|
);
|
|
14995
16122
|
}
|
|
14996
16123
|
tools.append(
|
|
@@ -15164,9 +16291,9 @@ var DrawingSettingsPopup = class {
|
|
|
15164
16291
|
* quick toolbar — the chip works without selecting the drawing first.
|
|
15165
16292
|
*/
|
|
15166
16293
|
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
15167
|
-
|
|
16294
|
+
ensureStyles5();
|
|
15168
16295
|
closeOpenPopovers();
|
|
15169
|
-
const options = this.
|
|
16296
|
+
const options = magnifierTimeframesBelow(this.chartBarMs());
|
|
15170
16297
|
const anchor = document.createElement("div");
|
|
15171
16298
|
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
15172
16299
|
this.host.appendChild(anchor);
|
|
@@ -15207,22 +16334,22 @@ var DrawingSettingsPopup = class {
|
|
|
15207
16334
|
zIndex: 26,
|
|
15208
16335
|
padding: "4px",
|
|
15209
16336
|
fill: (menu, pop) => {
|
|
15210
|
-
for (const
|
|
16337
|
+
for (const row2 of rows) {
|
|
15211
16338
|
const item = document.createElement("button");
|
|
15212
16339
|
item.type = "button";
|
|
15213
16340
|
item.className = "vela-dpop-item";
|
|
15214
16341
|
item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
|
|
15215
16342
|
const ic = document.createElement("span");
|
|
15216
16343
|
ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
|
|
15217
|
-
ic.innerHTML = sized(
|
|
16344
|
+
ic.innerHTML = sized(row2.icon, 18);
|
|
15218
16345
|
const tx = document.createElement("span");
|
|
15219
|
-
tx.textContent =
|
|
16346
|
+
tx.textContent = row2.label;
|
|
15220
16347
|
tx.style.cssText = "flex:1;";
|
|
15221
16348
|
item.append(ic, tx);
|
|
15222
16349
|
item.addEventListener("click", (e) => {
|
|
15223
16350
|
e.stopPropagation();
|
|
15224
16351
|
pop.hide();
|
|
15225
|
-
|
|
16352
|
+
row2.onClick();
|
|
15226
16353
|
});
|
|
15227
16354
|
menu.appendChild(item);
|
|
15228
16355
|
}
|
|
@@ -15447,12 +16574,12 @@ var DrawingSettingsPopup = class {
|
|
|
15447
16574
|
padding: "10px",
|
|
15448
16575
|
fill: (el, pop) => {
|
|
15449
16576
|
if (used.length > 1) {
|
|
15450
|
-
const
|
|
15451
|
-
|
|
16577
|
+
const row2 = document.createElement("div");
|
|
16578
|
+
row2.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
15452
16579
|
const lbl = document.createElement("span");
|
|
15453
16580
|
lbl.textContent = "In use";
|
|
15454
16581
|
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
15455
|
-
|
|
16582
|
+
row2.appendChild(lbl);
|
|
15456
16583
|
for (const c of used) {
|
|
15457
16584
|
const sw = document.createElement("button");
|
|
15458
16585
|
sw.type = "button";
|
|
@@ -15463,9 +16590,9 @@ var DrawingSettingsPopup = class {
|
|
|
15463
16590
|
pop.hide();
|
|
15464
16591
|
onChange(c);
|
|
15465
16592
|
});
|
|
15466
|
-
|
|
16593
|
+
row2.appendChild(sw);
|
|
15467
16594
|
}
|
|
15468
|
-
el.appendChild(
|
|
16595
|
+
el.appendChild(row2);
|
|
15469
16596
|
}
|
|
15470
16597
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
15471
16598
|
}
|
|
@@ -15539,7 +16666,7 @@ function labelSizeIcon(size) {
|
|
|
15539
16666
|
function capitalize(s) {
|
|
15540
16667
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
15541
16668
|
}
|
|
15542
|
-
function
|
|
16669
|
+
function sizeLabel2(v) {
|
|
15543
16670
|
return TEXT_SIZE_OPTIONS.find((o) => o.value === v)?.label ?? capitalize(String(v));
|
|
15544
16671
|
}
|
|
15545
16672
|
function styleLabel2(v) {
|
|
@@ -15762,7 +16889,9 @@ var UserDrawingController = class {
|
|
|
15762
16889
|
this.hoveredId = null;
|
|
15763
16890
|
// the drawing under the cursor (its handles show)
|
|
15764
16891
|
this.activeTool = null;
|
|
15765
|
-
//
|
|
16892
|
+
// the armed tool's remembered settings (seed the placement ghost)
|
|
16893
|
+
this.toolTemplates = {};
|
|
16894
|
+
// saved styles, per tool
|
|
15766
16895
|
this.intentCb = null;
|
|
15767
16896
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
15768
16897
|
this.externalGhost = null;
|
|
@@ -15810,7 +16939,11 @@ var UserDrawingController = class {
|
|
|
15810
16939
|
this.clearSelection();
|
|
15811
16940
|
};
|
|
15812
16941
|
this.ctx = canvas.getContext("2d");
|
|
15813
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(),
|
|
16942
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), {
|
|
16943
|
+
chartBarMs: () => deps.chartBarMs(),
|
|
16944
|
+
timeZone: () => deps.timeZone?.() ?? "UTC",
|
|
16945
|
+
priceDecimals: () => deps.priceDecimals?.() ?? null
|
|
16946
|
+
});
|
|
15814
16947
|
this.toolbar = new DrawingToolbar(
|
|
15815
16948
|
toolbarHost,
|
|
15816
16949
|
deps.theme(),
|
|
@@ -15833,7 +16966,7 @@ var UserDrawingController = class {
|
|
|
15833
16966
|
this.interaction = new DrawingInteraction({
|
|
15834
16967
|
projector: () => this.deps.projector(),
|
|
15835
16968
|
activeTool: () => this.activeTool,
|
|
15836
|
-
drawings: () => this.
|
|
16969
|
+
drawings: () => this.onScreen(),
|
|
15837
16970
|
hoveredId: () => this.hoveredId,
|
|
15838
16971
|
selectedIds: () => this.selectedIds,
|
|
15839
16972
|
emit: (i) => this.emit(i),
|
|
@@ -15843,7 +16976,7 @@ var UserDrawingController = class {
|
|
|
15843
16976
|
},
|
|
15844
16977
|
openSettings: (id, x, y) => this.openSettingsById(id, x, y),
|
|
15845
16978
|
snap: (pt, paneId, mode, cursorPx) => this.deps.snap(pt, paneId, mode, cursorPx),
|
|
15846
|
-
|
|
16979
|
+
toolDefaults: () => this.activeToolDefaults
|
|
15847
16980
|
});
|
|
15848
16981
|
}
|
|
15849
16982
|
// ── IDrawingsRendererPort (commands down) ──
|
|
@@ -15895,6 +17028,7 @@ var UserDrawingController = class {
|
|
|
15895
17028
|
this.invalidateSlices();
|
|
15896
17029
|
this.render();
|
|
15897
17030
|
this.deps.requestScaleUpdate();
|
|
17031
|
+
this.popup.syncPanel();
|
|
15898
17032
|
}
|
|
15899
17033
|
/** The pane's series stack in z terms — how the core places a new drawing (just under
|
|
15900
17034
|
* `price`) and computes "front of everything" / "behind everything" for the reorders. */
|
|
@@ -15922,7 +17056,7 @@ var UserDrawingController = class {
|
|
|
15922
17056
|
}
|
|
15923
17057
|
return lo <= hi ? { min: lo, max: hi } : null;
|
|
15924
17058
|
}
|
|
15925
|
-
setActiveTool(type, lastStyle) {
|
|
17059
|
+
setActiveTool(type, lastStyle, defaults) {
|
|
15926
17060
|
if (type != null) {
|
|
15927
17061
|
this.finishTextEditor(true);
|
|
15928
17062
|
this.withModeIntent(() => {
|
|
@@ -15931,7 +17065,7 @@ var UserDrawingController = class {
|
|
|
15931
17065
|
});
|
|
15932
17066
|
}
|
|
15933
17067
|
this.activeTool = type;
|
|
15934
|
-
this.
|
|
17068
|
+
this.activeToolDefaults = defaults ?? (lastStyle ? { style: lastStyle } : void 0);
|
|
15935
17069
|
this.toolbar.setActiveTool(type);
|
|
15936
17070
|
if (type == null) this.interaction.onToolCleared();
|
|
15937
17071
|
else this.clearSelection();
|
|
@@ -15941,6 +17075,10 @@ var UserDrawingController = class {
|
|
|
15941
17075
|
setFavorites(types) {
|
|
15942
17076
|
this.toolbar.setFavorites(types);
|
|
15943
17077
|
}
|
|
17078
|
+
/** Core push: every tool's saved styles — the settings panel's style menu reads them. */
|
|
17079
|
+
setToolTemplates(map2) {
|
|
17080
|
+
this.toolTemplates = map2;
|
|
17081
|
+
}
|
|
15944
17082
|
/** Core push: per-tool shortcut hints (display strings) shown in the toolbar flyouts. */
|
|
15945
17083
|
setToolShortcuts(map2) {
|
|
15946
17084
|
this.toolbar.setShortcuts(map2);
|
|
@@ -16013,8 +17151,9 @@ var UserDrawingController = class {
|
|
|
16013
17151
|
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
16014
17152
|
* the chip's rect is what the painter measured last frame. */
|
|
16015
17153
|
magnifierChipAt(x, y) {
|
|
16016
|
-
|
|
16017
|
-
|
|
17154
|
+
const shown = this.onScreen();
|
|
17155
|
+
for (let i = shown.length - 1; i >= 0; i -= 1) {
|
|
17156
|
+
const d = shown[i];
|
|
16018
17157
|
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
16019
17158
|
const r = d.chipRect;
|
|
16020
17159
|
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
@@ -16040,7 +17179,7 @@ var UserDrawingController = class {
|
|
|
16040
17179
|
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
16041
17180
|
* unlocked members — whichever member was hit, locked or not. */
|
|
16042
17181
|
deleteAt(x, y, withSelection = false) {
|
|
16043
|
-
const hit = topDrawingAt(this.
|
|
17182
|
+
const hit = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
16044
17183
|
if (!hit) return false;
|
|
16045
17184
|
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
16046
17185
|
if (ids.length === 0) return false;
|
|
@@ -16120,7 +17259,7 @@ var UserDrawingController = class {
|
|
|
16120
17259
|
updateHover(x, y, mod = false) {
|
|
16121
17260
|
let id = null;
|
|
16122
17261
|
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
16123
|
-
id = topDrawingAt(this.
|
|
17262
|
+
id = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
16124
17263
|
}
|
|
16125
17264
|
if (id !== this.hoveredId) {
|
|
16126
17265
|
this.hoveredId = id;
|
|
@@ -16355,16 +17494,20 @@ var UserDrawingController = class {
|
|
|
16355
17494
|
}
|
|
16356
17495
|
return false;
|
|
16357
17496
|
}
|
|
16358
|
-
/** Double-click over a drawing
|
|
16359
|
-
*
|
|
17497
|
+
/** Double-click over a drawing opens its full settings panel (a callout or text label edits
|
|
17498
|
+
* its words in place instead) and suppresses the chart's view reset. Returns true only
|
|
17499
|
+
* when a drawing is under the cursor. */
|
|
16360
17500
|
dblClick(x, y) {
|
|
16361
17501
|
if (this.interaction.finishPlacing(true)) return true;
|
|
16362
|
-
const hit = topDrawingAt(this.
|
|
17502
|
+
const hit = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
16363
17503
|
if (isInlineEditable(hit)) {
|
|
16364
17504
|
this.editTextInline(hit.id);
|
|
16365
17505
|
return true;
|
|
16366
17506
|
}
|
|
16367
|
-
|
|
17507
|
+
if (!hit) return false;
|
|
17508
|
+
if (this.popupMulti || !this.selectedIds.has(hit.id) || !this.popup.isOpen()) this.openSettingsById(hit.id, 0, 0);
|
|
17509
|
+
this.popup.openPanel();
|
|
17510
|
+
return true;
|
|
16368
17511
|
}
|
|
16369
17512
|
/** Keyboard pre-empt: Escape (popup/placing/selection), undo/redo, copy/paste/duplicate,
|
|
16370
17513
|
* delete (multi), and arrow-nudge. Stands down while a label text field is focused. */
|
|
@@ -16462,6 +17605,13 @@ var UserDrawingController = class {
|
|
|
16462
17605
|
this.popup.close();
|
|
16463
17606
|
this.render();
|
|
16464
17607
|
}
|
|
17608
|
+
/** The drawings shown on the chart's current timeframe. One limited to other timeframes
|
|
17609
|
+
* ("show on") is neither painted nor clickable here, but stays in the model: the object
|
|
17610
|
+
* tree still lists it, and its own visibility flag is untouched. */
|
|
17611
|
+
onScreen() {
|
|
17612
|
+
const barMs = this.deps.chartBarMs();
|
|
17613
|
+
return this.drawings.filter((d) => shownOnTimeframe(d.showOn, barMs));
|
|
17614
|
+
}
|
|
16465
17615
|
/** Whether the drawing's z puts it INSIDE the series stack (per the last-known boundaries)
|
|
16466
17616
|
* rather than over it — i.e. its body belongs to an interleave layer, not the top canvas. */
|
|
16467
17617
|
isInterleaved(d) {
|
|
@@ -16501,7 +17651,7 @@ var UserDrawingController = class {
|
|
|
16501
17651
|
const proj = this.deps.projector();
|
|
16502
17652
|
const theme = this.deps.theme();
|
|
16503
17653
|
const buckets = /* @__PURE__ */ new Map();
|
|
16504
|
-
for (const d of this.
|
|
17654
|
+
for (const d of this.onScreen()) {
|
|
16505
17655
|
if (!d.visible || this.lifted.has(d.id)) continue;
|
|
16506
17656
|
const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
|
|
16507
17657
|
if (beforeZ === null) continue;
|
|
@@ -16555,8 +17705,9 @@ var UserDrawingController = class {
|
|
|
16555
17705
|
};
|
|
16556
17706
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
16557
17707
|
const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
|
|
16558
|
-
|
|
16559
|
-
this.painter.
|
|
17708
|
+
const shown = this.onScreen();
|
|
17709
|
+
this.painter.paintAll(ctx, shown.filter(onTop), proj, this.deps.theme(), targets);
|
|
17710
|
+
this.painter.paintHighlights(ctx, shown.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
|
|
16560
17711
|
const clones = this.interaction.dragClones();
|
|
16561
17712
|
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
16562
17713
|
this.layoutTextEditor();
|
|
@@ -16666,6 +17817,7 @@ var UserDrawingController = class {
|
|
|
16666
17817
|
this.emit({ kind: "select", ids: [id] });
|
|
16667
17818
|
this.emit({ kind: "settings", id });
|
|
16668
17819
|
const anchor = drawing.bounds(this.deps.projector());
|
|
17820
|
+
let previewBase = null;
|
|
16669
17821
|
this.popup.open([drawing], anchor, {
|
|
16670
17822
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
16671
17823
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
@@ -16700,11 +17852,39 @@ var UserDrawingController = class {
|
|
|
16700
17852
|
restore: (doc) => {
|
|
16701
17853
|
const d = live();
|
|
16702
17854
|
if (!d) return;
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
|
|
17855
|
+
const text = doc.text ?? (d.text ? { ...d.text, value: "" } : void 0);
|
|
17856
|
+
restoreLook(d, { ...doc, text });
|
|
17857
|
+
d.anchors = doc.anchors.map((a) => ({ ...a }));
|
|
17858
|
+
d.showOn = doc.showOn ? [...doc.showOn] : void 0;
|
|
17859
|
+
this.render();
|
|
17860
|
+
this.emit({ kind: "edit", doc: { ...doc, text } });
|
|
17861
|
+
},
|
|
17862
|
+
templates: () => this.toolTemplates[drawing.type] ?? [],
|
|
17863
|
+
saveTemplate: (name) => {
|
|
17864
|
+
const d = live();
|
|
17865
|
+
if (d) this.emit({ kind: "template-save", type: d.type, name, settings: captureToolDefaults(d) });
|
|
17866
|
+
},
|
|
17867
|
+
removeTemplate: (name) => this.emit({ kind: "template-remove", type: drawing.type, name }),
|
|
17868
|
+
applyToolSettings: (settings) => {
|
|
17869
|
+
const d = live();
|
|
17870
|
+
if (!d) return;
|
|
17871
|
+
applyToolDefaults(d, settings);
|
|
17872
|
+
this.render();
|
|
17873
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
17874
|
+
},
|
|
17875
|
+
// A hovered style paints on the drawing without an edit; leaving puts the look back.
|
|
17876
|
+
preview: (settings) => {
|
|
17877
|
+
const d = live();
|
|
17878
|
+
if (!d) return;
|
|
17879
|
+
if (settings) {
|
|
17880
|
+
if (previewBase) restoreLook(d, previewBase);
|
|
17881
|
+
else previewBase = d.serialize();
|
|
17882
|
+
applyToolDefaults(d, settings);
|
|
17883
|
+
} else if (previewBase) {
|
|
17884
|
+
restoreLook(d, previewBase);
|
|
17885
|
+
previewBase = null;
|
|
17886
|
+
} else return;
|
|
16706
17887
|
this.render();
|
|
16707
|
-
this.emit({ kind: "edit", doc });
|
|
16708
17888
|
},
|
|
16709
17889
|
remove: () => {
|
|
16710
17890
|
this.closeTextEditor();
|
|
@@ -16753,6 +17933,11 @@ var UserDrawingController = class {
|
|
|
16753
17933
|
this.intentCb?.(i);
|
|
16754
17934
|
}
|
|
16755
17935
|
};
|
|
17936
|
+
function restoreLook(d, doc) {
|
|
17937
|
+
d.style = { ...doc.style };
|
|
17938
|
+
d.text = doc.text ? { ...doc.text } : void 0;
|
|
17939
|
+
if (doc.props !== void 0) d.applyProps(doc.props);
|
|
17940
|
+
}
|
|
16756
17941
|
|
|
16757
17942
|
// src/renderers/shared/TableOverlay.ts
|
|
16758
17943
|
var SIZE_PX2 = {
|
|
@@ -16768,7 +17953,7 @@ function fontPxOf(size) {
|
|
|
16768
17953
|
return SIZE_PX2[size] ?? SIZE_PX2.auto;
|
|
16769
17954
|
}
|
|
16770
17955
|
function tableHasContent(t) {
|
|
16771
|
-
return t.cells.some((
|
|
17956
|
+
return t.cells.some((row2) => row2?.some((c) => c != null && !c.merged));
|
|
16772
17957
|
}
|
|
16773
17958
|
function mergeRenderPlan(t) {
|
|
16774
17959
|
const span = /* @__PURE__ */ new Map();
|
|
@@ -17529,15 +18714,15 @@ var MarkPopover = class {
|
|
|
17529
18714
|
actions.className = "vela-callout-actions";
|
|
17530
18715
|
section.appendChild(actions);
|
|
17531
18716
|
}
|
|
17532
|
-
const
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
18717
|
+
const btn = doc.createElement("button");
|
|
18718
|
+
btn.type = "button";
|
|
18719
|
+
btn.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
18720
|
+
btn.textContent = item.label;
|
|
18721
|
+
btn.addEventListener("click", () => {
|
|
17537
18722
|
item.run();
|
|
17538
18723
|
if (item.close !== false) this.close();
|
|
17539
18724
|
});
|
|
17540
|
-
actions.appendChild(
|
|
18725
|
+
actions.appendChild(btn);
|
|
17541
18726
|
continue;
|
|
17542
18727
|
}
|
|
17543
18728
|
actions = null;
|
|
@@ -17547,16 +18732,16 @@ var MarkPopover = class {
|
|
|
17547
18732
|
text.textContent = item.text;
|
|
17548
18733
|
section.appendChild(text);
|
|
17549
18734
|
} else if (item.type === "field") {
|
|
17550
|
-
const
|
|
17551
|
-
|
|
18735
|
+
const row2 = doc.createElement("div");
|
|
18736
|
+
row2.className = "vela-marks-field";
|
|
17552
18737
|
const label = doc.createElement("span");
|
|
17553
18738
|
label.className = "vela-marks-field-label";
|
|
17554
18739
|
label.textContent = item.label;
|
|
17555
18740
|
const value = doc.createElement("span");
|
|
17556
18741
|
value.className = "vela-marks-field-value";
|
|
17557
18742
|
value.textContent = item.value;
|
|
17558
|
-
|
|
17559
|
-
section.appendChild(
|
|
18743
|
+
row2.append(label, value);
|
|
18744
|
+
section.appendChild(row2);
|
|
17560
18745
|
}
|
|
17561
18746
|
}
|
|
17562
18747
|
}
|
|
@@ -17815,7 +19000,7 @@ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"
|
|
|
17815
19000
|
|
|
17816
19001
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
17817
19002
|
var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
|
|
17818
|
-
var
|
|
19003
|
+
var STYLE_ID6 = "vela-attribution-styles";
|
|
17819
19004
|
var CSS = `
|
|
17820
19005
|
.vela-attribution {
|
|
17821
19006
|
text-decoration: none;
|
|
@@ -17871,11 +19056,11 @@ var CSS = `
|
|
|
17871
19056
|
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
17872
19057
|
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
17873
19058
|
`;
|
|
17874
|
-
function
|
|
17875
|
-
let s = doc.getElementById(
|
|
19059
|
+
function ensureStyles6(doc) {
|
|
19060
|
+
let s = doc.getElementById(STYLE_ID6);
|
|
17876
19061
|
if (!s) {
|
|
17877
19062
|
s = doc.createElement("style");
|
|
17878
|
-
s.id =
|
|
19063
|
+
s.id = STYLE_ID6;
|
|
17879
19064
|
doc.head.appendChild(s);
|
|
17880
19065
|
}
|
|
17881
19066
|
s.textContent = CSS;
|
|
@@ -17905,7 +19090,7 @@ function createCustomMark(doc, html, background) {
|
|
|
17905
19090
|
return el;
|
|
17906
19091
|
}
|
|
17907
19092
|
function createAttributionMark(doc, background) {
|
|
17908
|
-
|
|
19093
|
+
ensureStyles6(doc);
|
|
17909
19094
|
const a = doc.createElement("a");
|
|
17910
19095
|
a.className = "vela-attribution";
|
|
17911
19096
|
a.href = ATTRIBUTION_URL;
|
|
@@ -18178,13 +19363,13 @@ function paintVpvr(ctx, profile, geom, style) {
|
|
|
18178
19363
|
const { rightX, maxW, yOf } = geom;
|
|
18179
19364
|
if (maxTotal <= 0 || maxW <= 0) return;
|
|
18180
19365
|
for (let k = 0; k < rows.length; k += 1) {
|
|
18181
|
-
const
|
|
18182
|
-
const total =
|
|
19366
|
+
const row2 = rows[k];
|
|
19367
|
+
const total = row2.up + row2.down;
|
|
18183
19368
|
if (total <= 0) continue;
|
|
18184
|
-
const yTop = yOf(
|
|
18185
|
-
const h = Math.max(1, yOf(
|
|
19369
|
+
const yTop = yOf(row2.price + rowH);
|
|
19370
|
+
const h = Math.max(1, yOf(row2.price) - yTop - 1);
|
|
18186
19371
|
const w = total / maxTotal * maxW;
|
|
18187
|
-
const upW =
|
|
19372
|
+
const upW = row2.up / total * w;
|
|
18188
19373
|
ctx.globalAlpha = k >= vaFrom && k <= vaTo ? VPVR_VA_ALPHA : VPVR_OUTSIDE_ALPHA;
|
|
18189
19374
|
if (upW > 0) {
|
|
18190
19375
|
ctx.fillStyle = style.upColor;
|
|
@@ -18196,9 +19381,9 @@ function paintVpvr(ctx, profile, geom, style) {
|
|
|
18196
19381
|
}
|
|
18197
19382
|
}
|
|
18198
19383
|
if (style.showPoc) {
|
|
18199
|
-
const
|
|
18200
|
-
const yTop = yOf(
|
|
18201
|
-
const h = Math.max(1, yOf(
|
|
19384
|
+
const row2 = rows[poc];
|
|
19385
|
+
const yTop = yOf(row2.price + rowH);
|
|
19386
|
+
const h = Math.max(1, yOf(row2.price) - yTop - 1);
|
|
18202
19387
|
ctx.globalAlpha = VPVR_POC_ALPHA;
|
|
18203
19388
|
ctx.strokeStyle = style.pocColor;
|
|
18204
19389
|
ctx.lineWidth = 1;
|
|
@@ -19164,11 +20349,11 @@ var NativeRenderer = class {
|
|
|
19164
20349
|
}
|
|
19165
20350
|
}
|
|
19166
20351
|
makeSettingsButton() {
|
|
19167
|
-
const
|
|
19168
|
-
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
Object.assign(
|
|
20352
|
+
const btn = document.createElement("button");
|
|
20353
|
+
btn.type = "button";
|
|
20354
|
+
btn.title = "Chart settings";
|
|
20355
|
+
btn.innerHTML = iconAt("gear", 14);
|
|
20356
|
+
Object.assign(btn.style, {
|
|
19172
20357
|
position: "absolute",
|
|
19173
20358
|
bottom: `${TIME_AXIS_H + 10}px`,
|
|
19174
20359
|
right: `${RIGHT_AXIS_W + 10}px`,
|
|
@@ -19189,10 +20374,10 @@ var NativeRenderer = class {
|
|
|
19189
20374
|
pointerEvents: "auto",
|
|
19190
20375
|
padding: "0"
|
|
19191
20376
|
});
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
return
|
|
20377
|
+
btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
|
|
20378
|
+
btn.addEventListener("mouseleave", () => btn.style.opacity = "0.75");
|
|
20379
|
+
btn.addEventListener("click", () => this.toggleSettingsDialog());
|
|
20380
|
+
return btn;
|
|
19196
20381
|
}
|
|
19197
20382
|
/** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
|
|
19198
20383
|
* in-chart gear — created on demand, independent of the gear feature being enabled. */
|
|
@@ -19257,12 +20442,12 @@ var NativeRenderer = class {
|
|
|
19257
20442
|
}
|
|
19258
20443
|
/** Jump-back-to-latest button — same size/border chrome as the drawing-toolbar collapse toggle. */
|
|
19259
20444
|
makeScrollToRealtimeButton() {
|
|
19260
|
-
const
|
|
19261
|
-
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
Object.assign(
|
|
20445
|
+
const btn = document.createElement("button");
|
|
20446
|
+
btn.type = "button";
|
|
20447
|
+
btn.title = "Scroll to most recent bar (Alt+Shift+\u2192)";
|
|
20448
|
+
btn.setAttribute("aria-label", "Scroll to most recent bar");
|
|
20449
|
+
btn.innerHTML = this.scrollButtonIcon();
|
|
20450
|
+
Object.assign(btn.style, {
|
|
19266
20451
|
position: "absolute",
|
|
19267
20452
|
bottom: `${SCROLL_BTN_BOTTOM}px`,
|
|
19268
20453
|
right: `${SCROLL_BTN_RIGHT}px`,
|
|
@@ -19285,10 +20470,10 @@ var NativeRenderer = class {
|
|
|
19285
20470
|
pointerEvents: "auto",
|
|
19286
20471
|
padding: "0"
|
|
19287
20472
|
});
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
return
|
|
20473
|
+
btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
|
|
20474
|
+
btn.addEventListener("mouseleave", () => btn.style.opacity = "0.85");
|
|
20475
|
+
btn.addEventListener("click", () => this.scrollToRealtime());
|
|
20476
|
+
return btn;
|
|
19292
20477
|
}
|
|
19293
20478
|
/** Double-chevron icon — the "jump back to the latest bar" affordance. */
|
|
19294
20479
|
scrollButtonIcon() {
|
|
@@ -19311,14 +20496,14 @@ var NativeRenderer = class {
|
|
|
19311
20496
|
* appears whenever the latest bars are off-screen and hides again once the view is back
|
|
19312
20497
|
* at the right edge. */
|
|
19313
20498
|
updateScrollToRealtimeButton() {
|
|
19314
|
-
const
|
|
19315
|
-
if (!
|
|
20499
|
+
const btn = this.scrollButton;
|
|
20500
|
+
if (!btn) return;
|
|
19316
20501
|
const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
|
|
19317
20502
|
if (this.layoutMode === "mobile") {
|
|
19318
|
-
|
|
20503
|
+
btn.style.display = latestOffScreen ? "flex" : "none";
|
|
19319
20504
|
return;
|
|
19320
20505
|
}
|
|
19321
|
-
|
|
20506
|
+
btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
|
|
19322
20507
|
}
|
|
19323
20508
|
/** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
|
|
19324
20509
|
scrollToRealtime() {
|
|
@@ -19627,6 +20812,8 @@ var NativeRenderer = class {
|
|
|
19627
20812
|
};
|
|
19628
20813
|
},
|
|
19629
20814
|
chartBarMs: () => this.coords.barInterval,
|
|
20815
|
+
timeZone: () => this.scene.timezone,
|
|
20816
|
+
priceDecimals: () => this.scene.priceMintick ? tickDecimals(this.scene.priceMintick) : null,
|
|
19630
20817
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
19631
20818
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
19632
20819
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -20948,7 +22135,7 @@ var NativeRenderer = class {
|
|
|
20948
22135
|
if (bar) parts.push(formatStamp(bar.time, this.scene.timezone));
|
|
20949
22136
|
if (ohlc) parts.push(`Open ${ohlc.o}, High ${ohlc.h}, Low ${ohlc.l}, Close ${ohlc.c}`);
|
|
20950
22137
|
for (const group of this.dataWindowGroups(idx, pricePane)) {
|
|
20951
|
-
for (const
|
|
22138
|
+
for (const row2 of group.rows) parts.push(`${row2.label} ${row2.value}`);
|
|
20952
22139
|
}
|
|
20953
22140
|
this.liveRegion.textContent = parts.join(". ");
|
|
20954
22141
|
}
|
|
@@ -21904,7 +23091,10 @@ function applyPatch(model, patch) {
|
|
|
21904
23091
|
const s = model.series.find((x) => x.id === delta.seriesId);
|
|
21905
23092
|
if (!s) continue;
|
|
21906
23093
|
if (delta.kind === "points" && isLineLikeSeries(s)) s.points = delta.points;
|
|
21907
|
-
else if (delta.kind === "bars" && (s.kind === "candle" || s.kind === "bar"))
|
|
23094
|
+
else if (delta.kind === "bars" && (s.kind === "candle" || s.kind === "bar")) {
|
|
23095
|
+
s.bars = delta.bars;
|
|
23096
|
+
if (delta.barColors !== void 0) s.barColors = delta.barColors;
|
|
23097
|
+
}
|
|
21908
23098
|
}
|
|
21909
23099
|
if (patch.lines) model.lines = patch.lines;
|
|
21910
23100
|
if (patch.boxes) model.boxes = patch.boxes;
|
|
@@ -23263,14 +24453,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23263
24453
|
this.replayTickSource = source;
|
|
23264
24454
|
this.prefetchTicks();
|
|
23265
24455
|
}
|
|
23266
|
-
replayPlay(
|
|
24456
|
+
replayPlay(intervalMs) {
|
|
23267
24457
|
if (!this.replayQueue) return;
|
|
23268
|
-
if (
|
|
23269
|
-
if (!(Number.isFinite(
|
|
23270
|
-
console.warn(`[vela] chart.replay.play(${
|
|
24458
|
+
if (intervalMs !== void 0) {
|
|
24459
|
+
if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
|
|
24460
|
+
console.warn(`[vela] chart.replay.play(${intervalMs}) ignored \u2014 the interval must be a positive number of ms`);
|
|
23271
24461
|
return;
|
|
23272
24462
|
}
|
|
23273
|
-
this.replayIntervalMs =
|
|
24463
|
+
this.replayIntervalMs = intervalMs;
|
|
23274
24464
|
}
|
|
23275
24465
|
this.clearReplayTimer();
|
|
23276
24466
|
this.replayPlaying = true;
|
|
@@ -24810,7 +26000,7 @@ function modelToValuePatch(model) {
|
|
|
24810
26000
|
let to = 0;
|
|
24811
26001
|
for (const s of model.series) {
|
|
24812
26002
|
if (s.kind === "candle" || s.kind === "bar") {
|
|
24813
|
-
series.push({ seriesId: s.id, kind: "bars", bars: s.bars });
|
|
26003
|
+
series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
|
|
24814
26004
|
for (const b of s.bars) {
|
|
24815
26005
|
if (b.time < from) from = b.time;
|
|
24816
26006
|
if (b.time > to) to = b.time;
|