@luxalgo/vela 0.6.7 → 0.6.9
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-BsQM2WNH.d.cts → DataProvider-8Z95Q-RJ.d.cts} +1 -1
- package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-DExJrfut.d.ts} +1 -1
- package/dist/{chunk-2R7BANEM.js → chunk-62SVGONC.js} +2 -2
- package/dist/{chunk-6WFKTAT4.js → chunk-6WDDVMBJ.js} +548 -147
- package/dist/{chunk-L3I2CCYO.js → chunk-EQCHJZOT.js} +49 -2
- package/dist/{chunk-OICPLNU7.js → chunk-FCVIG7JG.js} +188 -2
- package/dist/{chunk-WVP4XV5A.js → chunk-STHSKXOR.js} +407 -159
- package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
- package/dist/{contributions-Z12BrWCy.d.ts → contributions-C1U2Krwg.d.cts} +112 -7
- package/dist/{contributions-Hv4_cOJo.d.cts → contributions-D7PVZO2i.d.ts} +112 -7
- package/dist/index.cjs +755 -152
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +4 -4
- package/dist/{options-BaVTMXaO.d.ts → options-FM0peknS.d.cts} +84 -7
- package/dist/{options-BaVTMXaO.d.cts → options-FM0peknS.d.ts} +84 -7
- package/dist/{plugin-9koBjc5H.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
- package/dist/{plugin-BXhDHTYn.d.cts → plugin-DfVqBz9p.d.cts} +3 -3
- package/dist/plugin.cjs +18 -2
- 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/{bottombar-Br4rzx_R.d.ts → statusline-DOPiT6I6.d.cts} +91 -6
- package/dist/{bottombar-Div5zibZ.d.cts → statusline-zdF4eZLr.d.ts} +91 -6
- package/dist/ui.cjs +201 -4
- package/dist/ui.d.cts +89 -2
- package/dist/ui.d.ts +89 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +755 -152
- package/dist/vela.global.min.js +93 -44
- package/dist/widget.cjs +1144 -271
- package/dist/widget.d.cts +12 -85
- package/dist/widget.d.ts +12 -85
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1144 -271
- package/dist/workspace.d.cts +65 -11
- package/dist/workspace.d.ts +65 -11
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/widget.cjs
CHANGED
|
@@ -1097,11 +1097,11 @@ registerIcon(
|
|
|
1097
1097
|
);
|
|
1098
1098
|
registerIcon(
|
|
1099
1099
|
"market-pre",
|
|
1100
|
-
svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="
|
|
1100
|
+
svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
|
|
1101
1101
|
);
|
|
1102
1102
|
registerIcon(
|
|
1103
1103
|
"market-post",
|
|
1104
|
-
svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16
|
|
1104
|
+
svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
|
|
1105
1105
|
);
|
|
1106
1106
|
registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
|
|
1107
1107
|
registerIcon(
|
|
@@ -4195,6 +4195,192 @@ function buildFieldControl(desc) {
|
|
|
4195
4195
|
return { el };
|
|
4196
4196
|
}
|
|
4197
4197
|
|
|
4198
|
+
// src/ui/components/callout-bubble/controller.ts
|
|
4199
|
+
function closesPanel(item) {
|
|
4200
|
+
return item.close !== false;
|
|
4201
|
+
}
|
|
4202
|
+
function calloutPanelRows(items) {
|
|
4203
|
+
const rows = [];
|
|
4204
|
+
for (const item of items) {
|
|
4205
|
+
if (item.type === "text") {
|
|
4206
|
+
rows.push({ type: "text", text: item.text });
|
|
4207
|
+
continue;
|
|
4208
|
+
}
|
|
4209
|
+
const last = rows[rows.length - 1];
|
|
4210
|
+
if (last && last.type === "buttons") last.buttons.push(item);
|
|
4211
|
+
else rows.push({ type: "buttons", buttons: [item] });
|
|
4212
|
+
}
|
|
4213
|
+
return rows;
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// src/ui/components/callout-bubble/styles.ts
|
|
4217
|
+
var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
|
|
4218
|
+
var CALLOUT_CSS = `
|
|
4219
|
+
.vela-callout {
|
|
4220
|
+
display: inline-grid;
|
|
4221
|
+
place-items: center;
|
|
4222
|
+
border-radius: 50%;
|
|
4223
|
+
flex: none;
|
|
4224
|
+
line-height: 0;
|
|
4225
|
+
box-sizing: border-box;
|
|
4226
|
+
cursor: default;
|
|
4227
|
+
user-select: none;
|
|
4228
|
+
-webkit-user-select: none;
|
|
4229
|
+
}
|
|
4230
|
+
.vela-callout[role='button'] { cursor: pointer; }
|
|
4231
|
+
.vela-callout svg { display: block; }
|
|
4232
|
+
/* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
|
|
4233
|
+
shell is bare positioning chrome). */
|
|
4234
|
+
.vela-callout-panel {
|
|
4235
|
+
display: flex;
|
|
4236
|
+
flex-direction: column;
|
|
4237
|
+
gap: 8px;
|
|
4238
|
+
padding: 10px 12px;
|
|
4239
|
+
max-width: 280px;
|
|
4240
|
+
box-sizing: border-box;
|
|
4241
|
+
background: var(--vela-surface-elev);
|
|
4242
|
+
border: 1px solid var(--vela-border-strong);
|
|
4243
|
+
border-radius: 6px;
|
|
4244
|
+
box-shadow: var(--vela-shadow);
|
|
4245
|
+
color: var(--vela-fg);
|
|
4246
|
+
font: var(--vela-font-size-md) var(--vela-font);
|
|
4247
|
+
}
|
|
4248
|
+
.vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
|
|
4249
|
+
.vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
|
|
4250
|
+
.vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
4251
|
+
.vela-callout-btn {
|
|
4252
|
+
cursor: pointer;
|
|
4253
|
+
height: 26px;
|
|
4254
|
+
padding: 0 10px;
|
|
4255
|
+
border-radius: 5px;
|
|
4256
|
+
border: 1px solid var(--vela-border);
|
|
4257
|
+
background: transparent;
|
|
4258
|
+
color: var(--vela-fg);
|
|
4259
|
+
font-size: var(--vela-font-size-md);
|
|
4260
|
+
font-family: inherit;
|
|
4261
|
+
transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
|
|
4262
|
+
}
|
|
4263
|
+
.vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
|
|
4264
|
+
.vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
4265
|
+
.vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
|
|
4266
|
+
`;
|
|
4267
|
+
|
|
4268
|
+
// src/ui/components/callout-bubble/view.ts
|
|
4269
|
+
var CalloutBubble = class {
|
|
4270
|
+
constructor(opts) {
|
|
4271
|
+
this.pop = null;
|
|
4272
|
+
this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
|
|
4273
|
+
if (opts.color !== void 0) this.spec.color = opts.color;
|
|
4274
|
+
if (opts.panel !== void 0) this.spec.panel = opts.panel;
|
|
4275
|
+
this.size = opts.size ?? 16;
|
|
4276
|
+
this.host = opts.host;
|
|
4277
|
+
this.theme = opts.theme;
|
|
4278
|
+
this.boundary = opts.boundary;
|
|
4279
|
+
const doc = (opts.host ?? document.body).ownerDocument;
|
|
4280
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
4281
|
+
this.el = doc.createElement("span");
|
|
4282
|
+
this.el.className = "vela-callout";
|
|
4283
|
+
this.el.style.width = `${this.size}px`;
|
|
4284
|
+
this.el.style.height = `${this.size}px`;
|
|
4285
|
+
this.el.addEventListener("click", (e) => {
|
|
4286
|
+
if (!this.spec.panel) return;
|
|
4287
|
+
e.stopPropagation();
|
|
4288
|
+
this.toggle();
|
|
4289
|
+
});
|
|
4290
|
+
this.el.addEventListener("dblclick", (e) => {
|
|
4291
|
+
if (this.spec.panel) e.stopPropagation();
|
|
4292
|
+
});
|
|
4293
|
+
this.el.addEventListener("keydown", (e) => {
|
|
4294
|
+
if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
|
|
4295
|
+
e.preventDefault();
|
|
4296
|
+
this.toggle();
|
|
4297
|
+
});
|
|
4298
|
+
this.dress();
|
|
4299
|
+
}
|
|
4300
|
+
/** Re-dress the bubble (a status change: new icon, tint, label, panel). */
|
|
4301
|
+
set(spec) {
|
|
4302
|
+
this.spec = { ...this.spec, ...spec };
|
|
4303
|
+
if ("panel" in spec) this.pop?.hide();
|
|
4304
|
+
this.dress();
|
|
4305
|
+
}
|
|
4306
|
+
/** Whether the deployed panel is currently open. */
|
|
4307
|
+
get open() {
|
|
4308
|
+
return this.pop?.open ?? false;
|
|
4309
|
+
}
|
|
4310
|
+
destroy() {
|
|
4311
|
+
this.pop?.destroy();
|
|
4312
|
+
this.pop = null;
|
|
4313
|
+
this.el.remove();
|
|
4314
|
+
}
|
|
4315
|
+
dress() {
|
|
4316
|
+
const clickable = this.spec.panel !== void 0;
|
|
4317
|
+
this.el.style.background = this.spec.background;
|
|
4318
|
+
this.el.style.color = this.spec.color ?? "";
|
|
4319
|
+
this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
|
|
4320
|
+
this.el.setAttribute("aria-label", this.spec.label);
|
|
4321
|
+
if (clickable) {
|
|
4322
|
+
this.el.setAttribute("role", "button");
|
|
4323
|
+
this.el.tabIndex = 0;
|
|
4324
|
+
} else {
|
|
4325
|
+
this.el.removeAttribute("role");
|
|
4326
|
+
this.el.removeAttribute("tabindex");
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
toggle() {
|
|
4330
|
+
if (this.pop?.open) {
|
|
4331
|
+
this.pop.hide();
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
const panel = this.spec.panel;
|
|
4335
|
+
if (!panel) return;
|
|
4336
|
+
this.pop?.destroy();
|
|
4337
|
+
this.pop = new Popover({
|
|
4338
|
+
trigger: this.el,
|
|
4339
|
+
gap: 6,
|
|
4340
|
+
content: (body) => this.buildPanel(body, panel),
|
|
4341
|
+
...this.host ? { host: this.host } : {},
|
|
4342
|
+
...this.theme ? { theme: this.theme() } : {},
|
|
4343
|
+
...this.boundary ? { boundary: this.boundary } : {}
|
|
4344
|
+
});
|
|
4345
|
+
this.pop.show();
|
|
4346
|
+
}
|
|
4347
|
+
buildPanel(body, panel) {
|
|
4348
|
+
const doc = body.ownerDocument;
|
|
4349
|
+
const root = doc.createElement("div");
|
|
4350
|
+
root.className = "vela-callout-panel";
|
|
4351
|
+
if (panel.title) {
|
|
4352
|
+
const title = doc.createElement("div");
|
|
4353
|
+
title.className = "vela-callout-title";
|
|
4354
|
+
title.textContent = panel.title;
|
|
4355
|
+
root.appendChild(title);
|
|
4356
|
+
}
|
|
4357
|
+
for (const row of calloutPanelRows(panel.items)) {
|
|
4358
|
+
if (row.type === "text") {
|
|
4359
|
+
const text = doc.createElement("div");
|
|
4360
|
+
text.className = "vela-callout-text";
|
|
4361
|
+
text.textContent = row.text;
|
|
4362
|
+
root.appendChild(text);
|
|
4363
|
+
continue;
|
|
4364
|
+
}
|
|
4365
|
+
const actions = doc.createElement("div");
|
|
4366
|
+
actions.className = "vela-callout-actions";
|
|
4367
|
+
for (const item of row.buttons) {
|
|
4368
|
+
const btn2 = doc.createElement("button");
|
|
4369
|
+
btn2.type = "button";
|
|
4370
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
4371
|
+
btn2.textContent = item.label;
|
|
4372
|
+
btn2.addEventListener("click", () => {
|
|
4373
|
+
item.run();
|
|
4374
|
+
if (closesPanel(item)) this.pop?.hide();
|
|
4375
|
+
});
|
|
4376
|
+
actions.appendChild(btn2);
|
|
4377
|
+
}
|
|
4378
|
+
root.appendChild(actions);
|
|
4379
|
+
}
|
|
4380
|
+
body.appendChild(root);
|
|
4381
|
+
}
|
|
4382
|
+
};
|
|
4383
|
+
|
|
4198
4384
|
// src/widget/layout-picker.ts
|
|
4199
4385
|
var STYLE_ID = "vela-widget-layout-picker-v14";
|
|
4200
4386
|
var CSS2 = `
|
|
@@ -4677,6 +4863,44 @@ function legendActionsProviderFor(chart, context) {
|
|
|
4677
4863
|
return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
|
|
4678
4864
|
};
|
|
4679
4865
|
}
|
|
4866
|
+
var calloutRegistry = /* @__PURE__ */ new Map();
|
|
4867
|
+
function legendCallouts() {
|
|
4868
|
+
return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
4869
|
+
}
|
|
4870
|
+
function legendCalloutsProviderFor(chart, context) {
|
|
4871
|
+
return (indicatorId) => {
|
|
4872
|
+
const handle = chart.indicators().find((h) => h.id === indicatorId);
|
|
4873
|
+
if (!handle) return [];
|
|
4874
|
+
const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
|
|
4875
|
+
const views = [];
|
|
4876
|
+
for (const d of legendCallouts()) {
|
|
4877
|
+
const spec = d.callout(info);
|
|
4878
|
+
if (!spec) continue;
|
|
4879
|
+
views.push({
|
|
4880
|
+
id: d.id,
|
|
4881
|
+
icon: spec.icon,
|
|
4882
|
+
background: spec.background,
|
|
4883
|
+
...spec.color !== void 0 ? { color: spec.color } : {},
|
|
4884
|
+
tooltip: spec.tooltip,
|
|
4885
|
+
...spec.content !== void 0 ? {
|
|
4886
|
+
content: {
|
|
4887
|
+
...spec.content.title !== void 0 ? { title: spec.content.title } : {},
|
|
4888
|
+
items: spec.content.items.map(
|
|
4889
|
+
(item) => item.type === "text" ? item : {
|
|
4890
|
+
type: "button",
|
|
4891
|
+
label: item.label,
|
|
4892
|
+
...item.primary !== void 0 ? { primary: item.primary } : {},
|
|
4893
|
+
...item.close !== void 0 ? { close: item.close } : {},
|
|
4894
|
+
run: () => item.run(context(), info)
|
|
4895
|
+
}
|
|
4896
|
+
)
|
|
4897
|
+
}
|
|
4898
|
+
} : {}
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
return views;
|
|
4902
|
+
};
|
|
4903
|
+
}
|
|
4680
4904
|
var stateHandlers = /* @__PURE__ */ new Map();
|
|
4681
4905
|
function registerStatePersistence(handler) {
|
|
4682
4906
|
stateHandlers.set(handler.key, handler);
|
|
@@ -5254,6 +5478,11 @@ var Topbar = class {
|
|
|
5254
5478
|
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
5255
5479
|
* render through the flow cluster like ordinary actions. */
|
|
5256
5480
|
this.flowingOverrides = /* @__PURE__ */ new Set();
|
|
5481
|
+
/** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
|
|
5482
|
+
* renderActions pass (contributed buttons are replaceChildren'd away). */
|
|
5483
|
+
this.actionTooltips = [];
|
|
5484
|
+
/** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
|
|
5485
|
+
this.warnedIconless = /* @__PURE__ */ new Set();
|
|
5257
5486
|
this.onHairlineSync = () => {
|
|
5258
5487
|
if (this.hairlineRaf) return;
|
|
5259
5488
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -5516,6 +5745,8 @@ var Topbar = class {
|
|
|
5516
5745
|
this.actionsHost.replaceChildren();
|
|
5517
5746
|
this.leftActionsHost.replaceChildren();
|
|
5518
5747
|
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
5748
|
+
for (const t of this.actionTooltips) t.destroy();
|
|
5749
|
+
this.actionTooltips = [];
|
|
5519
5750
|
const doc = this.actionsHost.ownerDocument;
|
|
5520
5751
|
const flowLeft = this.comp.left.includes("actions");
|
|
5521
5752
|
const flowRight = this.comp.right.includes("actions");
|
|
@@ -5525,10 +5756,20 @@ var Topbar = class {
|
|
|
5525
5756
|
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
5526
5757
|
const left = pin ? pin.left : action.align === "left";
|
|
5527
5758
|
if (!pin && !(left ? flowLeft : flowRight)) continue;
|
|
5759
|
+
const iconOnly = action.iconOnly === true && !!action.icon;
|
|
5760
|
+
if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
|
|
5761
|
+
this.warnedIconless.add(action.id);
|
|
5762
|
+
console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
|
|
5763
|
+
}
|
|
5528
5764
|
const b = doc.createElement("button");
|
|
5529
|
-
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
5765
|
+
b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
|
|
5530
5766
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
5531
|
-
|
|
5767
|
+
if (iconOnly) {
|
|
5768
|
+
b.setAttribute("aria-label", action.label);
|
|
5769
|
+
this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
|
|
5770
|
+
} else {
|
|
5771
|
+
b.appendChild(doc.createTextNode(action.label));
|
|
5772
|
+
}
|
|
5532
5773
|
b.addEventListener("click", () => {
|
|
5533
5774
|
const c = this.opts.getContext?.();
|
|
5534
5775
|
if (c) action.run(c);
|
|
@@ -5579,7 +5820,7 @@ var Topbar = class {
|
|
|
5579
5820
|
this.tfMenu.destroy();
|
|
5580
5821
|
this.styleMenu.destroy();
|
|
5581
5822
|
this.layoutPicker?.destroy();
|
|
5582
|
-
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
5823
|
+
for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
|
|
5583
5824
|
this.el.remove();
|
|
5584
5825
|
}
|
|
5585
5826
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
@@ -17315,19 +17556,8 @@ function registerBuiltinChartTypes() {
|
|
|
17315
17556
|
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
17316
17557
|
}
|
|
17317
17558
|
|
|
17318
|
-
// src/workspace/sync.ts
|
|
17319
|
-
function syncTargets(originId, setting, cellIds) {
|
|
17320
|
-
if (setting == null || setting === false) return [];
|
|
17321
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17322
|
-
const group = setting[originId];
|
|
17323
|
-
if (group == null) return [];
|
|
17324
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17325
|
-
}
|
|
17326
|
-
function rangesWithin(a, b, epsMs) {
|
|
17327
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17328
|
-
}
|
|
17329
|
-
|
|
17330
17559
|
// src/state/document.ts
|
|
17560
|
+
var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
|
|
17331
17561
|
function prefixedSymbol(cell) {
|
|
17332
17562
|
if (!cell?.symbol) return void 0;
|
|
17333
17563
|
if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
|
|
@@ -17415,7 +17645,7 @@ function sanitizeSync(raw) {
|
|
|
17415
17645
|
if (raw == null || typeof raw !== "object") return null;
|
|
17416
17646
|
const s = raw;
|
|
17417
17647
|
const out = {};
|
|
17418
|
-
for (const kind of
|
|
17648
|
+
for (const kind of SYNC_KINDS) {
|
|
17419
17649
|
const v = s[kind];
|
|
17420
17650
|
if (v === true) out[kind] = true;
|
|
17421
17651
|
else if (v != null && typeof v === "object") {
|
|
@@ -17458,6 +17688,28 @@ function sanitizeTrackSizes(raw) {
|
|
|
17458
17688
|
return Object.keys(out).length > 0 ? out : null;
|
|
17459
17689
|
}
|
|
17460
17690
|
|
|
17691
|
+
// src/workspace/sync.ts
|
|
17692
|
+
function syncTargets(originId, setting, cellIds) {
|
|
17693
|
+
if (setting == null || setting === false) return [];
|
|
17694
|
+
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17695
|
+
const group = setting[originId];
|
|
17696
|
+
if (group == null) return [];
|
|
17697
|
+
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17698
|
+
}
|
|
17699
|
+
function rangesWithin(a, b, epsMs) {
|
|
17700
|
+
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17701
|
+
}
|
|
17702
|
+
var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
|
|
17703
|
+
function styleConfigSlice(config) {
|
|
17704
|
+
if (config == null || typeof config !== "object") return null;
|
|
17705
|
+
const doc = config;
|
|
17706
|
+
const out = {};
|
|
17707
|
+
for (const key of STYLE_SYNC_CONFIG_KEYS) {
|
|
17708
|
+
if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
|
|
17709
|
+
}
|
|
17710
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
17711
|
+
}
|
|
17712
|
+
|
|
17461
17713
|
// src/widget/persist.ts
|
|
17462
17714
|
function localStorageAdapter(storageKey) {
|
|
17463
17715
|
return {
|
|
@@ -17550,6 +17802,7 @@ var IndicatorHandleImpl = class {
|
|
|
17550
17802
|
constructor(id, title, controller, source) {
|
|
17551
17803
|
this.controller = controller;
|
|
17552
17804
|
this.schema = [];
|
|
17805
|
+
this.propsSchema = [];
|
|
17553
17806
|
this.visibleState = true;
|
|
17554
17807
|
this.bus = new TypedEventBus();
|
|
17555
17808
|
this.id = id;
|
|
@@ -17559,6 +17812,9 @@ var IndicatorHandleImpl = class {
|
|
|
17559
17812
|
get inputs() {
|
|
17560
17813
|
return this.schema;
|
|
17561
17814
|
}
|
|
17815
|
+
get props() {
|
|
17816
|
+
return this.propsSchema;
|
|
17817
|
+
}
|
|
17562
17818
|
get visible() {
|
|
17563
17819
|
return this.visibleState;
|
|
17564
17820
|
}
|
|
@@ -17568,6 +17824,12 @@ var IndicatorHandleImpl = class {
|
|
|
17568
17824
|
setInputs(values) {
|
|
17569
17825
|
this.controller.applyInputs(this.id, values);
|
|
17570
17826
|
}
|
|
17827
|
+
setProp(key, value) {
|
|
17828
|
+
this.controller.applyProps(this.id, { [key]: value });
|
|
17829
|
+
}
|
|
17830
|
+
setProps(values) {
|
|
17831
|
+
this.controller.applyProps(this.id, values);
|
|
17832
|
+
}
|
|
17571
17833
|
setVisible(visible) {
|
|
17572
17834
|
this.controller.setVisible(this.id, visible);
|
|
17573
17835
|
}
|
|
@@ -17587,6 +17849,9 @@ var IndicatorHandleImpl = class {
|
|
|
17587
17849
|
setSchema(schema) {
|
|
17588
17850
|
this.schema = schema;
|
|
17589
17851
|
}
|
|
17852
|
+
setPropsSchema(schema) {
|
|
17853
|
+
this.propsSchema = schema;
|
|
17854
|
+
}
|
|
17590
17855
|
/** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
|
|
17591
17856
|
setVisibleState(visible) {
|
|
17592
17857
|
this.visibleState = visible;
|
|
@@ -17600,6 +17865,8 @@ var IndicatorHandleImpl = class {
|
|
|
17600
17865
|
function summarizeModel(model) {
|
|
17601
17866
|
const series = {};
|
|
17602
17867
|
for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
|
|
17868
|
+
const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
|
|
17869
|
+
const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
|
|
17603
17870
|
return {
|
|
17604
17871
|
id: model.id,
|
|
17605
17872
|
title: model.title,
|
|
@@ -17619,7 +17886,8 @@ function summarizeModel(model) {
|
|
|
17619
17886
|
tables: model.tables?.length ?? 0,
|
|
17620
17887
|
barColors: model.barColors?.length ?? 0,
|
|
17621
17888
|
trades: model.trades?.length ?? 0,
|
|
17622
|
-
inputs: model.inputs.length
|
|
17889
|
+
inputs: model.inputs.length,
|
|
17890
|
+
forcedOverlay
|
|
17623
17891
|
};
|
|
17624
17892
|
}
|
|
17625
17893
|
function inspectModels(models) {
|
|
@@ -17640,7 +17908,8 @@ function inspectModels(models) {
|
|
|
17640
17908
|
linefills: sum((s) => s.linefills),
|
|
17641
17909
|
tables: sum((s) => s.tables),
|
|
17642
17910
|
barColors: sum((s) => s.barColors),
|
|
17643
|
-
trades: sum((s) => s.trades)
|
|
17911
|
+
trades: sum((s) => s.trades),
|
|
17912
|
+
forcedOverlay: sum((s) => s.forcedOverlay)
|
|
17644
17913
|
}
|
|
17645
17914
|
};
|
|
17646
17915
|
}
|
|
@@ -17784,7 +18053,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17784
18053
|
for (const engine of engines) this.registerEngine(engine.language, engine);
|
|
17785
18054
|
this.defaultLanguage = config.defaultLanguage;
|
|
17786
18055
|
this.renderer.mount(container, config.theme);
|
|
17787
|
-
this.renderer.onInputChange(
|
|
18056
|
+
this.renderer.onInputChange(
|
|
18057
|
+
(e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
|
|
18058
|
+
);
|
|
17788
18059
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17789
18060
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17790
18061
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
@@ -18464,7 +18735,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18464
18735
|
const title = options.title ?? "Indicator";
|
|
18465
18736
|
const handle = new IndicatorHandleImpl(id, title, this, source);
|
|
18466
18737
|
this.handles.set(id, handle);
|
|
18467
|
-
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
|
|
18738
|
+
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
|
|
18468
18739
|
void this.startIndicator(id, source, options, handle);
|
|
18469
18740
|
return handle;
|
|
18470
18741
|
}
|
|
@@ -18487,7 +18758,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18487
18758
|
return handle;
|
|
18488
18759
|
}
|
|
18489
18760
|
const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
|
|
18490
|
-
this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
|
|
18761
|
+
this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
|
|
18491
18762
|
handle.setSchema(descriptor.inputsSchema());
|
|
18492
18763
|
void this.startNativeIndicator(id, handle);
|
|
18493
18764
|
return handle;
|
|
@@ -18540,6 +18811,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18540
18811
|
if (record.session) record.session.update(record.inputValues);
|
|
18541
18812
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
18542
18813
|
}
|
|
18814
|
+
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
18815
|
+
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
18816
|
+
* Script indicators only: natives have no declaration props. */
|
|
18817
|
+
applyProps(id, values) {
|
|
18818
|
+
const record = this.registry.get(id);
|
|
18819
|
+
if (!record?.session) return;
|
|
18820
|
+
record.propValues = { ...record.propValues, ...values };
|
|
18821
|
+
if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
18822
|
+
record.pendingStructural = true;
|
|
18823
|
+
if (!record.hidden) this.setLoading(record, true);
|
|
18824
|
+
record.pendingCause = "inputs";
|
|
18825
|
+
record.session.update(record.inputValues, record.propValues);
|
|
18826
|
+
}
|
|
18543
18827
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18544
18828
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
18545
18829
|
listIndicators() {
|
|
@@ -18688,6 +18972,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18688
18972
|
for (const input of prepared.inputs) defaults2[input.key] = input.defval;
|
|
18689
18973
|
record.inputValues = { ...defaults2, ...record.inputValues };
|
|
18690
18974
|
handle.setSchema(prepared.inputs);
|
|
18975
|
+
const propDefaults = {};
|
|
18976
|
+
for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
|
|
18977
|
+
record.propValues = { ...propDefaults, ...record.propValues };
|
|
18978
|
+
handle.setPropsSchema(prepared.props ?? []);
|
|
18691
18979
|
this.mountLoadingPlaceholder(id, record);
|
|
18692
18980
|
this.executeIndicator(id, handle);
|
|
18693
18981
|
} catch (err) {
|
|
@@ -18711,6 +18999,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18711
18999
|
getBars: () => this.bars,
|
|
18712
19000
|
fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
|
|
18713
19001
|
inputs: record.inputValues,
|
|
19002
|
+
props: record.propValues,
|
|
18714
19003
|
visibleRange: this.currentVisibleRange(),
|
|
18715
19004
|
mode,
|
|
18716
19005
|
// Mid-backfill session starts (add / re-show / price-style re-execute) let the
|
|
@@ -18804,6 +19093,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18804
19093
|
const meta = record.prepared.meta;
|
|
18805
19094
|
const model = {
|
|
18806
19095
|
id,
|
|
19096
|
+
// Deliberately the FULL title, never a shorttitle: while the script loads the
|
|
19097
|
+
// legend identifies it by its full name; the compact shorttitle arrives with
|
|
19098
|
+
// the first computed model and takes over from there.
|
|
18807
19099
|
title: record.options?.title ?? meta.title,
|
|
18808
19100
|
overlay: meta.overlay,
|
|
18809
19101
|
paneHint: meta.overlay ? "price" : "new",
|
|
@@ -18812,7 +19104,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18812
19104
|
backgrounds: [],
|
|
18813
19105
|
priceLines: [],
|
|
18814
19106
|
inputs: record.prepared.inputs,
|
|
18815
|
-
inputValues: record.inputValues
|
|
19107
|
+
inputValues: record.inputValues,
|
|
19108
|
+
...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
|
|
18816
19109
|
};
|
|
18817
19110
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
18818
19111
|
this.placeModel(model, id, paneId);
|
|
@@ -19045,7 +19338,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19045
19338
|
record.model = model;
|
|
19046
19339
|
if (record.pendingStructural) {
|
|
19047
19340
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19048
|
-
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
|
|
19341
|
+
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
19049
19342
|
record.pendingStructural = false;
|
|
19050
19343
|
} else {
|
|
19051
19344
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
@@ -19072,16 +19365,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19072
19365
|
placeModel(model, id, paneId) {
|
|
19073
19366
|
model.id = id;
|
|
19074
19367
|
model.paneId = paneId;
|
|
19075
|
-
|
|
19076
|
-
for (const
|
|
19077
|
-
for (const
|
|
19368
|
+
const paneOf = (item) => item.overlay === true ? "price" : paneId;
|
|
19369
|
+
for (const series of model.series) series.paneId = paneOf(series);
|
|
19370
|
+
for (const fill of model.fills) fill.paneId = paneOf(fill);
|
|
19371
|
+
for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
|
|
19078
19372
|
for (const line of model.priceLines) line.paneId = paneId;
|
|
19079
|
-
if (model.lines) for (const ln of model.lines) ln.paneId =
|
|
19080
|
-
if (model.boxes) for (const bx of model.boxes) bx.paneId =
|
|
19081
|
-
if (model.labels) for (const lb of model.labels) lb.paneId =
|
|
19082
|
-
if (model.polylines) for (const pl of model.polylines) pl.paneId =
|
|
19083
|
-
if (model.linefills) for (const lf of model.linefills) lf.paneId =
|
|
19084
|
-
if (model.tables) for (const tb of model.tables) tb.paneId =
|
|
19373
|
+
if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
|
|
19374
|
+
if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
|
|
19375
|
+
if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
|
|
19376
|
+
if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
|
|
19377
|
+
if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
|
|
19378
|
+
if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
|
|
19085
19379
|
}
|
|
19086
19380
|
emitContextChanged(id) {
|
|
19087
19381
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
@@ -19234,6 +19528,15 @@ var RendererControl = class {
|
|
|
19234
19528
|
setLegendActions(provider) {
|
|
19235
19529
|
this.renderer.setLegendActions?.(provider);
|
|
19236
19530
|
}
|
|
19531
|
+
/**
|
|
19532
|
+
* Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
|
|
19533
|
+
* plugin registry through this; see `registerLegendCallout`). Silent on a renderer
|
|
19534
|
+
* without the seam — contributed callouts simply never show there, same graceful
|
|
19535
|
+
* degradation as {@link setLegendActions}.
|
|
19536
|
+
*/
|
|
19537
|
+
setLegendCallouts(provider) {
|
|
19538
|
+
this.renderer.setLegendCallouts?.(provider);
|
|
19539
|
+
}
|
|
19237
19540
|
/**
|
|
19238
19541
|
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
19239
19542
|
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
@@ -19743,6 +20046,10 @@ function inputVisible(when, values) {
|
|
|
19743
20046
|
}
|
|
19744
20047
|
|
|
19745
20048
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
20049
|
+
var PROPS_TAB = "Properties";
|
|
20050
|
+
function bagOf(row, decl) {
|
|
20051
|
+
return decl.prop ? row.propValues : row.values;
|
|
20052
|
+
}
|
|
19746
20053
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
19747
20054
|
var IndicatorInputsDialog = class {
|
|
19748
20055
|
constructor(host) {
|
|
@@ -19753,6 +20060,7 @@ var IndicatorInputsDialog = class {
|
|
|
19753
20060
|
this.openId = null;
|
|
19754
20061
|
this.row = null;
|
|
19755
20062
|
this.snapshot = null;
|
|
20063
|
+
this.propSnapshot = null;
|
|
19756
20064
|
this.dialogTips = [];
|
|
19757
20065
|
/** Re-applies every `when` gate against current values; set while the dialog is open. */
|
|
19758
20066
|
this.refreshVisibility = null;
|
|
@@ -19775,10 +20083,11 @@ var IndicatorInputsDialog = class {
|
|
|
19775
20083
|
}
|
|
19776
20084
|
open(row) {
|
|
19777
20085
|
this.close();
|
|
19778
|
-
if (row.inputs.length === 0) return;
|
|
20086
|
+
if (row.inputs.length === 0 && row.props.length === 0) return;
|
|
19779
20087
|
this.row = row;
|
|
19780
20088
|
this.openId = row.id;
|
|
19781
20089
|
this.snapshot = { ...row.values };
|
|
20090
|
+
this.propSnapshot = { ...row.propValues };
|
|
19782
20091
|
ensureDialogStyles();
|
|
19783
20092
|
const t = this.host.theme();
|
|
19784
20093
|
const border = "var(--vela-border)";
|
|
@@ -19786,7 +20095,7 @@ var IndicatorInputsDialog = class {
|
|
|
19786
20095
|
const host = this.host.dialogHost() ?? this.host.container;
|
|
19787
20096
|
const ui = new Dialog({
|
|
19788
20097
|
host,
|
|
19789
|
-
title: row.
|
|
20098
|
+
title: row.title,
|
|
19790
20099
|
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
19791
20100
|
// whole body, killing the chart and the body-portaled popovers.
|
|
19792
20101
|
modal: false,
|
|
@@ -19798,12 +20107,16 @@ var IndicatorInputsDialog = class {
|
|
|
19798
20107
|
closeOnEscape: false,
|
|
19799
20108
|
footer: (foot) => {
|
|
19800
20109
|
foot.append(
|
|
20110
|
+
this.resetAction(),
|
|
19801
20111
|
this.dialogButton("Cancel", false, () => this.revertAndClose()),
|
|
19802
20112
|
this.dialogButton("Ok", true, () => this.close())
|
|
19803
20113
|
);
|
|
19804
20114
|
},
|
|
20115
|
+
// Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
|
|
20116
|
+
// asynchronously — after a reset rebuild that notification must not close
|
|
20117
|
+
// the replacement dialog.
|
|
19805
20118
|
onOpenChange: (open2) => {
|
|
19806
|
-
if (!open2) this.close();
|
|
20119
|
+
if (!open2 && this.uiDialog === ui) this.close();
|
|
19807
20120
|
}
|
|
19808
20121
|
});
|
|
19809
20122
|
applyChromeTokens(ui.panel, t);
|
|
@@ -19814,6 +20127,16 @@ var IndicatorInputsDialog = class {
|
|
|
19814
20127
|
text: () => "Close"
|
|
19815
20128
|
}));
|
|
19816
20129
|
const tabDefs = tabInputs(row.inputs);
|
|
20130
|
+
if (row.props.length > 0) {
|
|
20131
|
+
const propDecls = row.props.map((p) => {
|
|
20132
|
+
const d = { ...p, prop: true };
|
|
20133
|
+
delete d.tab;
|
|
20134
|
+
return d;
|
|
20135
|
+
});
|
|
20136
|
+
const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
|
|
20137
|
+
if (existing) existing.inputs.push(...propDecls);
|
|
20138
|
+
else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
|
|
20139
|
+
}
|
|
19817
20140
|
const tabs = document.createElement("div");
|
|
19818
20141
|
tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
19819
20142
|
const tabEls = [];
|
|
@@ -19916,9 +20239,10 @@ var IndicatorInputsDialog = class {
|
|
|
19916
20239
|
this.openId = null;
|
|
19917
20240
|
this.row = null;
|
|
19918
20241
|
this.snapshot = null;
|
|
20242
|
+
this.propSnapshot = null;
|
|
19919
20243
|
ui?.destroy();
|
|
19920
20244
|
}
|
|
19921
|
-
/** Restore every input to its open-time value (re-running the indicator), then close. */
|
|
20245
|
+
/** Restore every input and prop to its open-time value (re-running the indicator), then close. */
|
|
19922
20246
|
revertAndClose() {
|
|
19923
20247
|
const row = this.row;
|
|
19924
20248
|
const snap = this.snapshot;
|
|
@@ -19932,12 +20256,55 @@ var IndicatorInputsDialog = class {
|
|
|
19932
20256
|
}
|
|
19933
20257
|
}
|
|
19934
20258
|
}
|
|
20259
|
+
const propSnap = this.propSnapshot;
|
|
20260
|
+
if (row && propSnap) {
|
|
20261
|
+
for (const p of row.props) {
|
|
20262
|
+
const snapped = propSnap[p.key];
|
|
20263
|
+
const before = snapped !== void 0 ? snapped : p.defval;
|
|
20264
|
+
if (row.propValues[p.key] !== before) {
|
|
20265
|
+
row.propValues[p.key] = before;
|
|
20266
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
|
|
20267
|
+
}
|
|
20268
|
+
}
|
|
20269
|
+
}
|
|
19935
20270
|
this.close();
|
|
19936
20271
|
}
|
|
20272
|
+
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
20273
|
+
* (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
|
|
20274
|
+
resetAction() {
|
|
20275
|
+
const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
|
|
20276
|
+
b.style.marginRight = "auto";
|
|
20277
|
+
return b;
|
|
20278
|
+
}
|
|
20279
|
+
/** Restore every input to its declared default (re-running the indicator), then
|
|
20280
|
+
* re-open the form so each control re-reads the restored values — the same
|
|
20281
|
+
* rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
|
|
20282
|
+
* survives the rebuild, so Cancel after a reset still reverts the whole session. */
|
|
20283
|
+
resetToDefaults() {
|
|
20284
|
+
const row = this.row;
|
|
20285
|
+
if (!row) return;
|
|
20286
|
+
const snap = this.snapshot;
|
|
20287
|
+
const propSnap = this.propSnapshot;
|
|
20288
|
+
for (const inp of row.inputs) {
|
|
20289
|
+
if (row.values[inp.key] !== inp.defval) {
|
|
20290
|
+
row.values[inp.key] = inp.defval;
|
|
20291
|
+
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
20292
|
+
}
|
|
20293
|
+
}
|
|
20294
|
+
for (const p of row.props) {
|
|
20295
|
+
if (row.propValues[p.key] !== p.defval) {
|
|
20296
|
+
row.propValues[p.key] = p.defval;
|
|
20297
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
|
|
20298
|
+
}
|
|
20299
|
+
}
|
|
20300
|
+
this.open(row);
|
|
20301
|
+
if (snap) this.snapshot = snap;
|
|
20302
|
+
if (propSnap) this.propSnapshot = propSnap;
|
|
20303
|
+
}
|
|
19937
20304
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
19938
|
-
commit(row,
|
|
19939
|
-
row.
|
|
19940
|
-
this.host.onChange?.({ indicatorId: row.id, key, value });
|
|
20305
|
+
commit(row, decl, value) {
|
|
20306
|
+
bagOf(row, decl)[decl.key] = value;
|
|
20307
|
+
this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
|
|
19941
20308
|
this.refreshVisibility?.();
|
|
19942
20309
|
}
|
|
19943
20310
|
/** One settings row (or several `inline=` inputs) placed into a section's grid. */
|
|
@@ -19964,7 +20331,7 @@ var IndicatorInputsDialog = class {
|
|
|
19964
20331
|
const id = idOf(lead);
|
|
19965
20332
|
const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
|
|
19966
20333
|
if (lead.type === "bool") {
|
|
19967
|
-
const current = Boolean(row
|
|
20334
|
+
const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
|
|
19968
20335
|
return append(fieldRow({
|
|
19969
20336
|
label: nameOf(lead),
|
|
19970
20337
|
id,
|
|
@@ -19973,7 +20340,7 @@ var IndicatorInputsDialog = class {
|
|
|
19973
20340
|
toggle: {
|
|
19974
20341
|
id,
|
|
19975
20342
|
checked: current,
|
|
19976
|
-
onChange: (v) => this.commit(row, lead
|
|
20343
|
+
onChange: (v) => this.commit(row, lead, v)
|
|
19977
20344
|
}
|
|
19978
20345
|
}));
|
|
19979
20346
|
}
|
|
@@ -19998,8 +20365,8 @@ var IndicatorInputsDialog = class {
|
|
|
19998
20365
|
}
|
|
19999
20366
|
/** Build the typed control for one input, committing edits live via `onChange`. */
|
|
20000
20367
|
buildControl(row, inp, id) {
|
|
20001
|
-
const current = row
|
|
20002
|
-
const emit = (value) => this.commit(row, inp
|
|
20368
|
+
const current = bagOf(row, inp)[inp.key] ?? inp.defval;
|
|
20369
|
+
const emit = (value) => this.commit(row, inp, value);
|
|
20003
20370
|
if (inp.type === "bool") {
|
|
20004
20371
|
return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
|
|
20005
20372
|
}
|
|
@@ -20010,7 +20377,7 @@ var IndicatorInputsDialog = class {
|
|
|
20010
20377
|
kind: "color",
|
|
20011
20378
|
id,
|
|
20012
20379
|
theme: this.host.theme(),
|
|
20013
|
-
get: () => String(row
|
|
20380
|
+
get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
|
|
20014
20381
|
onChange: (v) => emit(v)
|
|
20015
20382
|
}).el;
|
|
20016
20383
|
}
|
|
@@ -20573,6 +20940,7 @@ function timeParts(ts) {
|
|
|
20573
20940
|
}
|
|
20574
20941
|
|
|
20575
20942
|
// src/renderers/shared/InputsUI.ts
|
|
20943
|
+
var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
|
|
20576
20944
|
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
20577
20945
|
var LEGEND_ICON_PX2 = 16;
|
|
20578
20946
|
var LEGEND_CTL_PX = 18;
|
|
@@ -20611,10 +20979,14 @@ var InputsUI = class {
|
|
|
20611
20979
|
this.moveApi = null;
|
|
20612
20980
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
20613
20981
|
this.legendActions = null;
|
|
20982
|
+
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
20983
|
+
this.legendCallouts = null;
|
|
20614
20984
|
/** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
|
|
20615
20985
|
this.rowTips = /* @__PURE__ */ new Map();
|
|
20616
20986
|
/** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
|
|
20617
20987
|
this.extrasTips = /* @__PURE__ */ new Map();
|
|
20988
|
+
/** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
|
|
20989
|
+
this.calloutTips = /* @__PURE__ */ new Map();
|
|
20618
20990
|
/** Open "Move to" menu (kept so it can be torn down). */
|
|
20619
20991
|
this.moveMenu = null;
|
|
20620
20992
|
this.moveTargets = [];
|
|
@@ -20772,6 +21144,42 @@ var InputsUI = class {
|
|
|
20772
21144
|
extrasEl.appendChild(btn2);
|
|
20773
21145
|
}
|
|
20774
21146
|
}
|
|
21147
|
+
/**
|
|
21148
|
+
* Wire the host-contributed callout bubbles. Same contract as
|
|
21149
|
+
* {@link setLegendActions}: re-calling replaces the provider and re-projects the
|
|
21150
|
+
* rows already on screen.
|
|
21151
|
+
*/
|
|
21152
|
+
setLegendCallouts(provider) {
|
|
21153
|
+
this.legendCallouts = provider;
|
|
21154
|
+
for (const row of this.rows.values()) this.renderCallouts(row);
|
|
21155
|
+
}
|
|
21156
|
+
/** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
|
|
21157
|
+
* click (when the view carries content) deploys a panel of text and actions. */
|
|
21158
|
+
renderCallouts(row) {
|
|
21159
|
+
this.disposeTips(this.calloutTips, row.id);
|
|
21160
|
+
for (const bubble of row.callouts) bubble.destroy();
|
|
21161
|
+
row.callouts = [];
|
|
21162
|
+
row.calloutsEl.replaceChildren();
|
|
21163
|
+
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21164
|
+
row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
|
|
21165
|
+
for (const view of views) {
|
|
21166
|
+
const bubble = new CalloutBubble({
|
|
21167
|
+
icon: view.icon,
|
|
21168
|
+
background: view.background,
|
|
21169
|
+
...view.color !== void 0 ? { color: view.color } : {},
|
|
21170
|
+
label: view.tooltip,
|
|
21171
|
+
...view.content !== void 0 ? { panel: view.content } : {},
|
|
21172
|
+
// The panel portals into the plot host and self-tokens: it must work on
|
|
21173
|
+
// a bare chart, where no `.vela-ui` token ancestor exists.
|
|
21174
|
+
host: this.container,
|
|
21175
|
+
theme: () => this.theme
|
|
21176
|
+
});
|
|
21177
|
+
bubble.el.dataset.legendCallout = view.id;
|
|
21178
|
+
this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
|
|
21179
|
+
row.callouts.push(bubble);
|
|
21180
|
+
row.calloutsEl.appendChild(bubble.el);
|
|
21181
|
+
}
|
|
21182
|
+
}
|
|
20775
21183
|
/** Reposition the per-pane legend containers after a layout change. */
|
|
20776
21184
|
reposition() {
|
|
20777
21185
|
for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
|
|
@@ -20939,6 +21347,7 @@ var InputsUI = class {
|
|
|
20939
21347
|
}
|
|
20940
21348
|
positionLegend(lg, paneId) {
|
|
20941
21349
|
const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
|
|
21350
|
+
lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
|
|
20942
21351
|
lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
|
|
20943
21352
|
const collapsed = this.paneCollapse.has(paneId);
|
|
20944
21353
|
const masterId = this.paneCollapse.get(paneId) ?? null;
|
|
@@ -21014,13 +21423,13 @@ var InputsUI = class {
|
|
|
21014
21423
|
}
|
|
21015
21424
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
21016
21425
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
21017
|
-
const settingsTitle = opts.settingsTitle ?? title;
|
|
21018
21426
|
const existing = this.rows.get(id);
|
|
21019
21427
|
if (existing) {
|
|
21020
21428
|
existing.title = title;
|
|
21021
|
-
existing.settingsTitle = settingsTitle;
|
|
21022
21429
|
existing.inputs = inputs;
|
|
21023
21430
|
existing.values = { ...values };
|
|
21431
|
+
existing.props = opts.props ?? [];
|
|
21432
|
+
existing.propValues = { ...opts.propValues ?? {} };
|
|
21024
21433
|
existing.titleEl.textContent = title;
|
|
21025
21434
|
if (existing.paneId !== paneId) {
|
|
21026
21435
|
existing.paneId = paneId;
|
|
@@ -21072,6 +21481,9 @@ var InputsUI = class {
|
|
|
21072
21481
|
titleWrap.appendChild(beta);
|
|
21073
21482
|
}
|
|
21074
21483
|
el.appendChild(titleWrap);
|
|
21484
|
+
const calloutsEl = document.createElement("span");
|
|
21485
|
+
calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
|
|
21486
|
+
el.appendChild(calloutsEl);
|
|
21075
21487
|
el.appendChild(statusEl);
|
|
21076
21488
|
const valuesEl = document.createElement("span");
|
|
21077
21489
|
valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
|
|
@@ -21088,13 +21500,13 @@ var InputsUI = class {
|
|
|
21088
21500
|
eye.className = "vela-ind-ctl";
|
|
21089
21501
|
eye.style.cssText = LEGEND_CTL_CSS;
|
|
21090
21502
|
eye.addEventListener("click", () => {
|
|
21091
|
-
const
|
|
21092
|
-
this.onToggleVisible?.(id, Boolean(
|
|
21503
|
+
const row2 = this.rows.get(id);
|
|
21504
|
+
this.onToggleVisible?.(id, Boolean(row2?.hidden));
|
|
21093
21505
|
});
|
|
21094
21506
|
controlsEl.appendChild(eye);
|
|
21095
21507
|
eyeEl = eye;
|
|
21096
21508
|
}
|
|
21097
|
-
if (inputs.length > 0) {
|
|
21509
|
+
if (inputs.length > 0 || (opts.props ?? []).length > 0) {
|
|
21098
21510
|
const gear = document.createElement("button");
|
|
21099
21511
|
gear.type = "button";
|
|
21100
21512
|
gear.setAttribute("aria-label", "Settings");
|
|
@@ -21134,7 +21546,9 @@ var InputsUI = class {
|
|
|
21134
21546
|
controlsEl.appendChild(close);
|
|
21135
21547
|
el.appendChild(controlsEl);
|
|
21136
21548
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
21137
|
-
|
|
21549
|
+
const row = { 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 };
|
|
21550
|
+
this.rows.set(id, row);
|
|
21551
|
+
this.renderCallouts(row);
|
|
21138
21552
|
this.syncFoldToggle();
|
|
21139
21553
|
}
|
|
21140
21554
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -21143,9 +21557,11 @@ var InputsUI = class {
|
|
|
21143
21557
|
else container.appendChild(el);
|
|
21144
21558
|
}
|
|
21145
21559
|
/** Reflect programmatic input changes (so a re-opened dialog shows current values). */
|
|
21146
|
-
setValues(id, values) {
|
|
21560
|
+
setValues(id, values, props) {
|
|
21147
21561
|
const row = this.rows.get(id);
|
|
21148
|
-
if (row)
|
|
21562
|
+
if (!row) return;
|
|
21563
|
+
row.values = { ...row.values, ...values };
|
|
21564
|
+
if (props) row.propValues = { ...row.propValues, ...props };
|
|
21149
21565
|
}
|
|
21150
21566
|
/**
|
|
21151
21567
|
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
@@ -21227,8 +21643,13 @@ var InputsUI = class {
|
|
|
21227
21643
|
syncRowActions(row) {
|
|
21228
21644
|
const open2 = row.highlighted;
|
|
21229
21645
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21230
|
-
if (open2)
|
|
21231
|
-
|
|
21646
|
+
if (open2) {
|
|
21647
|
+
row.el.appendChild(row.statusEl);
|
|
21648
|
+
row.el.appendChild(row.calloutsEl);
|
|
21649
|
+
} else {
|
|
21650
|
+
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21651
|
+
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
21652
|
+
}
|
|
21232
21653
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21233
21654
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21234
21655
|
if (child === row.extrasEl) {
|
|
@@ -21246,10 +21667,12 @@ var InputsUI = class {
|
|
|
21246
21667
|
}
|
|
21247
21668
|
remove(id) {
|
|
21248
21669
|
const row = this.rows.get(id);
|
|
21670
|
+
for (const bubble of row?.callouts ?? []) bubble.destroy();
|
|
21249
21671
|
row?.el.remove();
|
|
21250
21672
|
this.rows.delete(id);
|
|
21251
21673
|
this.disposeTips(this.rowTips, id);
|
|
21252
21674
|
this.disposeTips(this.extrasTips, id);
|
|
21675
|
+
this.disposeTips(this.calloutTips, id);
|
|
21253
21676
|
if (this.selectedId === id) this.selectedId = null;
|
|
21254
21677
|
if (this.inputsDialog.openId === id) this.inputsDialog.close();
|
|
21255
21678
|
this.syncFoldToggle();
|
|
@@ -21269,6 +21692,8 @@ var InputsUI = class {
|
|
|
21269
21692
|
this.rowMenu = null;
|
|
21270
21693
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
21271
21694
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
21695
|
+
for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
|
|
21696
|
+
for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
|
|
21272
21697
|
for (const lg of this.legends.values()) lg.remove();
|
|
21273
21698
|
this.legends.clear();
|
|
21274
21699
|
this.rows.clear();
|
|
@@ -21623,6 +22048,32 @@ var SIZE_PX3 = {
|
|
|
21623
22048
|
large: 16,
|
|
21624
22049
|
huge: 20
|
|
21625
22050
|
};
|
|
22051
|
+
function fontPxOf(size) {
|
|
22052
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
|
|
22053
|
+
return SIZE_PX3[size] ?? SIZE_PX3.auto;
|
|
22054
|
+
}
|
|
22055
|
+
function tableHasContent(t) {
|
|
22056
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
22057
|
+
}
|
|
22058
|
+
function mergeRenderPlan(t) {
|
|
22059
|
+
const span = /* @__PURE__ */ new Map();
|
|
22060
|
+
const omit = /* @__PURE__ */ new Set();
|
|
22061
|
+
for (const m of t.merges) {
|
|
22062
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
22063
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
22064
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
22065
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
22066
|
+
}
|
|
22067
|
+
}
|
|
22068
|
+
}
|
|
22069
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
22070
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
22071
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
22072
|
+
}
|
|
22073
|
+
}
|
|
22074
|
+
for (const key of span.keys()) omit.delete(key);
|
|
22075
|
+
return { span, omit };
|
|
22076
|
+
}
|
|
21626
22077
|
var TableOverlay = class {
|
|
21627
22078
|
constructor(container, theme, paneBounds) {
|
|
21628
22079
|
this.container = container;
|
|
@@ -21643,7 +22094,9 @@ var TableOverlay = class {
|
|
|
21643
22094
|
update(tables) {
|
|
21644
22095
|
this.lastTables = tables;
|
|
21645
22096
|
this.root.replaceChildren();
|
|
21646
|
-
for (const t of tables)
|
|
22097
|
+
for (const t of tables) {
|
|
22098
|
+
if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
|
|
22099
|
+
}
|
|
21647
22100
|
}
|
|
21648
22101
|
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
21649
22102
|
reposition() {
|
|
@@ -21658,39 +22111,36 @@ var TableOverlay = class {
|
|
|
21658
22111
|
this.root.remove();
|
|
21659
22112
|
}
|
|
21660
22113
|
renderTable(t) {
|
|
22114
|
+
const b = this.paneBounds(t.paneId);
|
|
21661
22115
|
const wrap = document.createElement("div");
|
|
21662
22116
|
wrap.style.position = "absolute";
|
|
21663
|
-
|
|
22117
|
+
if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
|
|
22118
|
+
this.anchor(wrap, t.position, b);
|
|
21664
22119
|
const table = document.createElement("table");
|
|
21665
22120
|
Object.assign(table.style, {
|
|
21666
22121
|
borderCollapse: "collapse",
|
|
21667
22122
|
background: t.bgColor ?? "transparent",
|
|
21668
22123
|
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
21669
|
-
// Frame as an inset shadow (not a `border`) so it stays independent of the
|
|
21670
|
-
// collapsed cell borders even when frame_width != border_width.
|
|
21671
|
-
boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
|
|
21672
22124
|
border: "none",
|
|
21673
22125
|
tableLayout: "auto",
|
|
21674
22126
|
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
21675
22127
|
pointerEvents: "auto"
|
|
21676
22128
|
});
|
|
21677
|
-
const span
|
|
21678
|
-
const
|
|
21679
|
-
|
|
21680
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
21681
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
21682
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
21683
|
-
if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
|
|
21684
|
-
}
|
|
21685
|
-
}
|
|
21686
|
-
}
|
|
22129
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
22130
|
+
const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
|
|
22131
|
+
const paneH = b.height;
|
|
21687
22132
|
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
21688
22133
|
for (let r = 0; r < t.rows; r += 1) {
|
|
21689
22134
|
const tr = document.createElement("tr");
|
|
21690
22135
|
for (let c = 0; c < t.columns; c += 1) {
|
|
22136
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
21691
22137
|
const cell = t.cells[r]?.[c] ?? null;
|
|
21692
|
-
if (skip.has(`${r}:${c}`) || cell?.merged) continue;
|
|
21693
22138
|
const td = document.createElement("td");
|
|
22139
|
+
if (cell === null) {
|
|
22140
|
+
Object.assign(td.style, { padding: "0", border: "none" });
|
|
22141
|
+
tr.appendChild(td);
|
|
22142
|
+
continue;
|
|
22143
|
+
}
|
|
21694
22144
|
const sp = span.get(`${r}:${c}`);
|
|
21695
22145
|
if (sp) {
|
|
21696
22146
|
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
@@ -21699,18 +22149,22 @@ var TableOverlay = class {
|
|
|
21699
22149
|
Object.assign(td.style, {
|
|
21700
22150
|
border: cellBorder,
|
|
21701
22151
|
padding: "2px 6px",
|
|
21702
|
-
background: cell
|
|
21703
|
-
color: cell
|
|
21704
|
-
textAlign: cell
|
|
21705
|
-
verticalAlign: cell
|
|
21706
|
-
fontSize: `${
|
|
21707
|
-
fontFamily: cell
|
|
21708
|
-
fontWeight: cell
|
|
21709
|
-
fontStyle: cell
|
|
21710
|
-
|
|
22152
|
+
background: cell.bgColor ?? "transparent",
|
|
22153
|
+
color: cell.textColor ?? this.theme.textColor,
|
|
22154
|
+
textAlign: cell.hAlign,
|
|
22155
|
+
verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
|
|
22156
|
+
fontSize: `${fontPxOf(cell.textSize)}px`,
|
|
22157
|
+
fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
22158
|
+
fontWeight: cell.bold ? "bold" : "normal",
|
|
22159
|
+
fontStyle: cell.italic ? "italic" : "normal",
|
|
22160
|
+
// Pine cell text never wraps; `\n` still breaks lines. Wrapping
|
|
22161
|
+
// used to collapse unicode sparklines and ━━━ dividers.
|
|
22162
|
+
whiteSpace: "pre"
|
|
21711
22163
|
});
|
|
21712
|
-
if (cell
|
|
21713
|
-
td.
|
|
22164
|
+
if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
|
|
22165
|
+
if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
|
|
22166
|
+
if (cell.tooltip) td.title = cell.tooltip;
|
|
22167
|
+
td.textContent = cell.text ?? "";
|
|
21714
22168
|
tr.appendChild(td);
|
|
21715
22169
|
}
|
|
21716
22170
|
table.appendChild(tr);
|
|
@@ -22312,8 +22766,14 @@ var WebGL2Backend = class {
|
|
|
22312
22766
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
22313
22767
|
};
|
|
22314
22768
|
b.alpha = this.modelAlpha;
|
|
22315
|
-
for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
|
|
22316
|
-
for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
22769
|
+
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22770
|
+
for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
22771
|
+
if (isPrice) {
|
|
22772
|
+
for (const m of scene.indicators.values()) {
|
|
22773
|
+
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22774
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
22775
|
+
}
|
|
22776
|
+
}
|
|
22317
22777
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
22318
22778
|
let candleDrawn = false;
|
|
22319
22779
|
for (const m of models) {
|
|
@@ -22327,7 +22787,7 @@ var WebGL2Backend = class {
|
|
|
22327
22787
|
b.alpha = this.modelAlpha;
|
|
22328
22788
|
const off = scene.offsetOf(m.id);
|
|
22329
22789
|
const mp = effPane(m);
|
|
22330
|
-
for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22790
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22331
22791
|
}
|
|
22332
22792
|
if (drawCandles && !candleDrawn) {
|
|
22333
22793
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -22336,6 +22796,12 @@ var WebGL2Backend = class {
|
|
|
22336
22796
|
}
|
|
22337
22797
|
drawSlicesUpTo(Infinity);
|
|
22338
22798
|
b.alpha = this.modelAlpha;
|
|
22799
|
+
if (isPrice) {
|
|
22800
|
+
for (const m of scene.indicators.values()) {
|
|
22801
|
+
const off = scene.offsetOf(m.id);
|
|
22802
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22803
|
+
}
|
|
22804
|
+
}
|
|
22339
22805
|
for (const m of models) {
|
|
22340
22806
|
const mp = effPane(m);
|
|
22341
22807
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -22382,7 +22848,7 @@ var WebGL2Backend = class {
|
|
|
22382
22848
|
for (const pane of scene.orderedPanes()) {
|
|
22383
22849
|
const b = this.glowBatch;
|
|
22384
22850
|
b.reset();
|
|
22385
|
-
this.emitGlowSources(b, scene
|
|
22851
|
+
this.emitGlowSources(b, scene, pane, coords, i0, i1);
|
|
22386
22852
|
if (b.vertexCount === 0) continue;
|
|
22387
22853
|
const topH = Math.round(pane.bounds.top * dpr * 0.5);
|
|
22388
22854
|
const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
|
|
@@ -22451,15 +22917,23 @@ var WebGL2Backend = class {
|
|
|
22451
22917
|
gl.bindVertexArray(null);
|
|
22452
22918
|
return true;
|
|
22453
22919
|
}
|
|
22454
|
-
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22455
|
-
|
|
22456
|
-
|
|
22457
|
-
|
|
22458
|
-
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
|
|
22920
|
+
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22921
|
+
* Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
|
|
22922
|
+
emitGlowSources(b, scene, pane, coords, i0, i1) {
|
|
22923
|
+
const emitOne = (s, off) => {
|
|
22924
|
+
if (!isLineLikeSeries(s) || s.visible === false) return;
|
|
22925
|
+
if (s.kind === "histogram" || s.kind === "columns") return;
|
|
22926
|
+
if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
|
|
22927
|
+
else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
|
|
22928
|
+
};
|
|
22929
|
+
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
22930
|
+
const off = scene.offsetOf(m.id);
|
|
22931
|
+
for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
|
|
22932
|
+
}
|
|
22933
|
+
if (pane.kind === "price") {
|
|
22934
|
+
for (const m of scene.indicators.values()) {
|
|
22935
|
+
const off = scene.offsetOf(m.id);
|
|
22936
|
+
for (const s of m.series) if (s.overlay === true) emitOne(s, off);
|
|
22463
22937
|
}
|
|
22464
22938
|
}
|
|
22465
22939
|
}
|
|
@@ -22742,9 +23216,9 @@ var WebGL2Backend = class {
|
|
|
22742
23216
|
b.alpha = this.candleBodyAlpha;
|
|
22743
23217
|
b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
|
|
22744
23218
|
}
|
|
22745
|
-
if (cs.borderVisible ||
|
|
23219
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
22746
23220
|
b.alpha = this.candleStructureAlpha;
|
|
22747
|
-
const bord = cs.borderVisible
|
|
23221
|
+
const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
|
|
22748
23222
|
const bw = Math.max(0, g.bodyW - 1);
|
|
22749
23223
|
const bh = Math.max(0, bodyH - 1);
|
|
22750
23224
|
b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
|
|
@@ -24296,8 +24770,14 @@ var Canvas2dBackend = class {
|
|
|
24296
24770
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
24297
24771
|
};
|
|
24298
24772
|
ctx.globalAlpha = this.modelAlpha;
|
|
24299
|
-
for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
|
|
24300
|
-
for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
24773
|
+
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
24774
|
+
for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
24775
|
+
if (isPrice) {
|
|
24776
|
+
for (const m of scene.indicators.values()) {
|
|
24777
|
+
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
24778
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
24779
|
+
}
|
|
24780
|
+
}
|
|
24301
24781
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
24302
24782
|
let si = 0;
|
|
24303
24783
|
const drawSlicesUpTo = (z) => {
|
|
@@ -24319,7 +24799,7 @@ var Canvas2dBackend = class {
|
|
|
24319
24799
|
ctx.globalAlpha = this.modelAlpha;
|
|
24320
24800
|
const off = scene.offsetOf(m.id);
|
|
24321
24801
|
const mp = effPane(m);
|
|
24322
|
-
for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24802
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24323
24803
|
}
|
|
24324
24804
|
if (drawCandles && !candleDrawn) {
|
|
24325
24805
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -24327,6 +24807,13 @@ var Canvas2dBackend = class {
|
|
|
24327
24807
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24328
24808
|
}
|
|
24329
24809
|
drawSlicesUpTo(Infinity);
|
|
24810
|
+
if (isPrice) {
|
|
24811
|
+
ctx.globalAlpha = this.modelAlpha;
|
|
24812
|
+
for (const m of scene.indicators.values()) {
|
|
24813
|
+
const off = scene.offsetOf(m.id);
|
|
24814
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24815
|
+
}
|
|
24816
|
+
}
|
|
24330
24817
|
ctx.globalAlpha = this.modelAlpha;
|
|
24331
24818
|
for (const m of models) {
|
|
24332
24819
|
const mp = effPane(m);
|
|
@@ -24598,9 +25085,9 @@ var Canvas2dBackend = class {
|
|
|
24598
25085
|
ctx.fillStyle = color;
|
|
24599
25086
|
ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
|
|
24600
25087
|
}
|
|
24601
|
-
if (cs.borderVisible ||
|
|
25088
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
24602
25089
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24603
|
-
ctx.strokeStyle = cs.borderVisible
|
|
25090
|
+
ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
|
|
24604
25091
|
ctx.lineWidth = 1;
|
|
24605
25092
|
const bw = Math.max(0, g.bodyW - 1);
|
|
24606
25093
|
const bh = Math.max(0, bodyH - 1);
|
|
@@ -25032,6 +25519,8 @@ var DrawingSceneRenderer = class {
|
|
|
25032
25519
|
this.set = set;
|
|
25033
25520
|
/** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
|
|
25034
25521
|
this.widthCache = /* @__PURE__ */ new Map();
|
|
25522
|
+
/** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
|
|
25523
|
+
this.tipRegions = [];
|
|
25035
25524
|
/**
|
|
25036
25525
|
* Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
|
|
25037
25526
|
* from the model's anchor bar, so they shift by this to land on chart logical indices.
|
|
@@ -25052,8 +25541,13 @@ var DrawingSceneRenderer = class {
|
|
|
25052
25541
|
const s = this.set;
|
|
25053
25542
|
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25054
25543
|
}
|
|
25544
|
+
/** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
|
|
25545
|
+
labelTipRegions() {
|
|
25546
|
+
return this.tipRegions;
|
|
25547
|
+
}
|
|
25055
25548
|
/** Draw the whole set into `ctx` using the supplied coordinate closures. */
|
|
25056
25549
|
render(ctx, W, H, xOf, yOf) {
|
|
25550
|
+
this.tipRegions = [];
|
|
25057
25551
|
if (this.isEmpty()) return;
|
|
25058
25552
|
ctx.save();
|
|
25059
25553
|
this.drawLinefills(ctx, W, H, xOf, yOf);
|
|
@@ -25401,13 +25895,35 @@ var DrawingSceneRenderer = class {
|
|
|
25401
25895
|
if (this.isPointShape(lb.style)) {
|
|
25402
25896
|
if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
|
|
25403
25897
|
if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
|
|
25404
|
-
|
|
25405
|
-
|
|
25898
|
+
if (lb.tooltip) {
|
|
25899
|
+
const r = Math.max(4, fontPx * 0.6) + 3;
|
|
25900
|
+
this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
|
|
25901
|
+
}
|
|
25902
|
+
} else if (lb.style === "none" || lb.style === "text_outline") {
|
|
25903
|
+
if (lb.text) {
|
|
25904
|
+
this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
|
|
25905
|
+
if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
|
|
25906
|
+
}
|
|
25406
25907
|
} else {
|
|
25407
|
-
this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
25908
|
+
const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
25909
|
+
if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
|
|
25408
25910
|
}
|
|
25409
25911
|
}
|
|
25410
25912
|
}
|
|
25913
|
+
/** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
|
|
25914
|
+
labelFont(lb, fontPx) {
|
|
25915
|
+
const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
|
|
25916
|
+
return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
|
|
25917
|
+
}
|
|
25918
|
+
/** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
|
|
25919
|
+
textRegion(ctx, lb, cx, cy, fontPx, text) {
|
|
25920
|
+
const font = this.labelFont(lb, fontPx);
|
|
25921
|
+
const lines = (lb.text ?? "").split("\n");
|
|
25922
|
+
const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
|
|
25923
|
+
const h = fontPx * 1.25 * lines.length;
|
|
25924
|
+
const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
|
|
25925
|
+
return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
|
|
25926
|
+
}
|
|
25411
25927
|
isPointShape(style) {
|
|
25412
25928
|
switch (style) {
|
|
25413
25929
|
case "circle":
|
|
@@ -25426,8 +25942,7 @@ var DrawingSceneRenderer = class {
|
|
|
25426
25942
|
}
|
|
25427
25943
|
}
|
|
25428
25944
|
drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
|
|
25429
|
-
|
|
25430
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
25945
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25431
25946
|
ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
|
|
25432
25947
|
ctx.textBaseline = "middle";
|
|
25433
25948
|
const lines = lb.text.split("\n");
|
|
@@ -25507,9 +26022,9 @@ var DrawingSceneRenderer = class {
|
|
|
25507
26022
|
ctx.fill();
|
|
25508
26023
|
}
|
|
25509
26024
|
}
|
|
26025
|
+
/** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
|
|
25510
26026
|
drawBubble(ctx, lb, px, py, fontPx, color) {
|
|
25511
|
-
|
|
25512
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
26027
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25513
26028
|
const lines = (lb.text ?? "").split("\n");
|
|
25514
26029
|
const padX = 6;
|
|
25515
26030
|
const padY = 4;
|
|
@@ -25567,38 +26082,51 @@ var DrawingSceneRenderer = class {
|
|
|
25567
26082
|
by = py - h - ptr;
|
|
25568
26083
|
pointer = "down";
|
|
25569
26084
|
}
|
|
25570
|
-
|
|
25571
|
-
|
|
25572
|
-
|
|
25573
|
-
if (pointer !== "none") {
|
|
25574
|
-
ctx.beginPath();
|
|
25575
|
-
if (pointer === "down") {
|
|
25576
|
-
ctx.moveTo(px - ptr, by + h);
|
|
25577
|
-
ctx.lineTo(px + ptr, by + h);
|
|
25578
|
-
ctx.lineTo(px, py);
|
|
25579
|
-
} else if (pointer === "up") {
|
|
25580
|
-
ctx.moveTo(px - ptr, by);
|
|
25581
|
-
ctx.lineTo(px + ptr, by);
|
|
25582
|
-
ctx.lineTo(px, py);
|
|
25583
|
-
} else if (pointer === "left") {
|
|
25584
|
-
ctx.moveTo(bx, py - ptr);
|
|
25585
|
-
ctx.lineTo(bx, py + ptr);
|
|
25586
|
-
ctx.lineTo(px, py);
|
|
25587
|
-
} else {
|
|
25588
|
-
ctx.moveTo(bx + w, py - ptr);
|
|
25589
|
-
ctx.lineTo(bx + w, py + ptr);
|
|
25590
|
-
ctx.lineTo(px, py);
|
|
25591
|
-
}
|
|
25592
|
-
ctx.closePath();
|
|
26085
|
+
if (!lb.noFill) {
|
|
26086
|
+
ctx.fillStyle = color;
|
|
26087
|
+
this.roundRect(ctx, bx, by, w, h, 4);
|
|
25593
26088
|
ctx.fill();
|
|
26089
|
+
if (pointer !== "none") {
|
|
26090
|
+
ctx.beginPath();
|
|
26091
|
+
if (pointer === "down") {
|
|
26092
|
+
ctx.moveTo(px - ptr, by + h);
|
|
26093
|
+
ctx.lineTo(px + ptr, by + h);
|
|
26094
|
+
ctx.lineTo(px, py);
|
|
26095
|
+
} else if (pointer === "up") {
|
|
26096
|
+
ctx.moveTo(px - ptr, by);
|
|
26097
|
+
ctx.lineTo(px + ptr, by);
|
|
26098
|
+
ctx.lineTo(px, py);
|
|
26099
|
+
} else if (pointer === "left") {
|
|
26100
|
+
ctx.moveTo(bx, py - ptr);
|
|
26101
|
+
ctx.lineTo(bx, py + ptr);
|
|
26102
|
+
ctx.lineTo(px, py);
|
|
26103
|
+
} else {
|
|
26104
|
+
ctx.moveTo(bx + w, py - ptr);
|
|
26105
|
+
ctx.lineTo(bx + w, py + ptr);
|
|
26106
|
+
ctx.lineTo(px, py);
|
|
26107
|
+
}
|
|
26108
|
+
ctx.closePath();
|
|
26109
|
+
ctx.fill();
|
|
26110
|
+
}
|
|
25594
26111
|
}
|
|
25595
26112
|
if (lb.text) {
|
|
25596
|
-
ctx.fillStyle = lb.textColor ?? contrastColor(color);
|
|
25597
|
-
ctx.textAlign = "center";
|
|
26113
|
+
ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
|
|
25598
26114
|
ctx.textBaseline = "middle";
|
|
26115
|
+
let tx;
|
|
26116
|
+
if (lb.textAlign === "left") {
|
|
26117
|
+
ctx.textAlign = "left";
|
|
26118
|
+
tx = bx + padX;
|
|
26119
|
+
} else if (lb.textAlign === "right") {
|
|
26120
|
+
ctx.textAlign = "right";
|
|
26121
|
+
tx = bx + w - padX;
|
|
26122
|
+
} else {
|
|
26123
|
+
ctx.textAlign = "center";
|
|
26124
|
+
tx = bx + w / 2;
|
|
26125
|
+
}
|
|
25599
26126
|
const startY = by + padY + lineH / 2;
|
|
25600
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i],
|
|
26127
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
|
|
25601
26128
|
}
|
|
26129
|
+
return { x: bx, y: by, w, h };
|
|
25602
26130
|
}
|
|
25603
26131
|
roundRect(ctx, x, y, w, h, r) {
|
|
25604
26132
|
const rr = Math.min(r, w / 2, h / 2);
|
|
@@ -25836,6 +26364,8 @@ var ChromeRenderer = class {
|
|
|
25836
26364
|
this.axisTextColor = DARK_THEME.textColor;
|
|
25837
26365
|
// Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
|
|
25838
26366
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
26367
|
+
// Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
|
|
26368
|
+
this.labelTips = [];
|
|
25839
26369
|
}
|
|
25840
26370
|
mount(canvas) {
|
|
25841
26371
|
this.canvas = canvas;
|
|
@@ -25877,6 +26407,7 @@ var ChromeRenderer = class {
|
|
|
25877
26407
|
const dataW = coords.width;
|
|
25878
26408
|
const dataH = coords.height;
|
|
25879
26409
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
26410
|
+
this.labelTips = [];
|
|
25880
26411
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
25881
26412
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
25882
26413
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -25986,6 +26517,17 @@ var ChromeRenderer = class {
|
|
|
25986
26517
|
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
25987
26518
|
);
|
|
25988
26519
|
ctx.restore();
|
|
26520
|
+
for (const r of this.drawScene.labelTipRegions()) {
|
|
26521
|
+
this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
26522
|
+
}
|
|
26523
|
+
}
|
|
26524
|
+
/** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
|
|
26525
|
+
labelTooltipAt(x, y) {
|
|
26526
|
+
for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
|
|
26527
|
+
const r = this.labelTips[i];
|
|
26528
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
26529
|
+
}
|
|
26530
|
+
return null;
|
|
25989
26531
|
}
|
|
25990
26532
|
// ── axes ──
|
|
25991
26533
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
@@ -26202,6 +26744,68 @@ function formatCountdown(ms) {
|
|
|
26202
26744
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
26203
26745
|
}
|
|
26204
26746
|
|
|
26747
|
+
// src/renderers/native/chrome/LabelTooltip.ts
|
|
26748
|
+
var HOVER_DELAY_MS = 350;
|
|
26749
|
+
var LabelTooltip = class {
|
|
26750
|
+
constructor(plot, deps) {
|
|
26751
|
+
this.plot = plot;
|
|
26752
|
+
this.deps = deps;
|
|
26753
|
+
this.tip = null;
|
|
26754
|
+
this.timer = null;
|
|
26755
|
+
/** Text of the currently open OR armed tip — dedupes moves inside one label. */
|
|
26756
|
+
this.current = null;
|
|
26757
|
+
this.onMove = (e) => {
|
|
26758
|
+
if (e.pointerType !== "mouse") return;
|
|
26759
|
+
const rect = this.plot.getBoundingClientRect();
|
|
26760
|
+
const x = e.clientX - rect.left;
|
|
26761
|
+
const y = e.clientY - rect.top;
|
|
26762
|
+
const text = this.deps.lookup(x, y);
|
|
26763
|
+
if (!text) {
|
|
26764
|
+
this.clear();
|
|
26765
|
+
return;
|
|
26766
|
+
}
|
|
26767
|
+
if (text === this.current) return;
|
|
26768
|
+
this.clear();
|
|
26769
|
+
this.current = text;
|
|
26770
|
+
this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
|
|
26771
|
+
};
|
|
26772
|
+
this.onLeave = () => {
|
|
26773
|
+
this.clear();
|
|
26774
|
+
};
|
|
26775
|
+
plot.addEventListener("pointermove", this.onMove);
|
|
26776
|
+
plot.addEventListener("pointerleave", this.onLeave);
|
|
26777
|
+
plot.addEventListener("pointerdown", this.onLeave);
|
|
26778
|
+
}
|
|
26779
|
+
destroy() {
|
|
26780
|
+
this.plot.removeEventListener("pointermove", this.onMove);
|
|
26781
|
+
this.plot.removeEventListener("pointerleave", this.onLeave);
|
|
26782
|
+
this.plot.removeEventListener("pointerdown", this.onLeave);
|
|
26783
|
+
this.clear();
|
|
26784
|
+
}
|
|
26785
|
+
show(text, x, y) {
|
|
26786
|
+
const doc = this.plot.ownerDocument;
|
|
26787
|
+
const tip = doc.createElement("div");
|
|
26788
|
+
tip.textContent = text;
|
|
26789
|
+
tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
|
|
26790
|
+
applyChromeTokens(tip, this.deps.theme());
|
|
26791
|
+
this.plot.appendChild(tip);
|
|
26792
|
+
const pw = this.plot.clientWidth;
|
|
26793
|
+
const ph = this.plot.clientHeight;
|
|
26794
|
+
tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
|
|
26795
|
+
tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
|
|
26796
|
+
this.tip = tip;
|
|
26797
|
+
}
|
|
26798
|
+
clear() {
|
|
26799
|
+
if (this.timer !== null) {
|
|
26800
|
+
clearTimeout(this.timer);
|
|
26801
|
+
this.timer = null;
|
|
26802
|
+
}
|
|
26803
|
+
this.tip?.remove();
|
|
26804
|
+
this.tip = null;
|
|
26805
|
+
this.current = null;
|
|
26806
|
+
}
|
|
26807
|
+
};
|
|
26808
|
+
|
|
26205
26809
|
// src/renderers/native/chrome/CrosshairRenderer.ts
|
|
26206
26810
|
var CrosshairRenderer = class {
|
|
26207
26811
|
constructor() {
|
|
@@ -31279,19 +31883,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31279
31883
|
for (const model of models) {
|
|
31280
31884
|
const off = offsetOf(model.id);
|
|
31281
31885
|
for (const s of model.series) {
|
|
31282
|
-
if (s.
|
|
31283
|
-
|
|
31284
|
-
const b = s.bars[i - off];
|
|
31285
|
-
if (b) {
|
|
31286
|
-
consider(b.high);
|
|
31287
|
-
consider(b.low);
|
|
31288
|
-
}
|
|
31289
|
-
}
|
|
31290
|
-
} else if (isLineLikeSeries(s)) {
|
|
31291
|
-
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
31292
|
-
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
31293
|
-
else if (s.style?.base != null) consider(s.style.base);
|
|
31294
|
-
}
|
|
31886
|
+
if (s.overlay === true) continue;
|
|
31887
|
+
considerSeries(s, i0, i1, off, consider);
|
|
31295
31888
|
}
|
|
31296
31889
|
for (const pl of model.priceLines) consider(pl.price);
|
|
31297
31890
|
}
|
|
@@ -31313,6 +31906,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31313
31906
|
const span = max - min;
|
|
31314
31907
|
return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
|
|
31315
31908
|
}
|
|
31909
|
+
function considerSeries(s, i0, i1, off, consider) {
|
|
31910
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
31911
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
31912
|
+
const b = s.bars[i - off];
|
|
31913
|
+
if (b) {
|
|
31914
|
+
consider(b.high);
|
|
31915
|
+
consider(b.low);
|
|
31916
|
+
}
|
|
31917
|
+
}
|
|
31918
|
+
} else if (isLineLikeSeries(s)) {
|
|
31919
|
+
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
31920
|
+
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
31921
|
+
else if (s.style?.base != null) consider(s.style.base);
|
|
31922
|
+
}
|
|
31923
|
+
}
|
|
31924
|
+
function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
|
|
31925
|
+
let min = Infinity;
|
|
31926
|
+
let max = -Infinity;
|
|
31927
|
+
const consider = (v) => {
|
|
31928
|
+
if (v != null && Number.isFinite(v)) {
|
|
31929
|
+
if (v < min) min = v;
|
|
31930
|
+
if (v > max) max = v;
|
|
31931
|
+
}
|
|
31932
|
+
};
|
|
31933
|
+
for (const model of models) {
|
|
31934
|
+
const off = offsetOf(model.id);
|
|
31935
|
+
for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
|
|
31936
|
+
}
|
|
31937
|
+
return min === Infinity ? null : { min, max };
|
|
31938
|
+
}
|
|
31316
31939
|
function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
31317
31940
|
if (abovePx <= 0 && belowPx <= 0) return scale;
|
|
31318
31941
|
const content = heightPx - abovePx - belowPx;
|
|
@@ -31367,6 +31990,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
31367
31990
|
}
|
|
31368
31991
|
|
|
31369
31992
|
// src/renderers/native/backdrop/BackdropRenderer.ts
|
|
31993
|
+
function clipHighlightRect(x1, x2, left, right) {
|
|
31994
|
+
const x = Math.max(left, x1);
|
|
31995
|
+
const end = Math.min(right, x2);
|
|
31996
|
+
return end > x ? { x, width: end - x } : null;
|
|
31997
|
+
}
|
|
31370
31998
|
var BackdropRenderer = class {
|
|
31371
31999
|
constructor() {
|
|
31372
32000
|
this.canvas = null;
|
|
@@ -31399,17 +32027,22 @@ var BackdropRenderer = class {
|
|
|
31399
32027
|
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
31400
32028
|
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
31401
32029
|
drawHighlights(ctx, scene, coords) {
|
|
31402
|
-
const
|
|
31403
|
-
if (
|
|
32030
|
+
const sessions = scene.sessionHighlightBands();
|
|
32031
|
+
if (sessions.length > 0) {
|
|
32032
|
+
const left = Math.max(0, coords.logicalToX(-0.5));
|
|
32033
|
+
const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
|
|
32034
|
+
this.drawHighlightSet(ctx, sessions, coords, left, right);
|
|
32035
|
+
}
|
|
32036
|
+
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
|
|
32037
|
+
}
|
|
32038
|
+
drawHighlightSet(ctx, bands, coords, left, right) {
|
|
31404
32039
|
for (const band of bands) {
|
|
31405
32040
|
const x1 = coords.timeToX(band.from);
|
|
31406
32041
|
const x2 = coords.timeToX(band.to);
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
31410
|
-
if (cw <= 0) continue;
|
|
32042
|
+
const rect = clipHighlightRect(x1, x2, left, right);
|
|
32043
|
+
if (!rect) continue;
|
|
31411
32044
|
ctx.fillStyle = band.color;
|
|
31412
|
-
ctx.fillRect(
|
|
32045
|
+
ctx.fillRect(rect.x, 0, rect.width, coords.height);
|
|
31413
32046
|
}
|
|
31414
32047
|
}
|
|
31415
32048
|
// ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
|
|
@@ -31951,6 +32584,8 @@ var NativeRenderer = class {
|
|
|
31951
32584
|
this.glowAmount = 0;
|
|
31952
32585
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
31953
32586
|
this.chrome = new ChromeRenderer();
|
|
32587
|
+
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32588
|
+
this.labelTooltip = null;
|
|
31954
32589
|
this.crosshairLayer = new CrosshairRenderer();
|
|
31955
32590
|
/** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
|
|
31956
32591
|
this.countdownTimer = null;
|
|
@@ -31961,6 +32596,8 @@ var NativeRenderer = class {
|
|
|
31961
32596
|
this.indicatorValuesOn = true;
|
|
31962
32597
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
31963
32598
|
this.legendActionsProvider = null;
|
|
32599
|
+
/** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
|
|
32600
|
+
this.legendCalloutsProvider = null;
|
|
31964
32601
|
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
31965
32602
|
this.legendOverviewAction = null;
|
|
31966
32603
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -33069,6 +33706,10 @@ var NativeRenderer = class {
|
|
|
33069
33706
|
this.plot.appendChild(this.scrollButton);
|
|
33070
33707
|
this.plot.addEventListener("pointermove", this.onScrollProximityMove);
|
|
33071
33708
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33709
|
+
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
33710
|
+
theme: () => this.chromeTheme(),
|
|
33711
|
+
lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
|
|
33712
|
+
});
|
|
33072
33713
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33073
33714
|
projector: () => this.drawingProjector(),
|
|
33074
33715
|
dpr: () => this.coords.dpr,
|
|
@@ -33108,9 +33749,10 @@ var NativeRenderer = class {
|
|
|
33108
33749
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
33109
33750
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
33110
33751
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
33752
|
+
this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
|
|
33111
33753
|
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
33112
33754
|
this.inputsUI.setOnChange((c) => {
|
|
33113
|
-
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
33755
|
+
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
|
|
33114
33756
|
});
|
|
33115
33757
|
this.inputsUI.setOnRemove((id) => {
|
|
33116
33758
|
for (const cb of this.removeIndicatorCbs) cb(id);
|
|
@@ -33300,6 +33942,8 @@ var NativeRenderer = class {
|
|
|
33300
33942
|
this.settingsButton = null;
|
|
33301
33943
|
this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
|
|
33302
33944
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33945
|
+
this.labelTooltip?.destroy();
|
|
33946
|
+
this.labelTooltip = null;
|
|
33303
33947
|
this.scrollButton?.remove();
|
|
33304
33948
|
this.scrollButton = null;
|
|
33305
33949
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -33540,7 +34184,7 @@ var NativeRenderer = class {
|
|
|
33540
34184
|
else this.scene.assignIndicatorZ(model.id);
|
|
33541
34185
|
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
33542
34186
|
native: !!model.native,
|
|
33543
|
-
...model.
|
|
34187
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
33544
34188
|
});
|
|
33545
34189
|
this.syncTables(model);
|
|
33546
34190
|
if (model.native?.type === "volume") {
|
|
@@ -33591,8 +34235,8 @@ var NativeRenderer = class {
|
|
|
33591
34235
|
this.paneControls?.refresh();
|
|
33592
34236
|
this.scheduler.invalidate(4 /* Full */);
|
|
33593
34237
|
}
|
|
33594
|
-
setIndicatorInputs(handle, values) {
|
|
33595
|
-
this.inputsUI.setValues(handle.id, values);
|
|
34238
|
+
setIndicatorInputs(handle, values, props) {
|
|
34239
|
+
this.inputsUI.setValues(handle.id, values, props);
|
|
33596
34240
|
}
|
|
33597
34241
|
setSymbolPicker(picker) {
|
|
33598
34242
|
this.symbolPicker = picker;
|
|
@@ -33602,6 +34246,10 @@ var NativeRenderer = class {
|
|
|
33602
34246
|
this.legendActionsProvider = provider;
|
|
33603
34247
|
this.inputsUI?.setLegendActions(provider);
|
|
33604
34248
|
}
|
|
34249
|
+
setLegendCallouts(provider) {
|
|
34250
|
+
this.legendCalloutsProvider = provider;
|
|
34251
|
+
this.inputsUI?.setLegendCallouts(provider);
|
|
34252
|
+
}
|
|
33605
34253
|
setLegendOverviewAction(action) {
|
|
33606
34254
|
this.legendOverviewAction = action;
|
|
33607
34255
|
this.inputsUI?.setLegendOverviewAction(action);
|
|
@@ -34582,7 +35230,11 @@ var NativeRenderer = class {
|
|
|
34582
35230
|
}
|
|
34583
35231
|
const models = this.scene.indicatorsForPane(pane.id);
|
|
34584
35232
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
34585
|
-
|
|
35233
|
+
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
35234
|
+
if (pane === pricePane) {
|
|
35235
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
|
|
35236
|
+
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
35237
|
+
}
|
|
34586
35238
|
const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
|
|
34587
35239
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34588
35240
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
@@ -34856,6 +35508,7 @@ var NativeRenderer = class {
|
|
|
34856
35508
|
for (const m of models) {
|
|
34857
35509
|
const off = this.scene.offsetOf(m.id);
|
|
34858
35510
|
for (const s of m.series) {
|
|
35511
|
+
if (s.overlay === true) continue;
|
|
34859
35512
|
if (isLineLikeSeries(s)) {
|
|
34860
35513
|
const v = s.points[i0 - off]?.value;
|
|
34861
35514
|
if (v != null && Number.isFinite(v)) return v;
|
|
@@ -35728,45 +36381,24 @@ var CSS21 = `
|
|
|
35728
36381
|
}
|
|
35729
36382
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
35730
36383
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
35731
|
-
/* Market status badge \u2014 icon-only 16px circle, label on hover
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
place-items: center;
|
|
35735
|
-
width: 16px;
|
|
35736
|
-
height: 16px;
|
|
35737
|
-
border-radius: 50%;
|
|
35738
|
-
flex: none;
|
|
35739
|
-
align-self: center;
|
|
35740
|
-
line-height: 0;
|
|
35741
|
-
cursor: default;
|
|
35742
|
-
}
|
|
35743
|
-
.vela-statusline .vela-sl-market svg {
|
|
35744
|
-
width: 12px;
|
|
35745
|
-
height: 12px;
|
|
35746
|
-
display: block;
|
|
35747
|
-
}
|
|
35748
|
-
/* Open wears the theme's up color; the other sessions are meaning constants from the
|
|
35749
|
-
* palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
|
|
35750
|
-
.vela-statusline .vela-sl-market[data-status='open'] {
|
|
35751
|
-
background: color-mix(in srgb, var(--vela-up) 20%, transparent);
|
|
35752
|
-
color: var(--vela-up);
|
|
35753
|
-
}
|
|
35754
|
-
.vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
|
|
35755
|
-
.vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
|
|
35756
|
-
.vela-statusline .vela-sl-market[data-status='closed'],
|
|
35757
|
-
.vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
|
|
36384
|
+
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
36385
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. */
|
|
36386
|
+
.vela-statusline .vela-sl-market { align-self: center; }
|
|
35758
36387
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
35759
36388
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
35760
36389
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
35761
36390
|
* these are the pre-ink fallbacks only. */
|
|
35762
36391
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
35763
36392
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
35764
|
-
/* Stack the
|
|
35765
|
-
* The renderer
|
|
35766
|
-
*
|
|
35767
|
-
*
|
|
35768
|
-
*
|
|
35769
|
-
|
|
36393
|
+
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36394
|
+
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36395
|
+
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
36396
|
+
* with the status line whichever pane owns the top. The renderer sets the legend's
|
|
36397
|
+
* inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
|
|
36398
|
+
* CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
|
|
36399
|
+
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
36400
|
+
* chart on the page, including statusline-less ones. */
|
|
36401
|
+
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35770
36402
|
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
35771
36403
|
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
35772
36404
|
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
@@ -35802,7 +36434,7 @@ var CSS21 = `
|
|
|
35802
36434
|
font-size: var(--vela-font-size-sm);
|
|
35803
36435
|
line-height: 1.2;
|
|
35804
36436
|
}
|
|
35805
|
-
[data-layout='mobile'] .vela-has-statusline [
|
|
36437
|
+
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
35806
36438
|
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
35807
36439
|
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
35808
36440
|
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
@@ -35819,7 +36451,7 @@ var CSS21 = `
|
|
|
35819
36451
|
padding-left: 0;
|
|
35820
36452
|
}
|
|
35821
36453
|
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
35822
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [
|
|
36454
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35823
36455
|
`;
|
|
35824
36456
|
function baseOfTicker(ticker) {
|
|
35825
36457
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -35831,6 +36463,13 @@ var MARKET_LABELS = {
|
|
|
35831
36463
|
closed: "Market Closed",
|
|
35832
36464
|
holiday: "Market Holiday"
|
|
35833
36465
|
};
|
|
36466
|
+
var MARKET_INKS = {
|
|
36467
|
+
open: "var(--vela-up)",
|
|
36468
|
+
pre: SESSION_PRE,
|
|
36469
|
+
post: SESSION_POST,
|
|
36470
|
+
closed: SESSION_OFF,
|
|
36471
|
+
holiday: SESSION_OFF
|
|
36472
|
+
};
|
|
35834
36473
|
function statuslineInkOf(renderer, priceStyle) {
|
|
35835
36474
|
const cfg = renderer.getConfig();
|
|
35836
36475
|
const c = (v) => typeof v === "string" ? v : null;
|
|
@@ -35889,8 +36528,15 @@ var Statusline = class {
|
|
|
35889
36528
|
this.symbolEl.textContent = ticker;
|
|
35890
36529
|
this.metaEl = doc.createElement("span");
|
|
35891
36530
|
this.metaEl.className = "vela-sl-meta";
|
|
35892
|
-
this.
|
|
35893
|
-
|
|
36531
|
+
this.marketBubble = new CalloutBubble({
|
|
36532
|
+
icon: "market-open",
|
|
36533
|
+
background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
|
|
36534
|
+
color: MARKET_INKS.open,
|
|
36535
|
+
label: MARKET_LABELS.open,
|
|
36536
|
+
host
|
|
36537
|
+
});
|
|
36538
|
+
this.marketEl = this.marketBubble.el;
|
|
36539
|
+
this.marketEl.classList.add("vela-sl-market");
|
|
35894
36540
|
this.ohlcEl = doc.createElement("span");
|
|
35895
36541
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
35896
36542
|
this.changeEl = doc.createElement("span");
|
|
@@ -35978,8 +36624,13 @@ var Statusline = class {
|
|
|
35978
36624
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
35979
36625
|
setMarketStatus(status) {
|
|
35980
36626
|
this.marketEl.dataset.status = status;
|
|
35981
|
-
|
|
35982
|
-
this.
|
|
36627
|
+
const ink = MARKET_INKS[status];
|
|
36628
|
+
this.marketBubble.set({
|
|
36629
|
+
icon: `market-${status}`,
|
|
36630
|
+
background: `color-mix(in srgb, ${ink} 20%, transparent)`,
|
|
36631
|
+
color: ink,
|
|
36632
|
+
label: MARKET_LABELS[status]
|
|
36633
|
+
});
|
|
35983
36634
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
35984
36635
|
}
|
|
35985
36636
|
setPartVisible(part, visible) {
|
|
@@ -36012,6 +36663,7 @@ var Statusline = class {
|
|
|
36012
36663
|
this.fitRO?.disconnect();
|
|
36013
36664
|
this.fitRO = null;
|
|
36014
36665
|
this.marketTip.destroy();
|
|
36666
|
+
this.marketBubble.destroy();
|
|
36015
36667
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
36016
36668
|
this.el.remove();
|
|
36017
36669
|
}
|
|
@@ -36151,61 +36803,148 @@ var MarketStatusTracker = class {
|
|
|
36151
36803
|
};
|
|
36152
36804
|
|
|
36153
36805
|
// src/widget/session-shading.ts
|
|
36154
|
-
|
|
36806
|
+
var DAY_MS2 = 864e5;
|
|
36807
|
+
var MINUTE_MS = 6e4;
|
|
36808
|
+
function parseWindow(text) {
|
|
36809
|
+
if (typeof text !== "string") return null;
|
|
36810
|
+
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
36811
|
+
if (!m) return null;
|
|
36812
|
+
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
36813
|
+
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
36814
|
+
if (start >= end || end > 1440) return null;
|
|
36815
|
+
return { start, end };
|
|
36816
|
+
}
|
|
36817
|
+
function parseSessionSpec(si) {
|
|
36818
|
+
const session = si?.["session"];
|
|
36819
|
+
if (typeof session !== "string" || session === "" || session === "24x7") return null;
|
|
36820
|
+
const regular = parseWindow(session);
|
|
36821
|
+
if (!regular) return null;
|
|
36822
|
+
const tz = si?.["timezone"];
|
|
36823
|
+
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
36824
|
+
const ext = parseWindow(si?.["session_extended"]);
|
|
36825
|
+
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
36826
|
+
return { regular, extended, timezone };
|
|
36827
|
+
}
|
|
36828
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
36829
|
+
function civilFormatter(tz) {
|
|
36830
|
+
const cached = dtfCache.get(tz);
|
|
36831
|
+
if (cached !== void 0) return cached;
|
|
36832
|
+
let dtf = null;
|
|
36833
|
+
try {
|
|
36834
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
36835
|
+
timeZone: tz,
|
|
36836
|
+
hourCycle: "h23",
|
|
36837
|
+
weekday: "short",
|
|
36838
|
+
year: "numeric",
|
|
36839
|
+
month: "2-digit",
|
|
36840
|
+
day: "2-digit",
|
|
36841
|
+
hour: "2-digit",
|
|
36842
|
+
minute: "2-digit"
|
|
36843
|
+
});
|
|
36844
|
+
} catch {
|
|
36845
|
+
dtf = null;
|
|
36846
|
+
}
|
|
36847
|
+
dtfCache.set(tz, dtf);
|
|
36848
|
+
return dtf;
|
|
36849
|
+
}
|
|
36850
|
+
function civilParts(dtf, ms) {
|
|
36851
|
+
const parts = dtf.formatToParts(ms);
|
|
36852
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
36853
|
+
return {
|
|
36854
|
+
weekday: get("weekday"),
|
|
36855
|
+
year: Number(get("year")),
|
|
36856
|
+
month: Number(get("month")),
|
|
36857
|
+
day: Number(get("day")),
|
|
36858
|
+
hour: Number(get("hour")) % 24,
|
|
36859
|
+
minute: Number(get("minute"))
|
|
36860
|
+
};
|
|
36861
|
+
}
|
|
36862
|
+
function expandSessionZones(spec, from, to) {
|
|
36155
36863
|
const pre = [];
|
|
36156
36864
|
const post = [];
|
|
36157
|
-
|
|
36158
|
-
|
|
36159
|
-
|
|
36160
|
-
|
|
36161
|
-
|
|
36162
|
-
|
|
36163
|
-
|
|
36164
|
-
|
|
36865
|
+
const dtf = civilFormatter(spec.timezone);
|
|
36866
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
36867
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36868
|
+
for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
|
|
36869
|
+
const civil = civilParts(dtf, cursor);
|
|
36870
|
+
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
36871
|
+
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
36872
|
+
seen.add(key);
|
|
36873
|
+
if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
|
|
36874
|
+
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
36875
|
+
const noonGuess = naive + 720 * MINUTE_MS;
|
|
36876
|
+
const atNoon = civilParts(dtf, noonGuess);
|
|
36877
|
+
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
36878
|
+
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
36879
|
+
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
36880
|
+
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
36165
36881
|
}
|
|
36166
36882
|
return { pre, post };
|
|
36167
36883
|
}
|
|
36168
|
-
var
|
|
36169
|
-
var MIN_LOOKBACK_MS = 3 * 864e5;
|
|
36170
|
-
var MAX_LOOKBACK_MS = 120 * 864e5;
|
|
36884
|
+
var COVER_PAD_MIN_MS = 2 * DAY_MS2;
|
|
36171
36885
|
var SessionShadingTracker = class {
|
|
36172
36886
|
constructor(onZones) {
|
|
36173
36887
|
this.onZones = onZones;
|
|
36174
|
-
/** Invalidates
|
|
36888
|
+
/** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
|
|
36175
36889
|
this.epoch = 0;
|
|
36176
|
-
|
|
36177
|
-
|
|
36890
|
+
this.spec = null;
|
|
36891
|
+
this.ready = false;
|
|
36892
|
+
this.session = "regular";
|
|
36893
|
+
/** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
|
|
36894
|
+
this.intraday = false;
|
|
36895
|
+
this.covered = null;
|
|
36896
|
+
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
36897
|
+
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
36898
|
+
this.lastRange = null;
|
|
36899
|
+
}
|
|
36900
|
+
/** (Re)bind to a chart's data surface + market and expand once metadata lands. */
|
|
36178
36901
|
track(data, symbol, opts) {
|
|
36179
36902
|
const my = ++this.epoch;
|
|
36180
|
-
|
|
36903
|
+
this.ready = false;
|
|
36904
|
+
this.spec = null;
|
|
36905
|
+
this.covered = null;
|
|
36906
|
+
this.session = opts.session;
|
|
36907
|
+
const tfMs = timeframeMs(opts.timeframe);
|
|
36908
|
+
this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
|
|
36909
|
+
this.lastRange = opts.range;
|
|
36910
|
+
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
36911
|
+
if (my !== this.epoch) return;
|
|
36912
|
+
this.ready = true;
|
|
36913
|
+
this.spec = parseSessionSpec(si);
|
|
36914
|
+
this.emit(this.lastRange ?? opts.range, true);
|
|
36915
|
+
});
|
|
36916
|
+
}
|
|
36917
|
+
/** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
|
|
36918
|
+
* leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
|
|
36919
|
+
updateRange(range) {
|
|
36920
|
+
this.lastRange = range;
|
|
36921
|
+
if (!this.ready) return;
|
|
36922
|
+
this.emit(range, false);
|
|
36181
36923
|
}
|
|
36182
36924
|
stop() {
|
|
36183
36925
|
this.epoch += 1;
|
|
36184
|
-
|
|
36185
|
-
|
|
36186
|
-
|
|
36187
|
-
|
|
36188
|
-
|
|
36189
|
-
|
|
36190
|
-
|
|
36191
|
-
|
|
36192
|
-
this.onZones(null);
|
|
36926
|
+
this.ready = false;
|
|
36927
|
+
this.spec = null;
|
|
36928
|
+
this.covered = null;
|
|
36929
|
+
this.lastRange = null;
|
|
36930
|
+
}
|
|
36931
|
+
emit(range, force) {
|
|
36932
|
+
if (!this.spec) {
|
|
36933
|
+
if (force) this.onZones(null);
|
|
36193
36934
|
return;
|
|
36194
36935
|
}
|
|
36195
|
-
if (
|
|
36196
|
-
this.onZones({ pre: [], post: [] });
|
|
36936
|
+
if (this.session !== "extended" || !this.intraday) {
|
|
36937
|
+
if (force) this.onZones({ pre: [], post: [] });
|
|
36197
36938
|
return;
|
|
36198
36939
|
}
|
|
36199
|
-
const
|
|
36200
|
-
const
|
|
36201
|
-
|
|
36202
|
-
|
|
36203
|
-
|
|
36204
|
-
|
|
36205
|
-
|
|
36206
|
-
|
|
36207
|
-
if (!regular || !extended) return;
|
|
36208
|
-
this.onZones(deriveSessionZones(regular, extended));
|
|
36940
|
+
const from = Math.min(range.from, range.to);
|
|
36941
|
+
const to = Math.max(range.from, range.to);
|
|
36942
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return;
|
|
36943
|
+
if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
|
|
36944
|
+
const pad = Math.max(to - from, COVER_PAD_MIN_MS);
|
|
36945
|
+
const covered = { from: from - pad, to: to + pad };
|
|
36946
|
+
this.covered = covered;
|
|
36947
|
+
this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
|
|
36209
36948
|
}
|
|
36210
36949
|
};
|
|
36211
36950
|
|
|
@@ -36609,6 +37348,7 @@ var ChartCell = class {
|
|
|
36609
37348
|
this.inner.renderer.set("attribution", false);
|
|
36610
37349
|
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
36611
37350
|
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
37351
|
+
this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
|
|
36612
37352
|
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
36613
37353
|
this.history.onChart(this.inner);
|
|
36614
37354
|
this.inner.renderer.onConfigChanged(() => {
|
|
@@ -36642,7 +37382,11 @@ var ChartCell = class {
|
|
|
36642
37382
|
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
36643
37383
|
});
|
|
36644
37384
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36645
|
-
this.inner.on("load:end", () =>
|
|
37385
|
+
this.inner.on("load:end", () => {
|
|
37386
|
+
this.watermark?.setLoading(false);
|
|
37387
|
+
this.refreshSessionShading();
|
|
37388
|
+
});
|
|
37389
|
+
this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
|
|
36646
37390
|
const tz = deps.timezone();
|
|
36647
37391
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
36648
37392
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
@@ -36721,20 +37465,30 @@ var ChartCell = class {
|
|
|
36721
37465
|
this.refreshNativeCatalog();
|
|
36722
37466
|
this.pushSettingsSections();
|
|
36723
37467
|
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
36724
|
-
this.
|
|
36725
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
36726
|
-
this.state.timeframe = timeframe;
|
|
36727
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
36728
|
-
this.watermark?.update(symbol2, timeframe);
|
|
36729
|
-
this.statusline?.setSymbol(symbol2);
|
|
36730
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
36731
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37468
|
+
this.projectMarket(symbol2, timeframe);
|
|
36732
37469
|
this.refreshNativeCatalog();
|
|
36733
37470
|
this.refreshSessionAvailable();
|
|
36734
37471
|
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
36735
37472
|
this.deps.onMarketChanged(this.id);
|
|
36736
37473
|
});
|
|
36737
37474
|
}
|
|
37475
|
+
/**
|
|
37476
|
+
* Project a market identity into the cell state and its display overlays (watermark,
|
|
37477
|
+
* statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
|
|
37478
|
+
* optimistically from the cell setters — the labels reflect the pick immediately, not
|
|
37479
|
+
* after the bars load — and again from `market:changed` (the committed pass, and the
|
|
37480
|
+
* only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
|
|
37481
|
+
*/
|
|
37482
|
+
projectMarket(symbol, timeframe) {
|
|
37483
|
+
this.state.symbol = symbol;
|
|
37484
|
+
this.state.provider = parseSymbol(symbol).provider ?? void 0;
|
|
37485
|
+
this.state.timeframe = timeframe;
|
|
37486
|
+
this.state.session = normalizeSession(this.inner?.market.session);
|
|
37487
|
+
this.watermark?.update(symbol, timeframe);
|
|
37488
|
+
this.statusline?.setSymbol(symbol);
|
|
37489
|
+
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
|
|
37490
|
+
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37491
|
+
}
|
|
36738
37492
|
/**
|
|
36739
37493
|
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
36740
37494
|
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
@@ -36769,14 +37523,18 @@ var ChartCell = class {
|
|
|
36769
37523
|
this.refreshSessionShading();
|
|
36770
37524
|
});
|
|
36771
37525
|
}
|
|
36772
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The
|
|
36773
|
-
*
|
|
37526
|
+
/** (Re)derive the pre/post-market shading bands for this cell's market. The bands
|
|
37527
|
+
* expand locally from the symbol's session vocabulary, so they paint as soon as
|
|
37528
|
+
* metadata is known and follow any pan depth without provider round trips. */
|
|
36774
37529
|
refreshSessionShading() {
|
|
36775
37530
|
const chart = this.inner;
|
|
36776
37531
|
const symbol = this.state.symbol;
|
|
36777
37532
|
if (!chart || !symbol) return;
|
|
36778
|
-
const
|
|
36779
|
-
this.
|
|
37533
|
+
const now = Date.now();
|
|
37534
|
+
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
37535
|
+
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
37536
|
+
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
37537
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
36780
37538
|
}
|
|
36781
37539
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
36782
37540
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -36872,10 +37630,10 @@ var ChartCell = class {
|
|
|
36872
37630
|
id: "status-line",
|
|
36873
37631
|
rows: [
|
|
36874
37632
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
36875
|
-
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) =>
|
|
36876
|
-
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) =>
|
|
36877
|
-
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) =>
|
|
36878
|
-
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) =>
|
|
37633
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
|
|
37634
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
|
|
37635
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
|
|
37636
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
|
|
36879
37637
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
36880
37638
|
{
|
|
36881
37639
|
kind: "toggle",
|
|
@@ -36910,12 +37668,40 @@ var ChartCell = class {
|
|
|
36910
37668
|
this.indicatorTitlesOn = visible;
|
|
36911
37669
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
36912
37670
|
this.deps.onStateDirty();
|
|
37671
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
36913
37672
|
}
|
|
36914
37673
|
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
36915
37674
|
setIndicatorValuesVisible(visible) {
|
|
36916
37675
|
this.indicatorValuesOn = visible;
|
|
36917
37676
|
this.inner?.renderer.set("indicatorValues", visible);
|
|
36918
37677
|
this.deps.onStateDirty();
|
|
37678
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37679
|
+
}
|
|
37680
|
+
/** Show/hide one status-line segment (the settings dialog's Status line tab). */
|
|
37681
|
+
setStatuslinePart(part, visible) {
|
|
37682
|
+
this.statusline?.setPartVisible(part, visible);
|
|
37683
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37684
|
+
}
|
|
37685
|
+
/** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
|
|
37686
|
+
statusPrefs() {
|
|
37687
|
+
const sl = this.statusline;
|
|
37688
|
+
return {
|
|
37689
|
+
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
37690
|
+
indicatorTitles: this.indicatorTitlesOn,
|
|
37691
|
+
indicatorValues: this.indicatorValuesOn
|
|
37692
|
+
};
|
|
37693
|
+
}
|
|
37694
|
+
/** Converge this cell's Status line tab prefs to `prefs` — the follower half of
|
|
37695
|
+
* the workspace's style link. Idempotent: matching values change nothing, so a
|
|
37696
|
+
* propagated echo dies on its own. */
|
|
37697
|
+
applyStatusPrefs(prefs) {
|
|
37698
|
+
if (prefs.parts && this.statusline) {
|
|
37699
|
+
for (const part of Object.keys(prefs.parts)) {
|
|
37700
|
+
if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
|
|
37701
|
+
}
|
|
37702
|
+
}
|
|
37703
|
+
if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
|
|
37704
|
+
if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
|
|
36919
37705
|
}
|
|
36920
37706
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
36921
37707
|
* identity is what endures; the chart dies with the cell). */
|
|
@@ -36937,17 +37723,23 @@ var ChartCell = class {
|
|
|
36937
37723
|
get indicatorCount() {
|
|
36938
37724
|
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
36939
37725
|
}
|
|
36940
|
-
/** Switch this cell's market in place (the chart instance survives).
|
|
37726
|
+
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
37727
|
+
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
37728
|
+
* the setMarket call so the statusline reads the already-blanked chart. */
|
|
36941
37729
|
setSymbol(symbol) {
|
|
36942
37730
|
if (!this.inner || symbol === this.symbol) return;
|
|
36943
37731
|
this.unresolvedToasted = null;
|
|
36944
37732
|
void this.inner.setMarket({ symbol });
|
|
37733
|
+
this.projectMarket(symbol, this.timeframe);
|
|
37734
|
+
this.deps.onMarketChanged(this.id);
|
|
36945
37735
|
}
|
|
36946
37736
|
setTimeframe(timeframe) {
|
|
36947
37737
|
if (!this.inner || timeframe === this.timeframe) return;
|
|
36948
37738
|
this.activeRangeId = null;
|
|
36949
37739
|
this.rangeBars = 0;
|
|
36950
37740
|
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
37741
|
+
this.projectMarket(this.symbol, timeframe);
|
|
37742
|
+
this.deps.onMarketChanged(this.id);
|
|
36951
37743
|
}
|
|
36952
37744
|
/** Applied live (renderer feature) — no reload. */
|
|
36953
37745
|
setPriceStyle(style) {
|
|
@@ -37322,16 +38114,21 @@ var ChartCell = class {
|
|
|
37322
38114
|
|
|
37323
38115
|
// src/workspace/context.ts
|
|
37324
38116
|
function buildContext(host) {
|
|
37325
|
-
const active = host.active();
|
|
37326
38117
|
return {
|
|
37327
38118
|
get chart() {
|
|
37328
38119
|
const cell = host.active();
|
|
37329
38120
|
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
37330
38121
|
return cell.chart;
|
|
37331
38122
|
},
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
38123
|
+
get symbol() {
|
|
38124
|
+
return host.active()?.symbol ?? "";
|
|
38125
|
+
},
|
|
38126
|
+
get timeframe() {
|
|
38127
|
+
return host.active()?.timeframe ?? "60";
|
|
38128
|
+
},
|
|
38129
|
+
get priceStyle() {
|
|
38130
|
+
return host.active()?.priceStyle ?? "candles";
|
|
38131
|
+
},
|
|
37335
38132
|
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
37336
38133
|
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
37337
38134
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
@@ -37342,8 +38139,12 @@ function buildContext(host) {
|
|
|
37342
38139
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
37343
38140
|
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
37344
38141
|
stateChanged: () => host.stateDirty(),
|
|
37345
|
-
|
|
37346
|
-
|
|
38142
|
+
get cells() {
|
|
38143
|
+
return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
|
|
38144
|
+
},
|
|
38145
|
+
get activeCellId() {
|
|
38146
|
+
return host.active()?.id ?? "";
|
|
38147
|
+
},
|
|
37347
38148
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
37348
38149
|
};
|
|
37349
38150
|
}
|
|
@@ -37676,6 +38477,10 @@ var VelaWorkspace = class {
|
|
|
37676
38477
|
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
37677
38478
|
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
37678
38479
|
this.drawingSyncBusy = false;
|
|
38480
|
+
/** Same guard for the style link: a follower's `applyConfig` re-fires its
|
|
38481
|
+
* `onConfigChanged` in the same tick, and a state restore applies per-cell
|
|
38482
|
+
* configs that legitimately differ — neither must propagate. */
|
|
38483
|
+
this.styleSyncBusy = false;
|
|
37679
38484
|
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
37680
38485
|
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
37681
38486
|
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
@@ -37736,9 +38541,7 @@ var VelaWorkspace = class {
|
|
|
37736
38541
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
37737
38542
|
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
37738
38543
|
const sync = boot?.sync ?? opts.sync;
|
|
37739
|
-
for (const kind of
|
|
37740
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
37741
|
-
}
|
|
38544
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
|
|
37742
38545
|
this.monoLayout = opts.layout === false;
|
|
37743
38546
|
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
37744
38547
|
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
@@ -37822,7 +38625,8 @@ var VelaWorkspace = class {
|
|
|
37822
38625
|
syncs: () => [
|
|
37823
38626
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
37824
38627
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
37825
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
38628
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
38629
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
37826
38630
|
],
|
|
37827
38631
|
onToggleSync: (id) => {
|
|
37828
38632
|
const kind = id;
|
|
@@ -38041,7 +38845,10 @@ var VelaWorkspace = class {
|
|
|
38041
38845
|
this.topbar.renderActions();
|
|
38042
38846
|
this.mobileBar?.renderActions();
|
|
38043
38847
|
this.dock.refresh();
|
|
38044
|
-
for (const cell of this.cells())
|
|
38848
|
+
for (const cell of this.cells()) {
|
|
38849
|
+
cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
38850
|
+
cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
|
|
38851
|
+
}
|
|
38045
38852
|
}
|
|
38046
38853
|
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
38047
38854
|
/**
|
|
@@ -38106,7 +38913,7 @@ var VelaWorkspace = class {
|
|
|
38106
38913
|
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
38107
38914
|
}
|
|
38108
38915
|
this.dock.applyState(st.panels);
|
|
38109
|
-
for (const kind of
|
|
38916
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
38110
38917
|
this.trackSizes.clear();
|
|
38111
38918
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
38112
38919
|
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
@@ -38117,9 +38924,14 @@ var VelaWorkspace = class {
|
|
|
38117
38924
|
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
38118
38925
|
}
|
|
38119
38926
|
this.drawingLinks.clear();
|
|
38120
|
-
|
|
38121
|
-
|
|
38122
|
-
this.
|
|
38927
|
+
this.styleSyncBusy = true;
|
|
38928
|
+
try {
|
|
38929
|
+
for (const [i] of this.def.cells.entries()) {
|
|
38930
|
+
const { id, ...cs } = st.charts[i];
|
|
38931
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
38932
|
+
}
|
|
38933
|
+
} finally {
|
|
38934
|
+
this.styleSyncBusy = false;
|
|
38123
38935
|
}
|
|
38124
38936
|
if (st.timezone) this.setTimezone(st.timezone);
|
|
38125
38937
|
this.pool.clear();
|
|
@@ -38219,6 +39031,7 @@ var VelaWorkspace = class {
|
|
|
38219
39031
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
38220
39032
|
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
38221
39033
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
39034
|
+
const preexisting = new Set(this.cellsById.keys());
|
|
38222
39035
|
for (const [id, cell] of [...this.cellsById]) {
|
|
38223
39036
|
if (!keep.has(id) || rebuildAll) {
|
|
38224
39037
|
this.poolSet(id, cell.dehydrate());
|
|
@@ -38231,6 +39044,7 @@ var VelaWorkspace = class {
|
|
|
38231
39044
|
this.cellBackend = nextBackend;
|
|
38232
39045
|
this.applyGrid();
|
|
38233
39046
|
this.buildCells();
|
|
39047
|
+
this.alignNewCellStyles(preexisting);
|
|
38234
39048
|
this.syncCellPresentation();
|
|
38235
39049
|
this.topbar.setLayout(next.id);
|
|
38236
39050
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
@@ -38434,6 +39248,7 @@ var VelaWorkspace = class {
|
|
|
38434
39248
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
38435
39249
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
38436
39250
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
39251
|
+
onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
|
|
38437
39252
|
onStateDirty: () => this.markStateDirty(),
|
|
38438
39253
|
manifestSettled: () => this.manifestSettled,
|
|
38439
39254
|
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
@@ -38509,6 +39324,7 @@ var VelaWorkspace = class {
|
|
|
38509
39324
|
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
38510
39325
|
});
|
|
38511
39326
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
39327
|
+
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
38512
39328
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
38513
39329
|
chart.renderer.onAxisLongPress((e) => {
|
|
38514
39330
|
if (this.layoutCtl.current !== "mobile") return;
|
|
@@ -38541,11 +39357,67 @@ var VelaWorkspace = class {
|
|
|
38541
39357
|
if (kind === "viewport") {
|
|
38542
39358
|
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
38543
39359
|
if (range) this.propagateViewport(this.activeId, range);
|
|
39360
|
+
} else if (kind === "style") {
|
|
39361
|
+
this.propagateStylePrefs(this.activeId);
|
|
38544
39362
|
} else {
|
|
38545
39363
|
this.propagateMarket(this.activeId);
|
|
38546
39364
|
}
|
|
38547
39365
|
}
|
|
38548
39366
|
}
|
|
39367
|
+
/**
|
|
39368
|
+
* Align cells minted by a layout change to their style group: with the link on, a
|
|
39369
|
+
* NEW cell (fresh slot or one returning from the pool, which missed edits while
|
|
39370
|
+
* dormant) inherits the presentation of a pre-existing group peer — the active
|
|
39371
|
+
* cell when it is one — instead of sitting on its own state beside a styled
|
|
39372
|
+
* group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
|
|
39373
|
+
* the group, and the equality short-circuits keep converged peers untouched.
|
|
39374
|
+
*/
|
|
39375
|
+
alignNewCellStyles(preexisting) {
|
|
39376
|
+
const setting = this.syncOpts.style;
|
|
39377
|
+
if (!setting) return;
|
|
39378
|
+
const ids = [...this.cellsById.keys()];
|
|
39379
|
+
const propagated = /* @__PURE__ */ new Set();
|
|
39380
|
+
for (const id of ids) {
|
|
39381
|
+
if (preexisting.has(id)) continue;
|
|
39382
|
+
const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
|
|
39383
|
+
if (peers.length === 0) continue;
|
|
39384
|
+
const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
|
|
39385
|
+
if (propagated.has(source)) continue;
|
|
39386
|
+
propagated.add(source);
|
|
39387
|
+
this.propagateStylePrefs(source);
|
|
39388
|
+
}
|
|
39389
|
+
}
|
|
39390
|
+
/**
|
|
39391
|
+
* Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
|
|
39392
|
+
* its renderer config plus its Status line tab prefs — onto its same-group
|
|
39393
|
+
* followers (the style link). Loop-safe two ways: the busy guard eats the
|
|
39394
|
+
* followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
|
|
39395
|
+
* in the same tick), and the equality short-circuits leave already-converged
|
|
39396
|
+
* followers untouched, so nothing re-emits once the group agrees.
|
|
39397
|
+
*/
|
|
39398
|
+
propagateStylePrefs(originId) {
|
|
39399
|
+
if (this.styleSyncBusy || this.destroyed) return;
|
|
39400
|
+
const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
|
|
39401
|
+
if (targets.length === 0) return;
|
|
39402
|
+
const origin = this.cellsById.get(originId);
|
|
39403
|
+
if (!origin) return;
|
|
39404
|
+
const slice = styleConfigSlice(origin.chart.renderer.getConfig());
|
|
39405
|
+
const sliceJson = slice ? JSON.stringify(slice) : null;
|
|
39406
|
+
const prefs = origin.statusPrefs();
|
|
39407
|
+
this.styleSyncBusy = true;
|
|
39408
|
+
try {
|
|
39409
|
+
for (const id of targets) {
|
|
39410
|
+
const cell = this.cellsById.get(id);
|
|
39411
|
+
if (!cell) continue;
|
|
39412
|
+
if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
|
|
39413
|
+
cell.chart.renderer.applyConfig(slice);
|
|
39414
|
+
}
|
|
39415
|
+
cell.applyStatusPrefs(prefs);
|
|
39416
|
+
}
|
|
39417
|
+
} finally {
|
|
39418
|
+
this.styleSyncBusy = false;
|
|
39419
|
+
}
|
|
39420
|
+
}
|
|
38549
39421
|
/**
|
|
38550
39422
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
38551
39423
|
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
@@ -38853,7 +39725,8 @@ var VelaWorkspace = class {
|
|
|
38853
39725
|
syncs: () => [
|
|
38854
39726
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
38855
39727
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
38856
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
39728
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
39729
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
38857
39730
|
],
|
|
38858
39731
|
onToggleSync: (id) => {
|
|
38859
39732
|
const kind = id;
|