@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/workspace.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 = `
|
|
@@ -4642,6 +4828,44 @@ function legendActionsProviderFor(chart, context) {
|
|
|
4642
4828
|
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) }));
|
|
4643
4829
|
};
|
|
4644
4830
|
}
|
|
4831
|
+
var calloutRegistry = /* @__PURE__ */ new Map();
|
|
4832
|
+
function legendCallouts() {
|
|
4833
|
+
return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
4834
|
+
}
|
|
4835
|
+
function legendCalloutsProviderFor(chart, context) {
|
|
4836
|
+
return (indicatorId) => {
|
|
4837
|
+
const handle = chart.indicators().find((h) => h.id === indicatorId);
|
|
4838
|
+
if (!handle) return [];
|
|
4839
|
+
const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
|
|
4840
|
+
const views = [];
|
|
4841
|
+
for (const d of legendCallouts()) {
|
|
4842
|
+
const spec = d.callout(info);
|
|
4843
|
+
if (!spec) continue;
|
|
4844
|
+
views.push({
|
|
4845
|
+
id: d.id,
|
|
4846
|
+
icon: spec.icon,
|
|
4847
|
+
background: spec.background,
|
|
4848
|
+
...spec.color !== void 0 ? { color: spec.color } : {},
|
|
4849
|
+
tooltip: spec.tooltip,
|
|
4850
|
+
...spec.content !== void 0 ? {
|
|
4851
|
+
content: {
|
|
4852
|
+
...spec.content.title !== void 0 ? { title: spec.content.title } : {},
|
|
4853
|
+
items: spec.content.items.map(
|
|
4854
|
+
(item) => item.type === "text" ? item : {
|
|
4855
|
+
type: "button",
|
|
4856
|
+
label: item.label,
|
|
4857
|
+
...item.primary !== void 0 ? { primary: item.primary } : {},
|
|
4858
|
+
...item.close !== void 0 ? { close: item.close } : {},
|
|
4859
|
+
run: () => item.run(context(), info)
|
|
4860
|
+
}
|
|
4861
|
+
)
|
|
4862
|
+
}
|
|
4863
|
+
} : {}
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
return views;
|
|
4867
|
+
};
|
|
4868
|
+
}
|
|
4645
4869
|
var stateHandlers = /* @__PURE__ */ new Map();
|
|
4646
4870
|
function statePersistenceHandlers(scope) {
|
|
4647
4871
|
return [...stateHandlers.values()].filter((h) => h.scope === scope);
|
|
@@ -5200,6 +5424,11 @@ var Topbar = class {
|
|
|
5200
5424
|
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
5201
5425
|
* render through the flow cluster like ordinary actions. */
|
|
5202
5426
|
this.flowingOverrides = /* @__PURE__ */ new Set();
|
|
5427
|
+
/** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
|
|
5428
|
+
* renderActions pass (contributed buttons are replaceChildren'd away). */
|
|
5429
|
+
this.actionTooltips = [];
|
|
5430
|
+
/** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
|
|
5431
|
+
this.warnedIconless = /* @__PURE__ */ new Set();
|
|
5203
5432
|
this.onHairlineSync = () => {
|
|
5204
5433
|
if (this.hairlineRaf) return;
|
|
5205
5434
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -5462,6 +5691,8 @@ var Topbar = class {
|
|
|
5462
5691
|
this.actionsHost.replaceChildren();
|
|
5463
5692
|
this.leftActionsHost.replaceChildren();
|
|
5464
5693
|
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
5694
|
+
for (const t of this.actionTooltips) t.destroy();
|
|
5695
|
+
this.actionTooltips = [];
|
|
5465
5696
|
const doc = this.actionsHost.ownerDocument;
|
|
5466
5697
|
const flowLeft = this.comp.left.includes("actions");
|
|
5467
5698
|
const flowRight = this.comp.right.includes("actions");
|
|
@@ -5471,10 +5702,20 @@ var Topbar = class {
|
|
|
5471
5702
|
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
5472
5703
|
const left = pin ? pin.left : action.align === "left";
|
|
5473
5704
|
if (!pin && !(left ? flowLeft : flowRight)) continue;
|
|
5705
|
+
const iconOnly = action.iconOnly === true && !!action.icon;
|
|
5706
|
+
if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
|
|
5707
|
+
this.warnedIconless.add(action.id);
|
|
5708
|
+
console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
|
|
5709
|
+
}
|
|
5474
5710
|
const b = doc.createElement("button");
|
|
5475
|
-
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
5711
|
+
b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
|
|
5476
5712
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
5477
|
-
|
|
5713
|
+
if (iconOnly) {
|
|
5714
|
+
b.setAttribute("aria-label", action.label);
|
|
5715
|
+
this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
|
|
5716
|
+
} else {
|
|
5717
|
+
b.appendChild(doc.createTextNode(action.label));
|
|
5718
|
+
}
|
|
5478
5719
|
b.addEventListener("click", () => {
|
|
5479
5720
|
const c = this.opts.getContext?.();
|
|
5480
5721
|
if (c) action.run(c);
|
|
@@ -5525,7 +5766,7 @@ var Topbar = class {
|
|
|
5525
5766
|
this.tfMenu.destroy();
|
|
5526
5767
|
this.styleMenu.destroy();
|
|
5527
5768
|
this.layoutPicker?.destroy();
|
|
5528
|
-
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
5769
|
+
for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
|
|
5529
5770
|
this.el.remove();
|
|
5530
5771
|
}
|
|
5531
5772
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
@@ -17233,19 +17474,8 @@ function registerBuiltinChartTypes() {
|
|
|
17233
17474
|
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
17234
17475
|
}
|
|
17235
17476
|
|
|
17236
|
-
// src/workspace/sync.ts
|
|
17237
|
-
function syncTargets(originId, setting, cellIds) {
|
|
17238
|
-
if (setting == null || setting === false) return [];
|
|
17239
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17240
|
-
const group = setting[originId];
|
|
17241
|
-
if (group == null) return [];
|
|
17242
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17243
|
-
}
|
|
17244
|
-
function rangesWithin(a, b, epsMs) {
|
|
17245
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17246
|
-
}
|
|
17247
|
-
|
|
17248
17477
|
// src/state/document.ts
|
|
17478
|
+
var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
|
|
17249
17479
|
function prefixedSymbol(cell) {
|
|
17250
17480
|
if (!cell?.symbol) return void 0;
|
|
17251
17481
|
if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
|
|
@@ -17333,7 +17563,7 @@ function sanitizeSync(raw) {
|
|
|
17333
17563
|
if (raw == null || typeof raw !== "object") return null;
|
|
17334
17564
|
const s = raw;
|
|
17335
17565
|
const out = {};
|
|
17336
|
-
for (const kind of
|
|
17566
|
+
for (const kind of SYNC_KINDS) {
|
|
17337
17567
|
const v = s[kind];
|
|
17338
17568
|
if (v === true) out[kind] = true;
|
|
17339
17569
|
else if (v != null && typeof v === "object") {
|
|
@@ -17376,6 +17606,28 @@ function sanitizeTrackSizes(raw) {
|
|
|
17376
17606
|
return Object.keys(out).length > 0 ? out : null;
|
|
17377
17607
|
}
|
|
17378
17608
|
|
|
17609
|
+
// src/workspace/sync.ts
|
|
17610
|
+
function syncTargets(originId, setting, cellIds) {
|
|
17611
|
+
if (setting == null || setting === false) return [];
|
|
17612
|
+
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17613
|
+
const group = setting[originId];
|
|
17614
|
+
if (group == null) return [];
|
|
17615
|
+
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17616
|
+
}
|
|
17617
|
+
function rangesWithin(a, b, epsMs) {
|
|
17618
|
+
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17619
|
+
}
|
|
17620
|
+
var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
|
|
17621
|
+
function styleConfigSlice(config) {
|
|
17622
|
+
if (config == null || typeof config !== "object") return null;
|
|
17623
|
+
const doc = config;
|
|
17624
|
+
const out = {};
|
|
17625
|
+
for (const key of STYLE_SYNC_CONFIG_KEYS) {
|
|
17626
|
+
if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
|
|
17627
|
+
}
|
|
17628
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
17629
|
+
}
|
|
17630
|
+
|
|
17379
17631
|
// src/workspace/persist.ts
|
|
17380
17632
|
var memoryStore = /* @__PURE__ */ new Map();
|
|
17381
17633
|
function memoryStorageAdapter() {
|
|
@@ -17482,6 +17734,7 @@ var IndicatorHandleImpl = class {
|
|
|
17482
17734
|
constructor(id, title, controller, source) {
|
|
17483
17735
|
this.controller = controller;
|
|
17484
17736
|
this.schema = [];
|
|
17737
|
+
this.propsSchema = [];
|
|
17485
17738
|
this.visibleState = true;
|
|
17486
17739
|
this.bus = new TypedEventBus();
|
|
17487
17740
|
this.id = id;
|
|
@@ -17491,6 +17744,9 @@ var IndicatorHandleImpl = class {
|
|
|
17491
17744
|
get inputs() {
|
|
17492
17745
|
return this.schema;
|
|
17493
17746
|
}
|
|
17747
|
+
get props() {
|
|
17748
|
+
return this.propsSchema;
|
|
17749
|
+
}
|
|
17494
17750
|
get visible() {
|
|
17495
17751
|
return this.visibleState;
|
|
17496
17752
|
}
|
|
@@ -17500,6 +17756,12 @@ var IndicatorHandleImpl = class {
|
|
|
17500
17756
|
setInputs(values) {
|
|
17501
17757
|
this.controller.applyInputs(this.id, values);
|
|
17502
17758
|
}
|
|
17759
|
+
setProp(key, value) {
|
|
17760
|
+
this.controller.applyProps(this.id, { [key]: value });
|
|
17761
|
+
}
|
|
17762
|
+
setProps(values) {
|
|
17763
|
+
this.controller.applyProps(this.id, values);
|
|
17764
|
+
}
|
|
17503
17765
|
setVisible(visible) {
|
|
17504
17766
|
this.controller.setVisible(this.id, visible);
|
|
17505
17767
|
}
|
|
@@ -17519,6 +17781,9 @@ var IndicatorHandleImpl = class {
|
|
|
17519
17781
|
setSchema(schema) {
|
|
17520
17782
|
this.schema = schema;
|
|
17521
17783
|
}
|
|
17784
|
+
setPropsSchema(schema) {
|
|
17785
|
+
this.propsSchema = schema;
|
|
17786
|
+
}
|
|
17522
17787
|
/** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
|
|
17523
17788
|
setVisibleState(visible) {
|
|
17524
17789
|
this.visibleState = visible;
|
|
@@ -17532,6 +17797,8 @@ var IndicatorHandleImpl = class {
|
|
|
17532
17797
|
function summarizeModel(model) {
|
|
17533
17798
|
const series = {};
|
|
17534
17799
|
for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
|
|
17800
|
+
const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
|
|
17801
|
+
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);
|
|
17535
17802
|
return {
|
|
17536
17803
|
id: model.id,
|
|
17537
17804
|
title: model.title,
|
|
@@ -17551,7 +17818,8 @@ function summarizeModel(model) {
|
|
|
17551
17818
|
tables: model.tables?.length ?? 0,
|
|
17552
17819
|
barColors: model.barColors?.length ?? 0,
|
|
17553
17820
|
trades: model.trades?.length ?? 0,
|
|
17554
|
-
inputs: model.inputs.length
|
|
17821
|
+
inputs: model.inputs.length,
|
|
17822
|
+
forcedOverlay
|
|
17555
17823
|
};
|
|
17556
17824
|
}
|
|
17557
17825
|
function inspectModels(models) {
|
|
@@ -17572,7 +17840,8 @@ function inspectModels(models) {
|
|
|
17572
17840
|
linefills: sum((s) => s.linefills),
|
|
17573
17841
|
tables: sum((s) => s.tables),
|
|
17574
17842
|
barColors: sum((s) => s.barColors),
|
|
17575
|
-
trades: sum((s) => s.trades)
|
|
17843
|
+
trades: sum((s) => s.trades),
|
|
17844
|
+
forcedOverlay: sum((s) => s.forcedOverlay)
|
|
17576
17845
|
}
|
|
17577
17846
|
};
|
|
17578
17847
|
}
|
|
@@ -17716,7 +17985,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17716
17985
|
for (const engine of engines) this.registerEngine(engine.language, engine);
|
|
17717
17986
|
this.defaultLanguage = config.defaultLanguage;
|
|
17718
17987
|
this.renderer.mount(container, config.theme);
|
|
17719
|
-
this.renderer.onInputChange(
|
|
17988
|
+
this.renderer.onInputChange(
|
|
17989
|
+
(e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
|
|
17990
|
+
);
|
|
17720
17991
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17721
17992
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17722
17993
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
@@ -18396,7 +18667,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18396
18667
|
const title = options.title ?? "Indicator";
|
|
18397
18668
|
const handle = new IndicatorHandleImpl(id, title, this, source);
|
|
18398
18669
|
this.handles.set(id, handle);
|
|
18399
|
-
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
|
|
18670
|
+
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
|
|
18400
18671
|
void this.startIndicator(id, source, options, handle);
|
|
18401
18672
|
return handle;
|
|
18402
18673
|
}
|
|
@@ -18419,7 +18690,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18419
18690
|
return handle;
|
|
18420
18691
|
}
|
|
18421
18692
|
const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
|
|
18422
|
-
this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
|
|
18693
|
+
this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
|
|
18423
18694
|
handle.setSchema(descriptor.inputsSchema());
|
|
18424
18695
|
void this.startNativeIndicator(id, handle);
|
|
18425
18696
|
return handle;
|
|
@@ -18472,6 +18743,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18472
18743
|
if (record.session) record.session.update(record.inputValues);
|
|
18473
18744
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
18474
18745
|
}
|
|
18746
|
+
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
18747
|
+
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
18748
|
+
* Script indicators only: natives have no declaration props. */
|
|
18749
|
+
applyProps(id, values) {
|
|
18750
|
+
const record = this.registry.get(id);
|
|
18751
|
+
if (!record?.session) return;
|
|
18752
|
+
record.propValues = { ...record.propValues, ...values };
|
|
18753
|
+
if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
18754
|
+
record.pendingStructural = true;
|
|
18755
|
+
if (!record.hidden) this.setLoading(record, true);
|
|
18756
|
+
record.pendingCause = "inputs";
|
|
18757
|
+
record.session.update(record.inputValues, record.propValues);
|
|
18758
|
+
}
|
|
18475
18759
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18476
18760
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
18477
18761
|
listIndicators() {
|
|
@@ -18620,6 +18904,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18620
18904
|
for (const input of prepared.inputs) defaults2[input.key] = input.defval;
|
|
18621
18905
|
record.inputValues = { ...defaults2, ...record.inputValues };
|
|
18622
18906
|
handle.setSchema(prepared.inputs);
|
|
18907
|
+
const propDefaults = {};
|
|
18908
|
+
for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
|
|
18909
|
+
record.propValues = { ...propDefaults, ...record.propValues };
|
|
18910
|
+
handle.setPropsSchema(prepared.props ?? []);
|
|
18623
18911
|
this.mountLoadingPlaceholder(id, record);
|
|
18624
18912
|
this.executeIndicator(id, handle);
|
|
18625
18913
|
} catch (err) {
|
|
@@ -18643,6 +18931,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18643
18931
|
getBars: () => this.bars,
|
|
18644
18932
|
fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
|
|
18645
18933
|
inputs: record.inputValues,
|
|
18934
|
+
props: record.propValues,
|
|
18646
18935
|
visibleRange: this.currentVisibleRange(),
|
|
18647
18936
|
mode,
|
|
18648
18937
|
// Mid-backfill session starts (add / re-show / price-style re-execute) let the
|
|
@@ -18736,6 +19025,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18736
19025
|
const meta = record.prepared.meta;
|
|
18737
19026
|
const model = {
|
|
18738
19027
|
id,
|
|
19028
|
+
// Deliberately the FULL title, never a shorttitle: while the script loads the
|
|
19029
|
+
// legend identifies it by its full name; the compact shorttitle arrives with
|
|
19030
|
+
// the first computed model and takes over from there.
|
|
18739
19031
|
title: record.options?.title ?? meta.title,
|
|
18740
19032
|
overlay: meta.overlay,
|
|
18741
19033
|
paneHint: meta.overlay ? "price" : "new",
|
|
@@ -18744,7 +19036,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18744
19036
|
backgrounds: [],
|
|
18745
19037
|
priceLines: [],
|
|
18746
19038
|
inputs: record.prepared.inputs,
|
|
18747
|
-
inputValues: record.inputValues
|
|
19039
|
+
inputValues: record.inputValues,
|
|
19040
|
+
...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
|
|
18748
19041
|
};
|
|
18749
19042
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
18750
19043
|
this.placeModel(model, id, paneId);
|
|
@@ -18977,7 +19270,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18977
19270
|
record.model = model;
|
|
18978
19271
|
if (record.pendingStructural) {
|
|
18979
19272
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
18980
|
-
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
|
|
19273
|
+
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
18981
19274
|
record.pendingStructural = false;
|
|
18982
19275
|
} else {
|
|
18983
19276
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
@@ -19004,16 +19297,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19004
19297
|
placeModel(model, id, paneId) {
|
|
19005
19298
|
model.id = id;
|
|
19006
19299
|
model.paneId = paneId;
|
|
19007
|
-
|
|
19008
|
-
for (const
|
|
19009
|
-
for (const
|
|
19300
|
+
const paneOf = (item) => item.overlay === true ? "price" : paneId;
|
|
19301
|
+
for (const series of model.series) series.paneId = paneOf(series);
|
|
19302
|
+
for (const fill of model.fills) fill.paneId = paneOf(fill);
|
|
19303
|
+
for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
|
|
19010
19304
|
for (const line of model.priceLines) line.paneId = paneId;
|
|
19011
|
-
if (model.lines) for (const ln of model.lines) ln.paneId =
|
|
19012
|
-
if (model.boxes) for (const bx of model.boxes) bx.paneId =
|
|
19013
|
-
if (model.labels) for (const lb of model.labels) lb.paneId =
|
|
19014
|
-
if (model.polylines) for (const pl of model.polylines) pl.paneId =
|
|
19015
|
-
if (model.linefills) for (const lf of model.linefills) lf.paneId =
|
|
19016
|
-
if (model.tables) for (const tb of model.tables) tb.paneId =
|
|
19305
|
+
if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
|
|
19306
|
+
if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
|
|
19307
|
+
if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
|
|
19308
|
+
if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
|
|
19309
|
+
if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
|
|
19310
|
+
if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
|
|
19017
19311
|
}
|
|
19018
19312
|
emitContextChanged(id) {
|
|
19019
19313
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
@@ -19166,6 +19460,15 @@ var RendererControl = class {
|
|
|
19166
19460
|
setLegendActions(provider) {
|
|
19167
19461
|
this.renderer.setLegendActions?.(provider);
|
|
19168
19462
|
}
|
|
19463
|
+
/**
|
|
19464
|
+
* Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
|
|
19465
|
+
* plugin registry through this; see `registerLegendCallout`). Silent on a renderer
|
|
19466
|
+
* without the seam — contributed callouts simply never show there, same graceful
|
|
19467
|
+
* degradation as {@link setLegendActions}.
|
|
19468
|
+
*/
|
|
19469
|
+
setLegendCallouts(provider) {
|
|
19470
|
+
this.renderer.setLegendCallouts?.(provider);
|
|
19471
|
+
}
|
|
19169
19472
|
/**
|
|
19170
19473
|
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
19171
19474
|
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
@@ -19675,6 +19978,10 @@ function inputVisible(when, values) {
|
|
|
19675
19978
|
}
|
|
19676
19979
|
|
|
19677
19980
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
19981
|
+
var PROPS_TAB = "Properties";
|
|
19982
|
+
function bagOf(row, decl) {
|
|
19983
|
+
return decl.prop ? row.propValues : row.values;
|
|
19984
|
+
}
|
|
19678
19985
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
19679
19986
|
var IndicatorInputsDialog = class {
|
|
19680
19987
|
constructor(host) {
|
|
@@ -19685,6 +19992,7 @@ var IndicatorInputsDialog = class {
|
|
|
19685
19992
|
this.openId = null;
|
|
19686
19993
|
this.row = null;
|
|
19687
19994
|
this.snapshot = null;
|
|
19995
|
+
this.propSnapshot = null;
|
|
19688
19996
|
this.dialogTips = [];
|
|
19689
19997
|
/** Re-applies every `when` gate against current values; set while the dialog is open. */
|
|
19690
19998
|
this.refreshVisibility = null;
|
|
@@ -19707,10 +20015,11 @@ var IndicatorInputsDialog = class {
|
|
|
19707
20015
|
}
|
|
19708
20016
|
open(row) {
|
|
19709
20017
|
this.close();
|
|
19710
|
-
if (row.inputs.length === 0) return;
|
|
20018
|
+
if (row.inputs.length === 0 && row.props.length === 0) return;
|
|
19711
20019
|
this.row = row;
|
|
19712
20020
|
this.openId = row.id;
|
|
19713
20021
|
this.snapshot = { ...row.values };
|
|
20022
|
+
this.propSnapshot = { ...row.propValues };
|
|
19714
20023
|
ensureDialogStyles();
|
|
19715
20024
|
const t = this.host.theme();
|
|
19716
20025
|
const border = "var(--vela-border)";
|
|
@@ -19718,7 +20027,7 @@ var IndicatorInputsDialog = class {
|
|
|
19718
20027
|
const host = this.host.dialogHost() ?? this.host.container;
|
|
19719
20028
|
const ui = new Dialog({
|
|
19720
20029
|
host,
|
|
19721
|
-
title: row.
|
|
20030
|
+
title: row.title,
|
|
19722
20031
|
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
19723
20032
|
// whole body, killing the chart and the body-portaled popovers.
|
|
19724
20033
|
modal: false,
|
|
@@ -19730,12 +20039,16 @@ var IndicatorInputsDialog = class {
|
|
|
19730
20039
|
closeOnEscape: false,
|
|
19731
20040
|
footer: (foot) => {
|
|
19732
20041
|
foot.append(
|
|
20042
|
+
this.resetAction(),
|
|
19733
20043
|
this.dialogButton("Cancel", false, () => this.revertAndClose()),
|
|
19734
20044
|
this.dialogButton("Ok", true, () => this.close())
|
|
19735
20045
|
);
|
|
19736
20046
|
},
|
|
20047
|
+
// Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
|
|
20048
|
+
// asynchronously — after a reset rebuild that notification must not close
|
|
20049
|
+
// the replacement dialog.
|
|
19737
20050
|
onOpenChange: (open2) => {
|
|
19738
|
-
if (!open2) this.close();
|
|
20051
|
+
if (!open2 && this.uiDialog === ui) this.close();
|
|
19739
20052
|
}
|
|
19740
20053
|
});
|
|
19741
20054
|
applyChromeTokens(ui.panel, t);
|
|
@@ -19746,6 +20059,16 @@ var IndicatorInputsDialog = class {
|
|
|
19746
20059
|
text: () => "Close"
|
|
19747
20060
|
}));
|
|
19748
20061
|
const tabDefs = tabInputs(row.inputs);
|
|
20062
|
+
if (row.props.length > 0) {
|
|
20063
|
+
const propDecls = row.props.map((p) => {
|
|
20064
|
+
const d = { ...p, prop: true };
|
|
20065
|
+
delete d.tab;
|
|
20066
|
+
return d;
|
|
20067
|
+
});
|
|
20068
|
+
const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
|
|
20069
|
+
if (existing) existing.inputs.push(...propDecls);
|
|
20070
|
+
else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
|
|
20071
|
+
}
|
|
19749
20072
|
const tabs = document.createElement("div");
|
|
19750
20073
|
tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
19751
20074
|
const tabEls = [];
|
|
@@ -19848,9 +20171,10 @@ var IndicatorInputsDialog = class {
|
|
|
19848
20171
|
this.openId = null;
|
|
19849
20172
|
this.row = null;
|
|
19850
20173
|
this.snapshot = null;
|
|
20174
|
+
this.propSnapshot = null;
|
|
19851
20175
|
ui?.destroy();
|
|
19852
20176
|
}
|
|
19853
|
-
/** Restore every input to its open-time value (re-running the indicator), then close. */
|
|
20177
|
+
/** Restore every input and prop to its open-time value (re-running the indicator), then close. */
|
|
19854
20178
|
revertAndClose() {
|
|
19855
20179
|
const row = this.row;
|
|
19856
20180
|
const snap = this.snapshot;
|
|
@@ -19864,12 +20188,55 @@ var IndicatorInputsDialog = class {
|
|
|
19864
20188
|
}
|
|
19865
20189
|
}
|
|
19866
20190
|
}
|
|
20191
|
+
const propSnap = this.propSnapshot;
|
|
20192
|
+
if (row && propSnap) {
|
|
20193
|
+
for (const p of row.props) {
|
|
20194
|
+
const snapped = propSnap[p.key];
|
|
20195
|
+
const before = snapped !== void 0 ? snapped : p.defval;
|
|
20196
|
+
if (row.propValues[p.key] !== before) {
|
|
20197
|
+
row.propValues[p.key] = before;
|
|
20198
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
|
|
20199
|
+
}
|
|
20200
|
+
}
|
|
20201
|
+
}
|
|
19867
20202
|
this.close();
|
|
19868
20203
|
}
|
|
20204
|
+
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
20205
|
+
* (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
|
|
20206
|
+
resetAction() {
|
|
20207
|
+
const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
|
|
20208
|
+
b.style.marginRight = "auto";
|
|
20209
|
+
return b;
|
|
20210
|
+
}
|
|
20211
|
+
/** Restore every input to its declared default (re-running the indicator), then
|
|
20212
|
+
* re-open the form so each control re-reads the restored values — the same
|
|
20213
|
+
* rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
|
|
20214
|
+
* survives the rebuild, so Cancel after a reset still reverts the whole session. */
|
|
20215
|
+
resetToDefaults() {
|
|
20216
|
+
const row = this.row;
|
|
20217
|
+
if (!row) return;
|
|
20218
|
+
const snap = this.snapshot;
|
|
20219
|
+
const propSnap = this.propSnapshot;
|
|
20220
|
+
for (const inp of row.inputs) {
|
|
20221
|
+
if (row.values[inp.key] !== inp.defval) {
|
|
20222
|
+
row.values[inp.key] = inp.defval;
|
|
20223
|
+
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
20224
|
+
}
|
|
20225
|
+
}
|
|
20226
|
+
for (const p of row.props) {
|
|
20227
|
+
if (row.propValues[p.key] !== p.defval) {
|
|
20228
|
+
row.propValues[p.key] = p.defval;
|
|
20229
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
|
|
20230
|
+
}
|
|
20231
|
+
}
|
|
20232
|
+
this.open(row);
|
|
20233
|
+
if (snap) this.snapshot = snap;
|
|
20234
|
+
if (propSnap) this.propSnapshot = propSnap;
|
|
20235
|
+
}
|
|
19869
20236
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
19870
|
-
commit(row,
|
|
19871
|
-
row.
|
|
19872
|
-
this.host.onChange?.({ indicatorId: row.id, key, value });
|
|
20237
|
+
commit(row, decl, value) {
|
|
20238
|
+
bagOf(row, decl)[decl.key] = value;
|
|
20239
|
+
this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
|
|
19873
20240
|
this.refreshVisibility?.();
|
|
19874
20241
|
}
|
|
19875
20242
|
/** One settings row (or several `inline=` inputs) placed into a section's grid. */
|
|
@@ -19896,7 +20263,7 @@ var IndicatorInputsDialog = class {
|
|
|
19896
20263
|
const id = idOf(lead);
|
|
19897
20264
|
const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
|
|
19898
20265
|
if (lead.type === "bool") {
|
|
19899
|
-
const current = Boolean(row
|
|
20266
|
+
const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
|
|
19900
20267
|
return append(fieldRow({
|
|
19901
20268
|
label: nameOf(lead),
|
|
19902
20269
|
id,
|
|
@@ -19905,7 +20272,7 @@ var IndicatorInputsDialog = class {
|
|
|
19905
20272
|
toggle: {
|
|
19906
20273
|
id,
|
|
19907
20274
|
checked: current,
|
|
19908
|
-
onChange: (v) => this.commit(row, lead
|
|
20275
|
+
onChange: (v) => this.commit(row, lead, v)
|
|
19909
20276
|
}
|
|
19910
20277
|
}));
|
|
19911
20278
|
}
|
|
@@ -19930,8 +20297,8 @@ var IndicatorInputsDialog = class {
|
|
|
19930
20297
|
}
|
|
19931
20298
|
/** Build the typed control for one input, committing edits live via `onChange`. */
|
|
19932
20299
|
buildControl(row, inp, id) {
|
|
19933
|
-
const current = row
|
|
19934
|
-
const emit = (value) => this.commit(row, inp
|
|
20300
|
+
const current = bagOf(row, inp)[inp.key] ?? inp.defval;
|
|
20301
|
+
const emit = (value) => this.commit(row, inp, value);
|
|
19935
20302
|
if (inp.type === "bool") {
|
|
19936
20303
|
return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
|
|
19937
20304
|
}
|
|
@@ -19942,7 +20309,7 @@ var IndicatorInputsDialog = class {
|
|
|
19942
20309
|
kind: "color",
|
|
19943
20310
|
id,
|
|
19944
20311
|
theme: this.host.theme(),
|
|
19945
|
-
get: () => String(row
|
|
20312
|
+
get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
|
|
19946
20313
|
onChange: (v) => emit(v)
|
|
19947
20314
|
}).el;
|
|
19948
20315
|
}
|
|
@@ -20505,6 +20872,7 @@ function timeParts(ts) {
|
|
|
20505
20872
|
}
|
|
20506
20873
|
|
|
20507
20874
|
// src/renderers/shared/InputsUI.ts
|
|
20875
|
+
var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
|
|
20508
20876
|
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
20509
20877
|
var LEGEND_ICON_PX2 = 16;
|
|
20510
20878
|
var LEGEND_CTL_PX = 18;
|
|
@@ -20543,10 +20911,14 @@ var InputsUI = class {
|
|
|
20543
20911
|
this.moveApi = null;
|
|
20544
20912
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
20545
20913
|
this.legendActions = null;
|
|
20914
|
+
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
20915
|
+
this.legendCallouts = null;
|
|
20546
20916
|
/** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
|
|
20547
20917
|
this.rowTips = /* @__PURE__ */ new Map();
|
|
20548
20918
|
/** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
|
|
20549
20919
|
this.extrasTips = /* @__PURE__ */ new Map();
|
|
20920
|
+
/** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
|
|
20921
|
+
this.calloutTips = /* @__PURE__ */ new Map();
|
|
20550
20922
|
/** Open "Move to" menu (kept so it can be torn down). */
|
|
20551
20923
|
this.moveMenu = null;
|
|
20552
20924
|
this.moveTargets = [];
|
|
@@ -20704,6 +21076,42 @@ var InputsUI = class {
|
|
|
20704
21076
|
extrasEl.appendChild(btn2);
|
|
20705
21077
|
}
|
|
20706
21078
|
}
|
|
21079
|
+
/**
|
|
21080
|
+
* Wire the host-contributed callout bubbles. Same contract as
|
|
21081
|
+
* {@link setLegendActions}: re-calling replaces the provider and re-projects the
|
|
21082
|
+
* rows already on screen.
|
|
21083
|
+
*/
|
|
21084
|
+
setLegendCallouts(provider) {
|
|
21085
|
+
this.legendCallouts = provider;
|
|
21086
|
+
for (const row of this.rows.values()) this.renderCallouts(row);
|
|
21087
|
+
}
|
|
21088
|
+
/** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
|
|
21089
|
+
* click (when the view carries content) deploys a panel of text and actions. */
|
|
21090
|
+
renderCallouts(row) {
|
|
21091
|
+
this.disposeTips(this.calloutTips, row.id);
|
|
21092
|
+
for (const bubble of row.callouts) bubble.destroy();
|
|
21093
|
+
row.callouts = [];
|
|
21094
|
+
row.calloutsEl.replaceChildren();
|
|
21095
|
+
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21096
|
+
row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
|
|
21097
|
+
for (const view of views) {
|
|
21098
|
+
const bubble = new CalloutBubble({
|
|
21099
|
+
icon: view.icon,
|
|
21100
|
+
background: view.background,
|
|
21101
|
+
...view.color !== void 0 ? { color: view.color } : {},
|
|
21102
|
+
label: view.tooltip,
|
|
21103
|
+
...view.content !== void 0 ? { panel: view.content } : {},
|
|
21104
|
+
// The panel portals into the plot host and self-tokens: it must work on
|
|
21105
|
+
// a bare chart, where no `.vela-ui` token ancestor exists.
|
|
21106
|
+
host: this.container,
|
|
21107
|
+
theme: () => this.theme
|
|
21108
|
+
});
|
|
21109
|
+
bubble.el.dataset.legendCallout = view.id;
|
|
21110
|
+
this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
|
|
21111
|
+
row.callouts.push(bubble);
|
|
21112
|
+
row.calloutsEl.appendChild(bubble.el);
|
|
21113
|
+
}
|
|
21114
|
+
}
|
|
20707
21115
|
/** Reposition the per-pane legend containers after a layout change. */
|
|
20708
21116
|
reposition() {
|
|
20709
21117
|
for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
|
|
@@ -20871,6 +21279,7 @@ var InputsUI = class {
|
|
|
20871
21279
|
}
|
|
20872
21280
|
positionLegend(lg, paneId) {
|
|
20873
21281
|
const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
|
|
21282
|
+
lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
|
|
20874
21283
|
lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
|
|
20875
21284
|
const collapsed = this.paneCollapse.has(paneId);
|
|
20876
21285
|
const masterId = this.paneCollapse.get(paneId) ?? null;
|
|
@@ -20946,13 +21355,13 @@ var InputsUI = class {
|
|
|
20946
21355
|
}
|
|
20947
21356
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
20948
21357
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
20949
|
-
const settingsTitle = opts.settingsTitle ?? title;
|
|
20950
21358
|
const existing = this.rows.get(id);
|
|
20951
21359
|
if (existing) {
|
|
20952
21360
|
existing.title = title;
|
|
20953
|
-
existing.settingsTitle = settingsTitle;
|
|
20954
21361
|
existing.inputs = inputs;
|
|
20955
21362
|
existing.values = { ...values };
|
|
21363
|
+
existing.props = opts.props ?? [];
|
|
21364
|
+
existing.propValues = { ...opts.propValues ?? {} };
|
|
20956
21365
|
existing.titleEl.textContent = title;
|
|
20957
21366
|
if (existing.paneId !== paneId) {
|
|
20958
21367
|
existing.paneId = paneId;
|
|
@@ -21004,6 +21413,9 @@ var InputsUI = class {
|
|
|
21004
21413
|
titleWrap.appendChild(beta);
|
|
21005
21414
|
}
|
|
21006
21415
|
el.appendChild(titleWrap);
|
|
21416
|
+
const calloutsEl = document.createElement("span");
|
|
21417
|
+
calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
|
|
21418
|
+
el.appendChild(calloutsEl);
|
|
21007
21419
|
el.appendChild(statusEl);
|
|
21008
21420
|
const valuesEl = document.createElement("span");
|
|
21009
21421
|
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;`;
|
|
@@ -21020,13 +21432,13 @@ var InputsUI = class {
|
|
|
21020
21432
|
eye.className = "vela-ind-ctl";
|
|
21021
21433
|
eye.style.cssText = LEGEND_CTL_CSS;
|
|
21022
21434
|
eye.addEventListener("click", () => {
|
|
21023
|
-
const
|
|
21024
|
-
this.onToggleVisible?.(id, Boolean(
|
|
21435
|
+
const row2 = this.rows.get(id);
|
|
21436
|
+
this.onToggleVisible?.(id, Boolean(row2?.hidden));
|
|
21025
21437
|
});
|
|
21026
21438
|
controlsEl.appendChild(eye);
|
|
21027
21439
|
eyeEl = eye;
|
|
21028
21440
|
}
|
|
21029
|
-
if (inputs.length > 0) {
|
|
21441
|
+
if (inputs.length > 0 || (opts.props ?? []).length > 0) {
|
|
21030
21442
|
const gear = document.createElement("button");
|
|
21031
21443
|
gear.type = "button";
|
|
21032
21444
|
gear.setAttribute("aria-label", "Settings");
|
|
@@ -21066,7 +21478,9 @@ var InputsUI = class {
|
|
|
21066
21478
|
controlsEl.appendChild(close);
|
|
21067
21479
|
el.appendChild(controlsEl);
|
|
21068
21480
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
21069
|
-
|
|
21481
|
+
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 };
|
|
21482
|
+
this.rows.set(id, row);
|
|
21483
|
+
this.renderCallouts(row);
|
|
21070
21484
|
this.syncFoldToggle();
|
|
21071
21485
|
}
|
|
21072
21486
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -21075,9 +21489,11 @@ var InputsUI = class {
|
|
|
21075
21489
|
else container.appendChild(el);
|
|
21076
21490
|
}
|
|
21077
21491
|
/** Reflect programmatic input changes (so a re-opened dialog shows current values). */
|
|
21078
|
-
setValues(id, values) {
|
|
21492
|
+
setValues(id, values, props) {
|
|
21079
21493
|
const row = this.rows.get(id);
|
|
21080
|
-
if (row)
|
|
21494
|
+
if (!row) return;
|
|
21495
|
+
row.values = { ...row.values, ...values };
|
|
21496
|
+
if (props) row.propValues = { ...row.propValues, ...props };
|
|
21081
21497
|
}
|
|
21082
21498
|
/**
|
|
21083
21499
|
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
@@ -21159,8 +21575,13 @@ var InputsUI = class {
|
|
|
21159
21575
|
syncRowActions(row) {
|
|
21160
21576
|
const open2 = row.highlighted;
|
|
21161
21577
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21162
|
-
if (open2)
|
|
21163
|
-
|
|
21578
|
+
if (open2) {
|
|
21579
|
+
row.el.appendChild(row.statusEl);
|
|
21580
|
+
row.el.appendChild(row.calloutsEl);
|
|
21581
|
+
} else {
|
|
21582
|
+
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21583
|
+
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
21584
|
+
}
|
|
21164
21585
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21165
21586
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21166
21587
|
if (child === row.extrasEl) {
|
|
@@ -21178,10 +21599,12 @@ var InputsUI = class {
|
|
|
21178
21599
|
}
|
|
21179
21600
|
remove(id) {
|
|
21180
21601
|
const row = this.rows.get(id);
|
|
21602
|
+
for (const bubble of row?.callouts ?? []) bubble.destroy();
|
|
21181
21603
|
row?.el.remove();
|
|
21182
21604
|
this.rows.delete(id);
|
|
21183
21605
|
this.disposeTips(this.rowTips, id);
|
|
21184
21606
|
this.disposeTips(this.extrasTips, id);
|
|
21607
|
+
this.disposeTips(this.calloutTips, id);
|
|
21185
21608
|
if (this.selectedId === id) this.selectedId = null;
|
|
21186
21609
|
if (this.inputsDialog.openId === id) this.inputsDialog.close();
|
|
21187
21610
|
this.syncFoldToggle();
|
|
@@ -21201,6 +21624,8 @@ var InputsUI = class {
|
|
|
21201
21624
|
this.rowMenu = null;
|
|
21202
21625
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
21203
21626
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
21627
|
+
for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
|
|
21628
|
+
for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
|
|
21204
21629
|
for (const lg of this.legends.values()) lg.remove();
|
|
21205
21630
|
this.legends.clear();
|
|
21206
21631
|
this.rows.clear();
|
|
@@ -21555,6 +21980,32 @@ var SIZE_PX3 = {
|
|
|
21555
21980
|
large: 16,
|
|
21556
21981
|
huge: 20
|
|
21557
21982
|
};
|
|
21983
|
+
function fontPxOf(size) {
|
|
21984
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
|
|
21985
|
+
return SIZE_PX3[size] ?? SIZE_PX3.auto;
|
|
21986
|
+
}
|
|
21987
|
+
function tableHasContent(t) {
|
|
21988
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
21989
|
+
}
|
|
21990
|
+
function mergeRenderPlan(t) {
|
|
21991
|
+
const span = /* @__PURE__ */ new Map();
|
|
21992
|
+
const omit = /* @__PURE__ */ new Set();
|
|
21993
|
+
for (const m of t.merges) {
|
|
21994
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
21995
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
21996
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
21997
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
21998
|
+
}
|
|
21999
|
+
}
|
|
22000
|
+
}
|
|
22001
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
22002
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
22003
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
22004
|
+
}
|
|
22005
|
+
}
|
|
22006
|
+
for (const key of span.keys()) omit.delete(key);
|
|
22007
|
+
return { span, omit };
|
|
22008
|
+
}
|
|
21558
22009
|
var TableOverlay = class {
|
|
21559
22010
|
constructor(container, theme, paneBounds) {
|
|
21560
22011
|
this.container = container;
|
|
@@ -21575,7 +22026,9 @@ var TableOverlay = class {
|
|
|
21575
22026
|
update(tables) {
|
|
21576
22027
|
this.lastTables = tables;
|
|
21577
22028
|
this.root.replaceChildren();
|
|
21578
|
-
for (const t of tables)
|
|
22029
|
+
for (const t of tables) {
|
|
22030
|
+
if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
|
|
22031
|
+
}
|
|
21579
22032
|
}
|
|
21580
22033
|
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
21581
22034
|
reposition() {
|
|
@@ -21590,39 +22043,36 @@ var TableOverlay = class {
|
|
|
21590
22043
|
this.root.remove();
|
|
21591
22044
|
}
|
|
21592
22045
|
renderTable(t) {
|
|
22046
|
+
const b = this.paneBounds(t.paneId);
|
|
21593
22047
|
const wrap = document.createElement("div");
|
|
21594
22048
|
wrap.style.position = "absolute";
|
|
21595
|
-
|
|
22049
|
+
if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
|
|
22050
|
+
this.anchor(wrap, t.position, b);
|
|
21596
22051
|
const table = document.createElement("table");
|
|
21597
22052
|
Object.assign(table.style, {
|
|
21598
22053
|
borderCollapse: "collapse",
|
|
21599
22054
|
background: t.bgColor ?? "transparent",
|
|
21600
22055
|
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
21601
|
-
// Frame as an inset shadow (not a `border`) so it stays independent of the
|
|
21602
|
-
// collapsed cell borders even when frame_width != border_width.
|
|
21603
|
-
boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
|
|
21604
22056
|
border: "none",
|
|
21605
22057
|
tableLayout: "auto",
|
|
21606
22058
|
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
21607
22059
|
pointerEvents: "auto"
|
|
21608
22060
|
});
|
|
21609
|
-
const span
|
|
21610
|
-
const
|
|
21611
|
-
|
|
21612
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
21613
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
21614
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
21615
|
-
if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
|
|
21616
|
-
}
|
|
21617
|
-
}
|
|
21618
|
-
}
|
|
22061
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
22062
|
+
const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
|
|
22063
|
+
const paneH = b.height;
|
|
21619
22064
|
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
21620
22065
|
for (let r = 0; r < t.rows; r += 1) {
|
|
21621
22066
|
const tr = document.createElement("tr");
|
|
21622
22067
|
for (let c = 0; c < t.columns; c += 1) {
|
|
22068
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
21623
22069
|
const cell = t.cells[r]?.[c] ?? null;
|
|
21624
|
-
if (skip.has(`${r}:${c}`) || cell?.merged) continue;
|
|
21625
22070
|
const td = document.createElement("td");
|
|
22071
|
+
if (cell === null) {
|
|
22072
|
+
Object.assign(td.style, { padding: "0", border: "none" });
|
|
22073
|
+
tr.appendChild(td);
|
|
22074
|
+
continue;
|
|
22075
|
+
}
|
|
21626
22076
|
const sp = span.get(`${r}:${c}`);
|
|
21627
22077
|
if (sp) {
|
|
21628
22078
|
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
@@ -21631,18 +22081,22 @@ var TableOverlay = class {
|
|
|
21631
22081
|
Object.assign(td.style, {
|
|
21632
22082
|
border: cellBorder,
|
|
21633
22083
|
padding: "2px 6px",
|
|
21634
|
-
background: cell
|
|
21635
|
-
color: cell
|
|
21636
|
-
textAlign: cell
|
|
21637
|
-
verticalAlign: cell
|
|
21638
|
-
fontSize: `${
|
|
21639
|
-
fontFamily: cell
|
|
21640
|
-
fontWeight: cell
|
|
21641
|
-
fontStyle: cell
|
|
21642
|
-
|
|
22084
|
+
background: cell.bgColor ?? "transparent",
|
|
22085
|
+
color: cell.textColor ?? this.theme.textColor,
|
|
22086
|
+
textAlign: cell.hAlign,
|
|
22087
|
+
verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
|
|
22088
|
+
fontSize: `${fontPxOf(cell.textSize)}px`,
|
|
22089
|
+
fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
22090
|
+
fontWeight: cell.bold ? "bold" : "normal",
|
|
22091
|
+
fontStyle: cell.italic ? "italic" : "normal",
|
|
22092
|
+
// Pine cell text never wraps; `\n` still breaks lines. Wrapping
|
|
22093
|
+
// used to collapse unicode sparklines and ━━━ dividers.
|
|
22094
|
+
whiteSpace: "pre"
|
|
21643
22095
|
});
|
|
21644
|
-
if (cell
|
|
21645
|
-
td.
|
|
22096
|
+
if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
|
|
22097
|
+
if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
|
|
22098
|
+
if (cell.tooltip) td.title = cell.tooltip;
|
|
22099
|
+
td.textContent = cell.text ?? "";
|
|
21646
22100
|
tr.appendChild(td);
|
|
21647
22101
|
}
|
|
21648
22102
|
table.appendChild(tr);
|
|
@@ -22244,8 +22698,14 @@ var WebGL2Backend = class {
|
|
|
22244
22698
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
22245
22699
|
};
|
|
22246
22700
|
b.alpha = this.modelAlpha;
|
|
22247
|
-
for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
|
|
22248
|
-
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));
|
|
22701
|
+
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22702
|
+
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));
|
|
22703
|
+
if (isPrice) {
|
|
22704
|
+
for (const m of scene.indicators.values()) {
|
|
22705
|
+
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22706
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
22707
|
+
}
|
|
22708
|
+
}
|
|
22249
22709
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
22250
22710
|
let candleDrawn = false;
|
|
22251
22711
|
for (const m of models) {
|
|
@@ -22259,7 +22719,7 @@ var WebGL2Backend = class {
|
|
|
22259
22719
|
b.alpha = this.modelAlpha;
|
|
22260
22720
|
const off = scene.offsetOf(m.id);
|
|
22261
22721
|
const mp = effPane(m);
|
|
22262
|
-
for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22722
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22263
22723
|
}
|
|
22264
22724
|
if (drawCandles && !candleDrawn) {
|
|
22265
22725
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -22268,6 +22728,12 @@ var WebGL2Backend = class {
|
|
|
22268
22728
|
}
|
|
22269
22729
|
drawSlicesUpTo(Infinity);
|
|
22270
22730
|
b.alpha = this.modelAlpha;
|
|
22731
|
+
if (isPrice) {
|
|
22732
|
+
for (const m of scene.indicators.values()) {
|
|
22733
|
+
const off = scene.offsetOf(m.id);
|
|
22734
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22735
|
+
}
|
|
22736
|
+
}
|
|
22271
22737
|
for (const m of models) {
|
|
22272
22738
|
const mp = effPane(m);
|
|
22273
22739
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -22314,7 +22780,7 @@ var WebGL2Backend = class {
|
|
|
22314
22780
|
for (const pane of scene.orderedPanes()) {
|
|
22315
22781
|
const b = this.glowBatch;
|
|
22316
22782
|
b.reset();
|
|
22317
|
-
this.emitGlowSources(b, scene
|
|
22783
|
+
this.emitGlowSources(b, scene, pane, coords, i0, i1);
|
|
22318
22784
|
if (b.vertexCount === 0) continue;
|
|
22319
22785
|
const topH = Math.round(pane.bounds.top * dpr * 0.5);
|
|
22320
22786
|
const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
|
|
@@ -22383,15 +22849,23 @@ var WebGL2Backend = class {
|
|
|
22383
22849
|
gl.bindVertexArray(null);
|
|
22384
22850
|
return true;
|
|
22385
22851
|
}
|
|
22386
|
-
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22387
|
-
|
|
22388
|
-
|
|
22389
|
-
|
|
22390
|
-
|
|
22391
|
-
|
|
22392
|
-
|
|
22393
|
-
|
|
22394
|
-
|
|
22852
|
+
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22853
|
+
* Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
|
|
22854
|
+
emitGlowSources(b, scene, pane, coords, i0, i1) {
|
|
22855
|
+
const emitOne = (s, off) => {
|
|
22856
|
+
if (!isLineLikeSeries(s) || s.visible === false) return;
|
|
22857
|
+
if (s.kind === "histogram" || s.kind === "columns") return;
|
|
22858
|
+
if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
|
|
22859
|
+
else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
|
|
22860
|
+
};
|
|
22861
|
+
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
22862
|
+
const off = scene.offsetOf(m.id);
|
|
22863
|
+
for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
|
|
22864
|
+
}
|
|
22865
|
+
if (pane.kind === "price") {
|
|
22866
|
+
for (const m of scene.indicators.values()) {
|
|
22867
|
+
const off = scene.offsetOf(m.id);
|
|
22868
|
+
for (const s of m.series) if (s.overlay === true) emitOne(s, off);
|
|
22395
22869
|
}
|
|
22396
22870
|
}
|
|
22397
22871
|
}
|
|
@@ -22674,9 +23148,9 @@ var WebGL2Backend = class {
|
|
|
22674
23148
|
b.alpha = this.candleBodyAlpha;
|
|
22675
23149
|
b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
|
|
22676
23150
|
}
|
|
22677
|
-
if (cs.borderVisible ||
|
|
23151
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
22678
23152
|
b.alpha = this.candleStructureAlpha;
|
|
22679
|
-
const bord = cs.borderVisible
|
|
23153
|
+
const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
|
|
22680
23154
|
const bw = Math.max(0, g.bodyW - 1);
|
|
22681
23155
|
const bh = Math.max(0, bodyH - 1);
|
|
22682
23156
|
b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
|
|
@@ -24228,8 +24702,14 @@ var Canvas2dBackend = class {
|
|
|
24228
24702
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
24229
24703
|
};
|
|
24230
24704
|
ctx.globalAlpha = this.modelAlpha;
|
|
24231
|
-
for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
|
|
24232
|
-
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));
|
|
24705
|
+
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
24706
|
+
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));
|
|
24707
|
+
if (isPrice) {
|
|
24708
|
+
for (const m of scene.indicators.values()) {
|
|
24709
|
+
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
24710
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
24711
|
+
}
|
|
24712
|
+
}
|
|
24233
24713
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
24234
24714
|
let si = 0;
|
|
24235
24715
|
const drawSlicesUpTo = (z) => {
|
|
@@ -24251,7 +24731,7 @@ var Canvas2dBackend = class {
|
|
|
24251
24731
|
ctx.globalAlpha = this.modelAlpha;
|
|
24252
24732
|
const off = scene.offsetOf(m.id);
|
|
24253
24733
|
const mp = effPane(m);
|
|
24254
|
-
for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24734
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24255
24735
|
}
|
|
24256
24736
|
if (drawCandles && !candleDrawn) {
|
|
24257
24737
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -24259,6 +24739,13 @@ var Canvas2dBackend = class {
|
|
|
24259
24739
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24260
24740
|
}
|
|
24261
24741
|
drawSlicesUpTo(Infinity);
|
|
24742
|
+
if (isPrice) {
|
|
24743
|
+
ctx.globalAlpha = this.modelAlpha;
|
|
24744
|
+
for (const m of scene.indicators.values()) {
|
|
24745
|
+
const off = scene.offsetOf(m.id);
|
|
24746
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24747
|
+
}
|
|
24748
|
+
}
|
|
24262
24749
|
ctx.globalAlpha = this.modelAlpha;
|
|
24263
24750
|
for (const m of models) {
|
|
24264
24751
|
const mp = effPane(m);
|
|
@@ -24530,9 +25017,9 @@ var Canvas2dBackend = class {
|
|
|
24530
25017
|
ctx.fillStyle = color;
|
|
24531
25018
|
ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
|
|
24532
25019
|
}
|
|
24533
|
-
if (cs.borderVisible ||
|
|
25020
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
24534
25021
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24535
|
-
ctx.strokeStyle = cs.borderVisible
|
|
25022
|
+
ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
|
|
24536
25023
|
ctx.lineWidth = 1;
|
|
24537
25024
|
const bw = Math.max(0, g.bodyW - 1);
|
|
24538
25025
|
const bh = Math.max(0, bodyH - 1);
|
|
@@ -24964,6 +25451,8 @@ var DrawingSceneRenderer = class {
|
|
|
24964
25451
|
this.set = set;
|
|
24965
25452
|
/** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
|
|
24966
25453
|
this.widthCache = /* @__PURE__ */ new Map();
|
|
25454
|
+
/** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
|
|
25455
|
+
this.tipRegions = [];
|
|
24967
25456
|
/**
|
|
24968
25457
|
* Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
|
|
24969
25458
|
* from the model's anchor bar, so they shift by this to land on chart logical indices.
|
|
@@ -24984,8 +25473,13 @@ var DrawingSceneRenderer = class {
|
|
|
24984
25473
|
const s = this.set;
|
|
24985
25474
|
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
24986
25475
|
}
|
|
25476
|
+
/** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
|
|
25477
|
+
labelTipRegions() {
|
|
25478
|
+
return this.tipRegions;
|
|
25479
|
+
}
|
|
24987
25480
|
/** Draw the whole set into `ctx` using the supplied coordinate closures. */
|
|
24988
25481
|
render(ctx, W, H, xOf, yOf) {
|
|
25482
|
+
this.tipRegions = [];
|
|
24989
25483
|
if (this.isEmpty()) return;
|
|
24990
25484
|
ctx.save();
|
|
24991
25485
|
this.drawLinefills(ctx, W, H, xOf, yOf);
|
|
@@ -25333,13 +25827,35 @@ var DrawingSceneRenderer = class {
|
|
|
25333
25827
|
if (this.isPointShape(lb.style)) {
|
|
25334
25828
|
if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
|
|
25335
25829
|
if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
|
|
25336
|
-
|
|
25337
|
-
|
|
25830
|
+
if (lb.tooltip) {
|
|
25831
|
+
const r = Math.max(4, fontPx * 0.6) + 3;
|
|
25832
|
+
this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
|
|
25833
|
+
}
|
|
25834
|
+
} else if (lb.style === "none" || lb.style === "text_outline") {
|
|
25835
|
+
if (lb.text) {
|
|
25836
|
+
this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
|
|
25837
|
+
if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
|
|
25838
|
+
}
|
|
25338
25839
|
} else {
|
|
25339
|
-
this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
25840
|
+
const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
25841
|
+
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 });
|
|
25340
25842
|
}
|
|
25341
25843
|
}
|
|
25342
25844
|
}
|
|
25845
|
+
/** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
|
|
25846
|
+
labelFont(lb, fontPx) {
|
|
25847
|
+
const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
|
|
25848
|
+
return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
|
|
25849
|
+
}
|
|
25850
|
+
/** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
|
|
25851
|
+
textRegion(ctx, lb, cx, cy, fontPx, text) {
|
|
25852
|
+
const font = this.labelFont(lb, fontPx);
|
|
25853
|
+
const lines = (lb.text ?? "").split("\n");
|
|
25854
|
+
const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
|
|
25855
|
+
const h = fontPx * 1.25 * lines.length;
|
|
25856
|
+
const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
|
|
25857
|
+
return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
|
|
25858
|
+
}
|
|
25343
25859
|
isPointShape(style) {
|
|
25344
25860
|
switch (style) {
|
|
25345
25861
|
case "circle":
|
|
@@ -25358,8 +25874,7 @@ var DrawingSceneRenderer = class {
|
|
|
25358
25874
|
}
|
|
25359
25875
|
}
|
|
25360
25876
|
drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
|
|
25361
|
-
|
|
25362
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
25877
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25363
25878
|
ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
|
|
25364
25879
|
ctx.textBaseline = "middle";
|
|
25365
25880
|
const lines = lb.text.split("\n");
|
|
@@ -25439,9 +25954,9 @@ var DrawingSceneRenderer = class {
|
|
|
25439
25954
|
ctx.fill();
|
|
25440
25955
|
}
|
|
25441
25956
|
}
|
|
25957
|
+
/** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
|
|
25442
25958
|
drawBubble(ctx, lb, px, py, fontPx, color) {
|
|
25443
|
-
|
|
25444
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
25959
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25445
25960
|
const lines = (lb.text ?? "").split("\n");
|
|
25446
25961
|
const padX = 6;
|
|
25447
25962
|
const padY = 4;
|
|
@@ -25499,38 +26014,51 @@ var DrawingSceneRenderer = class {
|
|
|
25499
26014
|
by = py - h - ptr;
|
|
25500
26015
|
pointer = "down";
|
|
25501
26016
|
}
|
|
25502
|
-
|
|
25503
|
-
|
|
25504
|
-
|
|
25505
|
-
if (pointer !== "none") {
|
|
25506
|
-
ctx.beginPath();
|
|
25507
|
-
if (pointer === "down") {
|
|
25508
|
-
ctx.moveTo(px - ptr, by + h);
|
|
25509
|
-
ctx.lineTo(px + ptr, by + h);
|
|
25510
|
-
ctx.lineTo(px, py);
|
|
25511
|
-
} else if (pointer === "up") {
|
|
25512
|
-
ctx.moveTo(px - ptr, by);
|
|
25513
|
-
ctx.lineTo(px + ptr, by);
|
|
25514
|
-
ctx.lineTo(px, py);
|
|
25515
|
-
} else if (pointer === "left") {
|
|
25516
|
-
ctx.moveTo(bx, py - ptr);
|
|
25517
|
-
ctx.lineTo(bx, py + ptr);
|
|
25518
|
-
ctx.lineTo(px, py);
|
|
25519
|
-
} else {
|
|
25520
|
-
ctx.moveTo(bx + w, py - ptr);
|
|
25521
|
-
ctx.lineTo(bx + w, py + ptr);
|
|
25522
|
-
ctx.lineTo(px, py);
|
|
25523
|
-
}
|
|
25524
|
-
ctx.closePath();
|
|
26017
|
+
if (!lb.noFill) {
|
|
26018
|
+
ctx.fillStyle = color;
|
|
26019
|
+
this.roundRect(ctx, bx, by, w, h, 4);
|
|
25525
26020
|
ctx.fill();
|
|
26021
|
+
if (pointer !== "none") {
|
|
26022
|
+
ctx.beginPath();
|
|
26023
|
+
if (pointer === "down") {
|
|
26024
|
+
ctx.moveTo(px - ptr, by + h);
|
|
26025
|
+
ctx.lineTo(px + ptr, by + h);
|
|
26026
|
+
ctx.lineTo(px, py);
|
|
26027
|
+
} else if (pointer === "up") {
|
|
26028
|
+
ctx.moveTo(px - ptr, by);
|
|
26029
|
+
ctx.lineTo(px + ptr, by);
|
|
26030
|
+
ctx.lineTo(px, py);
|
|
26031
|
+
} else if (pointer === "left") {
|
|
26032
|
+
ctx.moveTo(bx, py - ptr);
|
|
26033
|
+
ctx.lineTo(bx, py + ptr);
|
|
26034
|
+
ctx.lineTo(px, py);
|
|
26035
|
+
} else {
|
|
26036
|
+
ctx.moveTo(bx + w, py - ptr);
|
|
26037
|
+
ctx.lineTo(bx + w, py + ptr);
|
|
26038
|
+
ctx.lineTo(px, py);
|
|
26039
|
+
}
|
|
26040
|
+
ctx.closePath();
|
|
26041
|
+
ctx.fill();
|
|
26042
|
+
}
|
|
25526
26043
|
}
|
|
25527
26044
|
if (lb.text) {
|
|
25528
|
-
ctx.fillStyle = lb.textColor ?? contrastColor(color);
|
|
25529
|
-
ctx.textAlign = "center";
|
|
26045
|
+
ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
|
|
25530
26046
|
ctx.textBaseline = "middle";
|
|
26047
|
+
let tx;
|
|
26048
|
+
if (lb.textAlign === "left") {
|
|
26049
|
+
ctx.textAlign = "left";
|
|
26050
|
+
tx = bx + padX;
|
|
26051
|
+
} else if (lb.textAlign === "right") {
|
|
26052
|
+
ctx.textAlign = "right";
|
|
26053
|
+
tx = bx + w - padX;
|
|
26054
|
+
} else {
|
|
26055
|
+
ctx.textAlign = "center";
|
|
26056
|
+
tx = bx + w / 2;
|
|
26057
|
+
}
|
|
25531
26058
|
const startY = by + padY + lineH / 2;
|
|
25532
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i],
|
|
26059
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
|
|
25533
26060
|
}
|
|
26061
|
+
return { x: bx, y: by, w, h };
|
|
25534
26062
|
}
|
|
25535
26063
|
roundRect(ctx, x, y, w, h, r) {
|
|
25536
26064
|
const rr = Math.min(r, w / 2, h / 2);
|
|
@@ -25768,6 +26296,8 @@ var ChromeRenderer = class {
|
|
|
25768
26296
|
this.axisTextColor = DARK_THEME.textColor;
|
|
25769
26297
|
// Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
|
|
25770
26298
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
26299
|
+
// Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
|
|
26300
|
+
this.labelTips = [];
|
|
25771
26301
|
}
|
|
25772
26302
|
mount(canvas) {
|
|
25773
26303
|
this.canvas = canvas;
|
|
@@ -25809,6 +26339,7 @@ var ChromeRenderer = class {
|
|
|
25809
26339
|
const dataW = coords.width;
|
|
25810
26340
|
const dataH = coords.height;
|
|
25811
26341
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
26342
|
+
this.labelTips = [];
|
|
25812
26343
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
25813
26344
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
25814
26345
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -25918,6 +26449,17 @@ var ChromeRenderer = class {
|
|
|
25918
26449
|
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
25919
26450
|
);
|
|
25920
26451
|
ctx.restore();
|
|
26452
|
+
for (const r of this.drawScene.labelTipRegions()) {
|
|
26453
|
+
this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
26454
|
+
}
|
|
26455
|
+
}
|
|
26456
|
+
/** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
|
|
26457
|
+
labelTooltipAt(x, y) {
|
|
26458
|
+
for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
|
|
26459
|
+
const r = this.labelTips[i];
|
|
26460
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
26461
|
+
}
|
|
26462
|
+
return null;
|
|
25921
26463
|
}
|
|
25922
26464
|
// ── axes ──
|
|
25923
26465
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
@@ -26134,6 +26676,68 @@ function formatCountdown(ms) {
|
|
|
26134
26676
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
26135
26677
|
}
|
|
26136
26678
|
|
|
26679
|
+
// src/renderers/native/chrome/LabelTooltip.ts
|
|
26680
|
+
var HOVER_DELAY_MS = 350;
|
|
26681
|
+
var LabelTooltip = class {
|
|
26682
|
+
constructor(plot, deps) {
|
|
26683
|
+
this.plot = plot;
|
|
26684
|
+
this.deps = deps;
|
|
26685
|
+
this.tip = null;
|
|
26686
|
+
this.timer = null;
|
|
26687
|
+
/** Text of the currently open OR armed tip — dedupes moves inside one label. */
|
|
26688
|
+
this.current = null;
|
|
26689
|
+
this.onMove = (e) => {
|
|
26690
|
+
if (e.pointerType !== "mouse") return;
|
|
26691
|
+
const rect = this.plot.getBoundingClientRect();
|
|
26692
|
+
const x = e.clientX - rect.left;
|
|
26693
|
+
const y = e.clientY - rect.top;
|
|
26694
|
+
const text = this.deps.lookup(x, y);
|
|
26695
|
+
if (!text) {
|
|
26696
|
+
this.clear();
|
|
26697
|
+
return;
|
|
26698
|
+
}
|
|
26699
|
+
if (text === this.current) return;
|
|
26700
|
+
this.clear();
|
|
26701
|
+
this.current = text;
|
|
26702
|
+
this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
|
|
26703
|
+
};
|
|
26704
|
+
this.onLeave = () => {
|
|
26705
|
+
this.clear();
|
|
26706
|
+
};
|
|
26707
|
+
plot.addEventListener("pointermove", this.onMove);
|
|
26708
|
+
plot.addEventListener("pointerleave", this.onLeave);
|
|
26709
|
+
plot.addEventListener("pointerdown", this.onLeave);
|
|
26710
|
+
}
|
|
26711
|
+
destroy() {
|
|
26712
|
+
this.plot.removeEventListener("pointermove", this.onMove);
|
|
26713
|
+
this.plot.removeEventListener("pointerleave", this.onLeave);
|
|
26714
|
+
this.plot.removeEventListener("pointerdown", this.onLeave);
|
|
26715
|
+
this.clear();
|
|
26716
|
+
}
|
|
26717
|
+
show(text, x, y) {
|
|
26718
|
+
const doc = this.plot.ownerDocument;
|
|
26719
|
+
const tip = doc.createElement("div");
|
|
26720
|
+
tip.textContent = text;
|
|
26721
|
+
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;";
|
|
26722
|
+
applyChromeTokens(tip, this.deps.theme());
|
|
26723
|
+
this.plot.appendChild(tip);
|
|
26724
|
+
const pw = this.plot.clientWidth;
|
|
26725
|
+
const ph = this.plot.clientHeight;
|
|
26726
|
+
tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
|
|
26727
|
+
tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
|
|
26728
|
+
this.tip = tip;
|
|
26729
|
+
}
|
|
26730
|
+
clear() {
|
|
26731
|
+
if (this.timer !== null) {
|
|
26732
|
+
clearTimeout(this.timer);
|
|
26733
|
+
this.timer = null;
|
|
26734
|
+
}
|
|
26735
|
+
this.tip?.remove();
|
|
26736
|
+
this.tip = null;
|
|
26737
|
+
this.current = null;
|
|
26738
|
+
}
|
|
26739
|
+
};
|
|
26740
|
+
|
|
26137
26741
|
// src/renderers/native/chrome/CrosshairRenderer.ts
|
|
26138
26742
|
var CrosshairRenderer = class {
|
|
26139
26743
|
constructor() {
|
|
@@ -31211,19 +31815,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31211
31815
|
for (const model of models) {
|
|
31212
31816
|
const off = offsetOf(model.id);
|
|
31213
31817
|
for (const s of model.series) {
|
|
31214
|
-
if (s.
|
|
31215
|
-
|
|
31216
|
-
const b = s.bars[i - off];
|
|
31217
|
-
if (b) {
|
|
31218
|
-
consider(b.high);
|
|
31219
|
-
consider(b.low);
|
|
31220
|
-
}
|
|
31221
|
-
}
|
|
31222
|
-
} else if (isLineLikeSeries(s)) {
|
|
31223
|
-
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
31224
|
-
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
31225
|
-
else if (s.style?.base != null) consider(s.style.base);
|
|
31226
|
-
}
|
|
31818
|
+
if (s.overlay === true) continue;
|
|
31819
|
+
considerSeries(s, i0, i1, off, consider);
|
|
31227
31820
|
}
|
|
31228
31821
|
for (const pl of model.priceLines) consider(pl.price);
|
|
31229
31822
|
}
|
|
@@ -31245,6 +31838,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31245
31838
|
const span = max - min;
|
|
31246
31839
|
return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
|
|
31247
31840
|
}
|
|
31841
|
+
function considerSeries(s, i0, i1, off, consider) {
|
|
31842
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
31843
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
31844
|
+
const b = s.bars[i - off];
|
|
31845
|
+
if (b) {
|
|
31846
|
+
consider(b.high);
|
|
31847
|
+
consider(b.low);
|
|
31848
|
+
}
|
|
31849
|
+
}
|
|
31850
|
+
} else if (isLineLikeSeries(s)) {
|
|
31851
|
+
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
31852
|
+
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
31853
|
+
else if (s.style?.base != null) consider(s.style.base);
|
|
31854
|
+
}
|
|
31855
|
+
}
|
|
31856
|
+
function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
|
|
31857
|
+
let min = Infinity;
|
|
31858
|
+
let max = -Infinity;
|
|
31859
|
+
const consider = (v) => {
|
|
31860
|
+
if (v != null && Number.isFinite(v)) {
|
|
31861
|
+
if (v < min) min = v;
|
|
31862
|
+
if (v > max) max = v;
|
|
31863
|
+
}
|
|
31864
|
+
};
|
|
31865
|
+
for (const model of models) {
|
|
31866
|
+
const off = offsetOf(model.id);
|
|
31867
|
+
for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
|
|
31868
|
+
}
|
|
31869
|
+
return min === Infinity ? null : { min, max };
|
|
31870
|
+
}
|
|
31248
31871
|
function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
31249
31872
|
if (abovePx <= 0 && belowPx <= 0) return scale;
|
|
31250
31873
|
const content = heightPx - abovePx - belowPx;
|
|
@@ -31299,6 +31922,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
31299
31922
|
}
|
|
31300
31923
|
|
|
31301
31924
|
// src/renderers/native/backdrop/BackdropRenderer.ts
|
|
31925
|
+
function clipHighlightRect(x1, x2, left, right) {
|
|
31926
|
+
const x = Math.max(left, x1);
|
|
31927
|
+
const end = Math.min(right, x2);
|
|
31928
|
+
return end > x ? { x, width: end - x } : null;
|
|
31929
|
+
}
|
|
31302
31930
|
var BackdropRenderer = class {
|
|
31303
31931
|
constructor() {
|
|
31304
31932
|
this.canvas = null;
|
|
@@ -31331,17 +31959,22 @@ var BackdropRenderer = class {
|
|
|
31331
31959
|
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
31332
31960
|
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
31333
31961
|
drawHighlights(ctx, scene, coords) {
|
|
31334
|
-
const
|
|
31335
|
-
if (
|
|
31962
|
+
const sessions = scene.sessionHighlightBands();
|
|
31963
|
+
if (sessions.length > 0) {
|
|
31964
|
+
const left = Math.max(0, coords.logicalToX(-0.5));
|
|
31965
|
+
const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
|
|
31966
|
+
this.drawHighlightSet(ctx, sessions, coords, left, right);
|
|
31967
|
+
}
|
|
31968
|
+
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
|
|
31969
|
+
}
|
|
31970
|
+
drawHighlightSet(ctx, bands, coords, left, right) {
|
|
31336
31971
|
for (const band of bands) {
|
|
31337
31972
|
const x1 = coords.timeToX(band.from);
|
|
31338
31973
|
const x2 = coords.timeToX(band.to);
|
|
31339
|
-
|
|
31340
|
-
|
|
31341
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
31342
|
-
if (cw <= 0) continue;
|
|
31974
|
+
const rect = clipHighlightRect(x1, x2, left, right);
|
|
31975
|
+
if (!rect) continue;
|
|
31343
31976
|
ctx.fillStyle = band.color;
|
|
31344
|
-
ctx.fillRect(
|
|
31977
|
+
ctx.fillRect(rect.x, 0, rect.width, coords.height);
|
|
31345
31978
|
}
|
|
31346
31979
|
}
|
|
31347
31980
|
// ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
|
|
@@ -31883,6 +32516,8 @@ var NativeRenderer = class {
|
|
|
31883
32516
|
this.glowAmount = 0;
|
|
31884
32517
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
31885
32518
|
this.chrome = new ChromeRenderer();
|
|
32519
|
+
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32520
|
+
this.labelTooltip = null;
|
|
31886
32521
|
this.crosshairLayer = new CrosshairRenderer();
|
|
31887
32522
|
/** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
|
|
31888
32523
|
this.countdownTimer = null;
|
|
@@ -31893,6 +32528,8 @@ var NativeRenderer = class {
|
|
|
31893
32528
|
this.indicatorValuesOn = true;
|
|
31894
32529
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
31895
32530
|
this.legendActionsProvider = null;
|
|
32531
|
+
/** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
|
|
32532
|
+
this.legendCalloutsProvider = null;
|
|
31896
32533
|
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
31897
32534
|
this.legendOverviewAction = null;
|
|
31898
32535
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -33001,6 +33638,10 @@ var NativeRenderer = class {
|
|
|
33001
33638
|
this.plot.appendChild(this.scrollButton);
|
|
33002
33639
|
this.plot.addEventListener("pointermove", this.onScrollProximityMove);
|
|
33003
33640
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33641
|
+
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
33642
|
+
theme: () => this.chromeTheme(),
|
|
33643
|
+
lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
|
|
33644
|
+
});
|
|
33004
33645
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33005
33646
|
projector: () => this.drawingProjector(),
|
|
33006
33647
|
dpr: () => this.coords.dpr,
|
|
@@ -33040,9 +33681,10 @@ var NativeRenderer = class {
|
|
|
33040
33681
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
33041
33682
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
33042
33683
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
33684
|
+
this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
|
|
33043
33685
|
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
33044
33686
|
this.inputsUI.setOnChange((c) => {
|
|
33045
|
-
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
33687
|
+
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
|
|
33046
33688
|
});
|
|
33047
33689
|
this.inputsUI.setOnRemove((id) => {
|
|
33048
33690
|
for (const cb of this.removeIndicatorCbs) cb(id);
|
|
@@ -33232,6 +33874,8 @@ var NativeRenderer = class {
|
|
|
33232
33874
|
this.settingsButton = null;
|
|
33233
33875
|
this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
|
|
33234
33876
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33877
|
+
this.labelTooltip?.destroy();
|
|
33878
|
+
this.labelTooltip = null;
|
|
33235
33879
|
this.scrollButton?.remove();
|
|
33236
33880
|
this.scrollButton = null;
|
|
33237
33881
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -33472,7 +34116,7 @@ var NativeRenderer = class {
|
|
|
33472
34116
|
else this.scene.assignIndicatorZ(model.id);
|
|
33473
34117
|
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
33474
34118
|
native: !!model.native,
|
|
33475
|
-
...model.
|
|
34119
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
33476
34120
|
});
|
|
33477
34121
|
this.syncTables(model);
|
|
33478
34122
|
if (model.native?.type === "volume") {
|
|
@@ -33523,8 +34167,8 @@ var NativeRenderer = class {
|
|
|
33523
34167
|
this.paneControls?.refresh();
|
|
33524
34168
|
this.scheduler.invalidate(4 /* Full */);
|
|
33525
34169
|
}
|
|
33526
|
-
setIndicatorInputs(handle, values) {
|
|
33527
|
-
this.inputsUI.setValues(handle.id, values);
|
|
34170
|
+
setIndicatorInputs(handle, values, props) {
|
|
34171
|
+
this.inputsUI.setValues(handle.id, values, props);
|
|
33528
34172
|
}
|
|
33529
34173
|
setSymbolPicker(picker) {
|
|
33530
34174
|
this.symbolPicker = picker;
|
|
@@ -33534,6 +34178,10 @@ var NativeRenderer = class {
|
|
|
33534
34178
|
this.legendActionsProvider = provider;
|
|
33535
34179
|
this.inputsUI?.setLegendActions(provider);
|
|
33536
34180
|
}
|
|
34181
|
+
setLegendCallouts(provider) {
|
|
34182
|
+
this.legendCalloutsProvider = provider;
|
|
34183
|
+
this.inputsUI?.setLegendCallouts(provider);
|
|
34184
|
+
}
|
|
33537
34185
|
setLegendOverviewAction(action) {
|
|
33538
34186
|
this.legendOverviewAction = action;
|
|
33539
34187
|
this.inputsUI?.setLegendOverviewAction(action);
|
|
@@ -34514,7 +35162,11 @@ var NativeRenderer = class {
|
|
|
34514
35162
|
}
|
|
34515
35163
|
const models = this.scene.indicatorsForPane(pane.id);
|
|
34516
35164
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
34517
|
-
|
|
35165
|
+
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
35166
|
+
if (pane === pricePane) {
|
|
35167
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
|
|
35168
|
+
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
35169
|
+
}
|
|
34518
35170
|
const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
|
|
34519
35171
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34520
35172
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
@@ -34788,6 +35440,7 @@ var NativeRenderer = class {
|
|
|
34788
35440
|
for (const m of models) {
|
|
34789
35441
|
const off = this.scene.offsetOf(m.id);
|
|
34790
35442
|
for (const s of m.series) {
|
|
35443
|
+
if (s.overlay === true) continue;
|
|
34791
35444
|
if (isLineLikeSeries(s)) {
|
|
34792
35445
|
const v = s.points[i0 - off]?.value;
|
|
34793
35446
|
if (v != null && Number.isFinite(v)) return v;
|
|
@@ -35660,45 +36313,24 @@ var CSS21 = `
|
|
|
35660
36313
|
}
|
|
35661
36314
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
35662
36315
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
35663
|
-
/* Market status badge \u2014 icon-only 16px circle, label on hover
|
|
35664
|
-
|
|
35665
|
-
|
|
35666
|
-
place-items: center;
|
|
35667
|
-
width: 16px;
|
|
35668
|
-
height: 16px;
|
|
35669
|
-
border-radius: 50%;
|
|
35670
|
-
flex: none;
|
|
35671
|
-
align-self: center;
|
|
35672
|
-
line-height: 0;
|
|
35673
|
-
cursor: default;
|
|
35674
|
-
}
|
|
35675
|
-
.vela-statusline .vela-sl-market svg {
|
|
35676
|
-
width: 12px;
|
|
35677
|
-
height: 12px;
|
|
35678
|
-
display: block;
|
|
35679
|
-
}
|
|
35680
|
-
/* Open wears the theme's up color; the other sessions are meaning constants from the
|
|
35681
|
-
* palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
|
|
35682
|
-
.vela-statusline .vela-sl-market[data-status='open'] {
|
|
35683
|
-
background: color-mix(in srgb, var(--vela-up) 20%, transparent);
|
|
35684
|
-
color: var(--vela-up);
|
|
35685
|
-
}
|
|
35686
|
-
.vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
|
|
35687
|
-
.vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
|
|
35688
|
-
.vela-statusline .vela-sl-market[data-status='closed'],
|
|
35689
|
-
.vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
|
|
36316
|
+
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
36317
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. */
|
|
36318
|
+
.vela-statusline .vela-sl-market { align-self: center; }
|
|
35690
36319
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
35691
36320
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
35692
36321
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
35693
36322
|
* these are the pre-ink fallbacks only. */
|
|
35694
36323
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
35695
36324
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
35696
|
-
/* Stack the
|
|
35697
|
-
* The renderer
|
|
35698
|
-
*
|
|
35699
|
-
*
|
|
35700
|
-
*
|
|
35701
|
-
|
|
36325
|
+
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36326
|
+
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36327
|
+
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
36328
|
+
* with the status line whichever pane owns the top. The renderer sets the legend's
|
|
36329
|
+
* inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
|
|
36330
|
+
* CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
|
|
36331
|
+
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
36332
|
+
* chart on the page, including statusline-less ones. */
|
|
36333
|
+
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35702
36334
|
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
35703
36335
|
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
35704
36336
|
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
@@ -35734,7 +36366,7 @@ var CSS21 = `
|
|
|
35734
36366
|
font-size: var(--vela-font-size-sm);
|
|
35735
36367
|
line-height: 1.2;
|
|
35736
36368
|
}
|
|
35737
|
-
[data-layout='mobile'] .vela-has-statusline [
|
|
36369
|
+
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
35738
36370
|
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
35739
36371
|
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
35740
36372
|
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
@@ -35751,7 +36383,7 @@ var CSS21 = `
|
|
|
35751
36383
|
padding-left: 0;
|
|
35752
36384
|
}
|
|
35753
36385
|
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
35754
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [
|
|
36386
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35755
36387
|
`;
|
|
35756
36388
|
function baseOfTicker(ticker) {
|
|
35757
36389
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -35763,6 +36395,13 @@ var MARKET_LABELS = {
|
|
|
35763
36395
|
closed: "Market Closed",
|
|
35764
36396
|
holiday: "Market Holiday"
|
|
35765
36397
|
};
|
|
36398
|
+
var MARKET_INKS = {
|
|
36399
|
+
open: "var(--vela-up)",
|
|
36400
|
+
pre: SESSION_PRE,
|
|
36401
|
+
post: SESSION_POST,
|
|
36402
|
+
closed: SESSION_OFF,
|
|
36403
|
+
holiday: SESSION_OFF
|
|
36404
|
+
};
|
|
35766
36405
|
function statuslineInkOf(renderer, priceStyle) {
|
|
35767
36406
|
const cfg = renderer.getConfig();
|
|
35768
36407
|
const c = (v) => typeof v === "string" ? v : null;
|
|
@@ -35821,8 +36460,15 @@ var Statusline = class {
|
|
|
35821
36460
|
this.symbolEl.textContent = ticker;
|
|
35822
36461
|
this.metaEl = doc.createElement("span");
|
|
35823
36462
|
this.metaEl.className = "vela-sl-meta";
|
|
35824
|
-
this.
|
|
35825
|
-
|
|
36463
|
+
this.marketBubble = new CalloutBubble({
|
|
36464
|
+
icon: "market-open",
|
|
36465
|
+
background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
|
|
36466
|
+
color: MARKET_INKS.open,
|
|
36467
|
+
label: MARKET_LABELS.open,
|
|
36468
|
+
host
|
|
36469
|
+
});
|
|
36470
|
+
this.marketEl = this.marketBubble.el;
|
|
36471
|
+
this.marketEl.classList.add("vela-sl-market");
|
|
35826
36472
|
this.ohlcEl = doc.createElement("span");
|
|
35827
36473
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
35828
36474
|
this.changeEl = doc.createElement("span");
|
|
@@ -35910,8 +36556,13 @@ var Statusline = class {
|
|
|
35910
36556
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
35911
36557
|
setMarketStatus(status) {
|
|
35912
36558
|
this.marketEl.dataset.status = status;
|
|
35913
|
-
|
|
35914
|
-
this.
|
|
36559
|
+
const ink = MARKET_INKS[status];
|
|
36560
|
+
this.marketBubble.set({
|
|
36561
|
+
icon: `market-${status}`,
|
|
36562
|
+
background: `color-mix(in srgb, ${ink} 20%, transparent)`,
|
|
36563
|
+
color: ink,
|
|
36564
|
+
label: MARKET_LABELS[status]
|
|
36565
|
+
});
|
|
35915
36566
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
35916
36567
|
}
|
|
35917
36568
|
setPartVisible(part, visible) {
|
|
@@ -35944,6 +36595,7 @@ var Statusline = class {
|
|
|
35944
36595
|
this.fitRO?.disconnect();
|
|
35945
36596
|
this.fitRO = null;
|
|
35946
36597
|
this.marketTip.destroy();
|
|
36598
|
+
this.marketBubble.destroy();
|
|
35947
36599
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
35948
36600
|
this.el.remove();
|
|
35949
36601
|
}
|
|
@@ -36083,61 +36735,148 @@ var MarketStatusTracker = class {
|
|
|
36083
36735
|
};
|
|
36084
36736
|
|
|
36085
36737
|
// src/widget/session-shading.ts
|
|
36086
|
-
|
|
36738
|
+
var DAY_MS2 = 864e5;
|
|
36739
|
+
var MINUTE_MS = 6e4;
|
|
36740
|
+
function parseWindow(text) {
|
|
36741
|
+
if (typeof text !== "string") return null;
|
|
36742
|
+
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
36743
|
+
if (!m) return null;
|
|
36744
|
+
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
36745
|
+
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
36746
|
+
if (start >= end || end > 1440) return null;
|
|
36747
|
+
return { start, end };
|
|
36748
|
+
}
|
|
36749
|
+
function parseSessionSpec(si) {
|
|
36750
|
+
const session = si?.["session"];
|
|
36751
|
+
if (typeof session !== "string" || session === "" || session === "24x7") return null;
|
|
36752
|
+
const regular = parseWindow(session);
|
|
36753
|
+
if (!regular) return null;
|
|
36754
|
+
const tz = si?.["timezone"];
|
|
36755
|
+
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
36756
|
+
const ext = parseWindow(si?.["session_extended"]);
|
|
36757
|
+
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
36758
|
+
return { regular, extended, timezone };
|
|
36759
|
+
}
|
|
36760
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
36761
|
+
function civilFormatter(tz) {
|
|
36762
|
+
const cached = dtfCache.get(tz);
|
|
36763
|
+
if (cached !== void 0) return cached;
|
|
36764
|
+
let dtf = null;
|
|
36765
|
+
try {
|
|
36766
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
36767
|
+
timeZone: tz,
|
|
36768
|
+
hourCycle: "h23",
|
|
36769
|
+
weekday: "short",
|
|
36770
|
+
year: "numeric",
|
|
36771
|
+
month: "2-digit",
|
|
36772
|
+
day: "2-digit",
|
|
36773
|
+
hour: "2-digit",
|
|
36774
|
+
minute: "2-digit"
|
|
36775
|
+
});
|
|
36776
|
+
} catch {
|
|
36777
|
+
dtf = null;
|
|
36778
|
+
}
|
|
36779
|
+
dtfCache.set(tz, dtf);
|
|
36780
|
+
return dtf;
|
|
36781
|
+
}
|
|
36782
|
+
function civilParts(dtf, ms) {
|
|
36783
|
+
const parts = dtf.formatToParts(ms);
|
|
36784
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
36785
|
+
return {
|
|
36786
|
+
weekday: get("weekday"),
|
|
36787
|
+
year: Number(get("year")),
|
|
36788
|
+
month: Number(get("month")),
|
|
36789
|
+
day: Number(get("day")),
|
|
36790
|
+
hour: Number(get("hour")) % 24,
|
|
36791
|
+
minute: Number(get("minute"))
|
|
36792
|
+
};
|
|
36793
|
+
}
|
|
36794
|
+
function expandSessionZones(spec, from, to) {
|
|
36087
36795
|
const pre = [];
|
|
36088
36796
|
const post = [];
|
|
36089
|
-
|
|
36090
|
-
|
|
36091
|
-
|
|
36092
|
-
|
|
36093
|
-
|
|
36094
|
-
|
|
36095
|
-
|
|
36096
|
-
|
|
36797
|
+
const dtf = civilFormatter(spec.timezone);
|
|
36798
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
36799
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36800
|
+
for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
|
|
36801
|
+
const civil = civilParts(dtf, cursor);
|
|
36802
|
+
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
36803
|
+
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
36804
|
+
seen.add(key);
|
|
36805
|
+
if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
|
|
36806
|
+
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
36807
|
+
const noonGuess = naive + 720 * MINUTE_MS;
|
|
36808
|
+
const atNoon = civilParts(dtf, noonGuess);
|
|
36809
|
+
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
36810
|
+
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
36811
|
+
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
36812
|
+
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
36097
36813
|
}
|
|
36098
36814
|
return { pre, post };
|
|
36099
36815
|
}
|
|
36100
|
-
var
|
|
36101
|
-
var MIN_LOOKBACK_MS = 3 * 864e5;
|
|
36102
|
-
var MAX_LOOKBACK_MS = 120 * 864e5;
|
|
36816
|
+
var COVER_PAD_MIN_MS = 2 * DAY_MS2;
|
|
36103
36817
|
var SessionShadingTracker = class {
|
|
36104
36818
|
constructor(onZones) {
|
|
36105
36819
|
this.onZones = onZones;
|
|
36106
|
-
/** Invalidates
|
|
36820
|
+
/** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
|
|
36107
36821
|
this.epoch = 0;
|
|
36108
|
-
|
|
36109
|
-
|
|
36822
|
+
this.spec = null;
|
|
36823
|
+
this.ready = false;
|
|
36824
|
+
this.session = "regular";
|
|
36825
|
+
/** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
|
|
36826
|
+
this.intraday = false;
|
|
36827
|
+
this.covered = null;
|
|
36828
|
+
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
36829
|
+
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
36830
|
+
this.lastRange = null;
|
|
36831
|
+
}
|
|
36832
|
+
/** (Re)bind to a chart's data surface + market and expand once metadata lands. */
|
|
36110
36833
|
track(data, symbol, opts) {
|
|
36111
36834
|
const my = ++this.epoch;
|
|
36112
|
-
|
|
36835
|
+
this.ready = false;
|
|
36836
|
+
this.spec = null;
|
|
36837
|
+
this.covered = null;
|
|
36838
|
+
this.session = opts.session;
|
|
36839
|
+
const tfMs = timeframeMs(opts.timeframe);
|
|
36840
|
+
this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
|
|
36841
|
+
this.lastRange = opts.range;
|
|
36842
|
+
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
36843
|
+
if (my !== this.epoch) return;
|
|
36844
|
+
this.ready = true;
|
|
36845
|
+
this.spec = parseSessionSpec(si);
|
|
36846
|
+
this.emit(this.lastRange ?? opts.range, true);
|
|
36847
|
+
});
|
|
36848
|
+
}
|
|
36849
|
+
/** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
|
|
36850
|
+
* leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
|
|
36851
|
+
updateRange(range) {
|
|
36852
|
+
this.lastRange = range;
|
|
36853
|
+
if (!this.ready) return;
|
|
36854
|
+
this.emit(range, false);
|
|
36113
36855
|
}
|
|
36114
36856
|
stop() {
|
|
36115
36857
|
this.epoch += 1;
|
|
36116
|
-
|
|
36117
|
-
|
|
36118
|
-
|
|
36119
|
-
|
|
36120
|
-
|
|
36121
|
-
|
|
36122
|
-
|
|
36123
|
-
|
|
36124
|
-
this.onZones(null);
|
|
36858
|
+
this.ready = false;
|
|
36859
|
+
this.spec = null;
|
|
36860
|
+
this.covered = null;
|
|
36861
|
+
this.lastRange = null;
|
|
36862
|
+
}
|
|
36863
|
+
emit(range, force) {
|
|
36864
|
+
if (!this.spec) {
|
|
36865
|
+
if (force) this.onZones(null);
|
|
36125
36866
|
return;
|
|
36126
36867
|
}
|
|
36127
|
-
if (
|
|
36128
|
-
this.onZones({ pre: [], post: [] });
|
|
36868
|
+
if (this.session !== "extended" || !this.intraday) {
|
|
36869
|
+
if (force) this.onZones({ pre: [], post: [] });
|
|
36129
36870
|
return;
|
|
36130
36871
|
}
|
|
36131
|
-
const
|
|
36132
|
-
const
|
|
36133
|
-
|
|
36134
|
-
|
|
36135
|
-
|
|
36136
|
-
|
|
36137
|
-
|
|
36138
|
-
|
|
36139
|
-
if (!regular || !extended) return;
|
|
36140
|
-
this.onZones(deriveSessionZones(regular, extended));
|
|
36872
|
+
const from = Math.min(range.from, range.to);
|
|
36873
|
+
const to = Math.max(range.from, range.to);
|
|
36874
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return;
|
|
36875
|
+
if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
|
|
36876
|
+
const pad = Math.max(to - from, COVER_PAD_MIN_MS);
|
|
36877
|
+
const covered = { from: from - pad, to: to + pad };
|
|
36878
|
+
this.covered = covered;
|
|
36879
|
+
this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
|
|
36141
36880
|
}
|
|
36142
36881
|
};
|
|
36143
36882
|
|
|
@@ -36541,6 +37280,7 @@ var ChartCell = class {
|
|
|
36541
37280
|
this.inner.renderer.set("attribution", false);
|
|
36542
37281
|
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
36543
37282
|
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
37283
|
+
this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
|
|
36544
37284
|
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
36545
37285
|
this.history.onChart(this.inner);
|
|
36546
37286
|
this.inner.renderer.onConfigChanged(() => {
|
|
@@ -36574,7 +37314,11 @@ var ChartCell = class {
|
|
|
36574
37314
|
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
36575
37315
|
});
|
|
36576
37316
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36577
|
-
this.inner.on("load:end", () =>
|
|
37317
|
+
this.inner.on("load:end", () => {
|
|
37318
|
+
this.watermark?.setLoading(false);
|
|
37319
|
+
this.refreshSessionShading();
|
|
37320
|
+
});
|
|
37321
|
+
this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
|
|
36578
37322
|
const tz = deps.timezone();
|
|
36579
37323
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
36580
37324
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
@@ -36653,20 +37397,30 @@ var ChartCell = class {
|
|
|
36653
37397
|
this.refreshNativeCatalog();
|
|
36654
37398
|
this.pushSettingsSections();
|
|
36655
37399
|
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
36656
|
-
this.
|
|
36657
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
36658
|
-
this.state.timeframe = timeframe;
|
|
36659
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
36660
|
-
this.watermark?.update(symbol2, timeframe);
|
|
36661
|
-
this.statusline?.setSymbol(symbol2);
|
|
36662
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
36663
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37400
|
+
this.projectMarket(symbol2, timeframe);
|
|
36664
37401
|
this.refreshNativeCatalog();
|
|
36665
37402
|
this.refreshSessionAvailable();
|
|
36666
37403
|
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
36667
37404
|
this.deps.onMarketChanged(this.id);
|
|
36668
37405
|
});
|
|
36669
37406
|
}
|
|
37407
|
+
/**
|
|
37408
|
+
* Project a market identity into the cell state and its display overlays (watermark,
|
|
37409
|
+
* statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
|
|
37410
|
+
* optimistically from the cell setters — the labels reflect the pick immediately, not
|
|
37411
|
+
* after the bars load — and again from `market:changed` (the committed pass, and the
|
|
37412
|
+
* only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
|
|
37413
|
+
*/
|
|
37414
|
+
projectMarket(symbol, timeframe) {
|
|
37415
|
+
this.state.symbol = symbol;
|
|
37416
|
+
this.state.provider = parseSymbol(symbol).provider ?? void 0;
|
|
37417
|
+
this.state.timeframe = timeframe;
|
|
37418
|
+
this.state.session = normalizeSession(this.inner?.market.session);
|
|
37419
|
+
this.watermark?.update(symbol, timeframe);
|
|
37420
|
+
this.statusline?.setSymbol(symbol);
|
|
37421
|
+
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
|
|
37422
|
+
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37423
|
+
}
|
|
36670
37424
|
/**
|
|
36671
37425
|
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
36672
37426
|
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
@@ -36701,14 +37455,18 @@ var ChartCell = class {
|
|
|
36701
37455
|
this.refreshSessionShading();
|
|
36702
37456
|
});
|
|
36703
37457
|
}
|
|
36704
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The
|
|
36705
|
-
*
|
|
37458
|
+
/** (Re)derive the pre/post-market shading bands for this cell's market. The bands
|
|
37459
|
+
* expand locally from the symbol's session vocabulary, so they paint as soon as
|
|
37460
|
+
* metadata is known and follow any pan depth without provider round trips. */
|
|
36706
37461
|
refreshSessionShading() {
|
|
36707
37462
|
const chart = this.inner;
|
|
36708
37463
|
const symbol = this.state.symbol;
|
|
36709
37464
|
if (!chart || !symbol) return;
|
|
36710
|
-
const
|
|
36711
|
-
this.
|
|
37465
|
+
const now = Date.now();
|
|
37466
|
+
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
37467
|
+
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
37468
|
+
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
37469
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
36712
37470
|
}
|
|
36713
37471
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
36714
37472
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -36804,10 +37562,10 @@ var ChartCell = class {
|
|
|
36804
37562
|
id: "status-line",
|
|
36805
37563
|
rows: [
|
|
36806
37564
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
36807
|
-
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) =>
|
|
36808
|
-
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) =>
|
|
36809
|
-
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) =>
|
|
36810
|
-
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) =>
|
|
37565
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
|
|
37566
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
|
|
37567
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
|
|
37568
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
|
|
36811
37569
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
36812
37570
|
{
|
|
36813
37571
|
kind: "toggle",
|
|
@@ -36842,12 +37600,40 @@ var ChartCell = class {
|
|
|
36842
37600
|
this.indicatorTitlesOn = visible;
|
|
36843
37601
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
36844
37602
|
this.deps.onStateDirty();
|
|
37603
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
36845
37604
|
}
|
|
36846
37605
|
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
36847
37606
|
setIndicatorValuesVisible(visible) {
|
|
36848
37607
|
this.indicatorValuesOn = visible;
|
|
36849
37608
|
this.inner?.renderer.set("indicatorValues", visible);
|
|
36850
37609
|
this.deps.onStateDirty();
|
|
37610
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37611
|
+
}
|
|
37612
|
+
/** Show/hide one status-line segment (the settings dialog's Status line tab). */
|
|
37613
|
+
setStatuslinePart(part, visible) {
|
|
37614
|
+
this.statusline?.setPartVisible(part, visible);
|
|
37615
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37616
|
+
}
|
|
37617
|
+
/** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
|
|
37618
|
+
statusPrefs() {
|
|
37619
|
+
const sl = this.statusline;
|
|
37620
|
+
return {
|
|
37621
|
+
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
37622
|
+
indicatorTitles: this.indicatorTitlesOn,
|
|
37623
|
+
indicatorValues: this.indicatorValuesOn
|
|
37624
|
+
};
|
|
37625
|
+
}
|
|
37626
|
+
/** Converge this cell's Status line tab prefs to `prefs` — the follower half of
|
|
37627
|
+
* the workspace's style link. Idempotent: matching values change nothing, so a
|
|
37628
|
+
* propagated echo dies on its own. */
|
|
37629
|
+
applyStatusPrefs(prefs) {
|
|
37630
|
+
if (prefs.parts && this.statusline) {
|
|
37631
|
+
for (const part of Object.keys(prefs.parts)) {
|
|
37632
|
+
if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
|
|
37633
|
+
}
|
|
37634
|
+
}
|
|
37635
|
+
if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
|
|
37636
|
+
if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
|
|
36851
37637
|
}
|
|
36852
37638
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
36853
37639
|
* identity is what endures; the chart dies with the cell). */
|
|
@@ -36869,17 +37655,23 @@ var ChartCell = class {
|
|
|
36869
37655
|
get indicatorCount() {
|
|
36870
37656
|
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
36871
37657
|
}
|
|
36872
|
-
/** Switch this cell's market in place (the chart instance survives).
|
|
37658
|
+
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
37659
|
+
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
37660
|
+
* the setMarket call so the statusline reads the already-blanked chart. */
|
|
36873
37661
|
setSymbol(symbol) {
|
|
36874
37662
|
if (!this.inner || symbol === this.symbol) return;
|
|
36875
37663
|
this.unresolvedToasted = null;
|
|
36876
37664
|
void this.inner.setMarket({ symbol });
|
|
37665
|
+
this.projectMarket(symbol, this.timeframe);
|
|
37666
|
+
this.deps.onMarketChanged(this.id);
|
|
36877
37667
|
}
|
|
36878
37668
|
setTimeframe(timeframe) {
|
|
36879
37669
|
if (!this.inner || timeframe === this.timeframe) return;
|
|
36880
37670
|
this.activeRangeId = null;
|
|
36881
37671
|
this.rangeBars = 0;
|
|
36882
37672
|
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
37673
|
+
this.projectMarket(this.symbol, timeframe);
|
|
37674
|
+
this.deps.onMarketChanged(this.id);
|
|
36883
37675
|
}
|
|
36884
37676
|
/** Applied live (renderer feature) — no reload. */
|
|
36885
37677
|
setPriceStyle(style) {
|
|
@@ -37254,16 +38046,21 @@ var ChartCell = class {
|
|
|
37254
38046
|
|
|
37255
38047
|
// src/workspace/context.ts
|
|
37256
38048
|
function buildContext(host) {
|
|
37257
|
-
const active = host.active();
|
|
37258
38049
|
return {
|
|
37259
38050
|
get chart() {
|
|
37260
38051
|
const cell = host.active();
|
|
37261
38052
|
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
37262
38053
|
return cell.chart;
|
|
37263
38054
|
},
|
|
37264
|
-
|
|
37265
|
-
|
|
37266
|
-
|
|
38055
|
+
get symbol() {
|
|
38056
|
+
return host.active()?.symbol ?? "";
|
|
38057
|
+
},
|
|
38058
|
+
get timeframe() {
|
|
38059
|
+
return host.active()?.timeframe ?? "60";
|
|
38060
|
+
},
|
|
38061
|
+
get priceStyle() {
|
|
38062
|
+
return host.active()?.priceStyle ?? "candles";
|
|
38063
|
+
},
|
|
37267
38064
|
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
37268
38065
|
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
37269
38066
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
@@ -37274,8 +38071,12 @@ function buildContext(host) {
|
|
|
37274
38071
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
37275
38072
|
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
37276
38073
|
stateChanged: () => host.stateDirty(),
|
|
37277
|
-
|
|
37278
|
-
|
|
38074
|
+
get cells() {
|
|
38075
|
+
return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
|
|
38076
|
+
},
|
|
38077
|
+
get activeCellId() {
|
|
38078
|
+
return host.active()?.id ?? "";
|
|
38079
|
+
},
|
|
37279
38080
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
37280
38081
|
};
|
|
37281
38082
|
}
|
|
@@ -37614,6 +38415,10 @@ var VelaWorkspace = class {
|
|
|
37614
38415
|
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
37615
38416
|
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
37616
38417
|
this.drawingSyncBusy = false;
|
|
38418
|
+
/** Same guard for the style link: a follower's `applyConfig` re-fires its
|
|
38419
|
+
* `onConfigChanged` in the same tick, and a state restore applies per-cell
|
|
38420
|
+
* configs that legitimately differ — neither must propagate. */
|
|
38421
|
+
this.styleSyncBusy = false;
|
|
37617
38422
|
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
37618
38423
|
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
37619
38424
|
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
@@ -37674,9 +38479,7 @@ var VelaWorkspace = class {
|
|
|
37674
38479
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
37675
38480
|
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
37676
38481
|
const sync = boot?.sync ?? opts.sync;
|
|
37677
|
-
for (const kind of
|
|
37678
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
37679
|
-
}
|
|
38482
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
|
|
37680
38483
|
this.monoLayout = opts.layout === false;
|
|
37681
38484
|
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
37682
38485
|
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
@@ -37760,7 +38563,8 @@ var VelaWorkspace = class {
|
|
|
37760
38563
|
syncs: () => [
|
|
37761
38564
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
37762
38565
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
37763
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
38566
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
38567
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
37764
38568
|
],
|
|
37765
38569
|
onToggleSync: (id) => {
|
|
37766
38570
|
const kind = id;
|
|
@@ -37979,7 +38783,10 @@ var VelaWorkspace = class {
|
|
|
37979
38783
|
this.topbar.renderActions();
|
|
37980
38784
|
this.mobileBar?.renderActions();
|
|
37981
38785
|
this.dock.refresh();
|
|
37982
|
-
for (const cell of this.cells())
|
|
38786
|
+
for (const cell of this.cells()) {
|
|
38787
|
+
cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
38788
|
+
cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
|
|
38789
|
+
}
|
|
37983
38790
|
}
|
|
37984
38791
|
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
37985
38792
|
/**
|
|
@@ -38044,7 +38851,7 @@ var VelaWorkspace = class {
|
|
|
38044
38851
|
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
38045
38852
|
}
|
|
38046
38853
|
this.dock.applyState(st.panels);
|
|
38047
|
-
for (const kind of
|
|
38854
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
38048
38855
|
this.trackSizes.clear();
|
|
38049
38856
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
38050
38857
|
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
@@ -38055,9 +38862,14 @@ var VelaWorkspace = class {
|
|
|
38055
38862
|
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
38056
38863
|
}
|
|
38057
38864
|
this.drawingLinks.clear();
|
|
38058
|
-
|
|
38059
|
-
|
|
38060
|
-
this.
|
|
38865
|
+
this.styleSyncBusy = true;
|
|
38866
|
+
try {
|
|
38867
|
+
for (const [i] of this.def.cells.entries()) {
|
|
38868
|
+
const { id, ...cs } = st.charts[i];
|
|
38869
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
38870
|
+
}
|
|
38871
|
+
} finally {
|
|
38872
|
+
this.styleSyncBusy = false;
|
|
38061
38873
|
}
|
|
38062
38874
|
if (st.timezone) this.setTimezone(st.timezone);
|
|
38063
38875
|
this.pool.clear();
|
|
@@ -38157,6 +38969,7 @@ var VelaWorkspace = class {
|
|
|
38157
38969
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
38158
38970
|
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
38159
38971
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
38972
|
+
const preexisting = new Set(this.cellsById.keys());
|
|
38160
38973
|
for (const [id, cell] of [...this.cellsById]) {
|
|
38161
38974
|
if (!keep.has(id) || rebuildAll) {
|
|
38162
38975
|
this.poolSet(id, cell.dehydrate());
|
|
@@ -38169,6 +38982,7 @@ var VelaWorkspace = class {
|
|
|
38169
38982
|
this.cellBackend = nextBackend;
|
|
38170
38983
|
this.applyGrid();
|
|
38171
38984
|
this.buildCells();
|
|
38985
|
+
this.alignNewCellStyles(preexisting);
|
|
38172
38986
|
this.syncCellPresentation();
|
|
38173
38987
|
this.topbar.setLayout(next.id);
|
|
38174
38988
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
@@ -38372,6 +39186,7 @@ var VelaWorkspace = class {
|
|
|
38372
39186
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
38373
39187
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
38374
39188
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
39189
|
+
onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
|
|
38375
39190
|
onStateDirty: () => this.markStateDirty(),
|
|
38376
39191
|
manifestSettled: () => this.manifestSettled,
|
|
38377
39192
|
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
@@ -38447,6 +39262,7 @@ var VelaWorkspace = class {
|
|
|
38447
39262
|
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
38448
39263
|
});
|
|
38449
39264
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
39265
|
+
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
38450
39266
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
38451
39267
|
chart.renderer.onAxisLongPress((e) => {
|
|
38452
39268
|
if (this.layoutCtl.current !== "mobile") return;
|
|
@@ -38479,11 +39295,67 @@ var VelaWorkspace = class {
|
|
|
38479
39295
|
if (kind === "viewport") {
|
|
38480
39296
|
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
38481
39297
|
if (range) this.propagateViewport(this.activeId, range);
|
|
39298
|
+
} else if (kind === "style") {
|
|
39299
|
+
this.propagateStylePrefs(this.activeId);
|
|
38482
39300
|
} else {
|
|
38483
39301
|
this.propagateMarket(this.activeId);
|
|
38484
39302
|
}
|
|
38485
39303
|
}
|
|
38486
39304
|
}
|
|
39305
|
+
/**
|
|
39306
|
+
* Align cells minted by a layout change to their style group: with the link on, a
|
|
39307
|
+
* NEW cell (fresh slot or one returning from the pool, which missed edits while
|
|
39308
|
+
* dormant) inherits the presentation of a pre-existing group peer — the active
|
|
39309
|
+
* cell when it is one — instead of sitting on its own state beside a styled
|
|
39310
|
+
* group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
|
|
39311
|
+
* the group, and the equality short-circuits keep converged peers untouched.
|
|
39312
|
+
*/
|
|
39313
|
+
alignNewCellStyles(preexisting) {
|
|
39314
|
+
const setting = this.syncOpts.style;
|
|
39315
|
+
if (!setting) return;
|
|
39316
|
+
const ids = [...this.cellsById.keys()];
|
|
39317
|
+
const propagated = /* @__PURE__ */ new Set();
|
|
39318
|
+
for (const id of ids) {
|
|
39319
|
+
if (preexisting.has(id)) continue;
|
|
39320
|
+
const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
|
|
39321
|
+
if (peers.length === 0) continue;
|
|
39322
|
+
const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
|
|
39323
|
+
if (propagated.has(source)) continue;
|
|
39324
|
+
propagated.add(source);
|
|
39325
|
+
this.propagateStylePrefs(source);
|
|
39326
|
+
}
|
|
39327
|
+
}
|
|
39328
|
+
/**
|
|
39329
|
+
* Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
|
|
39330
|
+
* its renderer config plus its Status line tab prefs — onto its same-group
|
|
39331
|
+
* followers (the style link). Loop-safe two ways: the busy guard eats the
|
|
39332
|
+
* followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
|
|
39333
|
+
* in the same tick), and the equality short-circuits leave already-converged
|
|
39334
|
+
* followers untouched, so nothing re-emits once the group agrees.
|
|
39335
|
+
*/
|
|
39336
|
+
propagateStylePrefs(originId) {
|
|
39337
|
+
if (this.styleSyncBusy || this.destroyed) return;
|
|
39338
|
+
const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
|
|
39339
|
+
if (targets.length === 0) return;
|
|
39340
|
+
const origin = this.cellsById.get(originId);
|
|
39341
|
+
if (!origin) return;
|
|
39342
|
+
const slice = styleConfigSlice(origin.chart.renderer.getConfig());
|
|
39343
|
+
const sliceJson = slice ? JSON.stringify(slice) : null;
|
|
39344
|
+
const prefs = origin.statusPrefs();
|
|
39345
|
+
this.styleSyncBusy = true;
|
|
39346
|
+
try {
|
|
39347
|
+
for (const id of targets) {
|
|
39348
|
+
const cell = this.cellsById.get(id);
|
|
39349
|
+
if (!cell) continue;
|
|
39350
|
+
if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
|
|
39351
|
+
cell.chart.renderer.applyConfig(slice);
|
|
39352
|
+
}
|
|
39353
|
+
cell.applyStatusPrefs(prefs);
|
|
39354
|
+
}
|
|
39355
|
+
} finally {
|
|
39356
|
+
this.styleSyncBusy = false;
|
|
39357
|
+
}
|
|
39358
|
+
}
|
|
38487
39359
|
/**
|
|
38488
39360
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
38489
39361
|
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
@@ -38791,7 +39663,8 @@ var VelaWorkspace = class {
|
|
|
38791
39663
|
syncs: () => [
|
|
38792
39664
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
38793
39665
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
38794
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
39666
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
39667
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
38795
39668
|
],
|
|
38796
39669
|
onToggleSync: (id) => {
|
|
38797
39670
|
const kind = id;
|