@luxalgo/vela 0.6.6 → 0.6.8
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-JJq7M2it.d.ts → DataProvider-BSLlBpB9.d.ts} +10 -0
- package/dist/{DataProvider-DDUYw2qP.d.cts → DataProvider-BsQM2WNH.d.cts} +10 -0
- package/dist/{chunk-OEUGUXL7.js → chunk-L3I2CCYO.js} +49 -1
- package/dist/{chunk-TMXK2SJR.js → chunk-N7LGMKCE.js} +523 -142
- package/dist/{chunk-JQFO2WMU.js → chunk-SQETIBFU.js} +83 -9
- package/dist/chunk-W4EJWLEO.js +12 -0
- package/dist/{contributions-BqCIsbaP.d.ts → contributions-DLLdV9jD.d.ts} +59 -3
- package/dist/{contributions-DNMqrAuw.d.cts → contributions-Vw-Hm58R.d.cts} +59 -3
- package/dist/index.cjs +110 -8
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +2 -2
- package/dist/{plugin-CN8U2__Q.d.cts → plugin-BnJgjLAy.d.cts} +2 -2
- package/dist/{plugin-WnvNNJOp.d.ts → plugin-CC7rBrOY.d.ts} +2 -2
- package/dist/plugin.cjs +28 -0
- package/dist/plugin.d.cts +3 -3
- package/dist/plugin.d.ts +3 -3
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.cjs +16 -0
- package/dist/providers/binance.d.cts +4 -1
- package/dist/providers/binance.d.ts +4 -1
- package/dist/providers/binance.js +7 -0
- package/dist/providers/coinbase.cjs +16 -0
- package/dist/providers/coinbase.d.cts +4 -1
- package/dist/providers/coinbase.d.ts +4 -1
- package/dist/providers/coinbase.js +7 -0
- package/dist/providers/hyperliquid.cjs +16 -0
- package/dist/providers/hyperliquid.d.cts +4 -1
- package/dist/providers/hyperliquid.d.ts +4 -1
- package/dist/providers/hyperliquid.js +7 -0
- package/dist/{bottombar-COnL2Sk4.d.ts → statusline-CGkB2EOo.d.ts} +137 -8
- package/dist/{bottombar-DeDXbF_K.d.cts → statusline-DqzBqy42.d.cts} +137 -8
- package/dist/vela.global.js +136 -8
- package/dist/vela.global.min.js +42 -42
- package/dist/widget.cjs +667 -148
- package/dist/widget.d.cts +50 -81
- package/dist/widget.d.ts +50 -81
- package/dist/widget.js +5 -4
- package/dist/workspace.cjs +613 -150
- package/dist/workspace.d.cts +62 -9
- package/dist/workspace.d.ts +62 -9
- package/dist/workspace.js +4 -3
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -303,6 +303,28 @@ var ProviderRegistry = class {
|
|
|
303
303
|
const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
|
|
304
304
|
return `${d?.prefix ?? resolved.provider}:${d?.ticker ?? resolved.ticker}`;
|
|
305
305
|
}
|
|
306
|
+
/** The icon URL for a DESCRIPTOR — routed to its OWNING provider's resolver (the
|
|
307
|
+
* provider owns the knowledge of where its asset class's icons live). A missing
|
|
308
|
+
* provider, an absent resolver, or a resolver that throws all mean "no icon" —
|
|
309
|
+
* the shells' initials badge takes over, never an error. */
|
|
310
|
+
symbolIconOf(d) {
|
|
311
|
+
const name = d.provider ? normName(d.provider) : [...this.entries.keys()][0];
|
|
312
|
+
const entry = name !== void 0 ? this.entries.get(name) : void 0;
|
|
313
|
+
try {
|
|
314
|
+
return entry?.provider.resolveSymbolIcon?.(d) ?? void 0;
|
|
315
|
+
} catch {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** The icon URL for a raw SYMBOL string (the statusline/object-tree path): resolve
|
|
320
|
+
* to the owning provider, find its descriptor, route to the resolver. Before the
|
|
321
|
+
* index settles (or for a lenient sole-provider resolution) a minimal synthetic
|
|
322
|
+
* descriptor is offered — crypto resolvers can answer from the ticker alone. */
|
|
323
|
+
symbolIcon(resolved) {
|
|
324
|
+
if (!resolved) return void 0;
|
|
325
|
+
const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
|
|
326
|
+
return this.symbolIconOf({ ...d ?? { ticker: resolved.ticker }, provider: d?.provider ?? resolved.provider });
|
|
327
|
+
}
|
|
306
328
|
/** Indexed symbols for one provider (or all, concatenated) — for autocomplete. */
|
|
307
329
|
symbolsOf(rawName) {
|
|
308
330
|
if (rawName != null) return this.entries.get(normName(rawName))?.descriptors ?? [];
|
|
@@ -655,6 +677,14 @@ var MultiProviderFeed = class {
|
|
|
655
677
|
providerInstance(name) {
|
|
656
678
|
return this.registry.get(name);
|
|
657
679
|
}
|
|
680
|
+
/** The icon URL for a DESCRIPTOR — its owning provider's `resolveSymbolIcon` (picker rows). */
|
|
681
|
+
symbolIconOf(d) {
|
|
682
|
+
return this.registry.symbolIconOf(d);
|
|
683
|
+
}
|
|
684
|
+
/** The icon URL for a raw SYMBOL string — resolve, then route (statusline, object tree). */
|
|
685
|
+
symbolIcon(raw) {
|
|
686
|
+
return this.registry.symbolIcon(this.resolveSymbol(raw));
|
|
687
|
+
}
|
|
658
688
|
symbols(name) {
|
|
659
689
|
return this.registry.symbolsOf(name);
|
|
660
690
|
}
|
|
@@ -4554,6 +4584,29 @@ function tickerModifierIds() {
|
|
|
4554
4584
|
return [...registry2.values()].filter((d) => d.tickerModifier ?? d.barTransform != null).map((d) => d.id);
|
|
4555
4585
|
}
|
|
4556
4586
|
|
|
4587
|
+
// src/widget/topbar-composition.ts
|
|
4588
|
+
var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
|
|
4589
|
+
var TOPBAR_DEFAULT_LEFT = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo"];
|
|
4590
|
+
var TOPBAR_DEFAULT_RIGHT = ["actions", "alerts", "panels", "screenshot"];
|
|
4591
|
+
function resolveTopbarComposition(opt) {
|
|
4592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4593
|
+
const take = (list) => {
|
|
4594
|
+
const out = [];
|
|
4595
|
+
const local = /* @__PURE__ */ new Set();
|
|
4596
|
+
for (const id of list) {
|
|
4597
|
+
if (!id || local.has(id) || id !== "actions" && seen.has(id)) continue;
|
|
4598
|
+
local.add(id);
|
|
4599
|
+
seen.add(id);
|
|
4600
|
+
out.push(id);
|
|
4601
|
+
}
|
|
4602
|
+
return out;
|
|
4603
|
+
};
|
|
4604
|
+
return { left: take(opt?.left ?? TOPBAR_DEFAULT_LEFT), right: take(opt?.right ?? TOPBAR_DEFAULT_RIGHT) };
|
|
4605
|
+
}
|
|
4606
|
+
function topbarHas(comp, id) {
|
|
4607
|
+
return comp.left.includes(id) || comp.right.includes(id);
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4557
4610
|
// src/widget/contributions.ts
|
|
4558
4611
|
var registry3 = /* @__PURE__ */ new Map();
|
|
4559
4612
|
var attachments = /* @__PURE__ */ new Map();
|
|
@@ -4565,6 +4618,13 @@ var DEFAULT_PANEL_ORDER = 100;
|
|
|
4565
4618
|
function sidePanels() {
|
|
4566
4619
|
return [...panels.values()].sort((a, b) => (a.order ?? DEFAULT_PANEL_ORDER) - (b.order ?? DEFAULT_PANEL_ORDER));
|
|
4567
4620
|
}
|
|
4621
|
+
var OVERRIDABLE_TOPBAR_IDS = ["indicators", "screenshot"];
|
|
4622
|
+
var overridableSet = new Set(OVERRIDABLE_TOPBAR_IDS);
|
|
4623
|
+
new Set(TOPBAR_BUILTIN_IDS);
|
|
4624
|
+
function topbarActionOverride(id) {
|
|
4625
|
+
if (!overridableSet.has(id)) return void 0;
|
|
4626
|
+
return registry3.get(id);
|
|
4627
|
+
}
|
|
4568
4628
|
function widgetActions(target, ctx) {
|
|
4569
4629
|
const list = [...registry3.values()].filter((d) => d.target === target);
|
|
4570
4630
|
list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -5068,9 +5128,14 @@ var CSS3 = `
|
|
|
5068
5128
|
align-items: center;
|
|
5069
5129
|
justify-content: center;
|
|
5070
5130
|
}
|
|
5071
|
-
|
|
5131
|
+
/* The right side of the bar \u2014 whatever the composition puts there rides this one
|
|
5132
|
+
auto-margin push (the flow-actions host used to carry it; composition can omit it). */
|
|
5133
|
+
.vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
5134
|
+
.vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
|
|
5072
5135
|
/* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
|
|
5073
5136
|
.vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
5137
|
+
/* One PINNED contributed action's slot (a composition entry naming the action's id). */
|
|
5138
|
+
.vela-widget-action-pin { display: inline-flex; align-items: center; }
|
|
5074
5139
|
/* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
|
|
5075
5140
|
without disturbing the tools around it. */
|
|
5076
5141
|
.vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
@@ -5129,6 +5194,17 @@ var Topbar = class {
|
|
|
5129
5194
|
this.tooltips = [];
|
|
5130
5195
|
this.panelBtns = /* @__PURE__ */ new Map();
|
|
5131
5196
|
this.panelTooltips = [];
|
|
5197
|
+
/** Pinned contributed-action slots, by action id (composition entries that name one,
|
|
5198
|
+
* plus built-in slots taken over by an override). */
|
|
5199
|
+
this.pinned = /* @__PURE__ */ new Map();
|
|
5200
|
+
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
5201
|
+
* render through the flow cluster like ordinary actions. */
|
|
5202
|
+
this.flowingOverrides = /* @__PURE__ */ new Set();
|
|
5203
|
+
/** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
|
|
5204
|
+
* renderActions pass (contributed buttons are replaceChildren'd away). */
|
|
5205
|
+
this.actionTooltips = [];
|
|
5206
|
+
/** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
|
|
5207
|
+
this.warnedIconless = /* @__PURE__ */ new Set();
|
|
5132
5208
|
this.onHairlineSync = () => {
|
|
5133
5209
|
if (this.hairlineRaf) return;
|
|
5134
5210
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -5143,6 +5219,8 @@ var Topbar = class {
|
|
|
5143
5219
|
this.host = host;
|
|
5144
5220
|
this.timeframe = opts.timeframe;
|
|
5145
5221
|
this.priceStyle = opts.priceStyle;
|
|
5222
|
+
this.comp = resolveTopbarComposition(opts.composition);
|
|
5223
|
+
const vis = (id) => topbarHas(this.comp, id);
|
|
5146
5224
|
const doc = host.ownerDocument;
|
|
5147
5225
|
injectStyles(STYLE_ID2, CSS3, doc);
|
|
5148
5226
|
this.el = doc.createElement("div");
|
|
@@ -5165,7 +5243,7 @@ var Topbar = class {
|
|
|
5165
5243
|
this.styleButton.className = "vela-widget-style";
|
|
5166
5244
|
this.renderStyleButton(doc);
|
|
5167
5245
|
let indicatorsBtn = null;
|
|
5168
|
-
if (opts.onIndicatorsClick) {
|
|
5246
|
+
if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
|
|
5169
5247
|
indicatorsBtn = doc.createElement("button");
|
|
5170
5248
|
indicatorsBtn.className = "vela-widget-indicators";
|
|
5171
5249
|
indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
|
|
@@ -5178,18 +5256,23 @@ var Topbar = class {
|
|
|
5178
5256
|
this.panelsHost = doc.createElement("span");
|
|
5179
5257
|
this.panelsHost.className = "vela-widget-panels";
|
|
5180
5258
|
const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
|
|
5181
|
-
|
|
5182
|
-
|
|
5259
|
+
if (vis("undo-redo")) {
|
|
5260
|
+
this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
|
|
5261
|
+
this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
|
|
5262
|
+
} else {
|
|
5263
|
+
this.undoBtn = doc.createElement("button");
|
|
5264
|
+
this.redoBtn = doc.createElement("button");
|
|
5265
|
+
}
|
|
5183
5266
|
this.setHistoryState(false, false);
|
|
5184
|
-
const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
|
|
5185
|
-
this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
|
|
5267
|
+
const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
|
|
5268
|
+
this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
|
|
5186
5269
|
this.alertsBtn.style.position = "relative";
|
|
5187
5270
|
if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
|
|
5188
5271
|
this.alertsBadge = doc.createElement("span");
|
|
5189
5272
|
this.alertsBadge.className = "vela-alerts-badge";
|
|
5190
5273
|
this.alertsBadge.style.display = "none";
|
|
5191
5274
|
this.alertsBtn.appendChild(this.alertsBadge);
|
|
5192
|
-
if (opts.layout) {
|
|
5275
|
+
if (opts.layout && vis("layout")) {
|
|
5193
5276
|
this.layoutId = opts.layout.current;
|
|
5194
5277
|
this.layoutButton = doc.createElement("button");
|
|
5195
5278
|
this.layoutButton.className = "vela-widget-style";
|
|
@@ -5200,12 +5283,65 @@ var Topbar = class {
|
|
|
5200
5283
|
d.className = "vela-sep";
|
|
5201
5284
|
return d;
|
|
5202
5285
|
};
|
|
5203
|
-
const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
|
|
5204
|
-
if (this.layoutButton) leading.push(this.layoutButton, sep());
|
|
5205
|
-
if (indicatorsBtn) leading.push(indicatorsBtn, sep());
|
|
5206
5286
|
this.leftActionsSep = sep();
|
|
5207
5287
|
this.leftActionsSep.hidden = true;
|
|
5208
|
-
|
|
5288
|
+
const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
|
|
5289
|
+
const pinSlot = (id, left) => {
|
|
5290
|
+
const slot = doc.createElement("span");
|
|
5291
|
+
slot.className = "vela-widget-action-pin";
|
|
5292
|
+
this.pinned.set(id, { host: slot, left });
|
|
5293
|
+
return [slot];
|
|
5294
|
+
};
|
|
5295
|
+
const overridden = (id, left) => {
|
|
5296
|
+
const ov = topbarActionOverride(id);
|
|
5297
|
+
if (!ov) return null;
|
|
5298
|
+
const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
|
|
5299
|
+
if (!sideDeclared && ov.order !== void 0) {
|
|
5300
|
+
this.flowingOverrides.add(id);
|
|
5301
|
+
return [];
|
|
5302
|
+
}
|
|
5303
|
+
return pinSlot(id, left);
|
|
5304
|
+
};
|
|
5305
|
+
const elementsFor = (id, left) => {
|
|
5306
|
+
switch (id) {
|
|
5307
|
+
case "symbol":
|
|
5308
|
+
return [this.symbolEl];
|
|
5309
|
+
case "timeframes":
|
|
5310
|
+
return [tfGroup];
|
|
5311
|
+
case "style":
|
|
5312
|
+
return [this.styleButton];
|
|
5313
|
+
case "layout":
|
|
5314
|
+
return this.layoutButton ? [this.layoutButton] : [];
|
|
5315
|
+
case "indicators":
|
|
5316
|
+
return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
|
|
5317
|
+
case "actions":
|
|
5318
|
+
return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
|
|
5319
|
+
case "undo-redo":
|
|
5320
|
+
return [this.undoBtn, this.redoBtn];
|
|
5321
|
+
case "alerts":
|
|
5322
|
+
return [this.alertsBtn];
|
|
5323
|
+
case "panels":
|
|
5324
|
+
return [this.panelsHost];
|
|
5325
|
+
case "screenshot":
|
|
5326
|
+
return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
|
|
5327
|
+
default:
|
|
5328
|
+
return pinSlot(id, left);
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
const sideEls = (list, left) => {
|
|
5332
|
+
const out = [];
|
|
5333
|
+
for (const [i, id] of list.entries()) {
|
|
5334
|
+
const els = elementsFor(id, left);
|
|
5335
|
+
if (els.length === 0) continue;
|
|
5336
|
+
out.push(...els);
|
|
5337
|
+
if (primaries.has(id) && i < list.length - 1) out.push(sep());
|
|
5338
|
+
}
|
|
5339
|
+
return out;
|
|
5340
|
+
};
|
|
5341
|
+
const right = doc.createElement("span");
|
|
5342
|
+
right.className = "vela-topbar-right";
|
|
5343
|
+
right.append(...sideEls(this.comp.right, false));
|
|
5344
|
+
this.el.append(...sideEls(this.comp.left, true), right);
|
|
5209
5345
|
host.appendChild(this.el);
|
|
5210
5346
|
this.renderTfChips();
|
|
5211
5347
|
this.onHairlineSync();
|
|
@@ -5322,23 +5458,45 @@ var Topbar = class {
|
|
|
5322
5458
|
else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
|
|
5323
5459
|
this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
|
|
5324
5460
|
}
|
|
5325
|
-
/** Re-project the contributed topbar actions (call after registrations change).
|
|
5461
|
+
/** Re-project the contributed topbar actions (call after registrations change).
|
|
5462
|
+
* An action PINNED by the composition renders into its named slot (list position
|
|
5463
|
+
* wins over `align`/`order`); the rest flow into the side's `actions` slot — or
|
|
5464
|
+
* not at all when an explicit list omits it (the list is the side's contract). */
|
|
5326
5465
|
renderActions() {
|
|
5327
5466
|
const ctx = this.opts.getContext?.();
|
|
5328
5467
|
this.actionsHost.replaceChildren();
|
|
5329
5468
|
this.leftActionsHost.replaceChildren();
|
|
5469
|
+
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
5470
|
+
for (const t of this.actionTooltips) t.destroy();
|
|
5471
|
+
this.actionTooltips = [];
|
|
5330
5472
|
const doc = this.actionsHost.ownerDocument;
|
|
5473
|
+
const flowLeft = this.comp.left.includes("actions");
|
|
5474
|
+
const flowRight = this.comp.right.includes("actions");
|
|
5475
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
5331
5476
|
for (const action of widgetActions("topbar", ctx)) {
|
|
5332
|
-
const
|
|
5477
|
+
const pin = this.pinned.get(action.id);
|
|
5478
|
+
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
5479
|
+
const left = pin ? pin.left : action.align === "left";
|
|
5480
|
+
if (!pin && !(left ? flowLeft : flowRight)) continue;
|
|
5481
|
+
const iconOnly = action.iconOnly === true && !!action.icon;
|
|
5482
|
+
if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
|
|
5483
|
+
this.warnedIconless.add(action.id);
|
|
5484
|
+
console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
|
|
5485
|
+
}
|
|
5333
5486
|
const b = doc.createElement("button");
|
|
5334
|
-
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
5487
|
+
b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
|
|
5335
5488
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
5336
|
-
|
|
5489
|
+
if (iconOnly) {
|
|
5490
|
+
b.setAttribute("aria-label", action.label);
|
|
5491
|
+
this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
|
|
5492
|
+
} else {
|
|
5493
|
+
b.appendChild(doc.createTextNode(action.label));
|
|
5494
|
+
}
|
|
5337
5495
|
b.addEventListener("click", () => {
|
|
5338
5496
|
const c = this.opts.getContext?.();
|
|
5339
5497
|
if (c) action.run(c);
|
|
5340
5498
|
});
|
|
5341
|
-
(left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
5499
|
+
(pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
5342
5500
|
}
|
|
5343
5501
|
this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
|
|
5344
5502
|
this.onHairlineSync();
|
|
@@ -5384,7 +5542,7 @@ var Topbar = class {
|
|
|
5384
5542
|
this.tfMenu.destroy();
|
|
5385
5543
|
this.styleMenu.destroy();
|
|
5386
5544
|
this.layoutPicker?.destroy();
|
|
5387
|
-
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
5545
|
+
for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
|
|
5388
5546
|
this.el.remove();
|
|
5389
5547
|
}
|
|
5390
5548
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
@@ -5731,41 +5889,39 @@ var Bottombar = class {
|
|
|
5731
5889
|
}
|
|
5732
5890
|
};
|
|
5733
5891
|
|
|
5734
|
-
// src/
|
|
5735
|
-
var iconFailed = /* @__PURE__ */ new Set();
|
|
5736
|
-
function initialsOf(name) {
|
|
5737
|
-
return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
|
5738
|
-
}
|
|
5739
|
-
function cryptoIconUrl(base) {
|
|
5740
|
-
return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
|
|
5741
|
-
}
|
|
5892
|
+
// src/data/symbol-base.ts
|
|
5742
5893
|
function baseOf(d) {
|
|
5743
5894
|
const fromDesc = d.description?.split("/")[0]?.trim();
|
|
5744
5895
|
if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
|
|
5745
5896
|
return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
|
|
5746
5897
|
}
|
|
5747
|
-
|
|
5898
|
+
|
|
5899
|
+
// src/widget/symbol-icon.ts
|
|
5900
|
+
var iconFailed = /* @__PURE__ */ new Set();
|
|
5901
|
+
function initialsOf(name) {
|
|
5902
|
+
return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
|
5903
|
+
}
|
|
5904
|
+
function tickerIconEl(doc, base, name, className, iconUrl) {
|
|
5748
5905
|
const wrap = doc.createElement("span");
|
|
5749
5906
|
wrap.className = className;
|
|
5750
|
-
const key = base.toUpperCase();
|
|
5751
5907
|
const fallback = () => {
|
|
5752
5908
|
wrap.replaceChildren();
|
|
5753
5909
|
wrap.style.background = categoricalColor(name);
|
|
5754
5910
|
wrap.textContent = initialsOf(base || name);
|
|
5755
5911
|
};
|
|
5756
|
-
if (!
|
|
5912
|
+
if (!iconUrl || iconFailed.has(iconUrl)) {
|
|
5757
5913
|
fallback();
|
|
5758
5914
|
return wrap;
|
|
5759
5915
|
}
|
|
5760
5916
|
const img = doc.createElement("img");
|
|
5761
5917
|
img.alt = "";
|
|
5762
5918
|
img.crossOrigin = "anonymous";
|
|
5763
|
-
img.src =
|
|
5919
|
+
img.src = iconUrl;
|
|
5764
5920
|
img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
|
|
5765
5921
|
img.addEventListener(
|
|
5766
5922
|
"error",
|
|
5767
5923
|
() => {
|
|
5768
|
-
iconFailed.add(
|
|
5924
|
+
iconFailed.add(iconUrl);
|
|
5769
5925
|
fallback();
|
|
5770
5926
|
},
|
|
5771
5927
|
{ once: true }
|
|
@@ -12018,11 +12174,14 @@ var MenuBuilder = class {
|
|
|
12018
12174
|
}
|
|
12019
12175
|
};
|
|
12020
12176
|
var ObjectTree = class extends SidePanel {
|
|
12021
|
-
constructor(host) {
|
|
12177
|
+
constructor(host, iconFor) {
|
|
12022
12178
|
super(host, "Object tree", "vela-ot");
|
|
12179
|
+
this.iconFor = iconFor;
|
|
12023
12180
|
this.chart = null;
|
|
12024
12181
|
this.selectedDrawing = null;
|
|
12025
12182
|
this.symbolName = "";
|
|
12183
|
+
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
12184
|
+
this.symbolRaw = "";
|
|
12026
12185
|
/** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
|
|
12027
12186
|
* Kept per chart because a workspace points this one panel at whichever chart is active, and
|
|
12028
12187
|
* each chart's bundles have to survive the switch. */
|
|
@@ -12059,6 +12218,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
12059
12218
|
if (open2) this.refresh();
|
|
12060
12219
|
}
|
|
12061
12220
|
setSymbol(symbol) {
|
|
12221
|
+
this.symbolRaw = symbol;
|
|
12062
12222
|
this.symbolName = parseSymbol(symbol).ticker;
|
|
12063
12223
|
}
|
|
12064
12224
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
@@ -12259,7 +12419,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
12259
12419
|
const { chart } = pass;
|
|
12260
12420
|
if (row.kind === "price") {
|
|
12261
12421
|
const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
|
|
12262
|
-
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
|
|
12422
|
+
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
|
|
12263
12423
|
const el2 = this.row(icon2, row.label, row.visible, [
|
|
12264
12424
|
{
|
|
12265
12425
|
icon: row.visible ? "eye" : "eye-off",
|
|
@@ -13289,8 +13449,6 @@ var PanelDock = class {
|
|
|
13289
13449
|
for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
|
|
13290
13450
|
}
|
|
13291
13451
|
};
|
|
13292
|
-
|
|
13293
|
-
// src/widget/symbol-picker.ts
|
|
13294
13452
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
13295
13453
|
function parseQuery(raw, venues) {
|
|
13296
13454
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -13305,17 +13463,18 @@ function onlyOne(matches) {
|
|
|
13305
13463
|
return matches.length === 1 ? matches[0] : null;
|
|
13306
13464
|
}
|
|
13307
13465
|
var venueOf = (s) => s.prefix ?? s.provider;
|
|
13308
|
-
function filterSymbols(list, query, limit = 100) {
|
|
13466
|
+
function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
13309
13467
|
const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
|
|
13310
13468
|
const { scope, term } = parseQuery(query, venues);
|
|
13311
13469
|
const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
|
|
13312
13470
|
const q = term.toUpperCase();
|
|
13313
13471
|
if (!q) {
|
|
13314
13472
|
if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
|
|
13473
|
+
if (top === false) return pool.slice(0, limit);
|
|
13315
13474
|
const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
|
|
13316
|
-
const
|
|
13317
|
-
const rest = pool.filter((s) => !
|
|
13318
|
-
return [...
|
|
13475
|
+
const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
|
|
13476
|
+
const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
|
|
13477
|
+
return [...pinned, ...rest].slice(0, limit);
|
|
13319
13478
|
}
|
|
13320
13479
|
const qLower = term.toLowerCase();
|
|
13321
13480
|
const prefix = [];
|
|
@@ -13425,12 +13584,16 @@ var CSS8 = `
|
|
|
13425
13584
|
var PAGE = 100;
|
|
13426
13585
|
var SymbolPicker = class {
|
|
13427
13586
|
constructor(opts) {
|
|
13587
|
+
this.opts = opts;
|
|
13428
13588
|
this.source = () => [];
|
|
13429
13589
|
this.rows = [];
|
|
13430
13590
|
this.highlighted = 0;
|
|
13431
13591
|
this.seed = "";
|
|
13432
13592
|
this.activeTab = "All";
|
|
13433
13593
|
this.visible = PAGE;
|
|
13594
|
+
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
13595
|
+
this.ranked = null;
|
|
13596
|
+
this.ranking = false;
|
|
13434
13597
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
13435
13598
|
injectStyles(STYLE_ID7, CSS8, doc);
|
|
13436
13599
|
this.input = doc.createElement("input");
|
|
@@ -13528,10 +13691,19 @@ var SymbolPicker = class {
|
|
|
13528
13691
|
} else delete el.dataset.highlighted;
|
|
13529
13692
|
});
|
|
13530
13693
|
}
|
|
13694
|
+
/** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
|
|
13695
|
+
* the hook runs when the pool CHANGES (an index lands or refreshes), never per
|
|
13696
|
+
* keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
|
|
13697
|
+
* in between, the raw pool before the first resolve). */
|
|
13698
|
+
pool() {
|
|
13699
|
+
const raw = this.source();
|
|
13700
|
+
return raw;
|
|
13701
|
+
}
|
|
13531
13702
|
computeRows() {
|
|
13532
13703
|
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
13533
|
-
const
|
|
13534
|
-
|
|
13704
|
+
const all = this.pool();
|
|
13705
|
+
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
13706
|
+
return filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
|
|
13535
13707
|
}
|
|
13536
13708
|
refresh() {
|
|
13537
13709
|
const doc = this.list.ownerDocument;
|
|
@@ -13562,7 +13734,7 @@ var SymbolPicker = class {
|
|
|
13562
13734
|
row.dataset.ticker = s.ticker;
|
|
13563
13735
|
const venue = s.prefix ?? s.provider;
|
|
13564
13736
|
if (venue) row.dataset.venue = venue;
|
|
13565
|
-
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
|
|
13737
|
+
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
|
|
13566
13738
|
const main = doc.createElement("span");
|
|
13567
13739
|
main.className = "vela-sp-main";
|
|
13568
13740
|
const t = doc.createElement("span");
|
|
@@ -14243,7 +14415,8 @@ var MobileBar = class {
|
|
|
14243
14415
|
if (!ctx) return;
|
|
14244
14416
|
const doc = this.el.ownerDocument;
|
|
14245
14417
|
this.actionsHost.replaceChildren();
|
|
14246
|
-
|
|
14418
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
14419
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
|
|
14247
14420
|
const b = doc.createElement("button");
|
|
14248
14421
|
b.className = "vela-mb-item";
|
|
14249
14422
|
b.setAttribute("aria-label", action.label);
|
|
@@ -14772,10 +14945,10 @@ var MoreDrawer = class {
|
|
|
14772
14945
|
});
|
|
14773
14946
|
actions.appendChild(b);
|
|
14774
14947
|
};
|
|
14775
|
-
action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
14776
|
-
action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
14777
|
-
action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
14778
|
-
this.drawer.body.appendChild(actions);
|
|
14948
|
+
if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
14949
|
+
if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
14950
|
+
if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
14951
|
+
if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
|
|
14779
14952
|
const list = doc.createElement("div");
|
|
14780
14953
|
list.className = "vela-md-list";
|
|
14781
14954
|
const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
|
|
@@ -14796,8 +14969,10 @@ var MoreDrawer = class {
|
|
|
14796
14969
|
})
|
|
14797
14970
|
);
|
|
14798
14971
|
}
|
|
14799
|
-
|
|
14800
|
-
|
|
14972
|
+
if (this.opts.alerts) {
|
|
14973
|
+
const alertCount = this.opts.alerts().length;
|
|
14974
|
+
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
14975
|
+
}
|
|
14801
14976
|
for (const act of this.opts.actions()) {
|
|
14802
14977
|
list.appendChild(
|
|
14803
14978
|
this.row(doc, act.label, {
|
|
@@ -14878,7 +15053,7 @@ var MoreDrawer = class {
|
|
|
14878
15053
|
this.drawer.body.appendChild(list);
|
|
14879
15054
|
}
|
|
14880
15055
|
renderAlerts(doc) {
|
|
14881
|
-
const alerts = this.opts.alerts();
|
|
15056
|
+
const alerts = this.opts.alerts?.() ?? [];
|
|
14882
15057
|
if (alerts.length === 0) {
|
|
14883
15058
|
const empty = doc.createElement("div");
|
|
14884
15059
|
empty.className = "vela-md-empty";
|
|
@@ -17075,19 +17250,8 @@ function registerBuiltinChartTypes() {
|
|
|
17075
17250
|
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
17076
17251
|
}
|
|
17077
17252
|
|
|
17078
|
-
// src/workspace/sync.ts
|
|
17079
|
-
function syncTargets(originId, setting, cellIds) {
|
|
17080
|
-
if (setting == null || setting === false) return [];
|
|
17081
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17082
|
-
const group = setting[originId];
|
|
17083
|
-
if (group == null) return [];
|
|
17084
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17085
|
-
}
|
|
17086
|
-
function rangesWithin(a, b, epsMs) {
|
|
17087
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17088
|
-
}
|
|
17089
|
-
|
|
17090
17253
|
// src/state/document.ts
|
|
17254
|
+
var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
|
|
17091
17255
|
function prefixedSymbol(cell) {
|
|
17092
17256
|
if (!cell?.symbol) return void 0;
|
|
17093
17257
|
if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
|
|
@@ -17175,7 +17339,7 @@ function sanitizeSync(raw) {
|
|
|
17175
17339
|
if (raw == null || typeof raw !== "object") return null;
|
|
17176
17340
|
const s = raw;
|
|
17177
17341
|
const out = {};
|
|
17178
|
-
for (const kind of
|
|
17342
|
+
for (const kind of SYNC_KINDS) {
|
|
17179
17343
|
const v = s[kind];
|
|
17180
17344
|
if (v === true) out[kind] = true;
|
|
17181
17345
|
else if (v != null && typeof v === "object") {
|
|
@@ -17218,6 +17382,28 @@ function sanitizeTrackSizes(raw) {
|
|
|
17218
17382
|
return Object.keys(out).length > 0 ? out : null;
|
|
17219
17383
|
}
|
|
17220
17384
|
|
|
17385
|
+
// src/workspace/sync.ts
|
|
17386
|
+
function syncTargets(originId, setting, cellIds) {
|
|
17387
|
+
if (setting == null || setting === false) return [];
|
|
17388
|
+
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17389
|
+
const group = setting[originId];
|
|
17390
|
+
if (group == null) return [];
|
|
17391
|
+
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17392
|
+
}
|
|
17393
|
+
function rangesWithin(a, b, epsMs) {
|
|
17394
|
+
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17395
|
+
}
|
|
17396
|
+
var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
|
|
17397
|
+
function styleConfigSlice(config) {
|
|
17398
|
+
if (config == null || typeof config !== "object") return null;
|
|
17399
|
+
const doc = config;
|
|
17400
|
+
const out = {};
|
|
17401
|
+
for (const key of STYLE_SYNC_CONFIG_KEYS) {
|
|
17402
|
+
if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
|
|
17403
|
+
}
|
|
17404
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
17405
|
+
}
|
|
17406
|
+
|
|
17221
17407
|
// src/workspace/persist.ts
|
|
17222
17408
|
var memoryStore = /* @__PURE__ */ new Map();
|
|
17223
17409
|
function memoryStorageAdapter() {
|
|
@@ -19277,6 +19463,12 @@ var DataControl = class {
|
|
|
19277
19463
|
symbols(provider) {
|
|
19278
19464
|
return this.registry?.symbols(provider) ?? [];
|
|
19279
19465
|
}
|
|
19466
|
+
/** The icon URL for `symbol` — its owning provider's `resolveSymbolIcon`, routed
|
|
19467
|
+
* through resolution. Undefined while unresolvable, when the provider declares no
|
|
19468
|
+
* resolver, or on a custom `deps.dataFeed` — the shells then show initials. */
|
|
19469
|
+
symbolIcon(symbol) {
|
|
19470
|
+
return this.registry?.symbolIcon(symbol);
|
|
19471
|
+
}
|
|
19280
19472
|
/** Per-symbol metadata (Pine `syminfo.*`), resolved through the owning provider. */
|
|
19281
19473
|
symbolInfo(symbol) {
|
|
19282
19474
|
return this.registry?.symbolInfoFor(symbol) ?? Promise.resolve(void 0);
|
|
@@ -19566,12 +19758,16 @@ var IndicatorInputsDialog = class {
|
|
|
19566
19758
|
closeOnEscape: false,
|
|
19567
19759
|
footer: (foot) => {
|
|
19568
19760
|
foot.append(
|
|
19761
|
+
this.resetAction(),
|
|
19569
19762
|
this.dialogButton("Cancel", false, () => this.revertAndClose()),
|
|
19570
19763
|
this.dialogButton("Ok", true, () => this.close())
|
|
19571
19764
|
);
|
|
19572
19765
|
},
|
|
19766
|
+
// Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
|
|
19767
|
+
// asynchronously — after a reset rebuild that notification must not close
|
|
19768
|
+
// the replacement dialog.
|
|
19573
19769
|
onOpenChange: (open2) => {
|
|
19574
|
-
if (!open2) this.close();
|
|
19770
|
+
if (!open2 && this.uiDialog === ui) this.close();
|
|
19575
19771
|
}
|
|
19576
19772
|
});
|
|
19577
19773
|
applyChromeTokens(ui.panel, t);
|
|
@@ -19702,6 +19898,30 @@ var IndicatorInputsDialog = class {
|
|
|
19702
19898
|
}
|
|
19703
19899
|
this.close();
|
|
19704
19900
|
}
|
|
19901
|
+
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
19902
|
+
* (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
|
|
19903
|
+
resetAction() {
|
|
19904
|
+
const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
|
|
19905
|
+
b.style.marginRight = "auto";
|
|
19906
|
+
return b;
|
|
19907
|
+
}
|
|
19908
|
+
/** Restore every input to its declared default (re-running the indicator), then
|
|
19909
|
+
* re-open the form so each control re-reads the restored values — the same
|
|
19910
|
+
* rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
|
|
19911
|
+
* survives the rebuild, so Cancel after a reset still reverts the whole session. */
|
|
19912
|
+
resetToDefaults() {
|
|
19913
|
+
const row = this.row;
|
|
19914
|
+
if (!row) return;
|
|
19915
|
+
const snap = this.snapshot;
|
|
19916
|
+
for (const inp of row.inputs) {
|
|
19917
|
+
if (row.values[inp.key] !== inp.defval) {
|
|
19918
|
+
row.values[inp.key] = inp.defval;
|
|
19919
|
+
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
19920
|
+
}
|
|
19921
|
+
}
|
|
19922
|
+
this.open(row);
|
|
19923
|
+
if (snap) this.snapshot = snap;
|
|
19924
|
+
}
|
|
19705
19925
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
19706
19926
|
commit(row, key, value) {
|
|
19707
19927
|
row.values[key] = value;
|
|
@@ -31135,6 +31355,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
31135
31355
|
}
|
|
31136
31356
|
|
|
31137
31357
|
// src/renderers/native/backdrop/BackdropRenderer.ts
|
|
31358
|
+
function clipHighlightRect(x1, x2, left, right) {
|
|
31359
|
+
const x = Math.max(left, x1);
|
|
31360
|
+
const end = Math.min(right, x2);
|
|
31361
|
+
return end > x ? { x, width: end - x } : null;
|
|
31362
|
+
}
|
|
31138
31363
|
var BackdropRenderer = class {
|
|
31139
31364
|
constructor() {
|
|
31140
31365
|
this.canvas = null;
|
|
@@ -31167,17 +31392,22 @@ var BackdropRenderer = class {
|
|
|
31167
31392
|
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
31168
31393
|
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
31169
31394
|
drawHighlights(ctx, scene, coords) {
|
|
31170
|
-
const
|
|
31171
|
-
if (
|
|
31395
|
+
const sessions = scene.sessionHighlightBands();
|
|
31396
|
+
if (sessions.length > 0) {
|
|
31397
|
+
const left = Math.max(0, coords.logicalToX(-0.5));
|
|
31398
|
+
const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
|
|
31399
|
+
this.drawHighlightSet(ctx, sessions, coords, left, right);
|
|
31400
|
+
}
|
|
31401
|
+
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
|
|
31402
|
+
}
|
|
31403
|
+
drawHighlightSet(ctx, bands, coords, left, right) {
|
|
31172
31404
|
for (const band of bands) {
|
|
31173
31405
|
const x1 = coords.timeToX(band.from);
|
|
31174
31406
|
const x2 = coords.timeToX(band.to);
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
31178
|
-
if (cw <= 0) continue;
|
|
31407
|
+
const rect = clipHighlightRect(x1, x2, left, right);
|
|
31408
|
+
if (!rect) continue;
|
|
31179
31409
|
ctx.fillStyle = band.color;
|
|
31180
|
-
ctx.fillRect(
|
|
31410
|
+
ctx.fillRect(rect.x, 0, rect.width, coords.height);
|
|
31181
31411
|
}
|
|
31182
31412
|
}
|
|
31183
31413
|
// ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
|
|
@@ -35625,8 +35855,9 @@ function statuslineInkOf(renderer, priceStyle) {
|
|
|
35625
35855
|
}
|
|
35626
35856
|
}
|
|
35627
35857
|
var Statusline = class {
|
|
35628
|
-
constructor(host, symbol) {
|
|
35858
|
+
constructor(host, symbol, iconFor) {
|
|
35629
35859
|
this.host = host;
|
|
35860
|
+
this.iconFor = iconFor;
|
|
35630
35861
|
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
35631
35862
|
this.lastBar = null;
|
|
35632
35863
|
this.hoverBar = null;
|
|
@@ -35650,7 +35881,7 @@ var Statusline = class {
|
|
|
35650
35881
|
this.el.className = "vela-statusline";
|
|
35651
35882
|
this.el.dataset.velaScreenshot = "1";
|
|
35652
35883
|
const ticker = parseSymbol(symbol).ticker;
|
|
35653
|
-
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
35884
|
+
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
35654
35885
|
this.symbolEl = doc.createElement("span");
|
|
35655
35886
|
this.symbolEl.className = "vela-sl-symbol";
|
|
35656
35887
|
this.symbolEl.textContent = ticker;
|
|
@@ -35671,7 +35902,7 @@ var Statusline = class {
|
|
|
35671
35902
|
setSymbol(symbol) {
|
|
35672
35903
|
const ticker = parseSymbol(symbol).ticker;
|
|
35673
35904
|
this.symbolEl.textContent = ticker;
|
|
35674
|
-
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
35905
|
+
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
35675
35906
|
this.avatarEl.replaceWith(fresh);
|
|
35676
35907
|
this.avatarEl = fresh;
|
|
35677
35908
|
this.fit();
|
|
@@ -35918,61 +36149,144 @@ var MarketStatusTracker = class {
|
|
|
35918
36149
|
};
|
|
35919
36150
|
|
|
35920
36151
|
// src/widget/session-shading.ts
|
|
35921
|
-
|
|
36152
|
+
var DAY_MS2 = 864e5;
|
|
36153
|
+
var MINUTE_MS = 6e4;
|
|
36154
|
+
function parseWindow(text) {
|
|
36155
|
+
if (typeof text !== "string") return null;
|
|
36156
|
+
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
36157
|
+
if (!m) return null;
|
|
36158
|
+
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
36159
|
+
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
36160
|
+
if (start >= end || end > 1440) return null;
|
|
36161
|
+
return { start, end };
|
|
36162
|
+
}
|
|
36163
|
+
function parseSessionSpec(si) {
|
|
36164
|
+
const session = si?.["session"];
|
|
36165
|
+
if (typeof session !== "string" || session === "" || session === "24x7") return null;
|
|
36166
|
+
const regular = parseWindow(session);
|
|
36167
|
+
if (!regular) return null;
|
|
36168
|
+
const tz = si?.["timezone"];
|
|
36169
|
+
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
36170
|
+
const ext = parseWindow(si?.["session_extended"]);
|
|
36171
|
+
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
36172
|
+
return { regular, extended, timezone };
|
|
36173
|
+
}
|
|
36174
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
36175
|
+
function civilFormatter(tz) {
|
|
36176
|
+
const cached = dtfCache.get(tz);
|
|
36177
|
+
if (cached !== void 0) return cached;
|
|
36178
|
+
let dtf = null;
|
|
36179
|
+
try {
|
|
36180
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
36181
|
+
timeZone: tz,
|
|
36182
|
+
hourCycle: "h23",
|
|
36183
|
+
weekday: "short",
|
|
36184
|
+
year: "numeric",
|
|
36185
|
+
month: "2-digit",
|
|
36186
|
+
day: "2-digit",
|
|
36187
|
+
hour: "2-digit",
|
|
36188
|
+
minute: "2-digit"
|
|
36189
|
+
});
|
|
36190
|
+
} catch {
|
|
36191
|
+
dtf = null;
|
|
36192
|
+
}
|
|
36193
|
+
dtfCache.set(tz, dtf);
|
|
36194
|
+
return dtf;
|
|
36195
|
+
}
|
|
36196
|
+
function civilParts(dtf, ms) {
|
|
36197
|
+
const parts = dtf.formatToParts(ms);
|
|
36198
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
36199
|
+
return {
|
|
36200
|
+
weekday: get("weekday"),
|
|
36201
|
+
year: Number(get("year")),
|
|
36202
|
+
month: Number(get("month")),
|
|
36203
|
+
day: Number(get("day")),
|
|
36204
|
+
hour: Number(get("hour")) % 24,
|
|
36205
|
+
minute: Number(get("minute"))
|
|
36206
|
+
};
|
|
36207
|
+
}
|
|
36208
|
+
function expandSessionZones(spec, from, to) {
|
|
35922
36209
|
const pre = [];
|
|
35923
36210
|
const post = [];
|
|
35924
|
-
|
|
35925
|
-
|
|
35926
|
-
|
|
35927
|
-
|
|
35928
|
-
|
|
35929
|
-
|
|
35930
|
-
|
|
35931
|
-
|
|
36211
|
+
const dtf = civilFormatter(spec.timezone);
|
|
36212
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
36213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36214
|
+
for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
|
|
36215
|
+
const civil = civilParts(dtf, cursor);
|
|
36216
|
+
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
36217
|
+
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
36218
|
+
seen.add(key);
|
|
36219
|
+
if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
|
|
36220
|
+
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
36221
|
+
const noonGuess = naive + 720 * MINUTE_MS;
|
|
36222
|
+
const atNoon = civilParts(dtf, noonGuess);
|
|
36223
|
+
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
36224
|
+
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
36225
|
+
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
36226
|
+
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
35932
36227
|
}
|
|
35933
36228
|
return { pre, post };
|
|
35934
36229
|
}
|
|
35935
|
-
var
|
|
35936
|
-
var MIN_LOOKBACK_MS = 3 * 864e5;
|
|
35937
|
-
var MAX_LOOKBACK_MS = 120 * 864e5;
|
|
36230
|
+
var COVER_PAD_MIN_MS = 2 * DAY_MS2;
|
|
35938
36231
|
var SessionShadingTracker = class {
|
|
35939
36232
|
constructor(onZones) {
|
|
35940
36233
|
this.onZones = onZones;
|
|
35941
|
-
/** Invalidates
|
|
36234
|
+
/** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
|
|
35942
36235
|
this.epoch = 0;
|
|
35943
|
-
|
|
35944
|
-
|
|
36236
|
+
this.spec = null;
|
|
36237
|
+
this.ready = false;
|
|
36238
|
+
this.session = "regular";
|
|
36239
|
+
this.covered = null;
|
|
36240
|
+
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
36241
|
+
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
36242
|
+
this.lastRange = null;
|
|
36243
|
+
}
|
|
36244
|
+
/** (Re)bind to a chart's data surface + market and expand once metadata lands. */
|
|
35945
36245
|
track(data, symbol, opts) {
|
|
35946
36246
|
const my = ++this.epoch;
|
|
35947
|
-
|
|
36247
|
+
this.ready = false;
|
|
36248
|
+
this.spec = null;
|
|
36249
|
+
this.covered = null;
|
|
36250
|
+
this.session = opts.session;
|
|
36251
|
+
this.lastRange = opts.range;
|
|
36252
|
+
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
36253
|
+
if (my !== this.epoch) return;
|
|
36254
|
+
this.ready = true;
|
|
36255
|
+
this.spec = parseSessionSpec(si);
|
|
36256
|
+
this.emit(this.lastRange ?? opts.range, true);
|
|
36257
|
+
});
|
|
36258
|
+
}
|
|
36259
|
+
/** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
|
|
36260
|
+
* leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
|
|
36261
|
+
updateRange(range) {
|
|
36262
|
+
this.lastRange = range;
|
|
36263
|
+
if (!this.ready) return;
|
|
36264
|
+
this.emit(range, false);
|
|
35948
36265
|
}
|
|
35949
36266
|
stop() {
|
|
35950
36267
|
this.epoch += 1;
|
|
35951
|
-
|
|
35952
|
-
|
|
35953
|
-
|
|
35954
|
-
|
|
35955
|
-
|
|
35956
|
-
|
|
35957
|
-
|
|
35958
|
-
|
|
35959
|
-
this.onZones(null);
|
|
36268
|
+
this.ready = false;
|
|
36269
|
+
this.spec = null;
|
|
36270
|
+
this.covered = null;
|
|
36271
|
+
this.lastRange = null;
|
|
36272
|
+
}
|
|
36273
|
+
emit(range, force) {
|
|
36274
|
+
if (!this.spec) {
|
|
36275
|
+
if (force) this.onZones(null);
|
|
35960
36276
|
return;
|
|
35961
36277
|
}
|
|
35962
|
-
if (
|
|
35963
|
-
this.onZones({ pre: [], post: [] });
|
|
36278
|
+
if (this.session !== "extended") {
|
|
36279
|
+
if (force) this.onZones({ pre: [], post: [] });
|
|
35964
36280
|
return;
|
|
35965
36281
|
}
|
|
35966
|
-
const
|
|
35967
|
-
const
|
|
35968
|
-
|
|
35969
|
-
|
|
35970
|
-
|
|
35971
|
-
|
|
35972
|
-
|
|
35973
|
-
|
|
35974
|
-
if (!regular || !extended) return;
|
|
35975
|
-
this.onZones(deriveSessionZones(regular, extended));
|
|
36282
|
+
const from = Math.min(range.from, range.to);
|
|
36283
|
+
const to = Math.max(range.from, range.to);
|
|
36284
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return;
|
|
36285
|
+
if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
|
|
36286
|
+
const pad = Math.max(to - from, COVER_PAD_MIN_MS);
|
|
36287
|
+
const covered = { from: from - pad, to: to + pad };
|
|
36288
|
+
this.covered = covered;
|
|
36289
|
+
this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
|
|
35976
36290
|
}
|
|
35977
36291
|
};
|
|
35978
36292
|
|
|
@@ -36409,7 +36723,11 @@ var ChartCell = class {
|
|
|
36409
36723
|
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
36410
36724
|
});
|
|
36411
36725
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36412
|
-
this.inner.on("load:end", () =>
|
|
36726
|
+
this.inner.on("load:end", () => {
|
|
36727
|
+
this.watermark?.setLoading(false);
|
|
36728
|
+
this.refreshSessionShading();
|
|
36729
|
+
});
|
|
36730
|
+
this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
|
|
36413
36731
|
const tz = deps.timezone();
|
|
36414
36732
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
36415
36733
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
@@ -36419,12 +36737,15 @@ var ChartCell = class {
|
|
|
36419
36737
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
36420
36738
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
36421
36739
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
36422
|
-
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
|
|
36740
|
+
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
36423
36741
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
36424
36742
|
this.statusline?.onChart(this.inner);
|
|
36425
36743
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
36426
36744
|
void this.inner.data.ready().then(() => {
|
|
36427
|
-
if (this.inner && this.state.symbol)
|
|
36745
|
+
if (this.inner && this.state.symbol) {
|
|
36746
|
+
this.statusline?.setSymbol(this.state.symbol);
|
|
36747
|
+
this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
|
|
36748
|
+
}
|
|
36428
36749
|
this.refreshSessionAvailable();
|
|
36429
36750
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
36430
36751
|
});
|
|
@@ -36533,14 +36854,18 @@ var ChartCell = class {
|
|
|
36533
36854
|
this.refreshSessionShading();
|
|
36534
36855
|
});
|
|
36535
36856
|
}
|
|
36536
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The
|
|
36537
|
-
*
|
|
36857
|
+
/** (Re)derive the pre/post-market shading bands for this cell's market. The bands
|
|
36858
|
+
* expand locally from the symbol's session vocabulary, so they paint as soon as
|
|
36859
|
+
* metadata is known and follow any pan depth without provider round trips. */
|
|
36538
36860
|
refreshSessionShading() {
|
|
36539
36861
|
const chart = this.inner;
|
|
36540
36862
|
const symbol = this.state.symbol;
|
|
36541
36863
|
if (!chart || !symbol) return;
|
|
36542
|
-
const
|
|
36543
|
-
this.
|
|
36864
|
+
const now = Date.now();
|
|
36865
|
+
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
36866
|
+
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
36867
|
+
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
36868
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, range });
|
|
36544
36869
|
}
|
|
36545
36870
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
36546
36871
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -36636,10 +36961,10 @@ var ChartCell = class {
|
|
|
36636
36961
|
id: "status-line",
|
|
36637
36962
|
rows: [
|
|
36638
36963
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
36639
|
-
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) =>
|
|
36640
|
-
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) =>
|
|
36641
|
-
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) =>
|
|
36642
|
-
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) =>
|
|
36964
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
|
|
36965
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
|
|
36966
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
|
|
36967
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
|
|
36643
36968
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
36644
36969
|
{
|
|
36645
36970
|
kind: "toggle",
|
|
@@ -36674,12 +36999,40 @@ var ChartCell = class {
|
|
|
36674
36999
|
this.indicatorTitlesOn = visible;
|
|
36675
37000
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
36676
37001
|
this.deps.onStateDirty();
|
|
37002
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
36677
37003
|
}
|
|
36678
37004
|
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
36679
37005
|
setIndicatorValuesVisible(visible) {
|
|
36680
37006
|
this.indicatorValuesOn = visible;
|
|
36681
37007
|
this.inner?.renderer.set("indicatorValues", visible);
|
|
36682
37008
|
this.deps.onStateDirty();
|
|
37009
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37010
|
+
}
|
|
37011
|
+
/** Show/hide one status-line segment (the settings dialog's Status line tab). */
|
|
37012
|
+
setStatuslinePart(part, visible) {
|
|
37013
|
+
this.statusline?.setPartVisible(part, visible);
|
|
37014
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37015
|
+
}
|
|
37016
|
+
/** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
|
|
37017
|
+
statusPrefs() {
|
|
37018
|
+
const sl = this.statusline;
|
|
37019
|
+
return {
|
|
37020
|
+
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
37021
|
+
indicatorTitles: this.indicatorTitlesOn,
|
|
37022
|
+
indicatorValues: this.indicatorValuesOn
|
|
37023
|
+
};
|
|
37024
|
+
}
|
|
37025
|
+
/** Converge this cell's Status line tab prefs to `prefs` — the follower half of
|
|
37026
|
+
* the workspace's style link. Idempotent: matching values change nothing, so a
|
|
37027
|
+
* propagated echo dies on its own. */
|
|
37028
|
+
applyStatusPrefs(prefs) {
|
|
37029
|
+
if (prefs.parts && this.statusline) {
|
|
37030
|
+
for (const part of Object.keys(prefs.parts)) {
|
|
37031
|
+
if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
|
|
37032
|
+
}
|
|
37033
|
+
}
|
|
37034
|
+
if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
|
|
37035
|
+
if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
|
|
36683
37036
|
}
|
|
36684
37037
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
36685
37038
|
* identity is what endures; the chart dies with the cell). */
|
|
@@ -37086,16 +37439,21 @@ var ChartCell = class {
|
|
|
37086
37439
|
|
|
37087
37440
|
// src/workspace/context.ts
|
|
37088
37441
|
function buildContext(host) {
|
|
37089
|
-
const active = host.active();
|
|
37090
37442
|
return {
|
|
37091
37443
|
get chart() {
|
|
37092
37444
|
const cell = host.active();
|
|
37093
37445
|
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
37094
37446
|
return cell.chart;
|
|
37095
37447
|
},
|
|
37096
|
-
|
|
37097
|
-
|
|
37098
|
-
|
|
37448
|
+
get symbol() {
|
|
37449
|
+
return host.active()?.symbol ?? "";
|
|
37450
|
+
},
|
|
37451
|
+
get timeframe() {
|
|
37452
|
+
return host.active()?.timeframe ?? "60";
|
|
37453
|
+
},
|
|
37454
|
+
get priceStyle() {
|
|
37455
|
+
return host.active()?.priceStyle ?? "candles";
|
|
37456
|
+
},
|
|
37099
37457
|
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
37100
37458
|
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
37101
37459
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
@@ -37106,8 +37464,12 @@ function buildContext(host) {
|
|
|
37106
37464
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
37107
37465
|
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
37108
37466
|
stateChanged: () => host.stateDirty(),
|
|
37109
|
-
|
|
37110
|
-
|
|
37467
|
+
get cells() {
|
|
37468
|
+
return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
|
|
37469
|
+
},
|
|
37470
|
+
get activeCellId() {
|
|
37471
|
+
return host.active()?.id ?? "";
|
|
37472
|
+
},
|
|
37111
37473
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
37112
37474
|
};
|
|
37113
37475
|
}
|
|
@@ -37446,6 +37808,10 @@ var VelaWorkspace = class {
|
|
|
37446
37808
|
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
37447
37809
|
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
37448
37810
|
this.drawingSyncBusy = false;
|
|
37811
|
+
/** Same guard for the style link: a follower's `applyConfig` re-fires its
|
|
37812
|
+
* `onConfigChanged` in the same tick, and a state restore applies per-cell
|
|
37813
|
+
* configs that legitimately differ — neither must propagate. */
|
|
37814
|
+
this.styleSyncBusy = false;
|
|
37449
37815
|
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
37450
37816
|
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
37451
37817
|
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
@@ -37506,13 +37872,14 @@ var VelaWorkspace = class {
|
|
|
37506
37872
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
37507
37873
|
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
37508
37874
|
const sync = boot?.sync ?? opts.sync;
|
|
37509
|
-
for (const kind of
|
|
37510
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
37511
|
-
}
|
|
37875
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
|
|
37512
37876
|
this.monoLayout = opts.layout === false;
|
|
37513
37877
|
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
37514
37878
|
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
37515
37879
|
this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
|
|
37880
|
+
this.topbarComp = resolveTopbarComposition(opts.topbar);
|
|
37881
|
+
this.indicatorsOverride = topbarActionOverride("indicators");
|
|
37882
|
+
this.screenshotOverride = topbarActionOverride("screenshot");
|
|
37516
37883
|
if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
|
|
37517
37884
|
if (boot?.ext) this.extState = { ...boot.ext };
|
|
37518
37885
|
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
@@ -37529,6 +37896,8 @@ var VelaWorkspace = class {
|
|
|
37529
37896
|
});
|
|
37530
37897
|
this.symbolPicker = new SymbolPicker({
|
|
37531
37898
|
host: this.root,
|
|
37899
|
+
// Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
|
|
37900
|
+
iconFor: (d) => this.feed.symbolIconOf(d),
|
|
37532
37901
|
onSelect: (ticker) => this.active.setSymbol(ticker),
|
|
37533
37902
|
onOpenChange: (open2) => {
|
|
37534
37903
|
if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
|
|
@@ -37536,7 +37905,7 @@ var VelaWorkspace = class {
|
|
|
37536
37905
|
}
|
|
37537
37906
|
});
|
|
37538
37907
|
this.symbolPicker.setSource(() => this.feed.symbols());
|
|
37539
|
-
this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
|
|
37908
|
+
this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
|
|
37540
37909
|
host: this.root,
|
|
37541
37910
|
library: () => this.active.libraryRows(),
|
|
37542
37911
|
onChart: () => this.active.onChartRows(),
|
|
@@ -37555,6 +37924,9 @@ var VelaWorkspace = class {
|
|
|
37555
37924
|
});
|
|
37556
37925
|
this.topbar = new Topbar(this.root, {
|
|
37557
37926
|
symbol: "",
|
|
37927
|
+
// RAW option, not the resolved lists — the bar distinguishes a host-declared
|
|
37928
|
+
// side (list is law) from a default one (an override's `order` may flow).
|
|
37929
|
+
composition: opts.topbar,
|
|
37558
37930
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
37559
37931
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
37560
37932
|
onUndoClick: () => this.active.history.undo(),
|
|
@@ -37584,7 +37956,8 @@ var VelaWorkspace = class {
|
|
|
37584
37956
|
syncs: () => [
|
|
37585
37957
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
37586
37958
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
37587
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
37959
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
37960
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
37588
37961
|
],
|
|
37589
37962
|
onToggleSync: (id) => {
|
|
37590
37963
|
const kind = id;
|
|
@@ -37612,7 +37985,7 @@ var VelaWorkspace = class {
|
|
|
37612
37985
|
context: () => this.context(),
|
|
37613
37986
|
changed: () => this.markStateDirty()
|
|
37614
37987
|
});
|
|
37615
|
-
this.objectTree = new ObjectTree(main);
|
|
37988
|
+
this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
|
|
37616
37989
|
this.dataWindow = new DataWindow(main);
|
|
37617
37990
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
37618
37991
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
@@ -37684,7 +38057,9 @@ var VelaWorkspace = class {
|
|
|
37684
38057
|
timeframe: "60",
|
|
37685
38058
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
37686
38059
|
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
37687
|
-
|
|
38060
|
+
// Same visibility truth as the desktop bar: composition-hidden
|
|
38061
|
+
// indicators lose their mobile stop too; an override takes it over.
|
|
38062
|
+
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
37688
38063
|
getContext: () => this.context(),
|
|
37689
38064
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
37690
38065
|
onMoreClick: () => this.openMoreDrawer(),
|
|
@@ -37866,7 +38241,7 @@ var VelaWorkspace = class {
|
|
|
37866
38241
|
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
37867
38242
|
}
|
|
37868
38243
|
this.dock.applyState(st.panels);
|
|
37869
|
-
for (const kind of
|
|
38244
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
37870
38245
|
this.trackSizes.clear();
|
|
37871
38246
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
37872
38247
|
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
@@ -37877,9 +38252,14 @@ var VelaWorkspace = class {
|
|
|
37877
38252
|
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
37878
38253
|
}
|
|
37879
38254
|
this.drawingLinks.clear();
|
|
37880
|
-
|
|
37881
|
-
|
|
37882
|
-
this.
|
|
38255
|
+
this.styleSyncBusy = true;
|
|
38256
|
+
try {
|
|
38257
|
+
for (const [i] of this.def.cells.entries()) {
|
|
38258
|
+
const { id, ...cs } = st.charts[i];
|
|
38259
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
38260
|
+
}
|
|
38261
|
+
} finally {
|
|
38262
|
+
this.styleSyncBusy = false;
|
|
37883
38263
|
}
|
|
37884
38264
|
if (st.timezone) this.setTimezone(st.timezone);
|
|
37885
38265
|
this.pool.clear();
|
|
@@ -37979,6 +38359,7 @@ var VelaWorkspace = class {
|
|
|
37979
38359
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
37980
38360
|
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
37981
38361
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
38362
|
+
const preexisting = new Set(this.cellsById.keys());
|
|
37982
38363
|
for (const [id, cell] of [...this.cellsById]) {
|
|
37983
38364
|
if (!keep.has(id) || rebuildAll) {
|
|
37984
38365
|
this.poolSet(id, cell.dehydrate());
|
|
@@ -37991,6 +38372,7 @@ var VelaWorkspace = class {
|
|
|
37991
38372
|
this.cellBackend = nextBackend;
|
|
37992
38373
|
this.applyGrid();
|
|
37993
38374
|
this.buildCells();
|
|
38375
|
+
this.alignNewCellStyles(preexisting);
|
|
37994
38376
|
this.syncCellPresentation();
|
|
37995
38377
|
this.topbar.setLayout(next.id);
|
|
37996
38378
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
@@ -38194,6 +38576,7 @@ var VelaWorkspace = class {
|
|
|
38194
38576
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
38195
38577
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
38196
38578
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
38579
|
+
onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
|
|
38197
38580
|
onStateDirty: () => this.markStateDirty(),
|
|
38198
38581
|
manifestSettled: () => this.manifestSettled,
|
|
38199
38582
|
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
@@ -38269,6 +38652,7 @@ var VelaWorkspace = class {
|
|
|
38269
38652
|
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
38270
38653
|
});
|
|
38271
38654
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
38655
|
+
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
38272
38656
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
38273
38657
|
chart.renderer.onAxisLongPress((e) => {
|
|
38274
38658
|
if (this.layoutCtl.current !== "mobile") return;
|
|
@@ -38301,11 +38685,67 @@ var VelaWorkspace = class {
|
|
|
38301
38685
|
if (kind === "viewport") {
|
|
38302
38686
|
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
38303
38687
|
if (range) this.propagateViewport(this.activeId, range);
|
|
38688
|
+
} else if (kind === "style") {
|
|
38689
|
+
this.propagateStylePrefs(this.activeId);
|
|
38304
38690
|
} else {
|
|
38305
38691
|
this.propagateMarket(this.activeId);
|
|
38306
38692
|
}
|
|
38307
38693
|
}
|
|
38308
38694
|
}
|
|
38695
|
+
/**
|
|
38696
|
+
* Align cells minted by a layout change to their style group: with the link on, a
|
|
38697
|
+
* NEW cell (fresh slot or one returning from the pool, which missed edits while
|
|
38698
|
+
* dormant) inherits the presentation of a pre-existing group peer — the active
|
|
38699
|
+
* cell when it is one — instead of sitting on its own state beside a styled
|
|
38700
|
+
* group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
|
|
38701
|
+
* the group, and the equality short-circuits keep converged peers untouched.
|
|
38702
|
+
*/
|
|
38703
|
+
alignNewCellStyles(preexisting) {
|
|
38704
|
+
const setting = this.syncOpts.style;
|
|
38705
|
+
if (!setting) return;
|
|
38706
|
+
const ids = [...this.cellsById.keys()];
|
|
38707
|
+
const propagated = /* @__PURE__ */ new Set();
|
|
38708
|
+
for (const id of ids) {
|
|
38709
|
+
if (preexisting.has(id)) continue;
|
|
38710
|
+
const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
|
|
38711
|
+
if (peers.length === 0) continue;
|
|
38712
|
+
const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
|
|
38713
|
+
if (propagated.has(source)) continue;
|
|
38714
|
+
propagated.add(source);
|
|
38715
|
+
this.propagateStylePrefs(source);
|
|
38716
|
+
}
|
|
38717
|
+
}
|
|
38718
|
+
/**
|
|
38719
|
+
* Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
|
|
38720
|
+
* its renderer config plus its Status line tab prefs — onto its same-group
|
|
38721
|
+
* followers (the style link). Loop-safe two ways: the busy guard eats the
|
|
38722
|
+
* followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
|
|
38723
|
+
* in the same tick), and the equality short-circuits leave already-converged
|
|
38724
|
+
* followers untouched, so nothing re-emits once the group agrees.
|
|
38725
|
+
*/
|
|
38726
|
+
propagateStylePrefs(originId) {
|
|
38727
|
+
if (this.styleSyncBusy || this.destroyed) return;
|
|
38728
|
+
const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
|
|
38729
|
+
if (targets.length === 0) return;
|
|
38730
|
+
const origin = this.cellsById.get(originId);
|
|
38731
|
+
if (!origin) return;
|
|
38732
|
+
const slice = styleConfigSlice(origin.chart.renderer.getConfig());
|
|
38733
|
+
const sliceJson = slice ? JSON.stringify(slice) : null;
|
|
38734
|
+
const prefs = origin.statusPrefs();
|
|
38735
|
+
this.styleSyncBusy = true;
|
|
38736
|
+
try {
|
|
38737
|
+
for (const id of targets) {
|
|
38738
|
+
const cell = this.cellsById.get(id);
|
|
38739
|
+
if (!cell) continue;
|
|
38740
|
+
if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
|
|
38741
|
+
cell.chart.renderer.applyConfig(slice);
|
|
38742
|
+
}
|
|
38743
|
+
cell.applyStatusPrefs(prefs);
|
|
38744
|
+
}
|
|
38745
|
+
} finally {
|
|
38746
|
+
this.styleSyncBusy = false;
|
|
38747
|
+
}
|
|
38748
|
+
}
|
|
38309
38749
|
/**
|
|
38310
38750
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
38311
38751
|
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
@@ -38580,27 +39020,32 @@ var VelaWorkspace = class {
|
|
|
38580
39020
|
this.drawingsDrawer.open();
|
|
38581
39021
|
}
|
|
38582
39022
|
openMoreDrawer() {
|
|
39023
|
+
const has = (id) => topbarHas(this.topbarComp, id);
|
|
38583
39024
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
38584
39025
|
host: this.root,
|
|
38585
|
-
onUndo: () => this.active.history.undo(),
|
|
38586
|
-
|
|
38587
|
-
onScreenshot: () => this.active.downloadScreenshot(),
|
|
39026
|
+
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
39027
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
|
|
38588
39028
|
canUndo: () => this.active.history.canUndo,
|
|
38589
39029
|
canRedo: () => this.active.history.canRedo,
|
|
38590
39030
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
38591
39031
|
priceStyle: () => this.active.priceStyle,
|
|
38592
39032
|
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
38593
|
-
panels: () => [...this.dock.list()],
|
|
39033
|
+
panels: () => has("panels") ? [...this.dock.list()] : [],
|
|
38594
39034
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
38595
|
-
alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
|
|
39035
|
+
...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
|
|
38596
39036
|
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
38597
|
-
// lands in the drawer, or every left action would appear twice.
|
|
38598
|
-
actions
|
|
39037
|
+
// lands in the drawer, or every left action would appear twice. Built-in-id
|
|
39038
|
+
// actions are slot OVERRIDES: they reach the drawer through the slot's own
|
|
39039
|
+
// routed button (screenshot) or stop (indicators), never as an extra row.
|
|
39040
|
+
actions: () => {
|
|
39041
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
39042
|
+
return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
|
|
39043
|
+
},
|
|
38599
39044
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
38600
39045
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
38601
39046
|
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
38602
|
-
// single-chart mode
|
|
38603
|
-
layout: this.monoLayout ? void 0 : {
|
|
39047
|
+
// single-chart mode and a composition without 'layout' omit it here too.
|
|
39048
|
+
layout: this.monoLayout || !has("layout") ? void 0 : {
|
|
38604
39049
|
shape: () => layoutShape(this.def),
|
|
38605
39050
|
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
38606
39051
|
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
@@ -38608,7 +39053,8 @@ var VelaWorkspace = class {
|
|
|
38608
39053
|
syncs: () => [
|
|
38609
39054
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
38610
39055
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
38611
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
39056
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
39057
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
38612
39058
|
],
|
|
38613
39059
|
onToggleSync: (id) => {
|
|
38614
39060
|
const kind = id;
|
|
@@ -38667,9 +39113,23 @@ var VelaWorkspace = class {
|
|
|
38667
39113
|
}
|
|
38668
39114
|
}
|
|
38669
39115
|
}
|
|
39116
|
+
/** Invoke a slot override the way its button would: fresh context, `when` respected. */
|
|
39117
|
+
runOverride(action) {
|
|
39118
|
+
const ctx = this.context();
|
|
39119
|
+
if (!action.when || action.when(ctx)) action.run(ctx);
|
|
39120
|
+
}
|
|
38670
39121
|
/** The default shortcut set — every binding acts on the ACTIVE cell. */
|
|
38671
39122
|
registerDefaultKeys() {
|
|
38672
|
-
this.
|
|
39123
|
+
if (topbarHas(this.topbarComp, "screenshot")) {
|
|
39124
|
+
const ov = this.screenshotOverride;
|
|
39125
|
+
this.keymap.register({
|
|
39126
|
+
id: "chart.screenshot",
|
|
39127
|
+
keys: "mod+alt+s",
|
|
39128
|
+
label: ov ? ov.label : "Download a chart screenshot",
|
|
39129
|
+
category: "Chart",
|
|
39130
|
+
run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
|
|
39131
|
+
});
|
|
39132
|
+
}
|
|
38673
39133
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|
|
38674
39134
|
this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
|
|
38675
39135
|
this.keymap.register({
|
|
@@ -38709,7 +39169,10 @@ var VelaWorkspace = class {
|
|
|
38709
39169
|
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
38710
39170
|
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
|
|
38711
39171
|
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
|
|
38712
|
-
|
|
39172
|
+
const indOv = this.indicatorsOverride;
|
|
39173
|
+
if (indOv && topbarHas(this.topbarComp, "indicators")) {
|
|
39174
|
+
this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
|
|
39175
|
+
} else if (this.indicatorPicker) {
|
|
38713
39176
|
this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
|
|
38714
39177
|
}
|
|
38715
39178
|
this.keymap.register({
|