@luxalgo/vela 0.6.8 → 0.6.10
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-BSLlBpB9.d.ts → DataProvider-CWmp31dA.d.ts} +49 -1
- package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-l_eLMly_.d.cts} +49 -1
- package/dist/{chunk-L3I2CCYO.js → chunk-4KZVZ7QQ.js} +49 -2
- package/dist/{chunk-SQETIBFU.js → chunk-FFOP37FQ.js} +996 -377
- package/dist/{chunk-2R7BANEM.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-PN5KWFZ4.js → chunk-KG4YT3TI.js} +5 -3
- package/dist/{chunk-OICPLNU7.js → chunk-MHY7MVXH.js} +192 -2
- package/dist/{chunk-N7LGMKCE.js → chunk-NMTQXNT4.js} +637 -164
- package/dist/{contributions-DLLdV9jD.d.ts → contributions-BCz6Dr6a.d.ts} +99 -7
- package/dist/{contributions-Vw-Hm58R.d.cts → contributions-D9vTzm5p.d.cts} +99 -7
- package/dist/index.cjs +1210 -383
- package/dist/index.d.cts +37 -11
- package/dist/index.d.ts +37 -11
- package/dist/index.js +4 -4
- package/dist/{options-BaVTMXaO.d.ts → options-DSqHsQyN.d.cts} +84 -7
- package/dist/{options-BaVTMXaO.d.cts → options-DSqHsQyN.d.ts} +84 -7
- package/dist/{plugin-BnJgjLAy.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
- package/dist/{plugin-CC7rBrOY.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
- package/dist/plugin.cjs +21 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-CGkB2EOo.d.ts → statusline-5K7y4Ya2.d.ts} +5 -3
- package/dist/{statusline-DqzBqy42.d.cts → statusline-TYLQmoeh.d.cts} +5 -3
- package/dist/ui.cjs +208 -5
- package/dist/ui.d.cts +91 -2
- package/dist/ui.d.ts +91 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +1210 -383
- package/dist/vela.global.min.js +99 -48
- package/dist/widget.cjs +1771 -449
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1771 -449
- package/dist/workspace.d.cts +100 -6
- package/dist/workspace.d.ts +100 -6
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-
|
|
2
|
-
import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-
|
|
3
|
-
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-
|
|
4
|
-
import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-
|
|
5
|
-
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup,
|
|
1
|
+
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-FFOP37FQ.js';
|
|
2
|
+
import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-4KZVZ7QQ.js';
|
|
3
|
+
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-G77Y7LK2.js';
|
|
4
|
+
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-MHY7MVXH.js';
|
|
5
|
+
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-KG4YT3TI.js';
|
|
6
6
|
import { baseOf } from './chunk-W4EJWLEO.js';
|
|
7
7
|
|
|
8
8
|
// src/widget/timeframe.ts
|
|
@@ -2931,6 +2931,17 @@ function dedupeSymbols(list) {
|
|
|
2931
2931
|
}
|
|
2932
2932
|
return out;
|
|
2933
2933
|
}
|
|
2934
|
+
function foldGroups(list) {
|
|
2935
|
+
const groupsAbove = /* @__PURE__ */ new Set();
|
|
2936
|
+
const out = [];
|
|
2937
|
+
for (const s of list) {
|
|
2938
|
+
if (isGroupRow(s)) {
|
|
2939
|
+
groupsAbove.add(groupKeyOf(s));
|
|
2940
|
+
out.push(s);
|
|
2941
|
+
} else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
|
|
2942
|
+
}
|
|
2943
|
+
return out;
|
|
2944
|
+
}
|
|
2934
2945
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
2935
2946
|
function parseQuery(raw, venues) {
|
|
2936
2947
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -3062,6 +3073,21 @@ var CSS6 = `
|
|
|
3062
3073
|
.vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
|
|
3063
3074
|
.vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
|
|
3064
3075
|
.vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
|
|
3076
|
+
/* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
|
|
3077
|
+
.vela-sp-expander {
|
|
3078
|
+
all: unset;
|
|
3079
|
+
flex: none;
|
|
3080
|
+
display: inline-flex;
|
|
3081
|
+
align-items: center;
|
|
3082
|
+
justify-content: center;
|
|
3083
|
+
width: 22px;
|
|
3084
|
+
height: 22px;
|
|
3085
|
+
border-radius: 5px;
|
|
3086
|
+
cursor: pointer;
|
|
3087
|
+
color: var(--vela-fg-muted);
|
|
3088
|
+
}
|
|
3089
|
+
.vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
|
|
3090
|
+
.vela-sp-row[data-member] { padding-left: 34px; }
|
|
3065
3091
|
`;
|
|
3066
3092
|
var PAGE = 100;
|
|
3067
3093
|
var SymbolPicker = class {
|
|
@@ -3073,6 +3099,11 @@ var SymbolPicker = class {
|
|
|
3073
3099
|
this.seed = "";
|
|
3074
3100
|
this.activeTab = "All";
|
|
3075
3101
|
this.visible = PAGE;
|
|
3102
|
+
/** The last filter pass returned fewer raw rows than asked — the pool is drained
|
|
3103
|
+
* (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
|
|
3104
|
+
this.exhausted = false;
|
|
3105
|
+
/** Group rows currently expanded (venue-scoped keys) — members shown inline. */
|
|
3106
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
3076
3107
|
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
3077
3108
|
this.ranked = null;
|
|
3078
3109
|
this.ranking = false;
|
|
@@ -3087,7 +3118,7 @@ var SymbolPicker = class {
|
|
|
3087
3118
|
searchRow.append(iconEl("search", doc), this.input);
|
|
3088
3119
|
this.tabs = doc.createElement("div");
|
|
3089
3120
|
this.tabs.className = "vela-sp-tabs";
|
|
3090
|
-
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
|
|
3121
|
+
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
3091
3122
|
const b = doc.createElement("button");
|
|
3092
3123
|
b.className = "vela-sp-tab";
|
|
3093
3124
|
b.textContent = t;
|
|
@@ -3103,7 +3134,7 @@ var SymbolPicker = class {
|
|
|
3103
3134
|
this.list = doc.createElement("div");
|
|
3104
3135
|
this.list.className = "vela-sp-list";
|
|
3105
3136
|
this.list.addEventListener("scroll", () => {
|
|
3106
|
-
if (this.
|
|
3137
|
+
if (this.exhausted) return;
|
|
3107
3138
|
if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
|
|
3108
3139
|
this.visible += PAGE;
|
|
3109
3140
|
this.grow();
|
|
@@ -3132,14 +3163,19 @@ var SymbolPicker = class {
|
|
|
3132
3163
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
3133
3164
|
else if (e.key === "Enter") {
|
|
3134
3165
|
const pick = this.rows[this.highlighted];
|
|
3135
|
-
if (pick) this.
|
|
3166
|
+
if (pick) this.pick(pick);
|
|
3136
3167
|
return;
|
|
3137
3168
|
} else return;
|
|
3138
3169
|
e.preventDefault();
|
|
3139
3170
|
});
|
|
3140
3171
|
this.list.addEventListener("click", (e) => {
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3172
|
+
const target = e.target;
|
|
3173
|
+
const row = target.closest(".vela-sp-row");
|
|
3174
|
+
if (!row) return;
|
|
3175
|
+
const s = this.rows[Number(row.dataset.i)];
|
|
3176
|
+
if (!s) return;
|
|
3177
|
+
if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
|
|
3178
|
+
else this.pick(s);
|
|
3143
3179
|
});
|
|
3144
3180
|
}
|
|
3145
3181
|
/** Wire where symbols come from (re-called on every widget rebuild). */
|
|
@@ -3156,6 +3192,27 @@ var SymbolPicker = class {
|
|
|
3156
3192
|
destroy() {
|
|
3157
3193
|
this.dialog.destroy();
|
|
3158
3194
|
}
|
|
3195
|
+
/** Route a row activation: a GROUP row loads its default member (the root itself is
|
|
3196
|
+
* listed, never loadable), any other row loads itself. */
|
|
3197
|
+
pick(s) {
|
|
3198
|
+
const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
|
|
3199
|
+
this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
|
|
3200
|
+
}
|
|
3201
|
+
/** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
|
|
3202
|
+
* the member rows under the group appear or go. */
|
|
3203
|
+
toggleExpand(s) {
|
|
3204
|
+
const key = groupKeyOf(s);
|
|
3205
|
+
if (!this.expanded.delete(key)) this.expanded.add(key);
|
|
3206
|
+
const scrollTop = this.list.scrollTop;
|
|
3207
|
+
const focus = this.rows[this.highlighted];
|
|
3208
|
+
this.rows = this.computeRows();
|
|
3209
|
+
this.list.replaceChildren();
|
|
3210
|
+
this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
|
|
3211
|
+
const at = focus ? this.rows.indexOf(focus) : -1;
|
|
3212
|
+
this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
|
|
3213
|
+
this.renderHighlight();
|
|
3214
|
+
this.list.scrollTop = scrollTop;
|
|
3215
|
+
}
|
|
3159
3216
|
select(ticker, venue, onSelect) {
|
|
3160
3217
|
this.close();
|
|
3161
3218
|
onSelect(venue ? `${venue}:${ticker}` : ticker);
|
|
@@ -3197,10 +3254,26 @@ var SymbolPicker = class {
|
|
|
3197
3254
|
return this.ranked?.result ?? raw;
|
|
3198
3255
|
}
|
|
3199
3256
|
computeRows() {
|
|
3200
|
-
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
3257
|
+
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
3201
3258
|
const all = this.pool();
|
|
3202
3259
|
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
3203
|
-
|
|
3260
|
+
const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
|
|
3261
|
+
this.exhausted = filtered.length < this.visible;
|
|
3262
|
+
const folded = foldGroups(filtered);
|
|
3263
|
+
if (!this.expanded.size) return folded;
|
|
3264
|
+
const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
|
|
3265
|
+
const present = new Set(folded.map(keyOf));
|
|
3266
|
+
const out = [];
|
|
3267
|
+
for (const s of folded) {
|
|
3268
|
+
out.push(s);
|
|
3269
|
+
if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
|
|
3270
|
+
for (const m of groupMembers(all, s)) {
|
|
3271
|
+
if (present.has(keyOf(m))) continue;
|
|
3272
|
+
present.add(keyOf(m));
|
|
3273
|
+
out.push(m);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return out;
|
|
3204
3277
|
}
|
|
3205
3278
|
refresh() {
|
|
3206
3279
|
const doc = this.list.ownerDocument;
|
|
@@ -3215,19 +3288,20 @@ var SymbolPicker = class {
|
|
|
3215
3288
|
this.list.appendChild(empty);
|
|
3216
3289
|
return;
|
|
3217
3290
|
}
|
|
3218
|
-
|
|
3291
|
+
this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
|
|
3219
3292
|
this.renderHighlight();
|
|
3220
3293
|
}
|
|
3221
3294
|
/** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
|
|
3222
3295
|
grow() {
|
|
3223
3296
|
const already = this.rows.length;
|
|
3224
3297
|
this.rows = this.computeRows();
|
|
3225
|
-
|
|
3298
|
+
this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
|
|
3226
3299
|
}
|
|
3227
|
-
rowEl(s) {
|
|
3300
|
+
rowEl(s, i) {
|
|
3228
3301
|
const doc = this.list.ownerDocument;
|
|
3229
3302
|
const row = doc.createElement("div");
|
|
3230
3303
|
row.className = "vela-sp-row";
|
|
3304
|
+
row.dataset.i = String(i);
|
|
3231
3305
|
row.dataset.ticker = s.ticker;
|
|
3232
3306
|
const venue = s.prefix ?? s.provider;
|
|
3233
3307
|
if (venue) row.dataset.venue = venue;
|
|
@@ -3242,6 +3316,14 @@ var SymbolPicker = class {
|
|
|
3242
3316
|
d.textContent = s.description ?? (s.type ?? "");
|
|
3243
3317
|
main.append(t, d);
|
|
3244
3318
|
row.append(av, main);
|
|
3319
|
+
if (isGroupRow(s)) {
|
|
3320
|
+
row.dataset.group = "1";
|
|
3321
|
+
const expander = doc.createElement("button");
|
|
3322
|
+
expander.className = "vela-sp-expander";
|
|
3323
|
+
expander.setAttribute("aria-label", "Show contracts");
|
|
3324
|
+
expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
|
|
3325
|
+
row.appendChild(expander);
|
|
3326
|
+
} else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
|
|
3245
3327
|
if (venue) {
|
|
3246
3328
|
const badge = doc.createElement("span");
|
|
3247
3329
|
badge.className = "vela-sp-badge";
|
|
@@ -3940,45 +4022,24 @@ var CSS10 = `
|
|
|
3940
4022
|
}
|
|
3941
4023
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
3942
4024
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
3943
|
-
/* Market status badge \u2014 icon-only 16px circle, label on hover
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
place-items: center;
|
|
3947
|
-
width: 16px;
|
|
3948
|
-
height: 16px;
|
|
3949
|
-
border-radius: 50%;
|
|
3950
|
-
flex: none;
|
|
3951
|
-
align-self: center;
|
|
3952
|
-
line-height: 0;
|
|
3953
|
-
cursor: default;
|
|
3954
|
-
}
|
|
3955
|
-
.vela-statusline .vela-sl-market svg {
|
|
3956
|
-
width: 12px;
|
|
3957
|
-
height: 12px;
|
|
3958
|
-
display: block;
|
|
3959
|
-
}
|
|
3960
|
-
/* Open wears the theme's up color; the other sessions are meaning constants from the
|
|
3961
|
-
* palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
|
|
3962
|
-
.vela-statusline .vela-sl-market[data-status='open'] {
|
|
3963
|
-
background: color-mix(in srgb, var(--vela-up) 20%, transparent);
|
|
3964
|
-
color: var(--vela-up);
|
|
3965
|
-
}
|
|
3966
|
-
.vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
|
|
3967
|
-
.vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
|
|
3968
|
-
.vela-statusline .vela-sl-market[data-status='closed'],
|
|
3969
|
-
.vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
|
|
4025
|
+
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
4026
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. */
|
|
4027
|
+
.vela-statusline .vela-sl-market { align-self: center; }
|
|
3970
4028
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
3971
4029
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
3972
4030
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
3973
4031
|
* these are the pre-ink fallbacks only. */
|
|
3974
4032
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
3975
4033
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
3976
|
-
/* Stack the
|
|
3977
|
-
* The renderer
|
|
3978
|
-
*
|
|
3979
|
-
*
|
|
3980
|
-
*
|
|
3981
|
-
|
|
4034
|
+
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
4035
|
+
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
4036
|
+
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
4037
|
+
* with the status line whichever pane owns the top. The renderer sets the legend's
|
|
4038
|
+
* inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
|
|
4039
|
+
* CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
|
|
4040
|
+
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
4041
|
+
* chart on the page, including statusline-less ones. */
|
|
4042
|
+
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
3982
4043
|
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
3983
4044
|
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
3984
4045
|
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
@@ -4014,7 +4075,7 @@ var CSS10 = `
|
|
|
4014
4075
|
font-size: var(--vela-font-size-sm);
|
|
4015
4076
|
line-height: 1.2;
|
|
4016
4077
|
}
|
|
4017
|
-
[data-layout='mobile'] .vela-has-statusline [
|
|
4078
|
+
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
4018
4079
|
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
4019
4080
|
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
4020
4081
|
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
@@ -4031,7 +4092,7 @@ var CSS10 = `
|
|
|
4031
4092
|
padding-left: 0;
|
|
4032
4093
|
}
|
|
4033
4094
|
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
4034
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [
|
|
4095
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
4035
4096
|
`;
|
|
4036
4097
|
function baseOfTicker(ticker) {
|
|
4037
4098
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -4043,6 +4104,13 @@ var MARKET_LABELS = {
|
|
|
4043
4104
|
closed: "Market Closed",
|
|
4044
4105
|
holiday: "Market Holiday"
|
|
4045
4106
|
};
|
|
4107
|
+
var MARKET_INKS = {
|
|
4108
|
+
open: "var(--vela-up)",
|
|
4109
|
+
pre: SESSION_PRE,
|
|
4110
|
+
post: SESSION_POST,
|
|
4111
|
+
closed: SESSION_OFF,
|
|
4112
|
+
holiday: SESSION_OFF
|
|
4113
|
+
};
|
|
4046
4114
|
function statuslineInkOf(renderer, priceStyle) {
|
|
4047
4115
|
const cfg = renderer.getConfig();
|
|
4048
4116
|
const c = (v) => typeof v === "string" ? v : null;
|
|
@@ -4101,8 +4169,15 @@ var Statusline = class {
|
|
|
4101
4169
|
this.symbolEl.textContent = ticker;
|
|
4102
4170
|
this.metaEl = doc.createElement("span");
|
|
4103
4171
|
this.metaEl.className = "vela-sl-meta";
|
|
4104
|
-
this.
|
|
4105
|
-
|
|
4172
|
+
this.marketBubble = new CalloutBubble({
|
|
4173
|
+
icon: "market-open",
|
|
4174
|
+
background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
|
|
4175
|
+
color: MARKET_INKS.open,
|
|
4176
|
+
label: MARKET_LABELS.open,
|
|
4177
|
+
host
|
|
4178
|
+
});
|
|
4179
|
+
this.marketEl = this.marketBubble.el;
|
|
4180
|
+
this.marketEl.classList.add("vela-sl-market");
|
|
4106
4181
|
this.ohlcEl = doc.createElement("span");
|
|
4107
4182
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
4108
4183
|
this.changeEl = doc.createElement("span");
|
|
@@ -4190,8 +4265,13 @@ var Statusline = class {
|
|
|
4190
4265
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
4191
4266
|
setMarketStatus(status) {
|
|
4192
4267
|
this.marketEl.dataset.status = status;
|
|
4193
|
-
|
|
4194
|
-
this.
|
|
4268
|
+
const ink = MARKET_INKS[status];
|
|
4269
|
+
this.marketBubble.set({
|
|
4270
|
+
icon: `market-${status}`,
|
|
4271
|
+
background: `color-mix(in srgb, ${ink} 20%, transparent)`,
|
|
4272
|
+
color: ink,
|
|
4273
|
+
label: MARKET_LABELS[status]
|
|
4274
|
+
});
|
|
4195
4275
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
4196
4276
|
}
|
|
4197
4277
|
setPartVisible(part, visible) {
|
|
@@ -4224,6 +4304,7 @@ var Statusline = class {
|
|
|
4224
4304
|
this.fitRO?.disconnect();
|
|
4225
4305
|
this.fitRO = null;
|
|
4226
4306
|
this.marketTip.destroy();
|
|
4307
|
+
this.marketBubble.destroy();
|
|
4227
4308
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
4228
4309
|
this.el.remove();
|
|
4229
4310
|
}
|
|
@@ -4760,6 +4841,8 @@ var SessionShadingTracker = class {
|
|
|
4760
4841
|
this.spec = null;
|
|
4761
4842
|
this.ready = false;
|
|
4762
4843
|
this.session = "regular";
|
|
4844
|
+
/** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
|
|
4845
|
+
this.intraday = false;
|
|
4763
4846
|
this.covered = null;
|
|
4764
4847
|
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
4765
4848
|
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
@@ -4772,6 +4855,8 @@ var SessionShadingTracker = class {
|
|
|
4772
4855
|
this.spec = null;
|
|
4773
4856
|
this.covered = null;
|
|
4774
4857
|
this.session = opts.session;
|
|
4858
|
+
const tfMs = timeframeMs(opts.timeframe);
|
|
4859
|
+
this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS;
|
|
4775
4860
|
this.lastRange = opts.range;
|
|
4776
4861
|
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
4777
4862
|
if (my !== this.epoch) return;
|
|
@@ -4799,7 +4884,7 @@ var SessionShadingTracker = class {
|
|
|
4799
4884
|
if (force) this.onZones(null);
|
|
4800
4885
|
return;
|
|
4801
4886
|
}
|
|
4802
|
-
if (this.session !== "extended") {
|
|
4887
|
+
if (this.session !== "extended" || !this.intraday) {
|
|
4803
4888
|
if (force) this.onZones({ pre: [], post: [] });
|
|
4804
4889
|
return;
|
|
4805
4890
|
}
|
|
@@ -4814,6 +4899,229 @@ var SessionShadingTracker = class {
|
|
|
4814
4899
|
}
|
|
4815
4900
|
};
|
|
4816
4901
|
|
|
4902
|
+
// src/widget/glide.ts
|
|
4903
|
+
var MIN_SPAN_MS = 6e4;
|
|
4904
|
+
var FOLLOW = 0.25;
|
|
4905
|
+
var ZOOM_IN = 0.8;
|
|
4906
|
+
var ZOOM_OUT = 1.25;
|
|
4907
|
+
var PAN_FAST = 0.2;
|
|
4908
|
+
function zoomTarget(base, factor) {
|
|
4909
|
+
const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
|
|
4910
|
+
return { from: base.to - newSpan, to: base.to };
|
|
4911
|
+
}
|
|
4912
|
+
function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
|
|
4913
|
+
const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
|
|
4914
|
+
const nf = cur.from + (target.from - cur.from) * follow;
|
|
4915
|
+
const nt = cur.to + (target.to - cur.to) * follow;
|
|
4916
|
+
const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
|
|
4917
|
+
return { cur: done ? { ...target } : { from: nf, to: nt }, done };
|
|
4918
|
+
}
|
|
4919
|
+
var Glider = class {
|
|
4920
|
+
constructor(chart) {
|
|
4921
|
+
this.chart = chart;
|
|
4922
|
+
// The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
|
|
4923
|
+
// is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
|
|
4924
|
+
// clamps what gets applied — chasing that read-back can never converge on a
|
|
4925
|
+
// clamped step, leaving the rAF loop re-applying a stale target forever (and
|
|
4926
|
+
// grinding the span down when panning past the newest bar). The internal range
|
|
4927
|
+
// converges geometrically, so the glide always terminates; the renderer is free
|
|
4928
|
+
// to clamp each applied frame.
|
|
4929
|
+
this.cur = null;
|
|
4930
|
+
this.target = null;
|
|
4931
|
+
this.raf = 0;
|
|
4932
|
+
}
|
|
4933
|
+
zoom(factor) {
|
|
4934
|
+
this.to((base) => zoomTarget(base, factor));
|
|
4935
|
+
}
|
|
4936
|
+
stop() {
|
|
4937
|
+
if (this.raf) cancelAnimationFrame(this.raf);
|
|
4938
|
+
this.raf = 0;
|
|
4939
|
+
this.cur = null;
|
|
4940
|
+
this.target = null;
|
|
4941
|
+
}
|
|
4942
|
+
to(make) {
|
|
4943
|
+
const chart = this.chart();
|
|
4944
|
+
const base = this.target ?? chart?.getVisibleRange();
|
|
4945
|
+
if (!chart || !base) return;
|
|
4946
|
+
this.cur ?? (this.cur = { ...base });
|
|
4947
|
+
this.target = make(base);
|
|
4948
|
+
if (!this.raf) this.tick();
|
|
4949
|
+
}
|
|
4950
|
+
tick() {
|
|
4951
|
+
this.raf = requestAnimationFrame(() => {
|
|
4952
|
+
const chart = this.chart();
|
|
4953
|
+
if (!chart || !this.cur || !this.target) {
|
|
4954
|
+
this.stop();
|
|
4955
|
+
return;
|
|
4956
|
+
}
|
|
4957
|
+
const { cur: next, done } = followStep(this.cur, this.target);
|
|
4958
|
+
this.cur = next;
|
|
4959
|
+
chart.setVisibleRange(next);
|
|
4960
|
+
if (done) this.stop();
|
|
4961
|
+
else this.tick();
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4964
|
+
};
|
|
4965
|
+
|
|
4966
|
+
// src/widget/cell-controls.ts
|
|
4967
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
4968
|
+
var TIME_AXIS_H2 = 22;
|
|
4969
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
4970
|
+
var CLUSTER_H = 24;
|
|
4971
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
4972
|
+
var STYLE_ID12 = "vela-cell-controls";
|
|
4973
|
+
var CSS12 = `
|
|
4974
|
+
.vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
|
|
4975
|
+
.vela-cc-btn svg{display:block;}
|
|
4976
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
4977
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
4978
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
4979
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
4980
|
+
`;
|
|
4981
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
|
|
4982
|
+
const cx = width / 2;
|
|
4983
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H / 2;
|
|
4984
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
4985
|
+
}
|
|
4986
|
+
var CellControls = class {
|
|
4987
|
+
constructor(host, deps) {
|
|
4988
|
+
this.host = host;
|
|
4989
|
+
this.deps = deps;
|
|
4990
|
+
this.near = false;
|
|
4991
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
4992
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
4993
|
+
this.dragging = false;
|
|
4994
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
4995
|
+
* bar's maximize stop replaces the cluster. */
|
|
4996
|
+
this.suspended = false;
|
|
4997
|
+
this.onHostMove = (e) => {
|
|
4998
|
+
if (this.suspended) return;
|
|
4999
|
+
if (this.dragging) return;
|
|
5000
|
+
const rect = this.host.getBoundingClientRect();
|
|
5001
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
5002
|
+
};
|
|
5003
|
+
this.onHostLeave = () => {
|
|
5004
|
+
if (this.dragging) return;
|
|
5005
|
+
this.setNear(false);
|
|
5006
|
+
};
|
|
5007
|
+
injectStyles(STYLE_ID12, CSS12, host.ownerDocument);
|
|
5008
|
+
this.glider = new Glider(deps.chart);
|
|
5009
|
+
this.root = host.ownerDocument.createElement("div");
|
|
5010
|
+
Object.assign(this.root.style, {
|
|
5011
|
+
position: "absolute",
|
|
5012
|
+
left: "50%",
|
|
5013
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
5014
|
+
transform: "translateX(-50%)",
|
|
5015
|
+
zIndex: "6",
|
|
5016
|
+
display: "none",
|
|
5017
|
+
// revealed by cursor proximity (onHostMove)
|
|
5018
|
+
gap: "2px",
|
|
5019
|
+
padding: "2px",
|
|
5020
|
+
borderRadius: "var(--vela-radius-md)",
|
|
5021
|
+
background: CLUSTER_PILL,
|
|
5022
|
+
pointerEvents: "auto"
|
|
5023
|
+
});
|
|
5024
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
5025
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
5026
|
+
this.host.appendChild(this.root);
|
|
5027
|
+
this.refresh();
|
|
5028
|
+
}
|
|
5029
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
5030
|
+
refresh() {
|
|
5031
|
+
this.root.textContent = "";
|
|
5032
|
+
const multi = this.deps.multiCell();
|
|
5033
|
+
const maximized = multi && this.deps.isMaximized();
|
|
5034
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
5035
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
5036
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
5037
|
+
if (multi) {
|
|
5038
|
+
this.root.appendChild(
|
|
5039
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
5040
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
5041
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
5042
|
+
selected: maximized
|
|
5043
|
+
})
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
this.root.appendChild(
|
|
5047
|
+
this.button("reset", "Reset chart", () => {
|
|
5048
|
+
this.glider.stop();
|
|
5049
|
+
this.deps.reset();
|
|
5050
|
+
})
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
button(iconId, title, onClick, opts = {}) {
|
|
5054
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
5055
|
+
b.type = "button";
|
|
5056
|
+
b.title = title;
|
|
5057
|
+
b.setAttribute("aria-label", title);
|
|
5058
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
5059
|
+
b.innerHTML = icon(iconId);
|
|
5060
|
+
b.addEventListener("click", (e) => {
|
|
5061
|
+
e.stopPropagation();
|
|
5062
|
+
onClick();
|
|
5063
|
+
});
|
|
5064
|
+
return b;
|
|
5065
|
+
}
|
|
5066
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
5067
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
5068
|
+
* any other cell cancels. */
|
|
5069
|
+
makeGrip() {
|
|
5070
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
5071
|
+
b.type = "button";
|
|
5072
|
+
b.title = "Drag to move chart";
|
|
5073
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
5074
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
5075
|
+
b.innerHTML = icon("grip");
|
|
5076
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
5077
|
+
return b;
|
|
5078
|
+
}
|
|
5079
|
+
onGripDown(btn, e) {
|
|
5080
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
5081
|
+
e.preventDefault();
|
|
5082
|
+
e.stopPropagation();
|
|
5083
|
+
try {
|
|
5084
|
+
btn.setPointerCapture(e.pointerId);
|
|
5085
|
+
} catch {
|
|
5086
|
+
}
|
|
5087
|
+
this.dragging = true;
|
|
5088
|
+
let target = null;
|
|
5089
|
+
const move = (ev) => {
|
|
5090
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
5091
|
+
this.deps.previewDrop(target);
|
|
5092
|
+
};
|
|
5093
|
+
const finish = (commit) => () => {
|
|
5094
|
+
this.dragging = false;
|
|
5095
|
+
this.deps.previewDrop(null);
|
|
5096
|
+
btn.removeEventListener("pointermove", move);
|
|
5097
|
+
btn.removeEventListener("pointerup", onUp);
|
|
5098
|
+
btn.removeEventListener("pointercancel", onCancel);
|
|
5099
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
5100
|
+
};
|
|
5101
|
+
const onUp = finish(true);
|
|
5102
|
+
const onCancel = finish(false);
|
|
5103
|
+
btn.addEventListener("pointermove", move);
|
|
5104
|
+
btn.addEventListener("pointerup", onUp);
|
|
5105
|
+
btn.addEventListener("pointercancel", onCancel);
|
|
5106
|
+
}
|
|
5107
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
5108
|
+
setSuspended(on) {
|
|
5109
|
+
this.suspended = on;
|
|
5110
|
+
if (on) this.setNear(false);
|
|
5111
|
+
}
|
|
5112
|
+
setNear(near) {
|
|
5113
|
+
if (near === this.near) return;
|
|
5114
|
+
this.near = near;
|
|
5115
|
+
this.root.style.display = near ? "flex" : "none";
|
|
5116
|
+
}
|
|
5117
|
+
destroy() {
|
|
5118
|
+
this.glider.stop();
|
|
5119
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
5120
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
5121
|
+
this.root.remove();
|
|
5122
|
+
}
|
|
5123
|
+
};
|
|
5124
|
+
|
|
4817
5125
|
// src/widget/history.ts
|
|
4818
5126
|
var WidgetHistory = class {
|
|
4819
5127
|
/** `getChart` late-resolves the CURRENT chart: drawing steps recorded before a chart
|
|
@@ -5002,6 +5310,7 @@ var ChartCell = class {
|
|
|
5002
5310
|
this.inner.renderer.set("attribution", false);
|
|
5003
5311
|
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
5004
5312
|
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
5313
|
+
this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
|
|
5005
5314
|
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
5006
5315
|
this.history.onChart(this.inner);
|
|
5007
5316
|
this.inner.renderer.onConfigChanged(() => {
|
|
@@ -5062,11 +5371,18 @@ var ChartCell = class {
|
|
|
5062
5371
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
5063
5372
|
});
|
|
5064
5373
|
this.syncStatuslineColors();
|
|
5374
|
+
this.cellControls = new CellControls(this.host, {
|
|
5375
|
+
chart: () => this.inner,
|
|
5376
|
+
reset: () => this.resetView(),
|
|
5377
|
+
multiCell: () => deps.multiCell(),
|
|
5378
|
+
isMaximized: () => deps.isMaximized(id),
|
|
5379
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
5380
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
5381
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
5382
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
5383
|
+
});
|
|
5065
5384
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
5066
|
-
resetView: () =>
|
|
5067
|
-
this.inner?.renderer.set("autoScale", true);
|
|
5068
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
5069
|
-
},
|
|
5385
|
+
resetView: () => this.resetView(),
|
|
5070
5386
|
timezone: () => this.deps.timezone(),
|
|
5071
5387
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
5072
5388
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -5118,20 +5434,30 @@ var ChartCell = class {
|
|
|
5118
5434
|
this.refreshNativeCatalog();
|
|
5119
5435
|
this.pushSettingsSections();
|
|
5120
5436
|
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
5121
|
-
this.
|
|
5122
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
5123
|
-
this.state.timeframe = timeframe;
|
|
5124
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
5125
|
-
this.watermark?.update(symbol2, timeframe);
|
|
5126
|
-
this.statusline?.setSymbol(symbol2);
|
|
5127
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
5128
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
5437
|
+
this.projectMarket(symbol2, timeframe);
|
|
5129
5438
|
this.refreshNativeCatalog();
|
|
5130
5439
|
this.refreshSessionAvailable();
|
|
5131
5440
|
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
5132
5441
|
this.deps.onMarketChanged(this.id);
|
|
5133
5442
|
});
|
|
5134
5443
|
}
|
|
5444
|
+
/**
|
|
5445
|
+
* Project a market identity into the cell state and its display overlays (watermark,
|
|
5446
|
+
* statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
|
|
5447
|
+
* optimistically from the cell setters — the labels reflect the pick immediately, not
|
|
5448
|
+
* after the bars load — and again from `market:changed` (the committed pass, and the
|
|
5449
|
+
* only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
|
|
5450
|
+
*/
|
|
5451
|
+
projectMarket(symbol, timeframe) {
|
|
5452
|
+
this.state.symbol = symbol;
|
|
5453
|
+
this.state.provider = parseSymbol(symbol).provider ?? void 0;
|
|
5454
|
+
this.state.timeframe = timeframe;
|
|
5455
|
+
this.state.session = normalizeSession(this.inner?.market.session);
|
|
5456
|
+
this.watermark?.update(symbol, timeframe);
|
|
5457
|
+
this.statusline?.setSymbol(symbol);
|
|
5458
|
+
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
|
|
5459
|
+
if (this.inner) this.statusline?.onChart(this.inner);
|
|
5460
|
+
}
|
|
5135
5461
|
/**
|
|
5136
5462
|
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
5137
5463
|
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
@@ -5177,7 +5503,7 @@ var ChartCell = class {
|
|
|
5177
5503
|
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
5178
5504
|
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
5179
5505
|
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
5180
|
-
this.sessionShading.track(chart.data, symbol, { session: this.session, range });
|
|
5506
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
5181
5507
|
}
|
|
5182
5508
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
5183
5509
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -5366,17 +5692,23 @@ var ChartCell = class {
|
|
|
5366
5692
|
get indicatorCount() {
|
|
5367
5693
|
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
5368
5694
|
}
|
|
5369
|
-
/** Switch this cell's market in place (the chart instance survives).
|
|
5695
|
+
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
5696
|
+
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
5697
|
+
* the setMarket call so the statusline reads the already-blanked chart. */
|
|
5370
5698
|
setSymbol(symbol) {
|
|
5371
5699
|
if (!this.inner || symbol === this.symbol) return;
|
|
5372
5700
|
this.unresolvedToasted = null;
|
|
5373
5701
|
void this.inner.setMarket({ symbol });
|
|
5702
|
+
this.projectMarket(symbol, this.timeframe);
|
|
5703
|
+
this.deps.onMarketChanged(this.id);
|
|
5374
5704
|
}
|
|
5375
5705
|
setTimeframe(timeframe) {
|
|
5376
5706
|
if (!this.inner || timeframe === this.timeframe) return;
|
|
5377
5707
|
this.activeRangeId = null;
|
|
5378
5708
|
this.rangeBars = 0;
|
|
5379
5709
|
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
5710
|
+
this.projectMarket(this.symbol, timeframe);
|
|
5711
|
+
this.deps.onMarketChanged(this.id);
|
|
5380
5712
|
}
|
|
5381
5713
|
/** Applied live (renderer feature) — no reload. */
|
|
5382
5714
|
setPriceStyle(style) {
|
|
@@ -5424,6 +5756,20 @@ var ChartCell = class {
|
|
|
5424
5756
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
5425
5757
|
}
|
|
5426
5758
|
}
|
|
5759
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
5760
|
+
* the same action the chart context menu offers. */
|
|
5761
|
+
resetView() {
|
|
5762
|
+
this.inner?.renderer.set("autoScale", true);
|
|
5763
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
5764
|
+
}
|
|
5765
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
5766
|
+
refreshControls() {
|
|
5767
|
+
this.cellControls.refresh();
|
|
5768
|
+
}
|
|
5769
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
5770
|
+
setControlsSuspended(on) {
|
|
5771
|
+
this.cellControls.setSuspended(on);
|
|
5772
|
+
}
|
|
5427
5773
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
5428
5774
|
focus() {
|
|
5429
5775
|
this.deps.activate(this.id);
|
|
@@ -5737,6 +6083,7 @@ var ChartCell = class {
|
|
|
5737
6083
|
destroy() {
|
|
5738
6084
|
this.destroyed = true;
|
|
5739
6085
|
this.offMarket();
|
|
6086
|
+
this.cellControls.destroy();
|
|
5740
6087
|
this.contextMenu.destroy();
|
|
5741
6088
|
this.history.destroy();
|
|
5742
6089
|
this.marketStatus?.stop();
|
|
@@ -5995,8 +6342,8 @@ var SplitterLayer = class {
|
|
|
5995
6342
|
};
|
|
5996
6343
|
|
|
5997
6344
|
// src/widget/toast.ts
|
|
5998
|
-
var
|
|
5999
|
-
var
|
|
6345
|
+
var STYLE_ID13 = "vela-widget-toast";
|
|
6346
|
+
var CSS13 = `
|
|
6000
6347
|
.vela-toast {
|
|
6001
6348
|
position: absolute;
|
|
6002
6349
|
left: 50%;
|
|
@@ -6026,7 +6373,7 @@ var CSS12 = `
|
|
|
6026
6373
|
var Toast = class {
|
|
6027
6374
|
constructor(host) {
|
|
6028
6375
|
this.timer = null;
|
|
6029
|
-
injectStyles(
|
|
6376
|
+
injectStyles(STYLE_ID13, CSS13, host.ownerDocument);
|
|
6030
6377
|
this.el = host.ownerDocument.createElement("div");
|
|
6031
6378
|
this.el.className = "vela-toast";
|
|
6032
6379
|
host.appendChild(this.el);
|
|
@@ -6050,70 +6397,6 @@ var Toast = class {
|
|
|
6050
6397
|
}
|
|
6051
6398
|
};
|
|
6052
6399
|
|
|
6053
|
-
// src/widget/glide.ts
|
|
6054
|
-
var MIN_SPAN_MS = 6e4;
|
|
6055
|
-
var FOLLOW = 0.25;
|
|
6056
|
-
var ZOOM_IN = 0.8;
|
|
6057
|
-
var ZOOM_OUT = 1.25;
|
|
6058
|
-
var PAN_FAST = 0.2;
|
|
6059
|
-
function zoomTarget(base, factor) {
|
|
6060
|
-
const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
|
|
6061
|
-
return { from: base.to - newSpan, to: base.to };
|
|
6062
|
-
}
|
|
6063
|
-
function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
|
|
6064
|
-
const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
|
|
6065
|
-
const nf = cur.from + (target.from - cur.from) * follow;
|
|
6066
|
-
const nt = cur.to + (target.to - cur.to) * follow;
|
|
6067
|
-
const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
|
|
6068
|
-
return { cur: done ? { ...target } : { from: nf, to: nt }, done };
|
|
6069
|
-
}
|
|
6070
|
-
var Glider = class {
|
|
6071
|
-
constructor(chart) {
|
|
6072
|
-
this.chart = chart;
|
|
6073
|
-
// The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
|
|
6074
|
-
// is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
|
|
6075
|
-
// clamps what gets applied — chasing that read-back can never converge on a
|
|
6076
|
-
// clamped step, leaving the rAF loop re-applying a stale target forever (and
|
|
6077
|
-
// grinding the span down when panning past the newest bar). The internal range
|
|
6078
|
-
// converges geometrically, so the glide always terminates; the renderer is free
|
|
6079
|
-
// to clamp each applied frame.
|
|
6080
|
-
this.cur = null;
|
|
6081
|
-
this.target = null;
|
|
6082
|
-
this.raf = 0;
|
|
6083
|
-
}
|
|
6084
|
-
zoom(factor) {
|
|
6085
|
-
this.to((base) => zoomTarget(base, factor));
|
|
6086
|
-
}
|
|
6087
|
-
stop() {
|
|
6088
|
-
if (this.raf) cancelAnimationFrame(this.raf);
|
|
6089
|
-
this.raf = 0;
|
|
6090
|
-
this.cur = null;
|
|
6091
|
-
this.target = null;
|
|
6092
|
-
}
|
|
6093
|
-
to(make) {
|
|
6094
|
-
const chart = this.chart();
|
|
6095
|
-
const base = this.target ?? chart?.getVisibleRange();
|
|
6096
|
-
if (!chart || !base) return;
|
|
6097
|
-
this.cur ?? (this.cur = { ...base });
|
|
6098
|
-
this.target = make(base);
|
|
6099
|
-
if (!this.raf) this.tick();
|
|
6100
|
-
}
|
|
6101
|
-
tick() {
|
|
6102
|
-
this.raf = requestAnimationFrame(() => {
|
|
6103
|
-
const chart = this.chart();
|
|
6104
|
-
if (!chart || !this.cur || !this.target) {
|
|
6105
|
-
this.stop();
|
|
6106
|
-
return;
|
|
6107
|
-
}
|
|
6108
|
-
const { cur: next, done } = followStep(this.cur, this.target);
|
|
6109
|
-
this.cur = next;
|
|
6110
|
-
chart.setVisibleRange(next);
|
|
6111
|
-
if (done) this.stop();
|
|
6112
|
-
else this.tick();
|
|
6113
|
-
});
|
|
6114
|
-
}
|
|
6115
|
-
};
|
|
6116
|
-
|
|
6117
6400
|
// src/widget/tool-shortcuts.ts
|
|
6118
6401
|
var TOOL_BINDINGS = [
|
|
6119
6402
|
["trendline", "drawings.trendline"],
|
|
@@ -6185,8 +6468,8 @@ var LayoutModeController = class {
|
|
|
6185
6468
|
};
|
|
6186
6469
|
|
|
6187
6470
|
// src/widget/mobile-bar.ts
|
|
6188
|
-
var
|
|
6189
|
-
var
|
|
6471
|
+
var STYLE_ID14 = "vela-widget-mobilebar";
|
|
6472
|
+
var CSS14 = `
|
|
6190
6473
|
.vela-mobilebar {
|
|
6191
6474
|
display: none;
|
|
6192
6475
|
align-items: stretch;
|
|
@@ -6217,6 +6500,9 @@ var CSS13 = `
|
|
|
6217
6500
|
}
|
|
6218
6501
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
6219
6502
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
6503
|
+
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
6504
|
+
"selected" chip \u2014 white on the dark theme, dark on the light one. */
|
|
6505
|
+
.vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
6220
6506
|
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
6221
6507
|
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
6222
6508
|
.vela-mb-actions { display: contents; }
|
|
@@ -6236,7 +6522,7 @@ var MobileBar = class {
|
|
|
6236
6522
|
constructor(host, opts) {
|
|
6237
6523
|
this.opts = opts;
|
|
6238
6524
|
const doc = host.ownerDocument;
|
|
6239
|
-
injectStyles(
|
|
6525
|
+
injectStyles(STYLE_ID14, CSS14, doc);
|
|
6240
6526
|
this.el = doc.createElement("div");
|
|
6241
6527
|
this.el.className = "vela-mobilebar";
|
|
6242
6528
|
const item = (cls, label, onClick, icon2) => {
|
|
@@ -6257,9 +6543,11 @@ var MobileBar = class {
|
|
|
6257
6543
|
this.actionsHost.className = "vela-mb-actions";
|
|
6258
6544
|
const onDrawings = opts.onDrawingsClick;
|
|
6259
6545
|
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
6546
|
+
const onMaximize = opts.onMaximizeClick;
|
|
6547
|
+
this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
|
|
6260
6548
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
6261
6549
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
6262
|
-
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
6550
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
|
|
6263
6551
|
host.appendChild(this.el);
|
|
6264
6552
|
this.renderActions();
|
|
6265
6553
|
}
|
|
@@ -6291,14 +6579,22 @@ var MobileBar = class {
|
|
|
6291
6579
|
setTimeframe(tf) {
|
|
6292
6580
|
this.tfEl.textContent = timeframeLabel(tf);
|
|
6293
6581
|
}
|
|
6582
|
+
/** Light the maximize stop while something is isolated (a chart over the grid,
|
|
6583
|
+
* or a maximized pane inside the active chart) — inverse chip + restore glyph. */
|
|
6584
|
+
setMaximizeActive(on) {
|
|
6585
|
+
if (!this.maxEl) return;
|
|
6586
|
+
this.maxEl.classList.toggle("vela-mb-on", on);
|
|
6587
|
+
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
6588
|
+
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
6589
|
+
}
|
|
6294
6590
|
destroy() {
|
|
6295
6591
|
this.el.remove();
|
|
6296
6592
|
}
|
|
6297
6593
|
};
|
|
6298
6594
|
|
|
6299
6595
|
// src/widget/timeframe-drawer.ts
|
|
6300
|
-
var
|
|
6301
|
-
var
|
|
6596
|
+
var STYLE_ID15 = "vela-widget-tf-drawer";
|
|
6597
|
+
var CSS15 = `
|
|
6302
6598
|
.vela-tfd-heading {
|
|
6303
6599
|
padding: 4px 2px 8px;
|
|
6304
6600
|
font-size: 12px;
|
|
@@ -6354,7 +6650,7 @@ var TimeframeDrawer = class {
|
|
|
6354
6650
|
this.rangeChips = /* @__PURE__ */ new Map();
|
|
6355
6651
|
this.tfChips = /* @__PURE__ */ new Map();
|
|
6356
6652
|
const doc = opts.host.ownerDocument;
|
|
6357
|
-
injectStyles(
|
|
6653
|
+
injectStyles(STYLE_ID15, CSS15, doc);
|
|
6358
6654
|
this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
|
|
6359
6655
|
const rangeHeading = doc.createElement("div");
|
|
6360
6656
|
rangeHeading.className = "vela-tfd-heading";
|
|
@@ -6412,8 +6708,8 @@ var TimeframeDrawer = class {
|
|
|
6412
6708
|
};
|
|
6413
6709
|
|
|
6414
6710
|
// src/widget/drawings-drawer.ts
|
|
6415
|
-
var
|
|
6416
|
-
var
|
|
6711
|
+
var STYLE_ID16 = "vela-widget-drawings-drawer";
|
|
6712
|
+
var CSS16 = `
|
|
6417
6713
|
/* Search + tabs stay pinned while the tool list scrolls underneath. */
|
|
6418
6714
|
.vela-dd-sticky {
|
|
6419
6715
|
position: sticky;
|
|
@@ -6515,7 +6811,7 @@ var DrawingsDrawer = class {
|
|
|
6515
6811
|
this.definition = { groups: [] };
|
|
6516
6812
|
this.activeGroup = "";
|
|
6517
6813
|
const doc = opts.host.ownerDocument;
|
|
6518
|
-
injectStyles(
|
|
6814
|
+
injectStyles(STYLE_ID16, CSS16, doc);
|
|
6519
6815
|
this.drawer = new Drawer({
|
|
6520
6816
|
host: opts.host,
|
|
6521
6817
|
title: "Drawings",
|
|
@@ -6657,8 +6953,8 @@ var DrawingsDrawer = class {
|
|
|
6657
6953
|
};
|
|
6658
6954
|
|
|
6659
6955
|
// src/widget/more-drawer.ts
|
|
6660
|
-
var
|
|
6661
|
-
var
|
|
6956
|
+
var STYLE_ID17 = "vela-widget-more-drawer";
|
|
6957
|
+
var CSS17 = `
|
|
6662
6958
|
.vela-md-actions {
|
|
6663
6959
|
display: flex;
|
|
6664
6960
|
gap: 6px;
|
|
@@ -6733,7 +7029,7 @@ var MoreDrawer = class {
|
|
|
6733
7029
|
constructor(opts) {
|
|
6734
7030
|
this.opts = opts;
|
|
6735
7031
|
this.view = "main";
|
|
6736
|
-
injectStyles(
|
|
7032
|
+
injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
|
|
6737
7033
|
this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
|
|
6738
7034
|
}
|
|
6739
7035
|
open() {
|
|
@@ -6927,8 +7223,8 @@ var MoreDrawer = class {
|
|
|
6927
7223
|
};
|
|
6928
7224
|
|
|
6929
7225
|
// src/widget/timezone-drawer.ts
|
|
6930
|
-
var
|
|
6931
|
-
var
|
|
7226
|
+
var STYLE_ID18 = "vela-widget-timezone-drawer";
|
|
7227
|
+
var CSS18 = `
|
|
6932
7228
|
.vela-tzd-list { padding: 2px 0 4px; }
|
|
6933
7229
|
.vela-tzd-row {
|
|
6934
7230
|
display: flex;
|
|
@@ -6947,7 +7243,7 @@ var CSS17 = `
|
|
|
6947
7243
|
var TimezoneDrawer = class {
|
|
6948
7244
|
constructor(opts) {
|
|
6949
7245
|
this.opts = opts;
|
|
6950
|
-
injectStyles(
|
|
7246
|
+
injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
|
|
6951
7247
|
this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
|
|
6952
7248
|
}
|
|
6953
7249
|
open() {
|
|
@@ -6985,8 +7281,8 @@ var TimezoneDrawer = class {
|
|
|
6985
7281
|
};
|
|
6986
7282
|
|
|
6987
7283
|
// src/widget/price-scale-drawer.ts
|
|
6988
|
-
var
|
|
6989
|
-
var
|
|
7284
|
+
var STYLE_ID19 = "vela-widget-price-scale-drawer";
|
|
7285
|
+
var CSS19 = `
|
|
6990
7286
|
.vela-psd-list { padding: 2px 0 4px; }
|
|
6991
7287
|
.vela-psd-row {
|
|
6992
7288
|
display: flex;
|
|
@@ -7015,7 +7311,7 @@ var PriceScaleDrawer = class {
|
|
|
7015
7311
|
constructor(opts) {
|
|
7016
7312
|
this.opts = opts;
|
|
7017
7313
|
this.pane = null;
|
|
7018
|
-
injectStyles(
|
|
7314
|
+
injectStyles(STYLE_ID19, CSS19, opts.host.ownerDocument);
|
|
7019
7315
|
this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
|
|
7020
7316
|
}
|
|
7021
7317
|
open() {
|
|
@@ -7151,8 +7447,8 @@ var PriceScaleDrawer = class {
|
|
|
7151
7447
|
};
|
|
7152
7448
|
|
|
7153
7449
|
// src/widget/drawing-pill.ts
|
|
7154
|
-
var
|
|
7155
|
-
var
|
|
7450
|
+
var STYLE_ID20 = "vela-widget-drawing-pill";
|
|
7451
|
+
var CSS20 = `
|
|
7156
7452
|
.vela-drawpill { display: none; }
|
|
7157
7453
|
[data-layout='mobile'] .vela-drawpill {
|
|
7158
7454
|
position: absolute;
|
|
@@ -7212,7 +7508,7 @@ var DrawingPill = class {
|
|
|
7212
7508
|
this.chart = null;
|
|
7213
7509
|
this.chartSubs = [];
|
|
7214
7510
|
const doc = host.ownerDocument;
|
|
7215
|
-
injectStyles(
|
|
7511
|
+
injectStyles(STYLE_ID20, CSS20, doc);
|
|
7216
7512
|
this.el = doc.createElement("div");
|
|
7217
7513
|
this.el.className = "vela-drawpill";
|
|
7218
7514
|
this.el.hidden = true;
|
|
@@ -7325,10 +7621,10 @@ function buildContext(host) {
|
|
|
7325
7621
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
7326
7622
|
var GAP_PX = 2;
|
|
7327
7623
|
var POOL_CAP = 16;
|
|
7328
|
-
var
|
|
7624
|
+
var TIME_AXIS_H3 = 22;
|
|
7329
7625
|
var ALERT_CAP = 50;
|
|
7330
|
-
var
|
|
7331
|
-
var
|
|
7626
|
+
var STYLE_ID21 = "vela-workspace";
|
|
7627
|
+
var CSS21 = `
|
|
7332
7628
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
7333
7629
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
7334
7630
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -7355,6 +7651,21 @@ var CSS20 = `
|
|
|
7355
7651
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
7356
7652
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
7357
7653
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
7654
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
7655
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
7656
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
7657
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
7658
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
7659
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
7660
|
+
.vela-cell[data-drop-target='1']::before {
|
|
7661
|
+
content: '';
|
|
7662
|
+
position: absolute;
|
|
7663
|
+
inset: 0;
|
|
7664
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
7665
|
+
background: var(--vela-separator-hover-band);
|
|
7666
|
+
pointer-events: none;
|
|
7667
|
+
z-index: 11;
|
|
7668
|
+
}
|
|
7358
7669
|
`;
|
|
7359
7670
|
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
7360
7671
|
function declaredOrder(cells) {
|
|
@@ -7384,6 +7695,9 @@ var VelaWorkspace = class {
|
|
|
7384
7695
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
7385
7696
|
this.order = [];
|
|
7386
7697
|
this.activeId = null;
|
|
7698
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
7699
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
7700
|
+
this.maximizedId = null;
|
|
7387
7701
|
this.cellBackend = "auto";
|
|
7388
7702
|
this.destroyed = false;
|
|
7389
7703
|
this.shortcutsHelp = null;
|
|
@@ -7488,7 +7802,7 @@ var VelaWorkspace = class {
|
|
|
7488
7802
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
7489
7803
|
const bootActive = boot?.activeCellId ?? null;
|
|
7490
7804
|
const doc = hostEl.ownerDocument;
|
|
7491
|
-
injectStyles(
|
|
7805
|
+
injectStyles(STYLE_ID21, CSS21, doc);
|
|
7492
7806
|
this.root = doc.createElement("div");
|
|
7493
7807
|
this.root.className = "vela-workspace";
|
|
7494
7808
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -7598,7 +7912,11 @@ var VelaWorkspace = class {
|
|
|
7598
7912
|
if (attribution !== false) {
|
|
7599
7913
|
const background = resolveTheme(opts.theme).background;
|
|
7600
7914
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
7601
|
-
Object.assign(mark.style, {
|
|
7915
|
+
Object.assign(mark.style, {
|
|
7916
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
7917
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
|
|
7918
|
+
zIndex: "11"
|
|
7919
|
+
});
|
|
7602
7920
|
this.gridEl.appendChild(mark);
|
|
7603
7921
|
this.attributionMark = mark;
|
|
7604
7922
|
}
|
|
@@ -7664,6 +7982,9 @@ var VelaWorkspace = class {
|
|
|
7664
7982
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
7665
7983
|
getContext: () => this.context(),
|
|
7666
7984
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
7985
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
7986
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
7987
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
7667
7988
|
onMoreClick: () => this.openMoreDrawer(),
|
|
7668
7989
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
7669
7990
|
}) : null;
|
|
@@ -7778,7 +8099,10 @@ var VelaWorkspace = class {
|
|
|
7778
8099
|
this.topbar.renderActions();
|
|
7779
8100
|
this.mobileBar?.renderActions();
|
|
7780
8101
|
this.dock.refresh();
|
|
7781
|
-
for (const cell of this.cells())
|
|
8102
|
+
for (const cell of this.cells()) {
|
|
8103
|
+
cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
8104
|
+
cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
|
|
8105
|
+
}
|
|
7782
8106
|
}
|
|
7783
8107
|
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
7784
8108
|
/**
|
|
@@ -7867,7 +8191,9 @@ var VelaWorkspace = class {
|
|
|
7867
8191
|
this.pool.clear();
|
|
7868
8192
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
7869
8193
|
this.order = st.charts.map((c) => c.id);
|
|
8194
|
+
this.clearMaximized();
|
|
7870
8195
|
this.applyGrid();
|
|
8196
|
+
this.refreshCellControls();
|
|
7871
8197
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
7872
8198
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
7873
8199
|
else this.setActiveCell(nextActive2);
|
|
@@ -7893,6 +8219,7 @@ var VelaWorkspace = class {
|
|
|
7893
8219
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
7894
8220
|
if (def) this.def = def;
|
|
7895
8221
|
this.cellBackend = this.backendFor(this.def);
|
|
8222
|
+
this.clearMaximized();
|
|
7896
8223
|
this.applyGrid();
|
|
7897
8224
|
this.buildCells();
|
|
7898
8225
|
this.syncCellPresentation();
|
|
@@ -7956,6 +8283,7 @@ var VelaWorkspace = class {
|
|
|
7956
8283
|
setLayout(layout) {
|
|
7957
8284
|
if (this.destroyed) return;
|
|
7958
8285
|
if (this.monoLayout) return;
|
|
8286
|
+
this.clearMaximized();
|
|
7959
8287
|
const next = this.resolveLayout(layout);
|
|
7960
8288
|
const nextBackend = this.backendFor(next);
|
|
7961
8289
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -7976,6 +8304,7 @@ var VelaWorkspace = class {
|
|
|
7976
8304
|
this.buildCells();
|
|
7977
8305
|
this.alignNewCellStyles(preexisting);
|
|
7978
8306
|
this.syncCellPresentation();
|
|
8307
|
+
this.refreshCellControls();
|
|
7979
8308
|
this.topbar.setLayout(next.id);
|
|
7980
8309
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
7981
8310
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -7984,6 +8313,67 @@ var VelaWorkspace = class {
|
|
|
7984
8313
|
this.events.emit("layout:changed", { layout: next.id });
|
|
7985
8314
|
this.markStateDirty();
|
|
7986
8315
|
}
|
|
8316
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
8317
|
+
get maximizedCell() {
|
|
8318
|
+
return this.maximizedId;
|
|
8319
|
+
}
|
|
8320
|
+
/**
|
|
8321
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
8322
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
8323
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
8324
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
8325
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
8326
|
+
*/
|
|
8327
|
+
maximizeCell(id) {
|
|
8328
|
+
if (this.destroyed) return;
|
|
8329
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
8330
|
+
if (id === this.maximizedId) return;
|
|
8331
|
+
this.maximizedId = id;
|
|
8332
|
+
if (id) this.setActiveCell(id);
|
|
8333
|
+
this.applyGrid();
|
|
8334
|
+
this.refreshCellControls();
|
|
8335
|
+
this.syncMobileMaximize();
|
|
8336
|
+
this.events.emit("cell:maximized", { id });
|
|
8337
|
+
}
|
|
8338
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
8339
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
8340
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
8341
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
8342
|
+
* synchronous `pane:changed`). */
|
|
8343
|
+
toggleMobileMaximize() {
|
|
8344
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
8345
|
+
if (!cell) return;
|
|
8346
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
8347
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
8348
|
+
else this.maximizeCell(cell.id);
|
|
8349
|
+
}
|
|
8350
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
8351
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
8352
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
8353
|
+
syncMobileMaximize() {
|
|
8354
|
+
if (!this.mobileBar) return;
|
|
8355
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
8356
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
8357
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
8358
|
+
}
|
|
8359
|
+
/**
|
|
8360
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
8361
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
8362
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
8363
|
+
*/
|
|
8364
|
+
swapCells(a, b) {
|
|
8365
|
+
if (this.destroyed || a === b) return;
|
|
8366
|
+
const i = this.order.indexOf(a);
|
|
8367
|
+
const j = this.order.indexOf(b);
|
|
8368
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
8369
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
8370
|
+
for (const [k] of this.def.cells.entries()) {
|
|
8371
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
8372
|
+
if (host) this.gridEl.appendChild(host);
|
|
8373
|
+
}
|
|
8374
|
+
this.applyGrid();
|
|
8375
|
+
this.markStateDirty();
|
|
8376
|
+
}
|
|
7987
8377
|
resize() {
|
|
7988
8378
|
this.splitters.layout();
|
|
7989
8379
|
}
|
|
@@ -8053,6 +8443,7 @@ var VelaWorkspace = class {
|
|
|
8053
8443
|
this.mobileBar?.renderActions();
|
|
8054
8444
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
8055
8445
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
8446
|
+
this.syncMobileMaximize();
|
|
8056
8447
|
this.drawingPill?.onChart(cell.chart);
|
|
8057
8448
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
8058
8449
|
this.historyUnsub?.();
|
|
@@ -8142,8 +8533,78 @@ var VelaWorkspace = class {
|
|
|
8142
8533
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
8143
8534
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
8144
8535
|
}
|
|
8536
|
+
this.applyMaximizePresentation();
|
|
8537
|
+
this.mountAttributionMark();
|
|
8145
8538
|
this.splitters.layout();
|
|
8146
8539
|
}
|
|
8540
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
8541
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
8542
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
8543
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
8544
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
8545
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
8546
|
+
applyMaximizePresentation() {
|
|
8547
|
+
const maxId = this.maximizedId;
|
|
8548
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
8549
|
+
else delete this.gridEl.dataset.maximized;
|
|
8550
|
+
for (const [id, cell] of this.cellsById) {
|
|
8551
|
+
const style = cell.host.style;
|
|
8552
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
8553
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
8554
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
8558
|
+
refreshCellControls() {
|
|
8559
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
8560
|
+
}
|
|
8561
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
8562
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
8563
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
8564
|
+
clearMaximized() {
|
|
8565
|
+
if (this.maximizedId == null) return;
|
|
8566
|
+
this.maximizedId = null;
|
|
8567
|
+
this.events.emit("cell:maximized", { id: null });
|
|
8568
|
+
}
|
|
8569
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
8570
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
8571
|
+
cellAtPoint(x, y, excludeId) {
|
|
8572
|
+
for (const [id, cell] of this.cellsById) {
|
|
8573
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
8574
|
+
const r = cell.host.getBoundingClientRect();
|
|
8575
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
8576
|
+
}
|
|
8577
|
+
return null;
|
|
8578
|
+
}
|
|
8579
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
8580
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
8581
|
+
setDropTarget(id) {
|
|
8582
|
+
for (const [cid, cell] of this.cellsById) {
|
|
8583
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
8584
|
+
else delete cell.host.dataset.dropTarget;
|
|
8585
|
+
}
|
|
8586
|
+
}
|
|
8587
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
8588
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
8589
|
+
bottomLeftCell() {
|
|
8590
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
8591
|
+
const grid = occupancyGrid(this.def);
|
|
8592
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
8593
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
8594
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
8595
|
+
}
|
|
8596
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
8597
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
8598
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
8599
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
8600
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
8601
|
+
* the DOM, and this re-mount brings it back. */
|
|
8602
|
+
mountAttributionMark() {
|
|
8603
|
+
const mark = this.attributionMark;
|
|
8604
|
+
if (!mark) return;
|
|
8605
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
8606
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
8607
|
+
}
|
|
8147
8608
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
8148
8609
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
8149
8610
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -8175,6 +8636,12 @@ var VelaWorkspace = class {
|
|
|
8175
8636
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
8176
8637
|
context: () => this.context(),
|
|
8177
8638
|
activate: (id2) => this.setActiveCell(id2),
|
|
8639
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
8640
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
8641
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
8642
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
8643
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
8644
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
8178
8645
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
8179
8646
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
8180
8647
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -8188,6 +8655,7 @@ var VelaWorkspace = class {
|
|
|
8188
8655
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
8189
8656
|
this.wireCell(cell);
|
|
8190
8657
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
8658
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
8191
8659
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
8192
8660
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
8193
8661
|
cell.restorePersistedExt();
|
|
@@ -8197,6 +8665,7 @@ var VelaWorkspace = class {
|
|
|
8197
8665
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
8198
8666
|
if (host) this.gridEl.appendChild(host);
|
|
8199
8667
|
}
|
|
8668
|
+
this.mountAttributionMark();
|
|
8200
8669
|
}
|
|
8201
8670
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
8202
8671
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -8256,6 +8725,9 @@ var VelaWorkspace = class {
|
|
|
8256
8725
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
8257
8726
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
8258
8727
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
8728
|
+
chart.on("pane:changed", () => {
|
|
8729
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
8730
|
+
});
|
|
8259
8731
|
chart.renderer.onAxisLongPress((e) => {
|
|
8260
8732
|
if (this.layoutCtl.current !== "mobile") return;
|
|
8261
8733
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -8576,6 +9048,7 @@ var VelaWorkspace = class {
|
|
|
8576
9048
|
for (const cell of this.cellsById.values()) {
|
|
8577
9049
|
cell.chart.renderer.closeDialogs();
|
|
8578
9050
|
cell.chart.renderer.setLayoutMode(mode);
|
|
9051
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
8579
9052
|
}
|
|
8580
9053
|
this.syncCellPresentation();
|
|
8581
9054
|
}
|