@luxalgo/vela 0.6.7 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WVP4XV5A.js → chunk-N7LGMKCE.js} +303 -78
- package/dist/{chunk-6WFKTAT4.js → chunk-SQETIBFU.js} +46 -8
- package/dist/{contributions-Z12BrWCy.d.ts → contributions-DLLdV9jD.d.ts} +13 -0
- package/dist/{contributions-Hv4_cOJo.d.cts → contributions-Vw-Hm58R.d.cts} +13 -0
- package/dist/index.cjs +46 -8
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{plugin-BXhDHTYn.d.cts → plugin-BnJgjLAy.d.cts} +1 -1
- package/dist/{plugin-9koBjc5H.d.ts → plugin-CC7rBrOY.d.ts} +1 -1
- package/dist/plugin.d.cts +2 -2
- package/dist/plugin.d.ts +2 -2
- package/dist/{bottombar-Br4rzx_R.d.ts → statusline-CGkB2EOo.d.ts} +87 -4
- package/dist/{bottombar-Div5zibZ.d.cts → statusline-DqzBqy42.d.cts} +87 -4
- package/dist/vela.global.js +46 -8
- package/dist/vela.global.min.js +5 -5
- package/dist/widget.cjs +348 -85
- package/dist/widget.d.cts +10 -83
- package/dist/widget.d.ts +10 -83
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +348 -85
- package/dist/workspace.d.cts +52 -8
- package/dist/workspace.d.ts +52 -8
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
package/dist/widget.cjs
CHANGED
|
@@ -5254,6 +5254,11 @@ var Topbar = class {
|
|
|
5254
5254
|
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
5255
5255
|
* render through the flow cluster like ordinary actions. */
|
|
5256
5256
|
this.flowingOverrides = /* @__PURE__ */ new Set();
|
|
5257
|
+
/** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
|
|
5258
|
+
* renderActions pass (contributed buttons are replaceChildren'd away). */
|
|
5259
|
+
this.actionTooltips = [];
|
|
5260
|
+
/** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
|
|
5261
|
+
this.warnedIconless = /* @__PURE__ */ new Set();
|
|
5257
5262
|
this.onHairlineSync = () => {
|
|
5258
5263
|
if (this.hairlineRaf) return;
|
|
5259
5264
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -5516,6 +5521,8 @@ var Topbar = class {
|
|
|
5516
5521
|
this.actionsHost.replaceChildren();
|
|
5517
5522
|
this.leftActionsHost.replaceChildren();
|
|
5518
5523
|
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
5524
|
+
for (const t of this.actionTooltips) t.destroy();
|
|
5525
|
+
this.actionTooltips = [];
|
|
5519
5526
|
const doc = this.actionsHost.ownerDocument;
|
|
5520
5527
|
const flowLeft = this.comp.left.includes("actions");
|
|
5521
5528
|
const flowRight = this.comp.right.includes("actions");
|
|
@@ -5525,10 +5532,20 @@ var Topbar = class {
|
|
|
5525
5532
|
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
5526
5533
|
const left = pin ? pin.left : action.align === "left";
|
|
5527
5534
|
if (!pin && !(left ? flowLeft : flowRight)) continue;
|
|
5535
|
+
const iconOnly = action.iconOnly === true && !!action.icon;
|
|
5536
|
+
if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
|
|
5537
|
+
this.warnedIconless.add(action.id);
|
|
5538
|
+
console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
|
|
5539
|
+
}
|
|
5528
5540
|
const b = doc.createElement("button");
|
|
5529
|
-
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
5541
|
+
b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
|
|
5530
5542
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
5531
|
-
|
|
5543
|
+
if (iconOnly) {
|
|
5544
|
+
b.setAttribute("aria-label", action.label);
|
|
5545
|
+
this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
|
|
5546
|
+
} else {
|
|
5547
|
+
b.appendChild(doc.createTextNode(action.label));
|
|
5548
|
+
}
|
|
5532
5549
|
b.addEventListener("click", () => {
|
|
5533
5550
|
const c = this.opts.getContext?.();
|
|
5534
5551
|
if (c) action.run(c);
|
|
@@ -5579,7 +5596,7 @@ var Topbar = class {
|
|
|
5579
5596
|
this.tfMenu.destroy();
|
|
5580
5597
|
this.styleMenu.destroy();
|
|
5581
5598
|
this.layoutPicker?.destroy();
|
|
5582
|
-
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
5599
|
+
for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
|
|
5583
5600
|
this.el.remove();
|
|
5584
5601
|
}
|
|
5585
5602
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
@@ -17315,19 +17332,8 @@ function registerBuiltinChartTypes() {
|
|
|
17315
17332
|
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
17316
17333
|
}
|
|
17317
17334
|
|
|
17318
|
-
// src/workspace/sync.ts
|
|
17319
|
-
function syncTargets(originId, setting, cellIds) {
|
|
17320
|
-
if (setting == null || setting === false) return [];
|
|
17321
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17322
|
-
const group = setting[originId];
|
|
17323
|
-
if (group == null) return [];
|
|
17324
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17325
|
-
}
|
|
17326
|
-
function rangesWithin(a, b, epsMs) {
|
|
17327
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17328
|
-
}
|
|
17329
|
-
|
|
17330
17335
|
// src/state/document.ts
|
|
17336
|
+
var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
|
|
17331
17337
|
function prefixedSymbol(cell) {
|
|
17332
17338
|
if (!cell?.symbol) return void 0;
|
|
17333
17339
|
if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
|
|
@@ -17415,7 +17421,7 @@ function sanitizeSync(raw) {
|
|
|
17415
17421
|
if (raw == null || typeof raw !== "object") return null;
|
|
17416
17422
|
const s = raw;
|
|
17417
17423
|
const out = {};
|
|
17418
|
-
for (const kind of
|
|
17424
|
+
for (const kind of SYNC_KINDS) {
|
|
17419
17425
|
const v = s[kind];
|
|
17420
17426
|
if (v === true) out[kind] = true;
|
|
17421
17427
|
else if (v != null && typeof v === "object") {
|
|
@@ -17458,6 +17464,28 @@ function sanitizeTrackSizes(raw) {
|
|
|
17458
17464
|
return Object.keys(out).length > 0 ? out : null;
|
|
17459
17465
|
}
|
|
17460
17466
|
|
|
17467
|
+
// src/workspace/sync.ts
|
|
17468
|
+
function syncTargets(originId, setting, cellIds) {
|
|
17469
|
+
if (setting == null || setting === false) return [];
|
|
17470
|
+
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
17471
|
+
const group = setting[originId];
|
|
17472
|
+
if (group == null) return [];
|
|
17473
|
+
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
17474
|
+
}
|
|
17475
|
+
function rangesWithin(a, b, epsMs) {
|
|
17476
|
+
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
17477
|
+
}
|
|
17478
|
+
var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
|
|
17479
|
+
function styleConfigSlice(config) {
|
|
17480
|
+
if (config == null || typeof config !== "object") return null;
|
|
17481
|
+
const doc = config;
|
|
17482
|
+
const out = {};
|
|
17483
|
+
for (const key of STYLE_SYNC_CONFIG_KEYS) {
|
|
17484
|
+
if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
|
|
17485
|
+
}
|
|
17486
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
17487
|
+
}
|
|
17488
|
+
|
|
17461
17489
|
// src/widget/persist.ts
|
|
17462
17490
|
function localStorageAdapter(storageKey) {
|
|
17463
17491
|
return {
|
|
@@ -19798,12 +19826,16 @@ var IndicatorInputsDialog = class {
|
|
|
19798
19826
|
closeOnEscape: false,
|
|
19799
19827
|
footer: (foot) => {
|
|
19800
19828
|
foot.append(
|
|
19829
|
+
this.resetAction(),
|
|
19801
19830
|
this.dialogButton("Cancel", false, () => this.revertAndClose()),
|
|
19802
19831
|
this.dialogButton("Ok", true, () => this.close())
|
|
19803
19832
|
);
|
|
19804
19833
|
},
|
|
19834
|
+
// Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
|
|
19835
|
+
// asynchronously — after a reset rebuild that notification must not close
|
|
19836
|
+
// the replacement dialog.
|
|
19805
19837
|
onOpenChange: (open2) => {
|
|
19806
|
-
if (!open2) this.close();
|
|
19838
|
+
if (!open2 && this.uiDialog === ui) this.close();
|
|
19807
19839
|
}
|
|
19808
19840
|
});
|
|
19809
19841
|
applyChromeTokens(ui.panel, t);
|
|
@@ -19934,6 +19966,30 @@ var IndicatorInputsDialog = class {
|
|
|
19934
19966
|
}
|
|
19935
19967
|
this.close();
|
|
19936
19968
|
}
|
|
19969
|
+
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
19970
|
+
* (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
|
|
19971
|
+
resetAction() {
|
|
19972
|
+
const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
|
|
19973
|
+
b.style.marginRight = "auto";
|
|
19974
|
+
return b;
|
|
19975
|
+
}
|
|
19976
|
+
/** Restore every input to its declared default (re-running the indicator), then
|
|
19977
|
+
* re-open the form so each control re-reads the restored values — the same
|
|
19978
|
+
* rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
|
|
19979
|
+
* survives the rebuild, so Cancel after a reset still reverts the whole session. */
|
|
19980
|
+
resetToDefaults() {
|
|
19981
|
+
const row = this.row;
|
|
19982
|
+
if (!row) return;
|
|
19983
|
+
const snap = this.snapshot;
|
|
19984
|
+
for (const inp of row.inputs) {
|
|
19985
|
+
if (row.values[inp.key] !== inp.defval) {
|
|
19986
|
+
row.values[inp.key] = inp.defval;
|
|
19987
|
+
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
19988
|
+
}
|
|
19989
|
+
}
|
|
19990
|
+
this.open(row);
|
|
19991
|
+
if (snap) this.snapshot = snap;
|
|
19992
|
+
}
|
|
19937
19993
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
19938
19994
|
commit(row, key, value) {
|
|
19939
19995
|
row.values[key] = value;
|
|
@@ -31367,6 +31423,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
31367
31423
|
}
|
|
31368
31424
|
|
|
31369
31425
|
// src/renderers/native/backdrop/BackdropRenderer.ts
|
|
31426
|
+
function clipHighlightRect(x1, x2, left, right) {
|
|
31427
|
+
const x = Math.max(left, x1);
|
|
31428
|
+
const end = Math.min(right, x2);
|
|
31429
|
+
return end > x ? { x, width: end - x } : null;
|
|
31430
|
+
}
|
|
31370
31431
|
var BackdropRenderer = class {
|
|
31371
31432
|
constructor() {
|
|
31372
31433
|
this.canvas = null;
|
|
@@ -31399,17 +31460,22 @@ var BackdropRenderer = class {
|
|
|
31399
31460
|
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
31400
31461
|
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
31401
31462
|
drawHighlights(ctx, scene, coords) {
|
|
31402
|
-
const
|
|
31403
|
-
if (
|
|
31463
|
+
const sessions = scene.sessionHighlightBands();
|
|
31464
|
+
if (sessions.length > 0) {
|
|
31465
|
+
const left = Math.max(0, coords.logicalToX(-0.5));
|
|
31466
|
+
const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
|
|
31467
|
+
this.drawHighlightSet(ctx, sessions, coords, left, right);
|
|
31468
|
+
}
|
|
31469
|
+
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
|
|
31470
|
+
}
|
|
31471
|
+
drawHighlightSet(ctx, bands, coords, left, right) {
|
|
31404
31472
|
for (const band of bands) {
|
|
31405
31473
|
const x1 = coords.timeToX(band.from);
|
|
31406
31474
|
const x2 = coords.timeToX(band.to);
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
31410
|
-
if (cw <= 0) continue;
|
|
31475
|
+
const rect = clipHighlightRect(x1, x2, left, right);
|
|
31476
|
+
if (!rect) continue;
|
|
31411
31477
|
ctx.fillStyle = band.color;
|
|
31412
|
-
ctx.fillRect(
|
|
31478
|
+
ctx.fillRect(rect.x, 0, rect.width, coords.height);
|
|
31413
31479
|
}
|
|
31414
31480
|
}
|
|
31415
31481
|
// ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
|
|
@@ -36151,61 +36217,144 @@ var MarketStatusTracker = class {
|
|
|
36151
36217
|
};
|
|
36152
36218
|
|
|
36153
36219
|
// src/widget/session-shading.ts
|
|
36154
|
-
|
|
36220
|
+
var DAY_MS2 = 864e5;
|
|
36221
|
+
var MINUTE_MS = 6e4;
|
|
36222
|
+
function parseWindow(text) {
|
|
36223
|
+
if (typeof text !== "string") return null;
|
|
36224
|
+
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
36225
|
+
if (!m) return null;
|
|
36226
|
+
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
36227
|
+
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
36228
|
+
if (start >= end || end > 1440) return null;
|
|
36229
|
+
return { start, end };
|
|
36230
|
+
}
|
|
36231
|
+
function parseSessionSpec(si) {
|
|
36232
|
+
const session = si?.["session"];
|
|
36233
|
+
if (typeof session !== "string" || session === "" || session === "24x7") return null;
|
|
36234
|
+
const regular = parseWindow(session);
|
|
36235
|
+
if (!regular) return null;
|
|
36236
|
+
const tz = si?.["timezone"];
|
|
36237
|
+
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
36238
|
+
const ext = parseWindow(si?.["session_extended"]);
|
|
36239
|
+
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
36240
|
+
return { regular, extended, timezone };
|
|
36241
|
+
}
|
|
36242
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
36243
|
+
function civilFormatter(tz) {
|
|
36244
|
+
const cached = dtfCache.get(tz);
|
|
36245
|
+
if (cached !== void 0) return cached;
|
|
36246
|
+
let dtf = null;
|
|
36247
|
+
try {
|
|
36248
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
36249
|
+
timeZone: tz,
|
|
36250
|
+
hourCycle: "h23",
|
|
36251
|
+
weekday: "short",
|
|
36252
|
+
year: "numeric",
|
|
36253
|
+
month: "2-digit",
|
|
36254
|
+
day: "2-digit",
|
|
36255
|
+
hour: "2-digit",
|
|
36256
|
+
minute: "2-digit"
|
|
36257
|
+
});
|
|
36258
|
+
} catch {
|
|
36259
|
+
dtf = null;
|
|
36260
|
+
}
|
|
36261
|
+
dtfCache.set(tz, dtf);
|
|
36262
|
+
return dtf;
|
|
36263
|
+
}
|
|
36264
|
+
function civilParts(dtf, ms) {
|
|
36265
|
+
const parts = dtf.formatToParts(ms);
|
|
36266
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
36267
|
+
return {
|
|
36268
|
+
weekday: get("weekday"),
|
|
36269
|
+
year: Number(get("year")),
|
|
36270
|
+
month: Number(get("month")),
|
|
36271
|
+
day: Number(get("day")),
|
|
36272
|
+
hour: Number(get("hour")) % 24,
|
|
36273
|
+
minute: Number(get("minute"))
|
|
36274
|
+
};
|
|
36275
|
+
}
|
|
36276
|
+
function expandSessionZones(spec, from, to) {
|
|
36155
36277
|
const pre = [];
|
|
36156
36278
|
const post = [];
|
|
36157
|
-
|
|
36158
|
-
|
|
36159
|
-
|
|
36160
|
-
|
|
36161
|
-
|
|
36162
|
-
|
|
36163
|
-
|
|
36164
|
-
|
|
36279
|
+
const dtf = civilFormatter(spec.timezone);
|
|
36280
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
36281
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36282
|
+
for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
|
|
36283
|
+
const civil = civilParts(dtf, cursor);
|
|
36284
|
+
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
36285
|
+
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
36286
|
+
seen.add(key);
|
|
36287
|
+
if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
|
|
36288
|
+
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
36289
|
+
const noonGuess = naive + 720 * MINUTE_MS;
|
|
36290
|
+
const atNoon = civilParts(dtf, noonGuess);
|
|
36291
|
+
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
36292
|
+
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
36293
|
+
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
36294
|
+
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
36165
36295
|
}
|
|
36166
36296
|
return { pre, post };
|
|
36167
36297
|
}
|
|
36168
|
-
var
|
|
36169
|
-
var MIN_LOOKBACK_MS = 3 * 864e5;
|
|
36170
|
-
var MAX_LOOKBACK_MS = 120 * 864e5;
|
|
36298
|
+
var COVER_PAD_MIN_MS = 2 * DAY_MS2;
|
|
36171
36299
|
var SessionShadingTracker = class {
|
|
36172
36300
|
constructor(onZones) {
|
|
36173
36301
|
this.onZones = onZones;
|
|
36174
|
-
/** Invalidates
|
|
36302
|
+
/** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
|
|
36175
36303
|
this.epoch = 0;
|
|
36176
|
-
|
|
36177
|
-
|
|
36304
|
+
this.spec = null;
|
|
36305
|
+
this.ready = false;
|
|
36306
|
+
this.session = "regular";
|
|
36307
|
+
this.covered = null;
|
|
36308
|
+
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
36309
|
+
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
36310
|
+
this.lastRange = null;
|
|
36311
|
+
}
|
|
36312
|
+
/** (Re)bind to a chart's data surface + market and expand once metadata lands. */
|
|
36178
36313
|
track(data, symbol, opts) {
|
|
36179
36314
|
const my = ++this.epoch;
|
|
36180
|
-
|
|
36315
|
+
this.ready = false;
|
|
36316
|
+
this.spec = null;
|
|
36317
|
+
this.covered = null;
|
|
36318
|
+
this.session = opts.session;
|
|
36319
|
+
this.lastRange = opts.range;
|
|
36320
|
+
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
36321
|
+
if (my !== this.epoch) return;
|
|
36322
|
+
this.ready = true;
|
|
36323
|
+
this.spec = parseSessionSpec(si);
|
|
36324
|
+
this.emit(this.lastRange ?? opts.range, true);
|
|
36325
|
+
});
|
|
36326
|
+
}
|
|
36327
|
+
/** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
|
|
36328
|
+
* leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
|
|
36329
|
+
updateRange(range) {
|
|
36330
|
+
this.lastRange = range;
|
|
36331
|
+
if (!this.ready) return;
|
|
36332
|
+
this.emit(range, false);
|
|
36181
36333
|
}
|
|
36182
36334
|
stop() {
|
|
36183
36335
|
this.epoch += 1;
|
|
36184
|
-
|
|
36185
|
-
|
|
36186
|
-
|
|
36187
|
-
|
|
36188
|
-
|
|
36189
|
-
|
|
36190
|
-
|
|
36191
|
-
|
|
36192
|
-
this.onZones(null);
|
|
36336
|
+
this.ready = false;
|
|
36337
|
+
this.spec = null;
|
|
36338
|
+
this.covered = null;
|
|
36339
|
+
this.lastRange = null;
|
|
36340
|
+
}
|
|
36341
|
+
emit(range, force) {
|
|
36342
|
+
if (!this.spec) {
|
|
36343
|
+
if (force) this.onZones(null);
|
|
36193
36344
|
return;
|
|
36194
36345
|
}
|
|
36195
|
-
if (
|
|
36196
|
-
this.onZones({ pre: [], post: [] });
|
|
36346
|
+
if (this.session !== "extended") {
|
|
36347
|
+
if (force) this.onZones({ pre: [], post: [] });
|
|
36197
36348
|
return;
|
|
36198
36349
|
}
|
|
36199
|
-
const
|
|
36200
|
-
const
|
|
36201
|
-
|
|
36202
|
-
|
|
36203
|
-
|
|
36204
|
-
|
|
36205
|
-
|
|
36206
|
-
|
|
36207
|
-
if (!regular || !extended) return;
|
|
36208
|
-
this.onZones(deriveSessionZones(regular, extended));
|
|
36350
|
+
const from = Math.min(range.from, range.to);
|
|
36351
|
+
const to = Math.max(range.from, range.to);
|
|
36352
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return;
|
|
36353
|
+
if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
|
|
36354
|
+
const pad = Math.max(to - from, COVER_PAD_MIN_MS);
|
|
36355
|
+
const covered = { from: from - pad, to: to + pad };
|
|
36356
|
+
this.covered = covered;
|
|
36357
|
+
this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
|
|
36209
36358
|
}
|
|
36210
36359
|
};
|
|
36211
36360
|
|
|
@@ -36642,7 +36791,11 @@ var ChartCell = class {
|
|
|
36642
36791
|
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
36643
36792
|
});
|
|
36644
36793
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36645
|
-
this.inner.on("load:end", () =>
|
|
36794
|
+
this.inner.on("load:end", () => {
|
|
36795
|
+
this.watermark?.setLoading(false);
|
|
36796
|
+
this.refreshSessionShading();
|
|
36797
|
+
});
|
|
36798
|
+
this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
|
|
36646
36799
|
const tz = deps.timezone();
|
|
36647
36800
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
36648
36801
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
@@ -36769,14 +36922,18 @@ var ChartCell = class {
|
|
|
36769
36922
|
this.refreshSessionShading();
|
|
36770
36923
|
});
|
|
36771
36924
|
}
|
|
36772
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The
|
|
36773
|
-
*
|
|
36925
|
+
/** (Re)derive the pre/post-market shading bands for this cell's market. The bands
|
|
36926
|
+
* expand locally from the symbol's session vocabulary, so they paint as soon as
|
|
36927
|
+
* metadata is known and follow any pan depth without provider round trips. */
|
|
36774
36928
|
refreshSessionShading() {
|
|
36775
36929
|
const chart = this.inner;
|
|
36776
36930
|
const symbol = this.state.symbol;
|
|
36777
36931
|
if (!chart || !symbol) return;
|
|
36778
|
-
const
|
|
36779
|
-
this.
|
|
36932
|
+
const now = Date.now();
|
|
36933
|
+
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
36934
|
+
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
36935
|
+
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
36936
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, range });
|
|
36780
36937
|
}
|
|
36781
36938
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
36782
36939
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -36872,10 +37029,10 @@ var ChartCell = class {
|
|
|
36872
37029
|
id: "status-line",
|
|
36873
37030
|
rows: [
|
|
36874
37031
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
36875
|
-
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) =>
|
|
36876
|
-
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) =>
|
|
36877
|
-
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) =>
|
|
36878
|
-
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) =>
|
|
37032
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
|
|
37033
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
|
|
37034
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
|
|
37035
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
|
|
36879
37036
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
36880
37037
|
{
|
|
36881
37038
|
kind: "toggle",
|
|
@@ -36910,12 +37067,40 @@ var ChartCell = class {
|
|
|
36910
37067
|
this.indicatorTitlesOn = visible;
|
|
36911
37068
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
36912
37069
|
this.deps.onStateDirty();
|
|
37070
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
36913
37071
|
}
|
|
36914
37072
|
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
36915
37073
|
setIndicatorValuesVisible(visible) {
|
|
36916
37074
|
this.indicatorValuesOn = visible;
|
|
36917
37075
|
this.inner?.renderer.set("indicatorValues", visible);
|
|
36918
37076
|
this.deps.onStateDirty();
|
|
37077
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37078
|
+
}
|
|
37079
|
+
/** Show/hide one status-line segment (the settings dialog's Status line tab). */
|
|
37080
|
+
setStatuslinePart(part, visible) {
|
|
37081
|
+
this.statusline?.setPartVisible(part, visible);
|
|
37082
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
37083
|
+
}
|
|
37084
|
+
/** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
|
|
37085
|
+
statusPrefs() {
|
|
37086
|
+
const sl = this.statusline;
|
|
37087
|
+
return {
|
|
37088
|
+
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
37089
|
+
indicatorTitles: this.indicatorTitlesOn,
|
|
37090
|
+
indicatorValues: this.indicatorValuesOn
|
|
37091
|
+
};
|
|
37092
|
+
}
|
|
37093
|
+
/** Converge this cell's Status line tab prefs to `prefs` — the follower half of
|
|
37094
|
+
* the workspace's style link. Idempotent: matching values change nothing, so a
|
|
37095
|
+
* propagated echo dies on its own. */
|
|
37096
|
+
applyStatusPrefs(prefs) {
|
|
37097
|
+
if (prefs.parts && this.statusline) {
|
|
37098
|
+
for (const part of Object.keys(prefs.parts)) {
|
|
37099
|
+
if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
|
|
37100
|
+
}
|
|
37101
|
+
}
|
|
37102
|
+
if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
|
|
37103
|
+
if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
|
|
36919
37104
|
}
|
|
36920
37105
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
36921
37106
|
* identity is what endures; the chart dies with the cell). */
|
|
@@ -37322,16 +37507,21 @@ var ChartCell = class {
|
|
|
37322
37507
|
|
|
37323
37508
|
// src/workspace/context.ts
|
|
37324
37509
|
function buildContext(host) {
|
|
37325
|
-
const active = host.active();
|
|
37326
37510
|
return {
|
|
37327
37511
|
get chart() {
|
|
37328
37512
|
const cell = host.active();
|
|
37329
37513
|
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
37330
37514
|
return cell.chart;
|
|
37331
37515
|
},
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37516
|
+
get symbol() {
|
|
37517
|
+
return host.active()?.symbol ?? "";
|
|
37518
|
+
},
|
|
37519
|
+
get timeframe() {
|
|
37520
|
+
return host.active()?.timeframe ?? "60";
|
|
37521
|
+
},
|
|
37522
|
+
get priceStyle() {
|
|
37523
|
+
return host.active()?.priceStyle ?? "candles";
|
|
37524
|
+
},
|
|
37335
37525
|
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
37336
37526
|
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
37337
37527
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
@@ -37342,8 +37532,12 @@ function buildContext(host) {
|
|
|
37342
37532
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
37343
37533
|
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
37344
37534
|
stateChanged: () => host.stateDirty(),
|
|
37345
|
-
|
|
37346
|
-
|
|
37535
|
+
get cells() {
|
|
37536
|
+
return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
|
|
37537
|
+
},
|
|
37538
|
+
get activeCellId() {
|
|
37539
|
+
return host.active()?.id ?? "";
|
|
37540
|
+
},
|
|
37347
37541
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
37348
37542
|
};
|
|
37349
37543
|
}
|
|
@@ -37676,6 +37870,10 @@ var VelaWorkspace = class {
|
|
|
37676
37870
|
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
37677
37871
|
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
37678
37872
|
this.drawingSyncBusy = false;
|
|
37873
|
+
/** Same guard for the style link: a follower's `applyConfig` re-fires its
|
|
37874
|
+
* `onConfigChanged` in the same tick, and a state restore applies per-cell
|
|
37875
|
+
* configs that legitimately differ — neither must propagate. */
|
|
37876
|
+
this.styleSyncBusy = false;
|
|
37679
37877
|
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
37680
37878
|
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
37681
37879
|
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
@@ -37736,9 +37934,7 @@ var VelaWorkspace = class {
|
|
|
37736
37934
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
37737
37935
|
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
37738
37936
|
const sync = boot?.sync ?? opts.sync;
|
|
37739
|
-
for (const kind of
|
|
37740
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
37741
|
-
}
|
|
37937
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
|
|
37742
37938
|
this.monoLayout = opts.layout === false;
|
|
37743
37939
|
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
37744
37940
|
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
@@ -37822,7 +38018,8 @@ var VelaWorkspace = class {
|
|
|
37822
38018
|
syncs: () => [
|
|
37823
38019
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
37824
38020
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
37825
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
38021
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
38022
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
37826
38023
|
],
|
|
37827
38024
|
onToggleSync: (id) => {
|
|
37828
38025
|
const kind = id;
|
|
@@ -38106,7 +38303,7 @@ var VelaWorkspace = class {
|
|
|
38106
38303
|
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
38107
38304
|
}
|
|
38108
38305
|
this.dock.applyState(st.panels);
|
|
38109
|
-
for (const kind of
|
|
38306
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
38110
38307
|
this.trackSizes.clear();
|
|
38111
38308
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
38112
38309
|
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
@@ -38117,9 +38314,14 @@ var VelaWorkspace = class {
|
|
|
38117
38314
|
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
38118
38315
|
}
|
|
38119
38316
|
this.drawingLinks.clear();
|
|
38120
|
-
|
|
38121
|
-
|
|
38122
|
-
this.
|
|
38317
|
+
this.styleSyncBusy = true;
|
|
38318
|
+
try {
|
|
38319
|
+
for (const [i] of this.def.cells.entries()) {
|
|
38320
|
+
const { id, ...cs } = st.charts[i];
|
|
38321
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
38322
|
+
}
|
|
38323
|
+
} finally {
|
|
38324
|
+
this.styleSyncBusy = false;
|
|
38123
38325
|
}
|
|
38124
38326
|
if (st.timezone) this.setTimezone(st.timezone);
|
|
38125
38327
|
this.pool.clear();
|
|
@@ -38219,6 +38421,7 @@ var VelaWorkspace = class {
|
|
|
38219
38421
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
38220
38422
|
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
38221
38423
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
38424
|
+
const preexisting = new Set(this.cellsById.keys());
|
|
38222
38425
|
for (const [id, cell] of [...this.cellsById]) {
|
|
38223
38426
|
if (!keep.has(id) || rebuildAll) {
|
|
38224
38427
|
this.poolSet(id, cell.dehydrate());
|
|
@@ -38231,6 +38434,7 @@ var VelaWorkspace = class {
|
|
|
38231
38434
|
this.cellBackend = nextBackend;
|
|
38232
38435
|
this.applyGrid();
|
|
38233
38436
|
this.buildCells();
|
|
38437
|
+
this.alignNewCellStyles(preexisting);
|
|
38234
38438
|
this.syncCellPresentation();
|
|
38235
38439
|
this.topbar.setLayout(next.id);
|
|
38236
38440
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
@@ -38434,6 +38638,7 @@ var VelaWorkspace = class {
|
|
|
38434
38638
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
38435
38639
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
38436
38640
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
38641
|
+
onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
|
|
38437
38642
|
onStateDirty: () => this.markStateDirty(),
|
|
38438
38643
|
manifestSettled: () => this.manifestSettled,
|
|
38439
38644
|
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
@@ -38509,6 +38714,7 @@ var VelaWorkspace = class {
|
|
|
38509
38714
|
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
38510
38715
|
});
|
|
38511
38716
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
38717
|
+
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
38512
38718
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
38513
38719
|
chart.renderer.onAxisLongPress((e) => {
|
|
38514
38720
|
if (this.layoutCtl.current !== "mobile") return;
|
|
@@ -38541,11 +38747,67 @@ var VelaWorkspace = class {
|
|
|
38541
38747
|
if (kind === "viewport") {
|
|
38542
38748
|
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
38543
38749
|
if (range) this.propagateViewport(this.activeId, range);
|
|
38750
|
+
} else if (kind === "style") {
|
|
38751
|
+
this.propagateStylePrefs(this.activeId);
|
|
38544
38752
|
} else {
|
|
38545
38753
|
this.propagateMarket(this.activeId);
|
|
38546
38754
|
}
|
|
38547
38755
|
}
|
|
38548
38756
|
}
|
|
38757
|
+
/**
|
|
38758
|
+
* Align cells minted by a layout change to their style group: with the link on, a
|
|
38759
|
+
* NEW cell (fresh slot or one returning from the pool, which missed edits while
|
|
38760
|
+
* dormant) inherits the presentation of a pre-existing group peer — the active
|
|
38761
|
+
* cell when it is one — instead of sitting on its own state beside a styled
|
|
38762
|
+
* group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
|
|
38763
|
+
* the group, and the equality short-circuits keep converged peers untouched.
|
|
38764
|
+
*/
|
|
38765
|
+
alignNewCellStyles(preexisting) {
|
|
38766
|
+
const setting = this.syncOpts.style;
|
|
38767
|
+
if (!setting) return;
|
|
38768
|
+
const ids = [...this.cellsById.keys()];
|
|
38769
|
+
const propagated = /* @__PURE__ */ new Set();
|
|
38770
|
+
for (const id of ids) {
|
|
38771
|
+
if (preexisting.has(id)) continue;
|
|
38772
|
+
const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
|
|
38773
|
+
if (peers.length === 0) continue;
|
|
38774
|
+
const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
|
|
38775
|
+
if (propagated.has(source)) continue;
|
|
38776
|
+
propagated.add(source);
|
|
38777
|
+
this.propagateStylePrefs(source);
|
|
38778
|
+
}
|
|
38779
|
+
}
|
|
38780
|
+
/**
|
|
38781
|
+
* Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
|
|
38782
|
+
* its renderer config plus its Status line tab prefs — onto its same-group
|
|
38783
|
+
* followers (the style link). Loop-safe two ways: the busy guard eats the
|
|
38784
|
+
* followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
|
|
38785
|
+
* in the same tick), and the equality short-circuits leave already-converged
|
|
38786
|
+
* followers untouched, so nothing re-emits once the group agrees.
|
|
38787
|
+
*/
|
|
38788
|
+
propagateStylePrefs(originId) {
|
|
38789
|
+
if (this.styleSyncBusy || this.destroyed) return;
|
|
38790
|
+
const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
|
|
38791
|
+
if (targets.length === 0) return;
|
|
38792
|
+
const origin = this.cellsById.get(originId);
|
|
38793
|
+
if (!origin) return;
|
|
38794
|
+
const slice = styleConfigSlice(origin.chart.renderer.getConfig());
|
|
38795
|
+
const sliceJson = slice ? JSON.stringify(slice) : null;
|
|
38796
|
+
const prefs = origin.statusPrefs();
|
|
38797
|
+
this.styleSyncBusy = true;
|
|
38798
|
+
try {
|
|
38799
|
+
for (const id of targets) {
|
|
38800
|
+
const cell = this.cellsById.get(id);
|
|
38801
|
+
if (!cell) continue;
|
|
38802
|
+
if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
|
|
38803
|
+
cell.chart.renderer.applyConfig(slice);
|
|
38804
|
+
}
|
|
38805
|
+
cell.applyStatusPrefs(prefs);
|
|
38806
|
+
}
|
|
38807
|
+
} finally {
|
|
38808
|
+
this.styleSyncBusy = false;
|
|
38809
|
+
}
|
|
38810
|
+
}
|
|
38549
38811
|
/**
|
|
38550
38812
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
38551
38813
|
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
@@ -38853,7 +39115,8 @@ var VelaWorkspace = class {
|
|
|
38853
39115
|
syncs: () => [
|
|
38854
39116
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
38855
39117
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
38856
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
39118
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
39119
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
38857
39120
|
],
|
|
38858
39121
|
onToggleSync: (id) => {
|
|
38859
39122
|
const kind = id;
|