@luxalgo/vela 0.6.9 → 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-8Z95Q-RJ.d.cts → DataProvider-CWmp31dA.d.ts} +49 -1
- package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-l_eLMly_.d.cts} +49 -1
- package/dist/{chunk-EQCHJZOT.js → chunk-4KZVZ7QQ.js} +1 -1
- package/dist/{chunk-6WDDVMBJ.js → chunk-FFOP37FQ.js} +577 -321
- package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
- package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
- package/dist/{chunk-STHSKXOR.js → chunk-NMTQXNT4.js} +591 -141
- package/dist/{contributions-D7PVZO2i.d.ts → contributions-BCz6Dr6a.d.ts} +6 -6
- package/dist/{contributions-C1U2Krwg.d.cts → contributions-D9vTzm5p.d.cts} +6 -6
- package/dist/index.cjs +580 -318
- package/dist/index.d.cts +31 -10
- package/dist/index.d.ts +31 -10
- package/dist/index.js +4 -4
- package/dist/{options-FM0peknS.d.ts → options-DSqHsQyN.d.cts} +6 -6
- package/dist/{options-FM0peknS.d.cts → options-DSqHsQyN.d.ts} +6 -6
- package/dist/{plugin-DfVqBz9p.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
- package/dist/{plugin-7bkF32Rk.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
- package/dist/plugin.cjs +3 -1
- 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-zdF4eZLr.d.ts → statusline-5K7y4Ya2.d.ts} +3 -3
- package/dist/{statusline-DOPiT6I6.d.cts → statusline-TYLQmoeh.d.cts} +3 -3
- package/dist/ui.cjs +7 -1
- package/dist/ui.d.cts +3 -1
- package/dist/ui.d.ts +3 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +580 -318
- package/dist/vela.global.min.js +50 -48
- package/dist/widget.cjs +1052 -340
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1052 -340
- package/dist/workspace.d.cts +89 -5
- package/dist/workspace.d.ts +89 -5
- 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, LEGEND_AT_TOP_ATTR, 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, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-
|
|
3
|
-
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-
|
|
4
|
-
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-
|
|
5
|
-
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_OFF, SESSION_POST, SESSION_PRE, categoricalColor } from './chunk-
|
|
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
|
|
@@ -654,7 +654,7 @@ var Topbar = class {
|
|
|
654
654
|
this.leftActionsHost.className = "vela-widget-actions-left";
|
|
655
655
|
this.panelsHost = doc.createElement("span");
|
|
656
656
|
this.panelsHost.className = "vela-widget-panels";
|
|
657
|
-
const tool = (cls,
|
|
657
|
+
const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
|
|
658
658
|
if (vis("undo-redo")) {
|
|
659
659
|
this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
|
|
660
660
|
this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
|
|
@@ -852,8 +852,8 @@ var Topbar = class {
|
|
|
852
852
|
}
|
|
853
853
|
renderStyleButton(doc) {
|
|
854
854
|
this.styleButton.replaceChildren();
|
|
855
|
-
const
|
|
856
|
-
if (
|
|
855
|
+
const icon2 = priceStyleIcon(this.priceStyle);
|
|
856
|
+
if (icon2) this.styleButton.appendChild(iconEl(icon2, doc));
|
|
857
857
|
else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
|
|
858
858
|
this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
|
|
859
859
|
}
|
|
@@ -945,11 +945,11 @@ var Topbar = class {
|
|
|
945
945
|
this.el.remove();
|
|
946
946
|
}
|
|
947
947
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
948
|
-
toolButton(cls,
|
|
948
|
+
toolButton(cls, icon2, tip, onClick, sink) {
|
|
949
949
|
const doc = this.el.ownerDocument;
|
|
950
950
|
const b = doc.createElement("button");
|
|
951
951
|
b.className = `vela-widget-tool ${cls}`;
|
|
952
|
-
b.appendChild(iconEl(
|
|
952
|
+
b.appendChild(iconEl(icon2, doc));
|
|
953
953
|
b.setAttribute("aria-label", tip);
|
|
954
954
|
sink.push(new Tooltip(b, { content: tip, triggerId: `vela-tool-${cls}`, host: this.host }));
|
|
955
955
|
if (onClick) b.addEventListener("click", onClick);
|
|
@@ -1629,13 +1629,13 @@ var MenuBuilder = class {
|
|
|
1629
1629
|
this.actions = /* @__PURE__ */ new Map();
|
|
1630
1630
|
this.seq = 0;
|
|
1631
1631
|
}
|
|
1632
|
-
entry(label,
|
|
1632
|
+
entry(label, icon2, run, separatorBefore = false) {
|
|
1633
1633
|
const id = this.next();
|
|
1634
1634
|
this.actions.set(id, run);
|
|
1635
|
-
return { id, label, icon, separatorBefore };
|
|
1635
|
+
return { id, label, icon: icon2, separatorBefore };
|
|
1636
1636
|
}
|
|
1637
|
-
submenu(label,
|
|
1638
|
-
return { id: this.next(), label, icon, submenu, separatorBefore };
|
|
1637
|
+
submenu(label, icon2, submenu, separatorBefore = false) {
|
|
1638
|
+
return { id: this.next(), label, icon: icon2, submenu, separatorBefore };
|
|
1639
1639
|
}
|
|
1640
1640
|
next() {
|
|
1641
1641
|
this.seq += 1;
|
|
@@ -1872,8 +1872,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
1872
1872
|
const acts = doc.createElement("span");
|
|
1873
1873
|
acts.className = "vela-ot-acts";
|
|
1874
1874
|
head.appendChild(acts);
|
|
1875
|
-
const op = (
|
|
1876
|
-
acts.appendChild(this.btn({ icon, title, run }));
|
|
1875
|
+
const op = (icon2, title, run) => {
|
|
1876
|
+
acts.appendChild(this.btn({ icon: icon2, title, run }));
|
|
1877
1877
|
};
|
|
1878
1878
|
if (pane.kind !== "price") {
|
|
1879
1879
|
if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
|
|
@@ -1888,8 +1888,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
1888
1888
|
const { chart } = pass;
|
|
1889
1889
|
if (row.kind === "price") {
|
|
1890
1890
|
const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
|
|
1891
|
-
const
|
|
1892
|
-
const el2 = this.row(
|
|
1891
|
+
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
|
|
1892
|
+
const el2 = this.row(icon2, row.label, row.visible, [
|
|
1893
1893
|
{
|
|
1894
1894
|
icon: row.visible ? "eye" : "eye-off",
|
|
1895
1895
|
title: row.visible ? "Hide" : "Show",
|
|
@@ -2008,16 +2008,16 @@ var ObjectTree = class extends SidePanel {
|
|
|
2008
2008
|
const doc = this.el.ownerDocument;
|
|
2009
2009
|
const { chart } = pass;
|
|
2010
2010
|
const markup = drawingMeta(d.type).icon;
|
|
2011
|
-
let
|
|
2011
|
+
let icon2;
|
|
2012
2012
|
if (markup) {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2013
|
+
icon2 = doc.createElement("span");
|
|
2014
|
+
icon2.className = "vela-icon";
|
|
2015
|
+
icon2.setAttribute("aria-hidden", "true");
|
|
2016
|
+
icon2.innerHTML = markup;
|
|
2017
2017
|
} else {
|
|
2018
|
-
|
|
2018
|
+
icon2 = iconEl("pen", doc);
|
|
2019
2019
|
}
|
|
2020
|
-
const el = this.row(
|
|
2020
|
+
const el = this.row(icon2, drawingLabel(d), d.visible, [
|
|
2021
2021
|
{
|
|
2022
2022
|
icon: d.locked ? "lock" : "unlock",
|
|
2023
2023
|
title: d.locked ? "Unlock" : "Lock",
|
|
@@ -2083,7 +2083,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
2083
2083
|
}
|
|
2084
2084
|
}
|
|
2085
2085
|
/** The shared row shell: icon, name, optional tag, then the action buttons. */
|
|
2086
|
-
row(
|
|
2086
|
+
row(icon2, label, visible, actions, tag) {
|
|
2087
2087
|
const doc = this.el.ownerDocument;
|
|
2088
2088
|
const el = doc.createElement("div");
|
|
2089
2089
|
el.className = "vela-ot-row";
|
|
@@ -2092,7 +2092,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
2092
2092
|
name.className = "vela-ot-name";
|
|
2093
2093
|
name.textContent = label;
|
|
2094
2094
|
name.title = label;
|
|
2095
|
-
el.append(
|
|
2095
|
+
el.append(icon2, name);
|
|
2096
2096
|
if (tag) {
|
|
2097
2097
|
const t = doc.createElement("span");
|
|
2098
2098
|
t.className = "vela-ot-tag";
|
|
@@ -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";
|
|
@@ -4817,6 +4899,229 @@ var SessionShadingTracker = class {
|
|
|
4817
4899
|
}
|
|
4818
4900
|
};
|
|
4819
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
|
+
|
|
4820
5125
|
// src/widget/history.ts
|
|
4821
5126
|
var WidgetHistory = class {
|
|
4822
5127
|
/** `getChart` late-resolves the CURRENT chart: drawing steps recorded before a chart
|
|
@@ -5066,11 +5371,18 @@ var ChartCell = class {
|
|
|
5066
5371
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
5067
5372
|
});
|
|
5068
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
|
+
});
|
|
5069
5384
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
5070
|
-
resetView: () =>
|
|
5071
|
-
this.inner?.renderer.set("autoScale", true);
|
|
5072
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
5073
|
-
},
|
|
5385
|
+
resetView: () => this.resetView(),
|
|
5074
5386
|
timezone: () => this.deps.timezone(),
|
|
5075
5387
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
5076
5388
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -5444,6 +5756,20 @@ var ChartCell = class {
|
|
|
5444
5756
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
5445
5757
|
}
|
|
5446
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
|
+
}
|
|
5447
5773
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
5448
5774
|
focus() {
|
|
5449
5775
|
this.deps.activate(this.id);
|
|
@@ -5757,6 +6083,7 @@ var ChartCell = class {
|
|
|
5757
6083
|
destroy() {
|
|
5758
6084
|
this.destroyed = true;
|
|
5759
6085
|
this.offMarket();
|
|
6086
|
+
this.cellControls.destroy();
|
|
5760
6087
|
this.contextMenu.destroy();
|
|
5761
6088
|
this.history.destroy();
|
|
5762
6089
|
this.marketStatus?.stop();
|
|
@@ -6015,8 +6342,8 @@ var SplitterLayer = class {
|
|
|
6015
6342
|
};
|
|
6016
6343
|
|
|
6017
6344
|
// src/widget/toast.ts
|
|
6018
|
-
var
|
|
6019
|
-
var
|
|
6345
|
+
var STYLE_ID13 = "vela-widget-toast";
|
|
6346
|
+
var CSS13 = `
|
|
6020
6347
|
.vela-toast {
|
|
6021
6348
|
position: absolute;
|
|
6022
6349
|
left: 50%;
|
|
@@ -6046,7 +6373,7 @@ var CSS12 = `
|
|
|
6046
6373
|
var Toast = class {
|
|
6047
6374
|
constructor(host) {
|
|
6048
6375
|
this.timer = null;
|
|
6049
|
-
injectStyles(
|
|
6376
|
+
injectStyles(STYLE_ID13, CSS13, host.ownerDocument);
|
|
6050
6377
|
this.el = host.ownerDocument.createElement("div");
|
|
6051
6378
|
this.el.className = "vela-toast";
|
|
6052
6379
|
host.appendChild(this.el);
|
|
@@ -6070,70 +6397,6 @@ var Toast = class {
|
|
|
6070
6397
|
}
|
|
6071
6398
|
};
|
|
6072
6399
|
|
|
6073
|
-
// src/widget/glide.ts
|
|
6074
|
-
var MIN_SPAN_MS = 6e4;
|
|
6075
|
-
var FOLLOW = 0.25;
|
|
6076
|
-
var ZOOM_IN = 0.8;
|
|
6077
|
-
var ZOOM_OUT = 1.25;
|
|
6078
|
-
var PAN_FAST = 0.2;
|
|
6079
|
-
function zoomTarget(base, factor) {
|
|
6080
|
-
const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
|
|
6081
|
-
return { from: base.to - newSpan, to: base.to };
|
|
6082
|
-
}
|
|
6083
|
-
function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
|
|
6084
|
-
const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
|
|
6085
|
-
const nf = cur.from + (target.from - cur.from) * follow;
|
|
6086
|
-
const nt = cur.to + (target.to - cur.to) * follow;
|
|
6087
|
-
const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
|
|
6088
|
-
return { cur: done ? { ...target } : { from: nf, to: nt }, done };
|
|
6089
|
-
}
|
|
6090
|
-
var Glider = class {
|
|
6091
|
-
constructor(chart) {
|
|
6092
|
-
this.chart = chart;
|
|
6093
|
-
// The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
|
|
6094
|
-
// is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
|
|
6095
|
-
// clamps what gets applied — chasing that read-back can never converge on a
|
|
6096
|
-
// clamped step, leaving the rAF loop re-applying a stale target forever (and
|
|
6097
|
-
// grinding the span down when panning past the newest bar). The internal range
|
|
6098
|
-
// converges geometrically, so the glide always terminates; the renderer is free
|
|
6099
|
-
// to clamp each applied frame.
|
|
6100
|
-
this.cur = null;
|
|
6101
|
-
this.target = null;
|
|
6102
|
-
this.raf = 0;
|
|
6103
|
-
}
|
|
6104
|
-
zoom(factor) {
|
|
6105
|
-
this.to((base) => zoomTarget(base, factor));
|
|
6106
|
-
}
|
|
6107
|
-
stop() {
|
|
6108
|
-
if (this.raf) cancelAnimationFrame(this.raf);
|
|
6109
|
-
this.raf = 0;
|
|
6110
|
-
this.cur = null;
|
|
6111
|
-
this.target = null;
|
|
6112
|
-
}
|
|
6113
|
-
to(make) {
|
|
6114
|
-
const chart = this.chart();
|
|
6115
|
-
const base = this.target ?? chart?.getVisibleRange();
|
|
6116
|
-
if (!chart || !base) return;
|
|
6117
|
-
this.cur ?? (this.cur = { ...base });
|
|
6118
|
-
this.target = make(base);
|
|
6119
|
-
if (!this.raf) this.tick();
|
|
6120
|
-
}
|
|
6121
|
-
tick() {
|
|
6122
|
-
this.raf = requestAnimationFrame(() => {
|
|
6123
|
-
const chart = this.chart();
|
|
6124
|
-
if (!chart || !this.cur || !this.target) {
|
|
6125
|
-
this.stop();
|
|
6126
|
-
return;
|
|
6127
|
-
}
|
|
6128
|
-
const { cur: next, done } = followStep(this.cur, this.target);
|
|
6129
|
-
this.cur = next;
|
|
6130
|
-
chart.setVisibleRange(next);
|
|
6131
|
-
if (done) this.stop();
|
|
6132
|
-
else this.tick();
|
|
6133
|
-
});
|
|
6134
|
-
}
|
|
6135
|
-
};
|
|
6136
|
-
|
|
6137
6400
|
// src/widget/tool-shortcuts.ts
|
|
6138
6401
|
var TOOL_BINDINGS = [
|
|
6139
6402
|
["trendline", "drawings.trendline"],
|
|
@@ -6205,8 +6468,8 @@ var LayoutModeController = class {
|
|
|
6205
6468
|
};
|
|
6206
6469
|
|
|
6207
6470
|
// src/widget/mobile-bar.ts
|
|
6208
|
-
var
|
|
6209
|
-
var
|
|
6471
|
+
var STYLE_ID14 = "vela-widget-mobilebar";
|
|
6472
|
+
var CSS14 = `
|
|
6210
6473
|
.vela-mobilebar {
|
|
6211
6474
|
display: none;
|
|
6212
6475
|
align-items: stretch;
|
|
@@ -6237,6 +6500,9 @@ var CSS13 = `
|
|
|
6237
6500
|
}
|
|
6238
6501
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
6239
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); }
|
|
6240
6506
|
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
6241
6507
|
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
6242
6508
|
.vela-mb-actions { display: contents; }
|
|
@@ -6256,14 +6522,14 @@ var MobileBar = class {
|
|
|
6256
6522
|
constructor(host, opts) {
|
|
6257
6523
|
this.opts = opts;
|
|
6258
6524
|
const doc = host.ownerDocument;
|
|
6259
|
-
injectStyles(
|
|
6525
|
+
injectStyles(STYLE_ID14, CSS14, doc);
|
|
6260
6526
|
this.el = doc.createElement("div");
|
|
6261
6527
|
this.el.className = "vela-mobilebar";
|
|
6262
|
-
const item = (cls, label, onClick,
|
|
6528
|
+
const item = (cls, label, onClick, icon2) => {
|
|
6263
6529
|
const b = doc.createElement("button");
|
|
6264
6530
|
b.className = `vela-mb-item ${cls}`;
|
|
6265
6531
|
b.setAttribute("aria-label", label);
|
|
6266
|
-
if (
|
|
6532
|
+
if (icon2) b.appendChild(iconEl(icon2, doc));
|
|
6267
6533
|
b.addEventListener("click", onClick);
|
|
6268
6534
|
return b;
|
|
6269
6535
|
};
|
|
@@ -6277,9 +6543,11 @@ var MobileBar = class {
|
|
|
6277
6543
|
this.actionsHost.className = "vela-mb-actions";
|
|
6278
6544
|
const onDrawings = opts.onDrawingsClick;
|
|
6279
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;
|
|
6280
6548
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
6281
6549
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
6282
|
-
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);
|
|
6283
6551
|
host.appendChild(this.el);
|
|
6284
6552
|
this.renderActions();
|
|
6285
6553
|
}
|
|
@@ -6311,14 +6579,22 @@ var MobileBar = class {
|
|
|
6311
6579
|
setTimeframe(tf) {
|
|
6312
6580
|
this.tfEl.textContent = timeframeLabel(tf);
|
|
6313
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
|
+
}
|
|
6314
6590
|
destroy() {
|
|
6315
6591
|
this.el.remove();
|
|
6316
6592
|
}
|
|
6317
6593
|
};
|
|
6318
6594
|
|
|
6319
6595
|
// src/widget/timeframe-drawer.ts
|
|
6320
|
-
var
|
|
6321
|
-
var
|
|
6596
|
+
var STYLE_ID15 = "vela-widget-tf-drawer";
|
|
6597
|
+
var CSS15 = `
|
|
6322
6598
|
.vela-tfd-heading {
|
|
6323
6599
|
padding: 4px 2px 8px;
|
|
6324
6600
|
font-size: 12px;
|
|
@@ -6374,7 +6650,7 @@ var TimeframeDrawer = class {
|
|
|
6374
6650
|
this.rangeChips = /* @__PURE__ */ new Map();
|
|
6375
6651
|
this.tfChips = /* @__PURE__ */ new Map();
|
|
6376
6652
|
const doc = opts.host.ownerDocument;
|
|
6377
|
-
injectStyles(
|
|
6653
|
+
injectStyles(STYLE_ID15, CSS15, doc);
|
|
6378
6654
|
this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
|
|
6379
6655
|
const rangeHeading = doc.createElement("div");
|
|
6380
6656
|
rangeHeading.className = "vela-tfd-heading";
|
|
@@ -6432,8 +6708,8 @@ var TimeframeDrawer = class {
|
|
|
6432
6708
|
};
|
|
6433
6709
|
|
|
6434
6710
|
// src/widget/drawings-drawer.ts
|
|
6435
|
-
var
|
|
6436
|
-
var
|
|
6711
|
+
var STYLE_ID16 = "vela-widget-drawings-drawer";
|
|
6712
|
+
var CSS16 = `
|
|
6437
6713
|
/* Search + tabs stay pinned while the tool list scrolls underneath. */
|
|
6438
6714
|
.vela-dd-sticky {
|
|
6439
6715
|
position: sticky;
|
|
@@ -6535,7 +6811,7 @@ var DrawingsDrawer = class {
|
|
|
6535
6811
|
this.definition = { groups: [] };
|
|
6536
6812
|
this.activeGroup = "";
|
|
6537
6813
|
const doc = opts.host.ownerDocument;
|
|
6538
|
-
injectStyles(
|
|
6814
|
+
injectStyles(STYLE_ID16, CSS16, doc);
|
|
6539
6815
|
this.drawer = new Drawer({
|
|
6540
6816
|
host: opts.host,
|
|
6541
6817
|
title: "Drawings",
|
|
@@ -6677,8 +6953,8 @@ var DrawingsDrawer = class {
|
|
|
6677
6953
|
};
|
|
6678
6954
|
|
|
6679
6955
|
// src/widget/more-drawer.ts
|
|
6680
|
-
var
|
|
6681
|
-
var
|
|
6956
|
+
var STYLE_ID17 = "vela-widget-more-drawer";
|
|
6957
|
+
var CSS17 = `
|
|
6682
6958
|
.vela-md-actions {
|
|
6683
6959
|
display: flex;
|
|
6684
6960
|
gap: 6px;
|
|
@@ -6753,7 +7029,7 @@ var MoreDrawer = class {
|
|
|
6753
7029
|
constructor(opts) {
|
|
6754
7030
|
this.opts = opts;
|
|
6755
7031
|
this.view = "main";
|
|
6756
|
-
injectStyles(
|
|
7032
|
+
injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
|
|
6757
7033
|
this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
|
|
6758
7034
|
}
|
|
6759
7035
|
open() {
|
|
@@ -6809,11 +7085,11 @@ var MoreDrawer = class {
|
|
|
6809
7085
|
renderMain(doc) {
|
|
6810
7086
|
const actions = doc.createElement("div");
|
|
6811
7087
|
actions.className = "vela-md-actions";
|
|
6812
|
-
const action = (
|
|
7088
|
+
const action = (icon2, label, enabled, run) => {
|
|
6813
7089
|
const b = doc.createElement("button");
|
|
6814
7090
|
b.className = "vela-md-action";
|
|
6815
7091
|
b.disabled = !enabled;
|
|
6816
|
-
b.appendChild(iconEl(
|
|
7092
|
+
b.appendChild(iconEl(icon2, doc));
|
|
6817
7093
|
b.appendChild(doc.createTextNode(label));
|
|
6818
7094
|
b.addEventListener("click", () => {
|
|
6819
7095
|
run();
|
|
@@ -6947,8 +7223,8 @@ var MoreDrawer = class {
|
|
|
6947
7223
|
};
|
|
6948
7224
|
|
|
6949
7225
|
// src/widget/timezone-drawer.ts
|
|
6950
|
-
var
|
|
6951
|
-
var
|
|
7226
|
+
var STYLE_ID18 = "vela-widget-timezone-drawer";
|
|
7227
|
+
var CSS18 = `
|
|
6952
7228
|
.vela-tzd-list { padding: 2px 0 4px; }
|
|
6953
7229
|
.vela-tzd-row {
|
|
6954
7230
|
display: flex;
|
|
@@ -6967,7 +7243,7 @@ var CSS17 = `
|
|
|
6967
7243
|
var TimezoneDrawer = class {
|
|
6968
7244
|
constructor(opts) {
|
|
6969
7245
|
this.opts = opts;
|
|
6970
|
-
injectStyles(
|
|
7246
|
+
injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
|
|
6971
7247
|
this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
|
|
6972
7248
|
}
|
|
6973
7249
|
open() {
|
|
@@ -7005,8 +7281,8 @@ var TimezoneDrawer = class {
|
|
|
7005
7281
|
};
|
|
7006
7282
|
|
|
7007
7283
|
// src/widget/price-scale-drawer.ts
|
|
7008
|
-
var
|
|
7009
|
-
var
|
|
7284
|
+
var STYLE_ID19 = "vela-widget-price-scale-drawer";
|
|
7285
|
+
var CSS19 = `
|
|
7010
7286
|
.vela-psd-list { padding: 2px 0 4px; }
|
|
7011
7287
|
.vela-psd-row {
|
|
7012
7288
|
display: flex;
|
|
@@ -7035,7 +7311,7 @@ var PriceScaleDrawer = class {
|
|
|
7035
7311
|
constructor(opts) {
|
|
7036
7312
|
this.opts = opts;
|
|
7037
7313
|
this.pane = null;
|
|
7038
|
-
injectStyles(
|
|
7314
|
+
injectStyles(STYLE_ID19, CSS19, opts.host.ownerDocument);
|
|
7039
7315
|
this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
|
|
7040
7316
|
}
|
|
7041
7317
|
open() {
|
|
@@ -7171,8 +7447,8 @@ var PriceScaleDrawer = class {
|
|
|
7171
7447
|
};
|
|
7172
7448
|
|
|
7173
7449
|
// src/widget/drawing-pill.ts
|
|
7174
|
-
var
|
|
7175
|
-
var
|
|
7450
|
+
var STYLE_ID20 = "vela-widget-drawing-pill";
|
|
7451
|
+
var CSS20 = `
|
|
7176
7452
|
.vela-drawpill { display: none; }
|
|
7177
7453
|
[data-layout='mobile'] .vela-drawpill {
|
|
7178
7454
|
position: absolute;
|
|
@@ -7232,17 +7508,17 @@ var DrawingPill = class {
|
|
|
7232
7508
|
this.chart = null;
|
|
7233
7509
|
this.chartSubs = [];
|
|
7234
7510
|
const doc = host.ownerDocument;
|
|
7235
|
-
injectStyles(
|
|
7511
|
+
injectStyles(STYLE_ID20, CSS20, doc);
|
|
7236
7512
|
this.el = doc.createElement("div");
|
|
7237
7513
|
this.el.className = "vela-drawpill";
|
|
7238
7514
|
this.el.hidden = true;
|
|
7239
7515
|
this.toolGlyph = doc.createElement("span");
|
|
7240
7516
|
this.toolGlyph.className = "vela-drawpill-tool";
|
|
7241
|
-
const btn = (
|
|
7517
|
+
const btn = (icon2, label, onClick) => {
|
|
7242
7518
|
const b = doc.createElement("button");
|
|
7243
7519
|
b.className = "vela-drawpill-btn";
|
|
7244
7520
|
b.setAttribute("aria-label", label);
|
|
7245
|
-
b.appendChild(iconEl(
|
|
7521
|
+
b.appendChild(iconEl(icon2, doc));
|
|
7246
7522
|
b.addEventListener("click", onClick);
|
|
7247
7523
|
return b;
|
|
7248
7524
|
};
|
|
@@ -7345,10 +7621,10 @@ function buildContext(host) {
|
|
|
7345
7621
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
7346
7622
|
var GAP_PX = 2;
|
|
7347
7623
|
var POOL_CAP = 16;
|
|
7348
|
-
var
|
|
7624
|
+
var TIME_AXIS_H3 = 22;
|
|
7349
7625
|
var ALERT_CAP = 50;
|
|
7350
|
-
var
|
|
7351
|
-
var
|
|
7626
|
+
var STYLE_ID21 = "vela-workspace";
|
|
7627
|
+
var CSS21 = `
|
|
7352
7628
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
7353
7629
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
7354
7630
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -7375,6 +7651,21 @@ var CSS20 = `
|
|
|
7375
7651
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
7376
7652
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
7377
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
|
+
}
|
|
7378
7669
|
`;
|
|
7379
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"/>'));
|
|
7380
7671
|
function declaredOrder(cells) {
|
|
@@ -7404,6 +7695,9 @@ var VelaWorkspace = class {
|
|
|
7404
7695
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
7405
7696
|
this.order = [];
|
|
7406
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;
|
|
7407
7701
|
this.cellBackend = "auto";
|
|
7408
7702
|
this.destroyed = false;
|
|
7409
7703
|
this.shortcutsHelp = null;
|
|
@@ -7508,7 +7802,7 @@ var VelaWorkspace = class {
|
|
|
7508
7802
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
7509
7803
|
const bootActive = boot?.activeCellId ?? null;
|
|
7510
7804
|
const doc = hostEl.ownerDocument;
|
|
7511
|
-
injectStyles(
|
|
7805
|
+
injectStyles(STYLE_ID21, CSS21, doc);
|
|
7512
7806
|
this.root = doc.createElement("div");
|
|
7513
7807
|
this.root.className = "vela-workspace";
|
|
7514
7808
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -7618,7 +7912,11 @@ var VelaWorkspace = class {
|
|
|
7618
7912
|
if (attribution !== false) {
|
|
7619
7913
|
const background = resolveTheme(opts.theme).background;
|
|
7620
7914
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
7621
|
-
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
|
+
});
|
|
7622
7920
|
this.gridEl.appendChild(mark);
|
|
7623
7921
|
this.attributionMark = mark;
|
|
7624
7922
|
}
|
|
@@ -7684,6 +7982,9 @@ var VelaWorkspace = class {
|
|
|
7684
7982
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
7685
7983
|
getContext: () => this.context(),
|
|
7686
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() },
|
|
7687
7988
|
onMoreClick: () => this.openMoreDrawer(),
|
|
7688
7989
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
7689
7990
|
}) : null;
|
|
@@ -7890,7 +8191,9 @@ var VelaWorkspace = class {
|
|
|
7890
8191
|
this.pool.clear();
|
|
7891
8192
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
7892
8193
|
this.order = st.charts.map((c) => c.id);
|
|
8194
|
+
this.clearMaximized();
|
|
7893
8195
|
this.applyGrid();
|
|
8196
|
+
this.refreshCellControls();
|
|
7894
8197
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
7895
8198
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
7896
8199
|
else this.setActiveCell(nextActive2);
|
|
@@ -7916,6 +8219,7 @@ var VelaWorkspace = class {
|
|
|
7916
8219
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
7917
8220
|
if (def) this.def = def;
|
|
7918
8221
|
this.cellBackend = this.backendFor(this.def);
|
|
8222
|
+
this.clearMaximized();
|
|
7919
8223
|
this.applyGrid();
|
|
7920
8224
|
this.buildCells();
|
|
7921
8225
|
this.syncCellPresentation();
|
|
@@ -7979,6 +8283,7 @@ var VelaWorkspace = class {
|
|
|
7979
8283
|
setLayout(layout) {
|
|
7980
8284
|
if (this.destroyed) return;
|
|
7981
8285
|
if (this.monoLayout) return;
|
|
8286
|
+
this.clearMaximized();
|
|
7982
8287
|
const next = this.resolveLayout(layout);
|
|
7983
8288
|
const nextBackend = this.backendFor(next);
|
|
7984
8289
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -7999,6 +8304,7 @@ var VelaWorkspace = class {
|
|
|
7999
8304
|
this.buildCells();
|
|
8000
8305
|
this.alignNewCellStyles(preexisting);
|
|
8001
8306
|
this.syncCellPresentation();
|
|
8307
|
+
this.refreshCellControls();
|
|
8002
8308
|
this.topbar.setLayout(next.id);
|
|
8003
8309
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
8004
8310
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -8007,6 +8313,67 @@ var VelaWorkspace = class {
|
|
|
8007
8313
|
this.events.emit("layout:changed", { layout: next.id });
|
|
8008
8314
|
this.markStateDirty();
|
|
8009
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
|
+
}
|
|
8010
8377
|
resize() {
|
|
8011
8378
|
this.splitters.layout();
|
|
8012
8379
|
}
|
|
@@ -8076,6 +8443,7 @@ var VelaWorkspace = class {
|
|
|
8076
8443
|
this.mobileBar?.renderActions();
|
|
8077
8444
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
8078
8445
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
8446
|
+
this.syncMobileMaximize();
|
|
8079
8447
|
this.drawingPill?.onChart(cell.chart);
|
|
8080
8448
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
8081
8449
|
this.historyUnsub?.();
|
|
@@ -8165,8 +8533,78 @@ var VelaWorkspace = class {
|
|
|
8165
8533
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
8166
8534
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
8167
8535
|
}
|
|
8536
|
+
this.applyMaximizePresentation();
|
|
8537
|
+
this.mountAttributionMark();
|
|
8168
8538
|
this.splitters.layout();
|
|
8169
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
|
+
}
|
|
8170
8608
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
8171
8609
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
8172
8610
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -8198,6 +8636,12 @@ var VelaWorkspace = class {
|
|
|
8198
8636
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
8199
8637
|
context: () => this.context(),
|
|
8200
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),
|
|
8201
8645
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
8202
8646
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
8203
8647
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -8211,6 +8655,7 @@ var VelaWorkspace = class {
|
|
|
8211
8655
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
8212
8656
|
this.wireCell(cell);
|
|
8213
8657
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
8658
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
8214
8659
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
8215
8660
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
8216
8661
|
cell.restorePersistedExt();
|
|
@@ -8220,6 +8665,7 @@ var VelaWorkspace = class {
|
|
|
8220
8665
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
8221
8666
|
if (host) this.gridEl.appendChild(host);
|
|
8222
8667
|
}
|
|
8668
|
+
this.mountAttributionMark();
|
|
8223
8669
|
}
|
|
8224
8670
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
8225
8671
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -8279,6 +8725,9 @@ var VelaWorkspace = class {
|
|
|
8279
8725
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
8280
8726
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
8281
8727
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
8728
|
+
chart.on("pane:changed", () => {
|
|
8729
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
8730
|
+
});
|
|
8282
8731
|
chart.renderer.onAxisLongPress((e) => {
|
|
8283
8732
|
if (this.layoutCtl.current !== "mobile") return;
|
|
8284
8733
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -8599,6 +9048,7 @@ var VelaWorkspace = class {
|
|
|
8599
9048
|
for (const cell of this.cellsById.values()) {
|
|
8600
9049
|
cell.chart.renderer.closeDialogs();
|
|
8601
9050
|
cell.chart.renderer.setLayoutMode(mode);
|
|
9051
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
8602
9052
|
}
|
|
8603
9053
|
this.syncCellPresentation();
|
|
8604
9054
|
}
|